package Ora2Pg;
#------------------------------------------------------------------------------
# Project  : Oracle to PostgreSQL database schema converter
# Name     : Ora2Pg.pm
# Language : Perl
# Authors  : Gilles Darold, gilles _AT_ darold _DOT_ net
# Copyright: Copyright (c) 2000-2025 : Gilles Darold - All rights reserved -
# Function : Main module used to export Oracle database schema to PostgreSQL
# Usage    : See documentation in this file with perldoc.
#------------------------------------------------------------------------------
#
#        This program is free software: you can redistribute it and/or modify
#        it under the terms of the GNU General Public License as published by
#        the Free Software Foundation, either version 3 of the License, or
#        any later version.
#
#        This program is distributed in the hope that it will be useful,
#        but WITHOUT ANY WARRANTY; without even the implied warranty of
#        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#        GNU General Public License for more details.
#
#        You should have received a copy of the GNU General Public License
#        along with this program. If not, see < http://www.gnu.org/licenses/ >.
#
#------------------------------------------------------------------------------

use vars qw($VERSION $PSQL %AConfig);
use Carp qw(confess);
use DBI;
use POSIX qw(locale_h _exit :sys_wait_h strftime);
use IO::File;
use Config;
use Time::HiRes qw/usleep/;
use Fcntl qw/ :flock /;
use IO::Handle;
use IO::Pipe;
use File::Basename;
use File::Spec;
use File::Temp qw/ tempfile /;
use Benchmark;
use Encode;

#set locale to LC_NUMERIC C
setlocale(LC_NUMERIC,"C");

$VERSION = '25.0';
$PSQL = $ENV{PLSQL} || 'psql';

$| = 1;

our %RUNNING_PIDS = ();
# Multiprocess communication pipe
our $pipe = undef;
our $TMP_DIR = File::Spec->tmpdir() || '/tmp';
our %ordered_views = ();

# Character that must be escaped in COPY statement
my $ESCAPE_COPY = { "\0" => "", "\\" => "\\\\", "\r" => "\\r", "\n" => "\\n", "\t" => "\\t"};

# Oracle internal timestamp month equivalent
our %ORACLE_MONTHS = ('JAN'=>'01', 'FEB'=>'02','MAR'=>'03','APR'=>'04','MAY'=>'05','JUN'=>'06','JUL'=>'07','AUG'=>'08','SEP'=>'09','OCT'=>10,'NOV'=>11,'DEC'=>12);

# Exclude table generated by partition logging, materialized view logs, statistis on spatial index,
# spatial index tables, sequence index tables, interMedia Text index tables and Unified Audit tables.
# LogMiner, Oracle Advanced Replication, hash table used by loadjava.
our @EXCLUDED_TABLES = ('USLOG\$_.*', 'MLOG\$_.*', 'RUPD\$_.*', 'MDXT_.*', 'MDRT_.*', 'MDRS_.*', 'DR\$.*', 'CLI_SWP\$.*', 'LOGMNR\$.*', 'REPCAT\$.*', 'JAVA\$.*', 'AQ\$.*', 'BIN\$.*', 'SDO_GR_.*', '.*\$JAVA\$.*', 'PROF\$.*', 'TOAD_PLAN_.*', 'SYS_.*\$', 'QUEST_SL_.*', 'SYS_EXPORT_SCHEMA_.*', 'SYS_IMPORT_.*');
our @EXCLUDED_TABLES_8I = ('USLOG$_%', 'MLOG$_%', 'RUPD$_%', 'MDXT_%', 'MDRT_%', 'MDRS_%', 'DR$%', 'CLI_SWP$%', 'LOGMNR$%', 'REPCAT$%', 'JAVA$%', 'AQ$%', 'BIN$%', '%$JAVA$%', 'PROF$%', 'TOAD_PLAN_%', 'SYS_%$', 'QUEST_SL_%', 'SYS_EXPORT_SCHEMA_%', 'SYS_IMPORT_%');

our @Oracle_tables = qw(
EVT_CARRIER_CONFIGURATION
EVT_DEST_PROFILE
EVT_HISTORY
EVT_INSTANCE
EVT_MAIL_CONFIGURATION
EVT_MONITOR_NODE
EVT_NOTIFY_STATUS
EVT_OPERATORS
EVT_OPERATORS_ADDITIONAL
EVT_OPERATORS_SYSTEMS
EVT_OUTSTANDING
EVT_PROFILE
EVT_PROFILE_EVENTS
EVT_REGISTRY
EVT_REGISTRY_BACKLOG
OLS_DIR_BUSINESSE
OLS_DIR_BUSINESSES
SDO_COORD_REF_SYS
SDO_CS_SRS
SDO_INDEX_METADATA_TABLE
SDO_INDEX_METADATA_TABLES
SDO_PC_BLK_TABLE
SDO_STYLES_TABLE
SDO_TIN_BLK_TABLE
SMACTUALPARAMETER_S
SMGLOBALCONFIGURATION_S
SMFORMALPARAMETER_S
SMFOLDER_S
SMDISTRIBUTIONSET_S
SMDEPENDENTLINKS
SMDEPENDENTINDEX
SMDEPENDEEINDEX
SMDEFAUTH_S
SMDBAUTH_S
SMPARALLELJOB_S
SMPACKAGE_S
SMOWNERLINKS
SMOWNERINDEX
SMOWNEEINDEX
SMOSNAMES_X
SMOMSTRING_S
SMMOWNERLINKS
SMMOWNERINDEX
SMPACKAGE_S
SMPARALLELJOB_S
SMPARALLELOPERATION_S
SMPARALLELSTATEMENT_S
SMPRODUCT_S
SMP_AD_ADDRESSES_
SMP_AD_DISCOVERED_NODES_
SMP_AD_NODES_
SMP_AD_PARMS_
SMP_AUTO_DISCOVERY_ITEM_
SMP_AUTO_DISCOVERY_PARMS_
SMP_BLOB_
SMP_CREDENTIALS\$
SMP_JOB_
SMP_JOB_EVENTLIST_
SMP_JOB_HISTORY_
SMP_JOB_INSTANCE_
SMP_JOB_LIBRARY_
SMP_JOB_TASK_INSTANCE_
SMP_LONG_TEXT_
SMP_REP_VERSION
SMP_SERVICES
SMP_SERVICE_GROUP_DEFN_
SMP_SERVICE_GROUP_ITEM_
SMP_SERVICE_ITEM_
SMP_UPDATESERVICES_CALLED_
SMAGENTJOB_S
SMARCHIVE_S
SMBREAKABLELINKS
SMCLIQUE
SMCONFIGURATION
SMCONSOLESOSETTING_S
SMDATABASE_S
SMHOSTAUTH_S
SMHOST_S
SMINSTALLATION_S
SMLOGMESSAGE_S
SMMONTHLYENTRY_S
SMMONTHWEEKENTRY_S
SMP_USER_DETAILS
SMRELEASE_S
SMRUN_S
SMSCHEDULE_S
SMSHAREDORACLECLIENT_S
SMSHAREDORACLECONFIGURATION_S
SMTABLESPACE_S
SMVCENDPOINT_S
SMWEEKLYENTRY_S
);
push(@EXCLUDED_TABLES, @Oracle_tables);

# Some function might be excluded from export and assessment.
our @EXCLUDED_FUNCTION = ('SQUIRREL_GET_ERROR_OFFSET');

our @FKEY_OPTIONS = ('NEVER', 'DELETE', 'ALWAYS');

# Minimized the footprint on disc, so that more rows fit on a data page,
# which is the most important factor for speed.
our %TYPALIGN = (
	# Types and size, 1000 = variable
	'boolean' => 1,
	'smallint' => 2,
	'smallserial' => 2,
	'integer' => 4,
	'real' => 4,
	'serial' => 4,
	'date' => 4,
	'oid' => 4,
	'macaddr' => 6,
	'bigint' => 8,
	'bigserial' => 8,
	'double precision' => 8,
	'macaddr8' => 8,
	'money' => 8,
	'time' => 8,
	'timestamp' => 8,
	'timestamp without time zone' => 8,
	'timestamp with time zone' => 8,
	'interval' => 16,
	'point' => 16,
	'tinterval' => 16,
	'uuid' => 16,
	'circle' => 24,
	'box' => 32,
	'line' => 32,
	'lseg' => 32,
	'bit' => 1000,
	'bytea' => 1000,
	'character varying' => 1000,
	'cidr' => 19,
	'json' => 1000,
	'jsonb' => 1000,
	'numeric' => 1000,
	'path' => 1000,
	'polygon' => 1000,
	'text' => 1000,
	'xml' => 1000,
	# aliases
	'bool' => 1,
	'timetz' => 12,
	'char' => 1000,
	'decimal' => 1000,
	# deprecated
	'int2' => 2,
	'abstime' => 4,
	'bpchar' => 4,
	'int4' => 4,
	'reltime' => 4,
	'float4' => 4,
	'timestamptz' => 8,
	'float8' => 8,
	'int8' => 8,
	'name' => 64,
	'inet' => 19,
	'varbit' => 1000,
	'varchar' => 1000
);

our %INDEX_TYPE = (
	'NORMAL' => 'b-tree',
	'NORMAL/REV' => 'reversed b-tree',
	'FUNCTION-BASED NORMAL' => 'function based b-tree',
	'FUNCTION-BASED NORMAL/REV' => 'function based reversed b-tree',
	'BITMAP' => 'bitmap',
	'BITMAP JOIN' => 'bitmap join',
	'FUNCTION-BASED BITMAP' => 'function based bitmap',
	'FUNCTION-BASED BITMAP JOIN' => 'function based bitmap join',
	'CLUSTER' => 'cluster',
	'DOMAIN' => 'domain',
	'IOT - TOP' => 'IOT',
	'SPATIAL INDEX' => 'spatial index',
);

# Reserved keywords in PostgreSQL
our @KEYWORDS = qw(
	ALL ANALYSE ANALYZE AND ANY ARRAY AS ASC ASYMMETRIC AUTHORIZATION BINARY
	BOTH CASE CAST CHECK CMAX CMIN COLLATE COLLATION COLUMN CONCURRENTLY CONSTRAINT CREATE
	CROSS CTID CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA CURRENT_TIME
	CURRENT_TIMESTAMP CURRENT_USER DEFAULT DEFERRABLE DESC DISTINCT DO ELSE END
	EXCEPT FALSE FETCH FOR FOREIGN FREEZE FROM FULL GRANT GROUP HAVING ILIKE IN
	INITIALLY INNER INTERSECT INTO IS ISNULL JOIN KEY LATERAL LEADING LEFT LIKE LIMIT
	LOCALTIME LOCALTIMESTAMP NATURAL NOT NOTNULL NULL OFFSET ON ONLY OR ORDER OUTER
	OVERLAPS PARTITION PLACING PRIMARY REFERENCES REF RETURNING RIGHT SELECT SESSION_USER
	SIMILAR SOME SYMMETRIC TABLE TABLESAMPLE THEN TO TRAILING TRUE UNION UNIQUE USER
	USING VARIADIC VERBOSE WHEN WHERE WINDOW WITH
);

# Reserved keywords that can be used in PostgreSQL as function or type name
our @FCT_TYPE_KEYWORDS = qw(
	AUTHORIZATION BINARY COLLATION CONCURRENTLY CROSS CURRENT_SCHEMA FREEZE
	FULL ILIKE INNER IS ISNULL JOIN LEFT LIKE NATURAL NOTNULL OUTER OVERLAPS
	RIGHT SIMILAR TABLESAMPLE VERBOSE
);


our @SYSTEM_FIELDS = qw(oid tableoid xmin xmin cmin xmax cmax ctid);
our %BOOLEAN_MAP = (
	'yes' => 't',
	'no' => 'f',
	'y' => 't',
	'n' => 'f',
	'1' => 't',
	'0' => 'f',
	'true' => 't',
	'false' => 'f',
	'enabled'=> 't',
	'disabled'=> 'f',
	't' => 't',
	'f' => 'f',
);

our @GRANTS = (
	'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE',
	'REFERENCES', 'TRIGGER', 'USAGE', 'CREATE', 'CONNECT',
	'TEMPORARY', 'TEMP', 'USAGE', 'ALL', 'ALL PRIVILEGES',
	'EXECUTE'
);

our @ORACLE_FDW_COPY_MODES = qw( local server );

our @ORACLE_FDW_COPY_FORMATS = qw( binary csv );

$SIG{'CHLD'} = 'DEFAULT';

####
# method used to fork as many child as wanted
##
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();
}

# With multiprocess we need to wait all childs
sub wait_child
{
        my $sig = shift;
        print STDERR "Received terminating signal ($sig).\n";
	if ($^O !~ /MSWin32|dos/i) {
		1 while wait != -1;
		$SIG{INT} = \&wait_child;
		$SIG{TERM} = \&wait_child;
	}
        print STDERR "Aborting.\n";
        _exit(0);
}
$SIG{INT} = \&wait_child;
$SIG{TERM} = \&wait_child;

=head1 PUBLIC METHODS

=head2 new HASH_OPTIONS

Creates a new Ora2Pg object.

The only required option is:

    - config : Path to the configuration file (required).

All directives found in the configuration file can be overwritten in the
instance call by passing them in lowercase as arguments.

=cut

sub new
{
	my ($class, %options) = @_;

	# This create an OO perl object
	my $self = {};
	bless ($self, $class);

	# Initialize this object
	$self->_init(%options);
	
	# Return the instance
	return($self);
}



=head2 export_schema FILENAME

Print SQL data output to a file name or
to STDOUT if no file name is specified.

=cut

sub export_schema
{
	my $self = shift;

	# Create default export file where things will be written with the dump() method
	# First remove it if the output file already exists
	foreach my $t (@{$self->{export_type}})
	{
		next if ($t =~ /^(?:SHOW_|TEST)/i); # SHOW_* commands are not concerned here

		# Set current export type
		$self->{type} = $t;

		if ($self->{type} ne 'LOAD')
		{
			# Close open main output file
			if (defined $self->{fhout}) {
				$self->close_export_file($self->{fhout});
			}
			# Remove old export file if it already exists
			$self->remove_export_file();
			# then create a new one
			$self->create_export_file();
		}

		# Dump exported statement to output
		$self->_get_sql_statements();

		if ($self->{type} ne 'LOAD')
		{
			# Close output export file create above
			$self->close_export_file($self->{fhout}) if (defined $self->{fhout});
		}
	}

	# Disconnect from the database
	$self->{dbh}->disconnect() if ($self->{dbh});
	$self->{dbhdest}->disconnect() if ($self->{dbhdest});

	# Try to requalify package function call
	if (!$self->{package_as_schema}) {
		$self->fix_function_call();
	}

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	unlink($dirprefix . 'temp_pass2_file.dat');
	if ($self->{type} eq 'COPY' && !$self->{quiet}) {
		print "\nSchema Export Complete\n\n";
	}
}


=head2 open_export_file FILENAME

Open a file handle to a given filename.

=cut

sub open_export_file
{
	my ($self, $outfile, $noprefix) = @_;

	my $filehdl = undef;

	if ($outfile && $outfile ne '-') {
		if ($outfile ne '-') {
			if ($self->{output_dir} && !$noprefix) {
				$outfile = $self->{output_dir} . '/' . $outfile;
			}
			if ($self->{input_file} && ($outfile eq $self->{input_file})) {
				$self->logit("FATAL: input file is the same as output file: $outfile, can not overwrite it.\n",0,1);
			}
		}
		# If user request data compression
		if ($outfile =~ /\.gz$/i)
		{
			eval("use Compress::Zlib;");
			$self->{compress} = 'Zlib';
			$filehdl = gzopen("$outfile", "wb") or $self->logit("FATAL: Can't create deflation file $outfile\n",0,1);
			if ($self->{'binmode'} =~ /^:/) {
				binmode($filehdl, $self->{'binmode'});
			} else {
				binmode($filehdl, ":utf8");
			}
		}
		elsif ($outfile =~ /\.bz2$/i)
		{
			$self->logit("Error: can't run bzip2\n",0,1) if (!-x $self->{bzip2});
			$self->{compress} = 'Bzip2';
			$filehdl = new IO::File;
			$filehdl->open("|$self->{bzip2} --stdout >$outfile") or $self->logit("FATAL: Can't open pipe to $self->{bzip2} --stdout >$outfile: $!\n", 0,1);
			if ($self->{'binmode'} =~ /^:/) {
				binmode($filehdl, $self->{'binmode'});
			} else {
				binmode($filehdl, ":utf8");
			}
		}
		else
		{
			$filehdl = new IO::File;
			$filehdl->open(">$outfile") or $self->logit("FATAL: Can't open $outfile: $!\n", 0, 1);
		}
		$filehdl->autoflush(1) if (defined $filehdl && !$self->{compress});
	}

	return $filehdl;
}

=head2 create_export_file FILENAME

Set output file and open a file handle on it,
will use STDOUT if no file name is specified.

=cut

sub create_export_file
{
	my ($self, $outfile) = @_;

	# Do not create the default export file with direct data export
	if (($self->{type} eq 'INSERT') || ($self->{type} eq 'COPY')) {
		return if ($self->{pg_dsn});
	}

	# Init with configuration OUTPUT filename
	$outfile ||= $self->{output};
	if ($outfile)
	{
		if ($outfile ne '-')
		{
			# Prefix out file with export type in multiple export type call
			$outfile = $self->{type} . "_$outfile" if ($#{$self->{export_type}} > 0);
			if ($self->{output_dir} && $outfile) {
				$outfile = $self->{output_dir} . "/" . $outfile;
			}
			if ($self->{input_file} && ($outfile eq $self->{input_file})) {
				$self->logit("FATAL: input file is the same as output file: $outfile, can not overwrite it.\n",0,1);
			}
		}

		# Send output to the specified file
		if ($outfile =~ /\.gz$/)
		{
			eval("use Compress::Zlib;");
			$self->{compress} = 'Zlib';
			$self->{fhout} = gzopen($outfile, "wb") or $self->logit("FATAL: Can't create deflation file $outfile\n", 0, 1);
		}
		elsif ($outfile =~ /\.bz2$/)
		{
			$self->logit("FATAL: can't run bzip2\n",0,1) if (!-x $self->{bzip2});
			$self->{compress} = 'Bzip2';
			$self->{fhout} = new IO::File;
			$self->{fhout}->open("|$self->{bzip2} --stdout >$outfile") or $self->logit("FATAL: Can't open pipe to $self->{bzip2} --stdout >$outfile: $!\n", 0, 1);
		}
		else
		{
			$self->{fhout} = new IO::File;
			$self->{fhout}->open(">>$outfile") or $self->logit("FATAL: Can't open $outfile: $!\n", 0, 1);
			$self->set_binmode($self->{fhout});
		}
		if ( $self->{compress} && (($self->{jobs} > 1) || ($self->{oracle_copies} > 1)) )
		{
			die "FATAL: you can't use compressed output with parallel dump\n";
		}
	}
}

sub remove_export_file
{
	my ($self, $outfile) = @_;

	# Init with configuration OUTPUT filename
	$outfile ||= $self->{output};
	if ($outfile && $outfile ne '-')
	{
		# Prefix out file with export type in multiple export type call
		$outfile = $self->{type} . "_$outfile" if ($#{$self->{export_type}} > 0);
		if ($self->{output_dir} && $outfile)
		{
			$outfile = $self->{output_dir} . "/" . $outfile;
		}
		if ($self->{input_file} && ($outfile eq $self->{input_file}))
		{
			$self->logit("FATAL: input file is the same as output file: $outfile, can not overwrite it.\n",0,1);
		}
		unlink($outfile);
	}
}

=head2 append_export_file FILENAME

Open a file handle to a given filename to append data.

=cut

sub append_export_file
{
	my ($self, $outfile, $noprefix) = @_;

	my $filehdl = undef;

	if ($outfile)
	{
		if ($self->{output_dir} && !$noprefix) {
			$outfile = $self->{output_dir} . '/' . $outfile;
		}
		# If user request data compression
		if ($self->{compress} && (($self->{jobs} > 1) || ($self->{oracle_copies} > 1))) {
			die "FATAL: you can't use compressed output with parallel dump\n";
		} else {
			$filehdl = new IO::File;
			$filehdl->open(">>$outfile") or $self->logit("FATAL: Can't open $outfile: $!\n", 0, 1);
			$filehdl->autoflush(1);
		}
	}

	return $filehdl;
}

=head2 append_lo_import_file FILENAME

Open a file handle to lo_import-$table.sh file to append data.

=cut

sub append_lo_import_file
{
	my ($self, $table, $noprefix) = @_;

	my $filehdl = undef;
	my $new = 0;

	my $outfile = "lo_import-$table.sh";
	if ($self->{output_dir} && !$noprefix) {
		$outfile = $self->{output_dir} . '/' . $outfile;
	}
	$filehdl = new IO::File;
	$new = 1 if (!-e $outfile);
	$filehdl->open(">>$outfile") or $self->logit("FATAL: Can't open $outfile: $!\n", 0, 1);
	$filehdl->autoflush(1);
	$self->set_binmode($filehdl);
	flock($filehdl, 2) || die "FATAL: can't lock file $outfile\n";
	# At file creation append the verification of the required environment variable
	if ($new)
	{
		$self->{post_lo_script} = qq{#!/bin/sh
if [ "\${PGDATABASE}a" = "a" ]; then
	echo "You must set the environment variable PGDATABASE to defined the database where the commands"
	echo "will be executed. And optionally PGHOST, PGUSER, etc. if they don't correspond to the default."
	exit 1
fi
$self->{post_lo_script}
};
	}
	$filehdl->print($self->{post_lo_script});
	$filehdl->close();
}

=head2 read_export_file FILENAME

Open a file handle to a given filename to read data.

=cut

sub read_export_file
{
	my ($self, $infile) = @_;

	my $filehdl = new IO::File;
	$filehdl->open("<$infile") or $self->logit("FATAL: Can't read $infile: $!\n", 0, 1);

	return $filehdl;
}


=head2 close_export_file FILEHANDLE

Close a file handle.

=cut

sub close_export_file
{
	my ($self, $filehdl, $not_compressed) = @_;

	return if (!defined $filehdl);

	if (!$not_compressed && $self->{output} =~ /\.gz$/) {
		if ($filehdl =~ /IO::File=/) {
			$filehdl->close();
		} else {
			$filehdl->gzclose();
		}
	} else {
		$filehdl->close();
	}
}

=head2 modify_struct TABLE_NAME ARRAYOF_FIELDNAME

Modify the table structure during the export. Only the specified columns
will be exported. 

=cut

sub modify_struct
{
	my ($self, $table, @fields) = @_;

	if (!$self->{preserve_case}) {
		map { $_ = lc($_) } @fields;
		$table = lc($table);
	}
	push(@{$self->{modify}{$table}}, @fields);

}

=head2 exclude_columns TABLE_NAME ARRAYOF_FIELDNAME

Modify the table structure during the export. The specified columns
will NOT be exported. 

=cut

sub exclude_columns
{
	my ($self, $table, @fields) = @_;

	if (!$self->{preserve_case}) {
		delete $self->{exclude_columns}{$table};
		map { $_ = lc($_) } @fields;
		$table = lc($table);
		push(@{$self->{exclude_columns}{$table}}, @fields);
	}
}


=head2 is_reserved_words

Returns 1 if the given object name is a PostgreSQL reserved word
Returns 2 if the object name is only numeric
Returns 3 if the object name is a system column

=cut

sub is_reserved_words
{
	my ($self, $obj_name) = @_;

	if ($obj_name && grep(/^\Q$obj_name\E$/i, @KEYWORDS)) {
		return 1 if (!grep(/^$self->{type}/, 'FUNCTION', 'PACKAGE', 'PROCEDURE') || grep(/^\Q$obj_name\E$/i, @FCT_TYPE_KEYWORDS));
	}
	# columns starting by numbers need to be quoted unless it is an operation
	if ($obj_name =~ /^\d+/ && $obj_name !~ /[\+\-\/\*]/) {
		return 2;
	}
	if ($obj_name && grep(/^\Q$obj_name\E$/i, @SYSTEM_FIELDS)) {
		return 3;
	}

	return 0;
}

=head2 quote_object_name

Return a quoted object named when needed:
	- PostgreSQL reserved word
	- unsupported character
	- start with a digit or digit only
=cut


sub quote_object_name
{
	my ($self, @obj_list) = @_;

	my @ret = ();

	foreach my $obj_name (@obj_list)
	{
		next if ($obj_name =~ /^SYS_NC\d+/);

		# Start by removing any double quote and extra space
		$obj_name =~ s/"//g;
		$obj_name =~ s/^\s+//;
		$obj_name =~ s/\s+$//;

		# When PRESERVE_CASE is not enabled set object name to lower case
		if (!$self->{preserve_case})
		{
			$obj_name = lc($obj_name);
			# then if there is non alphanumeric or the object name is a reserved word
			if ($obj_name =~ /[^a-z0-9\_\.\$]/ || ($self->{use_reserved_words} && $self->is_reserved_words($obj_name)) || $obj_name =~ /^\d+/)
			{
				# Add double quote to [schema.] object name 
				if ($obj_name !~ /^[^\.]+\.[^\.]+$/ && $obj_name !~ /^[^\.]+\.[^\.]+\.[^\.]+$/) {
					$obj_name = '"' . $obj_name . '"';
				} elsif ($obj_name =~ /^[^\.]+\.[^\.]+$/) {
					$obj_name =~ s/^([^\.]+)\.([^\.]+)$/"$1"\."$2"/;
				} else {
					$obj_name =~ s/^([^\.]+)\.([^\.]+)\.([^\.]+)$/"$1"\."$2"\."$3"/;
				}
				$obj_name = '"' . $obj_name . '"' if ($obj_name =~ /^\d+/);
			}
		}
		# Add double quote to [schema.] object name 
		elsif ($obj_name !~ /^[^\.]+\.[^\.]+$/ && $obj_name !~ /^[^\.]+\.[^\.]+\.[^\.]+$/) {
			$obj_name = "\"$obj_name\"";
		} elsif ($obj_name =~ /^[^\.]+\.[^\.]+$/) {
			$obj_name =~ s/^([^\.]+)\.([^\.]+)$/"$1"\."$2"/;
		} else {
			$obj_name =~ s/^([^\.]+)\.([^\.]+)\.([^\.]+)$/"$1"\."$2"\."$3"/;
		}
		if ($obj_name =~ /^"[^\s]+\s+(ASC|DESC)"$/i) {
			$obj_name =~ s/"//g;
			$obj_name =~ s/\s+ASC$//ig;
		}
		push(@ret, $obj_name);
	}

	return join(',', @ret);
}

=head2 replace_tables HASH

Modify table names during the export.

=cut

sub replace_tables
{
	my ($self, %tables) = @_;

	foreach my $t (keys %tables) {
		$self->{replaced_tables}{"\L$t\E"} = $tables{$t};
	}

}

=head2 replace_cols HASH

Modify column names during the export.

=cut

sub replace_cols
{
	my ($self, %cols) = @_;

	foreach my $t (keys %cols) {
		foreach my $c (keys %{$cols{$t}}) {
			$self->{replaced_cols}{"\L$t\E"}{"\L$c\E"} = $cols{$t}{$c};
		}
	}

}

=head2 set_where_clause HASH

Add a WHERE clause during data export on specific tables or on all tables

=cut

sub set_where_clause
{
	my ($self, $global, %table_clause) = @_;

	$self->{global_where} = $global;
	foreach my $t (keys %table_clause) {
		$self->{where}{"\L$t\E"} = $table_clause{$t};
	}

}

=head2 set_delete_clause HASH

Add a DELETE clause before data export on specific tables or on all tables

=cut

sub set_delete_clause
{
	my ($self, $global, %table_clause) = @_;

	$self->{global_delete} = $global;
	foreach my $t (keys %table_clause) {
		$self->{delete}{"\L$t\E"} = $table_clause{$t};
	}

}


#### Private subroutines ####

=head1 PRIVATE METHODS

=head2 _db_connection

Initialize a connexion to the Oracle database.

=cut

sub _db_connection
{
	my $self = shift;

	if ($self->{is_mysql})
	{
		use Ora2Pg::MySQL;
		return Ora2Pg::MySQL::_db_connection($self);
	}
	elsif ($self->{is_mssql})
	{
		use Ora2Pg::MSSQL;
		return Ora2Pg::MSSQL::_db_connection($self);
	}
	else
	{
		use Ora2Pg::Oracle;
		return Ora2Pg::Oracle::_db_connection($self);
	}
}


=head2 _init HASH_OPTIONS

Initialize an Ora2Pg object instance with a connexion to the
Oracle database.

=cut

sub _init
{
	my ($self, %options) = @_;

	# Use custom temp directory if specified
	$TMP_DIR = $options{temp_dir} || $TMP_DIR;

	# Read configuration file
	$self->read_config($options{config}) if ($options{config});

	# Override any Ora2Pg configuration directive from command line options
	if (exists $options{options})
	{
		my @opt = split(/\s*\|\s*/, $options{options});
		foreach my $o (@opt)
		{
			next if ($o =~ /^options\s*=/i);
			$o =~ s/\s*=\s*/\t/;
			&parse_config($o);
		}
		delete $options{options};
	}

	# Those are needed by DBI
	$ENV{ORACLE_HOME} = $AConfig{'ORACLE_HOME'} if ($AConfig{'ORACLE_HOME'});
	$ENV{NLS_LANG} = $AConfig{'NLS_LANG'} if ($AConfig{'NLS_LANG'});

	# Init arrays
	$self->{default_tablespaces} = ();
	$self->{limited} = ();
	$self->{excluded} = ();
	$self->{view_as_table} = ();
	$self->{mview_as_table} = ();
	$self->{modify} = ();
	$self->{replaced_tables} = ();
	$self->{replaced_cols} = ();
	$self->{replace_as_boolean} = ();
	$self->{ora_boolean_values} = ();
	$self->{null_equal_empty} = 1;
	$self->{estimate_cost} = 0;
	$self->{where} = ();
	$self->{delete} = ();
	$self->{replace_query} = ();
	$self->{ora_reserved_words} = (); 
	$self->{defined_pk} = ();
	$self->{allow_partition} = ();
	$self->{empty_lob_null} = 0;
	$self->{look_forward_function} = ();
	$self->{no_function_metadata} = 0;
	$self->{transform_value} = ();
	$self->{all_objects} = ();

	# Initial command to execute at Oracle and PostgreSQL connexion
	$self->{ora_initial_command} = ();
	$self->{pg_initial_command} = ();

	# To register user defined exception
	$self->{custom_exception} = ();
	$self->{exception_id} = 50001;

	# Init PostgreSQL DB handle
	$self->{dbhdest} = undef;
	$self->{standard_conforming_strings} = 1;
	$self->{create_schema} = 1;

	# Init some arrays
	$self->{external_table} = ();
	$self->{function_metadata} = ();
	$self->{grant_object} = '';

	# Used to precise if we need to rename the partitions
	$self->{rename_partition} = 0;

	# Do not use parellel export for partition, backward compatibility with <= 25.0
	$self->{disable_parallel_partition} = 0;

	# Use to preserve the data export type with geometry objects
	$self->{local_type} = '';

	# Shall we log on error during data import or abort.
	$self->{log_on_error} = 0;

	# Initialize some variable related to export of mysql database
	$self->{is_mysql} = 0;
	$self->{mysql_mode} = '';
	$self->{mysql_internal_extract_format} = 0;
	$self->{mysql_pipes_as_concat} = 0;

	# Initialize some variable related to export of mssql database
	$self->{is_mssql} = 0;
	$self->{drop_rowversion} = 0;
	$self->{case_insensitive_search} = 'citext';

	# List of users for audit trail
	$self->{audit_user} = '';

	# Disable copy freeze by default
	$self->{copy_freeze} = '';

	# Use FTS index to convert CONTEXT Oracle's indexes by default
	$self->{context_as_trgm} = 0;
	$self->{fts_index_only}  = 1;
	$self->{fts_config}      = '';
	$self->{use_unaccent}    = 1;
	$self->{use_lower_unaccent} = 1;

	# Enable rewrite of outer join by default.
	$self->{rewrite_outer_join} = 1;

	# Init comment and text constant storage variables
	$self->{idxcomment} = 0;
	$self->{comment_values} = ();
	$self->{text_values} = ();
	$self->{text_values_pos} = 0;

	# Remove comments when reading an input file before parsing
	$self->{no_clean_comment} = 0;

	# Keep commit/rollback in converted pl/sql code by default
	$self->{comment_commit_rollback} = 0;

	# Keep savepoint in converted pl/sql code by default
	$self->{comment_savepoint} = 0;

	# Storage of string constant placeholder regexp
	$self->{string_constant_regexp} = ();
	$self->{alternative_quoting_regexp} = ();

	# Number of row for data validation
	$self->{data_validation_rows} = 10000;
	$self->{data_validation_error} = 10;
	$self->{data_validation_ordering} = 1;

	# Global file handle
	$self->{cfhout} = undef;

	# oracle_fdw foreign server
	$self->{fdw_server} = '';

	# oracle_fdw copy mode
	$self->{oracle_fdw_copy_mode} = '';

	# oracle_fdw copy format
	$self->{oracle_fdw_copy_format} = '';

	# AS OF SCN related variables
	$self->{start_scn} = $options{start_scn} || '';
	$self->{no_start_scn} = $options{no_start_scn} || '';
	$self->{current_oracle_scn} = ();
	$self->{cdc_ready} = $options{cdc_ready} || '';

	# Wether we load the pgtt extension as superuser or not
	$self->{pgtt_nosuperuser} ||= 0;

	# Initialyze following configuration file
	foreach my $k (sort keys %AConfig)
	{
		if (lc($k) eq 'allow') {
			$self->{limited} = $AConfig{ALLOW};
		} elsif (lc($k) eq 'exclude') {
			$self->{excluded} = $AConfig{EXCLUDE};
		} else {
			$self->{lc($k)} = $AConfig{$k};
		}
	}

	# Set default tablespace to exclude when using USE_TABLESPACE
	push(@{$self->{default_tablespaces}}, 'TEMP', 'USERS','SYSTEM');

	# Add the custom reserved keywords defined in configuration file
	push(@KEYWORDS, @{$self->{ora_reserved_words}});

	# Verify grant objects
	if ($self->{type} eq 'GRANT' && $self->{grant_object})
	{
		die "FATAL: wrong object type in GRANT_OBJECTS directive.\n" if (!grep(/^$self->{grant_object}$/, 'USER', 'TABLE', 'VIEW', 'MATERIALIZED VIEW', 'SEQUENCE', 'PROCEDURE', 'FUNCTION', 'PACKAGE BODY', 'TYPE', 'SYNONYM', 'DIRECTORY'));
	}

	# Default boolean values
	foreach my $k (keys %BOOLEAN_MAP) {
		$self->{ora_boolean_values}{lc($k)} = $BOOLEAN_MAP{$k};
	}
	# additional boolean values given from config file
	foreach my $k (keys %{$self->{boolean_values}}) {
		$self->{ora_boolean_values}{lc($k)} = $AConfig{BOOLEAN_VALUES}{$k};
	}

	# Set transaction isolation level
	if ($self->{transaction} eq 'readonly') {
		$self->{transaction} = 'SET TRANSACTION READ ONLY';
	} elsif ($self->{transaction} eq 'readwrite') {
		$self->{transaction} = 'SET TRANSACTION READ WRITE';
	} elsif ($self->{transaction} eq 'committed') {
		$self->{transaction} = 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED';
	} elsif ($self->{transaction} eq 'serializable') {
		$self->{transaction} = 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE';
	} else {
		if (grep(/^$self->{type}$/, 'COPY', 'INSERT')) {
			$self->{transaction} = 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE';
		} else {
			$self->{transaction} = 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED';
		}
	}
	$self->{function_check} = 1 if (not defined $self->{function_check} || $self->{function_check} eq '');
	$self->{qualify_function} = 1 if (!exists $self->{qualify_function});

	# Set default function to use for uuid generation
	$self->{uuid_function} ||= 'uuid_generate_v4';
	$self->{use_uuid} = 0;

	# Set default cost unit value to 5 minutes
	$self->{cost_unit_value} ||= 5;

	# Set default human days limit for type C migration level
	$self->{human_days_limit} ||= 5;

	# Defined if column order must be optimized
	$self->{reordering_columns} ||= 0;

	# Initialize suffix that may be added to the index name
	$self->{indexes_suffix} ||= '';

	# Disable synchronous commit for pg data load
	$self->{synchronous_commit} ||= 0;

	# Disallow NOLOGGING / UNLOGGED table creation
        $self->{disable_unlogged} ||= 0;

	# Change the varchar max length value
	$self->{double_max_varchar} ||= 0;

	# Default degree for Oracle parallelism
	if ($self->{default_parallelism_degree} eq '') {
		$self->{default_parallelism_degree} = 0;
	}

	# For utf8 encoding of the stored procedure code
	$self->{force_plsql_encoding} ||= 0;

	# Add header to output file
	$self->{no_header} ||= 0;

	# Mark function as STABLE by default
	if (not defined $self->{function_stable} || $self->{function_stable} ne '0') {
		$self->{function_stable} = 1;
	}

	# Initialize rewriting of index name
	if (not defined $self->{indexes_renaming} || $self->{indexes_renaming} ne '0') {
		$self->{indexes_renaming} = 1;
	}

	# Enable autonomous transaction conversion. Default is enable it.
	if (!exists $self->{autonomous_transaction} || $self->{autonomous_transaction} ne '0') {
		$self->{autonomous_transaction} = 1;
	}

	# By default we force identity column to be bigint
	if (!exists $self->{force_identity_bigint} || $self->{force_identity_bigint} ne '0') {
		$self->{force_identity_bigint} = 1;
	}

	# by default we don't remove TZ part of the TO_CHAR() format
	if (!exists $self->{to_char_notimezone}) {
		$self->{to_char_notimezone} = 0;
	}

	# Don't use *_pattern_ops with indexes by default
	$self->{use_index_opclass} ||= 0;

	# Autodetect spatial type
	$self->{autodetect_spatial_type} ||= 0;

	# Use btree_gin extenstion to create bitmap like index with pg >= 9.4
	$self->{bitmap_as_gin} = 1 if ($self->{bitmap_as_gin} ne '0');

	# Create tables with OIDs or not, default to not create OIDs
	$self->{with_oid} ||= 0;

	# Minimum of lines required in a table to use parallelism
	$self->{parallel_min_rows} ||= 100000;

	# Should we export global temporary table
	$self->{export_gtt} ||= 0;

	# Should we replace zero date with something else than NULL
	$self->{replace_zero_date} ||= '';
	if ($self->{replace_zero_date} && (uc($self->{replace_zero_date}) ne '-INFINITY') && ($self->{replace_zero_date} !~ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) {
		die "FATAL: wrong format in REPLACE_ZERO_DATE value, should be YYYY-MM-DD HH:MM:SS or -INFINITY\n";
	}

	# Defined default value for to_number translation
	$self->{to_number_conversion} ||= 'numeric';

	# Set regexp to detect parts of statements that need to be considered as text
	if ($AConfig{STRING_CONSTANT_REGEXP}) {
		push(@{ $self->{string_constant_regexp} } , split(/;/, $AConfig{STRING_CONSTANT_REGEXP}));
	}
	if ($AConfig{ALTERNATIVE_QUOTING_REGEXP}) {
		push(@{ $self->{alternative_quoting_regexp} } , split(/;/, $AConfig{ALTERNATIVE_QUOTING_REGEXP}));
	}

	# Defined if we must add a drop if exists statement before creating an object
	$self->{drop_if_exists} ||= 0;

	# Disable ON CONFLICT clause by default
	$self->{insert_on_conflict} ||= 0;

	# Overwrite configuration with all given parameters
	# and try to preserve backward compatibility
	foreach my $k (keys %options)
	{
		next if ($options{options});
		if (($k eq 'allow') && $options{allow})
		{
			$self->{limited} = ();
			# Syntax: TABLE[regex1 regex2 ...];VIEW[regex1 regex2 ...];glob_regex1 glob_regex2 ...
			my @allow_vlist = split(/\s*;\s*/, $options{allow});
			foreach my $a (@allow_vlist)
			{
				if ($a =~ /^([^\[]+)\[(.*)\]$/) {
					push(@{$self->{limited}{"\U$1\E"}}, split(/[\s,]+/, $2) );
				} else {
					push(@{$self->{limited}{ALL}}, split(/[\s,]+/, $a) );
				}
			}
		}
		elsif (($k eq 'exclude') && $options{exclude})
		{
			$self->{excluded} = ();
			# Syntax: TABLE[regex1 regex2 ...];VIEW[regex1 regex2 ...];glob_regex1 glob_regex2 ...
			my @exclude_vlist = split(/\s*;\s*/, $options{exclude});
			foreach my $a (@exclude_vlist)
			{
				if ($a =~ /^([^\[]+)\[(.*)\]$/) {
					push(@{$self->{excluded}{"\U$1\E"}}, split(/[\s,]+/, $2) );
				} else {
					push(@{$self->{excluded}{ALL}}, split(/[\s,]+/, $a) );
				}
			}
		}
		elsif (($k eq 'view_as_table') && $options{view_as_table})
		{
			$self->{view_as_table} = ();
			push(@{$self->{view_as_table}}, split(/[\s;,]+/, $options{view_as_table}) );
		}
		elsif (($k eq 'mview_as_table') && $options{mview_as_table})
		{
			$self->{mview_as_table} = ();
			push(@{$self->{mview_as_table}}, split(/[\s;,]+/, $options{mview_as_table}) );
		}
		elsif (($k eq 'datasource') && $options{datasource}) {
			$self->{oracle_dsn} = $options{datasource};
		} elsif (($k eq 'user') && $options{user}) {
			$self->{oracle_user} = $options{user};
		} elsif (($k eq 'password') && $options{password}) {
			$self->{oracle_pwd} = $options{password};
		} elsif (($k eq 'is_mysql') && $options{is_mysql}) {
			$self->{is_mysql} = $options{is_mysql};
		} elsif (($k eq 'is_mssql') && $options{is_mssql}) {
			$self->{is_mssql} = $options{is_mssql};
		}
		elsif ($k eq 'where')
		{
			while ($options{where} =~ s/([^\[\s]+)\s*\[([^\]]+)\]\s*//)
			{
				my $table = $1;
				my $where = $2;
				$where =~ s/^\s+//;
				$where =~ s/\s+$//;
				$self->{where}{$table} = $where;
			}
			if ($options{where}) {
				$self->{global_where} = $options{where};
			}
		}
		elsif ($k eq 'drop_if_exists') # do not override config parameter, when option is not set
		{
			$self->{$k} ||= $options{$k};
		}
		elsif ($options{$k} ne '')
		{
			$self->{"\L$k\E"} = $options{$k};
		}
	}

	# Do not allow global allow/exclude with SHOW_* reports
	if ($self->{type} eq 'SHOW_REPORT' && ($#{$self->{limited}{ALL}} >= 0 || $#{$self->{excluded}{ALL}} >= 0)) {
		$self->logit("FATAL: you can not use global filters in ALLOW/EXCLUDE directive with SHOW_REPORT\n", 0, 1);
	}

	# Global regex will be applied to the export type only
	foreach my $i (@{$self->{limited}{ALL}})
	{
		my $typ = $self->{type} || 'TABLE';
		$typ = 'TABLE' if ($self->{type} =~ /(SHOW_TABLE|SHOW_COLUMN|FDW|KETTLE|COPY|INSERT|TEST)/);
		push(@{$self->{limited}{$typ}}, $i);
	}
	delete $self->{limited}{ALL};
	foreach my $i (@{$self->{excluded}{ALL}})
	{
		my $typ = $self->{type} || 'TABLE';
		$typ = 'TABLE' if ($self->{type} =~ /(SHOW_TABLE|SHOW_COLUMN|FDW|KETTLE|COPY|INSERT|TEST)/);
		push(@{$self->{excluded}{$typ}}, $i);
	}
	delete $self->{excluded}{ALL};

	$self->{debug} = $AConfig{'DEBUG'} if ($AConfig{'DEBUG'} >= 1);

	# Set default XML data extract method
	if (not defined $self->{xml_pretty} || ($self->{xml_pretty} != 0)) {
		$self->{xml_pretty} = 1;
	}

	# Set a default name for the foreign server
	if (!$self->{fdw_server} && $self->{type} eq 'FDW') {
		$self->{fdw_server} = 'orcl';
	}

	# Validate the oracle_fdw copy mode and format, and set a default mode and format if undefined
        if ($self->{fdw_server} && $self->{type} eq 'COPY') {
                $self->{oracle_fdw_copy_mode} ||= 'local';
                $self->{oracle_fdw_copy_mode} = lc($self->{oracle_fdw_copy_mode});
		if (!grep(/^$self->{oracle_fdw_copy_mode}$/, @ORACLE_FDW_COPY_MODES))
		{
			$self->logit("FATAL: Unknown oracle_fdw copy mode: $self->{oracle_fdw_copy_mode}. Valid modes: " . join(', ', @ORACLE_FDW_COPY_MODES) . "\n",0,1);
		}
                $self->{oracle_fdw_copy_format} ||= 'binary';
                $self->{oracle_fdw_copy_format} = lc($self->{oracle_fdw_copy_format});
		if (!grep(/^$self->{oracle_fdw_copy_format}$/, @ORACLE_FDW_COPY_FORMATS))
		{
			$self->logit("FATAL: Unknown oracle_fdw copy format: $self->{oracle_fdw_copy_format}. Valid formats: " . join(', ', @ORACLE_FDW_COPY_FORMATS) . "\n",0,1);
		}
	}

	# Set the schema where the foreign tables will be created
	$self->{fdw_import_schema} ||= 'ora2pg_fdw_import';

	# By default we will drop the temporary schema used for foreign import
	if (not defined $self->{drop_foreign_schema} || $self->{drop_foreign_schema} != 0) {
		$self->{drop_foreign_schema} = 1;
	}

	# By default varchar without size constraint are translated into text
	if (not defined $self->{varchar_to_text} || $self->{varchar_to_text} != 0) {
		$self->{varchar_to_text} = 1;
	}

	# Should we use \i or \ir in psql scripts
	if ($AConfig{PSQL_RELATIVE_PATH}) {
		$self->{psql_relative_path} = 'r';
	} else {
		$self->{psql_relative_path} = '';
	}

	# Clean potential remaining temporary files
	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	unlink($dirprefix . 'temp_pass2_file.dat');
	unlink($dirprefix . 'temp_cost_file.dat');

	# Autodetexct if we are exporting a MySQL database
	if ($self->{oracle_dsn} =~ /dbi:mysql/i) {
		$self->{is_mysql} = 1;
	} elsif ($self->{oracle_dsn} =~ /dbi:ODBC:driver=msodbcsql/i) {
		$self->{is_mssql} = 1;
	}

	# Preload our dedicated function per DBMS
	if ($self->{is_mysql}) {
		@{$self->{sysusers}} = ();
		import Ora2Pg::MySQL;
		$self->{sgbd_name} = 'MySQL';
	} elsif ($self->{is_mssql}) {
		push(@{$self->{sysusers}}, 'sys');
		import Ora2Pg::MSSQL;
		$self->{sgbd_name} = 'MSSQL';
	} else {
		import Ora2Pg::Oracle;
		$self->{sgbd_name} = 'Oracle';
	}

	# Export json configuration test
	$self->{json_test} ||= 0;
	# Show dependencies between stored procedures
	$self->{print_dependencies} ||= 0;

	# Set default system user/schema to not export. Most of them are extracted from this doc:
	# http://docs.oracle.com/cd/E11882_01/server.112/e10575/tdpsg_user_accounts.htm#TDPSG20030
	if (!$self->{is_mysql} && !$self->{is_mssql}) {
		push(@{$self->{sysusers}},'SYSTEM','CTXSYS','DBSNMP','EXFSYS','LBACSYS','MDSYS','MGMT_VIEW','OLAPSYS','ORDDATA','OWBSYS','ORDPLUGINS','ORDSYS','OUTLN','SI_INFORMTN_SCHEMA','SYS','SYSMAN','WK_TEST','WKSYS','WKPROXY','WMSYS','XDB','APEX_PUBLIC_USER','DIP','FLOWS_020100','FLOWS_030000','FLOWS_040100','FLOWS_010600','FLOWS_FILES','MDDATA','ORACLE_OCM','SPATIAL_CSW_ADMIN_USR','SPATIAL_WFS_ADMIN_USR','XS$NULL','PERFSTAT','SQLTXPLAIN','DMSYS','TSMSYS','WKSYS','APEX_040000','APEX_040200','DVSYS','OJVMSYS','GSMADMIN_INTERNAL','APPQOSSYS','DVSYS','DVF','AUDSYS','APEX_030200','MGMT_VIEW','ODM','ODM_MTR','TRACESRV','MTMSYS','OWBSYS_AUDIT','WEBSYS','WK_PROXY','OSE$HTTP$ADMIN','AURORA$JIS$UTILITY$','AURORA$ORB$UNAUTHENTICATED','DBMS_PRIVILEGE_CAPTURE','CSMIG', 'MGDSYS', 'SDE','DBSFWUSER');
	}

	# Log file handle
	$self->{fhlog} = undef;
	if ($self->{logfile})
	{
		$self->{fhlog} = new IO::File;
		$self->{fhlog}->open(">>$self->{logfile}") or $self->logit("FATAL: can't log to $self->{logfile}, $!\n", 0, 1);
	}

	# Autoconvert SRID
	if (not defined $self->{convert_srid} || ($self->{convert_srid} != 0)) {
		$self->{convert_srid} = 1;
	}
	if (not defined $self->{default_srid}) {
		$self->{default_srid} = 4326;
	}
	# Default function to use for ST_Geometry
	$self->{st_srid_function} ||= 'ST_SRID';
	$self->{st_dimension_function} ||= 'ST_DIMENSION';
	$self->{st_geometrytype_function} ||=  'ST_GeometryType';
	$self->{st_asbinary_function} ||= 'ST_AsBinary';
	$self->{st_astext_function} ||= 'ST_AsText';

	# Force Ora2Pg to extract spatial object in binary format
	$self->{geometry_extract_type} = uc($self->{geometry_extract_type});
	if (!$self->{geometry_extract_type} || !grep(/^$self->{geometry_extract_type}$/, 'WKT','WKB','INTERNAL')) {
		$self->{geometry_extract_type} = 'INTERNAL';
	}

	# Default value for triming can be LEADING, TRAILING or BOTH
	$self->{trim_type} = 'BOTH' if (!$self->{trim_type} || !grep(/^$self->{trim_type}/, 'BOTH', 'LEADING', 'TRAILING')); 
	# Default triming character is space
	$self->{trim_char} = ' ' if ($self->{trim_char} eq ''); 

	# Disable the use of orafce library by default
	$self->{use_orafce} ||= 0;
	if ($self->{type} eq 'SHOW_REPORT') {
		$self->{use_orafce} = 0;
	}

	# Disable the use of mssqlfce library by default
	$self->{use_mssqlfce} ||= 0;
	$self->{local_schemas} = ();
	$self->{local_schemas_regex} = '';

	# Do not apply any default table filtering to improve performances by not applying regexp
	$self->{no_excluded_table} ||= 0;

	# Enable BLOB data export by default
	if (not defined $self->{enable_blob_export}) {
		$self->{enable_blob_export} = 1;
	}
	# Enable CLOB data export by default
	if (not defined $self->{enable_clob_export}) {
		$self->{enable_clob_export} = 1;
	}

	# Table data export will be sorted by name by default
	$self->{data_export_order} ||= 'name';

	$self->{export_gtt} = 0 if ($self->{type} ne 'TABLE');

	# Free some memory
	%options = ();
	%AConfig = ();

	# Enable create or replace by default
	if ($self->{create_or_replace} || not defined $self->{create_or_replace}) {
		$self->{create_or_replace} = ' OR REPLACE';
	} else {
		$self->{create_or_replace} = '';
	}

	# Export to files using lo_import to be loaded
	# into PG need blob_to_lo to be activated and
	# some other configuration disabled.
	if ($self->{lo_import})
	{
		if ($self->{type} eq 'INSERT') {
			$self->logit("FATAL: You must use action COPY, not INSERT with --lo_import.\n", 0, 1);
		}
		$self->{blob_to_lo} = 1;
		$self->{truncate_table} = 0;
		$self->{drop_fkeys} = 0;
		$self->{drop_indexes} = 0;
	}

	$self->{copy_freeze} = ' FREEZE' if ($self->{copy_freeze});
	# Prevent use of COPY FREEZE with some incompatible case
	if ($self->{copy_freeze})
	{
		if ($self->{pg_dsn} && ($self->{jobs} > 1)) {
			$self->logit("FATAL: You can not use COPY FREEZE with -j (JOBS) > 1 and direct import to PostgreSQL.\n", 0, 1);
		} elsif ($self->{oracle_copies} > 1) {
			$self->logit("FATAL: You can not use COPY FREEZE with -J (ORACLE_COPIES) > 1.\n", 0, 1);
		}
	}
	else
	{
		$self->{copy_freeze} = '';
	}

	# Multiprocess init
	$self->{jobs} ||= 1;
	$self->{child_count}  = 0;
	# backward compatibility
	if ($self->{thread_count}) {
		$self->{jobs} = $self->{thread_count} || 1;
	}
	$self->{has_utf8_fct} = 1;
	eval { utf8::valid("test utf8 function"); };
	if ($@) {
		# Old perl install doesn't include these functions
		$self->{has_utf8_fct} = 0;
	}

	if ($self->{is_mysql} or $self->{is_mssql}) {
		# MySQL and MSQL do not supports this syntax fallback to read committed
		$self->{transaction} =~ s/(READ ONLY|READ WRITE)/ISOLATION LEVEL READ COMMITTED/;
	}

	# Set Oracle, Perl and PostgreSQL encoding that will be used
	$self->_init_environment();

	# Backward compatibility
	$self->{rename_partition} = 1 if (!$self->{rename_partition} && $self->{prefix_partition});

	# Multiple Oracle connection
	$self->{oracle_copies} ||= 0;
	$self->{ora_conn_count} = 0;
	$self->{data_limit} ||= 10000;
	$self->{blob_limit} ||= 0;
	$self->{clob_as_blob} ||= 0;
	$self->{disable_partition} ||= 0;
	$self->{parallel_tables} ||= 0;
	$self->{use_lob_locator} ||= 0;

        $self->{disable_partition} = 1 if ($self->{is_mssql} and
                                ($self->{type} eq 'COPY' or $self->{type} eq 'INSERT'));

	# Transformation and output during data export
	$self->{oracle_speed} ||= 0;
	$self->{ora2pg_speed} ||= 0;
	if (($self->{oracle_speed} || $self->{ora2pg_speed}) && !grep(/^$self->{type}$/, 'COPY', 'INSERT', 'DATA')) {
		# No output is only available for data export.
		die "FATAL: --oracle_speed or --ora2pg_speed can only be use with data export.\n";
	}
	$self->{oracle_speed} = 1 if ($self->{ora2pg_speed});

	# Shall we prefix function with a schema name to emulate a package?
	$self->{package_as_schema} = 1 if (not exists $self->{package_as_schema} || ($self->{package_as_schema} eq ''));
	$self->{package_functions} = ();

	# Set user defined data type translation
	if ($self->{data_type})
	{
		$self->{data_type} =~ s/\\,/#NOSEP#/gs;
		my @transl = split(/[,;]/, uc($self->{data_type}));
		$self->{data_type} = ();
		# Set default type conversion
		if ($self->{is_mysql}) {
			%{$self->{data_type}} = %Ora2Pg::MySQL::SQL_TYPE;
		} elsif ($self->{is_mssql}) {
			%{$self->{data_type}} = %Ora2Pg::MSSQL::SQL_TYPE;
		} else {
			%{$self->{data_type}} = %Ora2Pg::Oracle::SQL_TYPE;
		}
		# then set custom type conversion from the DATA_TYPE
		# configuration directive 
		foreach my $t (@transl)
		{
			my ($typ, $val) = split(/:/, $t);
			$typ =~ s/^\s+//;
			$typ =~ s/\s+$//;
			$val =~ s/^\s+//;
			$val =~ s/\s+$//;
			$typ =~ s/#NOSEP#/,/g;
			$val =~ s/#NOSEP#/,/g;
			$self->{data_type}{$typ} = lc($val) if ($val);
		}
	}
	else
	{
		# Set default type conversion
		if ($self->{is_mysql}) {
			%{$self->{data_type}} = %Ora2Pg::MySQL::SQL_TYPE;
		} elsif ($self->{is_mssql}) {
			%{$self->{data_type}} = %Ora2Pg::MSSQL::SQL_TYPE;
		} else {
			%{$self->{data_type}} = %Ora2Pg::Oracle::SQL_TYPE;
		}
	}

	# Set some default
	$self->{global_where} ||= '';
	$self->{global_delete} ||= '';
	$self->{prefix} = 'DBA';
	if ($self->{user_grants}) {
		$self->{prefix} = 'ALL';
	}
	$self->{bzip2} ||= '/usr/bin/bzip2';
	$self->{default_numeric} ||= 'bigint';
	$self->{type_of_type} = ();
	$self->{dump_as_html} ||= 0;
	$self->{dump_as_csv} ||= 0;
	$self->{dump_as_json} ||= 0;
	$self->{dump_as_sheet} ||= 0;
	$self->{dump_as_file_prefix} ||= '';
	$self->{top_max} ||= 10;
	$self->{print_header} ||= 0;
	$self->{use_default_null} = 1 if (!defined $self->{use_default_null});

	$self->{estimate_cost} = 1 if ($self->{dump_as_sheet});
	$self->{count_rows} ||= 0;
	$self->{count_rows} = 1 if ($self->{type} eq 'TEST_COUNT');

	# Enforce preservation of primary and unique keys
	# when USE_TABLESPACE is enabled
	if ($self->{use_tablespace} && !$self->{keep_pkey_names})
	{
	    print STDERR "WARNING: Enforcing KEEP_PKEY_NAMES to 1 because USE_TABLESPACE is enabled.\n";
	    $self->{keep_pkey_names} = 1;
	}

	# DATADIFF defaults
	$self->{datadiff} ||= 0;
	$self->{datadiff_del_suffix} ||= '_del';
	$self->{datadiff_ins_suffix} ||= '_ins';
	$self->{datadiff_upd_suffix} ||= '_upd';

	# Internal date boundary. Date below will be added to 2000, others will used 1900
	$self->{internal_date_max} ||= 49;

	# Set the target PostgreSQL major version
	if (!$self->{pg_version})
	{
		print STDERR "WARNING: target PostgreSQL version must be set in PG_VERSION configuration directive. Using default: 11\n";
		$self->{pg_version} = 11;
	}

	if ($self->{pg_version} >= 15) {
		$self->{pg_supports_negative_scale} //= 1;
	}
	# Compatibility with PostgreSQL versions
	if ($self->{pg_version} >= 9.0)
	{
		$self->{pg_supports_when} //= 1;
		$self->{pg_supports_ifexists} //= 1;
	}
	if ($self->{pg_supports_ifexists} == 1) {
		$self->{pg_supports_ifexists} = 'IF EXISTS';
	}
	if ($self->{pg_version} >= 9.1) {
		$self->{pg_supports_insteadof} //= 1;
	}
	if ($self->{pg_version} >= 9.3) {
		$self->{pg_supports_mview} //= 1;
		$self->{pg_supports_lateral} //= 1;
	}
	if ($self->{pg_version} >= 9.4) {
		$self->{pg_supports_checkoption} //= 1;
	}
	if ($self->{pg_version} >= 9.5) {
		$self->{pg_supports_named_operator} //= 1;
	}
	if ($self->{pg_version} >= 10) {
		$self->{pg_supports_partition} //= 1;
		$self->{pg_supports_identity} //= 1;
	}
	if ($self->{pg_version} >= 11) {
		$self->{pg_supports_procedure} //= 1;
	}
	if ($self->{pg_version} >= 12) {
		$self->{pg_supports_virtualcol} //= 1;
	}
	if ($self->{pg_version} >= 14) {
		$self->{pg_supports_outparam} //= 1;
	}
	if (!$self->{pg_supports_procedure}) {
		$self->{pg_supports_outparam} = 0;
	}

	# Other PostgreSQL fork compatibility
	# Redshift
	if ($self->{pg_supports_substr} eq '') {
		$self->{pg_supports_substr} //= 1;
	}

	$self->{pg_background} ||= 0;

	# Backward compatibility with LongTrunkOk with typo
	if ($self->{longtrunkok} && not defined $self->{longtruncok}) {
		$self->{longtruncok} = $self->{longtrunkok};
	}
	$self->{use_lob_locator} = 0 if ($self->{is_mssql});
	$self->{longtruncok} = 0 if (not defined $self->{longtruncok});
	# With lob locators LONGREADLEN must at least be 1MB
	if (!$self->{longreadlen} || $self->{use_lob_locator}) {
		$self->{longreadlen} = (1023*1024);
		$self->{longtruncok} = 1;
	}

	# Limit he number of row extracted from MSSQL
	$self->{select_top} ||= 0;
	$self->{select_top} = 0 if (!$self->{is_mssql});

	# Backward compatibility with PG_NUMERIC_TYPE alone
	$self->{pg_integer_type} = 1 if (not defined $self->{pg_integer_type});
	# Backward compatibility with CASE_SENSITIVE
	$self->{preserve_case} = $self->{case_sensitive} if (defined $self->{case_sensitive} && not defined $self->{preserve_case});
	$self->{schema} = uc($self->{schema}) if (!$self->{preserve_case} && ($self->{oracle_dsn} !~ /:mysql/i));
	# With MySQL override schema with the database name
	if ($self->{oracle_dsn} =~ /:mysql:.*database=([^;]+)/i)
	{
		if ($self->{schema} ne $1)
		{
			$self->{schema} = $1;
			#$self->logit("WARNING: setting SCHEMA to MySQL database name $1.\n", 0);
		}
		if (!$self->{schema}) {
			$self->logit("FATAL: cannot find a valid mysql database in DSN, $self->{oracle_dsn}.\n", 0, 1);
		}
	}

	# Force disabling USE_LOB_LOCATOR with WKT geometry export type,
	# ST_GeomFromText and SDO_UTIL.TO_WKTGEOMETRY functions return a
	# CLOB instead of a geometry object
	if ($self->{use_lob_locator} && uc($self->{geometry_extract_type}) eq 'WKT') {
		#$self->logit("WARNING: disabling USE_LOB_LOCATOR with WKT geometry export.\n", 0);
		$self->{use_lob_locator} = 0;
	}

	if (($self->{standard_conforming_strings} =~ /^off$/i) || ($self->{standard_conforming_strings} == 0)) {
		$self->{standard_conforming_strings} = 0;
	} else {
		$self->{standard_conforming_strings} = 1;
	}
	if (!defined $self->{compile_schema} || $self->{compile_schema}) {
		$self->{compile_schema} = 1;
	} else {
		$self->{compile_schema} = 0;
	}
	$self->{export_invalid} ||= 0;
	$self->{use_reserved_words} ||= 0;
	$self->{pkey_in_create} ||= 0;
	$self->{security} = ();
	# Should we add SET ON_ERROR_STOP to generated SQL files
	$self->{stop_on_error} = 1 if (not defined $self->{stop_on_error});
	# Force foreign keys to be created initialy deferred if export type
	# is TABLE or to set constraint deferred with data export types/
	$self->{defer_fkey} ||= 0;

	# How to export partition by reference (none, duplicate or number of hash)
	$self->{partition_by_reference} ||= 'none';

	# Allow multiple or chained extraction export type
	$self->{export_type} = ();
	if ($self->{type})
	{
		@{$self->{export_type}} = split(/[\s,;]+/, $self->{type});
		# Assume backward compatibility with DATA replacement by INSERT
		map { s/^DATA$/INSERT/; } @{$self->{export_type}};
	} else {
		@{$self->{export_type}} = ('TABLE');
	}

	# If you decide to autorewrite PLSQL code, this load the dedicated
	# Perl module
	$self->{plsql_pgsql} = 1 if ($self->{plsql_pgsql} eq '');
	$self->{plsql_pgsql} = 1 if ($self->{estimate_cost});
	if ($self->{plsql_pgsql}) {
		use Ora2Pg::PLSQL;
	}

	$self->{fhout} = undef;
	$self->{compress} = '';
	$self->{pkgcost} = 0;
	$self->{total_pkgcost} = 0;

	if ($^O =~ /MSWin32|dos/i)
	{
		if ( ($self->{oracle_copies} > 1) || ($self->{jobs} > 1) || ($self->{parallel_tables} > 1) )
		{
			$self->logit("WARNING: multiprocess is not supported under that kind of OS.\n", 0);
			$self->logit("If you need full speed at data export, please use Linux instead.\n", 0);
		}
		$self->{oracle_copies} = 0;
		$self->{jobs} = 0;
		$self->{parallel_tables} = 0;
	}
	if ($self->{parallel_tables} > 1) {
		$self->{file_per_table} = 1;
	}
	if ($self->{jobs} > 1) {
		$self->{file_per_function} = 1;
	}

	if ($self->{debug})
	{
		$self->logit("Ora2Pg version: $VERSION\n");
		$self->logit("Export type: $self->{type}\n", 1);
		$self->logit("Geometry export type: $self->{geometry_extract_type}\n", 1);
	}

	# Replace ; or space by comma in the audit user list
	$self->{audit_user} =~ s/[;\s]+/,/g;

	# TEST* action need PG_DSN to be set
	if ($self->{type} =~ /^TEST/ && !$self->{pg_dsn}) {
		$self->logit("FATAL: export type $self->{type} required PG_DSN to be set.\n", 0, 1);
	}

	# FOREIGN_SERVER and PARTITION_BY_REFERENCE set to duplicate is not possible
	if ($self->{partition_by_reference} eq 'duplicate' && $self->{fdw_server}) {
		$self->logit("FATAL: PARTITION_BY_REFERENCE set to duplicate with FDW_SERVER set is not supported.\n", 0, 1);
	}

	# Set the PostgreSQL connection information for data import or to
	# defined the dblink connection to use in autonomous transaction
	$self->set_pg_conn_details();

	# Set stdout encoding to UTF8 to avoid "Wide character in print" warning
	if ($^O !~ /MSWin32|dos/i) {
		binmode(STDOUT, "encoding(UTF-8)");
	}

	# Mark that we are exporting data using oracle_fdw
	$self->{oracle_fdw_data_export} = 0;
	if ($self->{fdw_server} && $self->{type} =~ /^(INSERT|COPY)$/) {
		$self->{oracle_fdw_data_export} = 1;
	}

	if (!$self->{input_file})
	{
		if ($self->{type} eq 'LOAD') {
			$self->logit("FATAL: with LOAD you must provide an input file\n", 0, 1);
		}
		if (!$self->{oracle_dsn} || ($self->{oracle_dsn} =~ /;sid=SIDNAME/)) {
			$self->logit("FATAL: you must set ORACLE_DSN in ora2pg.conf or use a DDL input file.\n", 0, 1);
		}
		# Connect the database
		if ($self->{oracle_dsn} =~ /dbi:mysql/i) {
			$self->{is_mysql} = 1;
		} elsif ($self->{oracle_dsn} =~ /dbi:ODBC:driver=msodbcsql/i) {
			$self->{is_mssql} = 1;
		}
		$self->{dbh} = $self->_db_connection();

		# Get the Oracle version
		$self->{db_version} = $self->_get_version();

		# Compile again all objects in the schema
		if (!$self->{is_mysql} && !$self->{is_mssql} && $self->{compile_schema}) {
			$self->_compile_schema(uc($self->{compile_schema}));
		}

		if (!grep(/^$self->{type}$/, 'COPY', 'INSERT', 'SEQUENCE', 'SEQUENCE_VALUES', 'GRANT', 'TABLESPACE', 'QUERY', 'SYNONYM', 'FDW', 'KETTLE', 'DBLINK', 'DIRECTORY') && $self->{type} !~ /SHOW_/)
		{
			if ($self->{plsql_pgsql} && !$self->{no_function_metadata})
			{
				my @done = ();
				if ($#{ $self->{look_forward_function} } >= 0)
				{
					foreach my $o (@{ $self->{look_forward_function} })
					{
						next if (grep(/^$o$/i, @done) || uc($o) eq uc($self->{schema}));
						push(@done, $o);
						if ($self->{type} eq 'VIEW')
						{
							# Limit to package lookup with VIEW export type
							$self->_get_package_function_list($o) if (!$self->{is_mysql} && !$self->{is_mssql});
						}
						else
						{
							# Extract all package/function/procedure meta information
							$self->_get_plsql_metadata($o);
						}
					}
				}
				if ($self->{type} eq 'VIEW')
				{
					# Limit to package lookup with WIEW export type
					$self->_get_package_function_list() if (!$self->{is_mysql} && !$self->{is_mssql});
				}
				else
				{
					# Extract all package/function/procedure meta information
					$self->_get_plsql_metadata();
				}
			}

			$self->{security} = $self->_get_security_definer($self->{type}) if (grep(/^$self->{type}$/, 'TRIGGER', 'FUNCTION','PROCEDURE','PACKAGE'));
		}
	}
	else
	{
		$self->{plsql_pgsql} = 1;

		$self->replace_tables(%{$self->{'replace_tables'}});
		$self->replace_cols(%{$self->{'replace_cols'}});

		if (grep(/^$self->{type}$/, 'TABLE', 'SEQUENCE', 'SEQUENCE_VALUES', 'GRANT', 'TABLESPACE', 'VIEW', 'TRIGGER', 'QUERY', 'FUNCTION','PROCEDURE','PACKAGE','TYPE','SYNONYM', 'DIRECTORY', 'DBLINK', 'LOAD', 'SCRIPT'))
		{
			if ($self->{type} eq 'LOAD')
			{
				if (!$self->{pg_dsn}) {
					$self->logit("FATAL: You must set PG_DSN to connect to PostgreSQL to be able to dispatch load over multiple connections.\n", 0, 1);
				} elsif ($self->{jobs} <= 1) {
					$self->logit("FATAL: You must set set -j (JOBS) > 1 to be able to dispatch load over multiple connections.\n", 0, 1);
				}
			}
			$self->export_schema();
		}
		else
		{
			$self->logit("FATAL: bad export type using input file option\n", 0, 1);
		}
		return;
	}

	# Register export structure modification
	if ($self->{type} =~ /^(INSERT|COPY|TABLE|TEST|TEST_DATA)$/)
	{
		for my $t (keys %{$self->{'modify_struct'}}) {
			$self->modify_struct($t, @{$self->{'modify_struct'}{$t}});
		}
		for my $t (keys %{$self->{'exclude_columns'}}) {
			$self->exclude_columns($t, @{$self->{'exclude_columns'}{$t}});
		}
		# Look for custom data type
		if ($self->{is_mssql}) {
			$self->logit("Looking for user defined data type of type FROM => DOMAIN...\n", 1);
			$self->_get_types();
		}
		if ($self->{type} eq 'TEST_DATA' && !$self->{schema}) {
			$self->logit("FATAL: the TEST_DATA action requires the SCHEMA directive to be set.\n", 0, 1);
		}
	}

	if ($self->{oracle_fdw_data_export} && scalar keys %{$self->{'modify_struct'}} > 0) {
		$self->logit("FATAL: MODIFY_STRUCT is not supported with oracle_fdw data export.\n", 0, 1);
	}
	if ($self->{oracle_fdw_data_export} && scalar keys %{$self->{'exclude_columns'}} > 0) {
		$self->logit("FATAL: EXCLUDE_COLUMNS is not supported with oracle_fdw data export.\n", 0, 1);
	}

	# backup output filename in multiple export mode
	$self->{output_origin} = '';
	if ($#{$self->{export_type}} > 0) {
		$self->{output_origin} = $self->{output};
	}

	# Retreive all export types information
        foreach my $t (@{$self->{export_type}})
	{
                $self->{type} = $t;

		if (($self->{type} eq 'TABLE') || ($self->{type} eq 'FDW') || ($self->{type} eq 'INSERT') || ($self->{type} eq 'COPY') || ($self->{type} eq 'KETTLE'))
		{
			$self->{plsql_pgsql} = 1;
			# Partitionned table do not accept NOT VALID constraint
			if ($self->{pg_supports_partition} && $self->{type} eq 'TABLE')
			{
				# Get the list of partition
				($self->{partitions}, $self->{partitions_default}) = $self->_get_partitions();
			}
			# Get table informations
			$self->_tables();
		}
		elsif ($self->{type} eq 'VIEW') {
			$self->_views();
		} elsif ($self->{type} eq 'SYNONYM') {
			$self->_synonyms();
		} elsif ($self->{type} eq 'GRANT') {
			$self->_grants();
		} elsif ($self->{type} eq 'SEQUENCE' || $self->{type} eq 'SEQUENCE_VALUES') {
			$self->_sequences();
		} elsif ($self->{type} eq 'TRIGGER') {
			$self->_triggers();
		} elsif ($self->{type} eq 'FUNCTION') {
			$self->_functions(); 
		} elsif ($self->{type} eq 'PROCEDURE') {
			$self->_procedures();
		} elsif ($self->{type} eq 'PACKAGE') {
			$self->_packages();
		} elsif ($self->{type} eq 'TYPE') {
			$self->_types();
		} elsif ($self->{type} eq 'TABLESPACE') {
			# Partitionned table do not accept NOT VALID constraint
			if ($self->{pg_supports_partition})
			{
				# Get the list of partition
				($self->{partitions}, $self->{partitions_default}) = $self->_get_partitions();
				($self->{subpartitions}, $self->{subpartitions_default}) = $self->_get_subpartitions();
			}
			$self->_tablespaces();
		} elsif ($self->{type} eq 'PARTITION') {
			$self->_partitions();
		} elsif ($self->{type} eq 'DBLINK') {
			$self->_dblinks();
		} elsif ($self->{type} eq 'DIRECTORY') {
			$self->_directories();
		} elsif ($self->{type} eq 'MVIEW') {
			$self->_materialized_views();
		} elsif ($self->{type} eq 'QUERY') {
			$self->_queries();
		} elsif ( ($self->{type} eq 'SHOW_REPORT') || ($self->{type} eq 'SHOW_VERSION')
				|| ($self->{type} eq 'SHOW_SCHEMA') || ($self->{type} eq 'SHOW_TABLE')
				|| ($self->{type} eq 'SHOW_COLUMN') || ($self->{type} eq 'SHOW_ENCODING'))
		{
			$self->_show_infos($self->{type});
			$self->{dbh}->disconnect() if ($self->{dbh}); 
			exit 0;
		}
		elsif ($self->{type} eq 'TEST')
		{
			$self->replace_tables(%{$self->{'replace_tables'}});
			$self->replace_cols(%{$self->{'replace_cols'}});
			$self->{dbhdest} = $self->_send_to_pgdb() if ($self->{pg_dsn} && !$self->{dbhdest});
			# Check if all tables have the same number of indexes, constraints, etc.
			$self->_test_table();
			# Count each object at both sides
			foreach my $o ('VIEW', 'MVIEW', 'SEQUENCE', 'TYPE', 'FDW')
			{
				next if ($self->{is_mysql} && grep(/^$o$/, 'MVIEW','TYPE','FDW'));
				next if ($self->{is_mssql} && grep(/^$o$/, 'FDW'));
				$self->_count_object($o);
			}
			# count function/procedure/package function
			$self->_test_function();
			# compare sequences values except for mysql as we don't know how to get
			# the correspondance with mysql auto_increment and PG sequences
			if (!$self->{is_mysql}) {
				$self->_test_seq_values();
			}
			# Count row in each table
			if ($self->{count_rows}) {
				$self->_table_row_count();
			}
			$self->{dbhdest}->disconnect() if ($self->{dbhdest}); 
			$self->{dbh}->disconnect() if ($self->{dbh}); 
			exit 0;
		}
		elsif ($self->{type} eq 'TEST_COUNT')
		{
			$self->replace_tables(%{$self->{'replace_tables'}});
			$self->replace_cols(%{$self->{'replace_cols'}});
			$self->{dbhdest} = $self->_send_to_pgdb() if ($self->{pg_dsn} && !$self->{dbhdest});
			# Count row in each table
			$self->_table_row_count();
			$self->{dbhdest}->disconnect() if ($self->{dbhdest}); 
			$self->{dbh}->disconnect() if ($self->{dbh}); 
			exit 0;
		}
		elsif ($self->{type} eq 'TEST_VIEW')
		{
			$self->{dbhdest} = $self->_send_to_pgdb() if ($self->{pg_dsn} && !$self->{dbhdest});
			$self->_unitary_test_views();
			$self->{dbhdest}->disconnect() if ($self->{dbhdest});
			$self->{dbh}->disconnect() if ($self->{dbh}); 
			exit 0;
		}
		elsif ($self->{type} eq 'TEST_DATA')
		{
			$self->replace_tables(%{$self->{'replace_tables'}});
			$self->replace_cols(%{$self->{'replace_cols'}});
			$self->set_where_clause($self->{'global_where'}, %{$self->{'where'}});
			# Create a connection to PostgreSQL
			$self->{dbhdest} = $self->_send_to_pgdb() if (!$self->{dbhdest});
			if ($self->{fdw_server})
			{
				# Create the oracle_fdw extension and the foreign server
				$self->_create_foreign_server();
				# Import the foreign tables following ALLOW or EXCLUDE
				$self->_import_foreign_schema() if ($self->{drop_foreign_schema});
			}
			else
			{
				$self->_tables();
			}
			if (!$self->{is_mysql} && !$self->{is_mssql})
			{
				# Check for DBMS EXECUTE privilege
				$self->{has_dbms_log_execute_privilege} = Ora2Pg::Oracle::_has_dbms_log_execute_privilege($self);
			}
			# Check that data are the same.
			$self->_data_validation();
			$self->{dbhdest}->disconnect() if ($self->{dbhdest});
			$self->{dbh}->disconnect() if ($self->{dbh});
			exit 0;
		}
		else
		{
			warn "type option must be (TABLE, VIEW, GRANT, SEQUENCE, SEQUENCE_VALUES, TRIGGER, PACKAGE, FUNCTION, PROCEDURE, PARTITION, TYPE, INSERT, COPY, TABLESPACE, SHOW_REPORT, SHOW_VERSION, SHOW_SCHEMA, SHOW_TABLE, SHOW_COLUMN, SHOW_ENCODING, FDW, MVIEW, QUERY, KETTLE, DBLINK, SYNONYM, DIRECTORY, LOAD, TEST, TEST_COUNT, TEST_VIEW, TEST_DATA), unknown $self->{type}\n";
		}
		$self->replace_tables(%{$self->{'replace_tables'}});
		$self->replace_cols(%{$self->{'replace_cols'}});
		$self->set_where_clause($self->{'global_where'}, %{$self->{'where'}});
		$self->set_delete_clause($self->{'global_delete'}, %{$self->{'delete'}});
	}

	if ( ($self->{type} eq 'INSERT') || ($self->{type} eq 'COPY') || ($self->{type} eq 'KETTLE') )
	{
		if ( ($self->{type} eq 'KETTLE') && !$self->{pg_dsn} )
		{
			$self->logit("FATAL: PostgreSQL connection datasource must be defined with KETTLE export.\n", 0, 1);
		}
		elsif ($self->{type} ne 'KETTLE')
		{
			if ($self->{defer_fkey} && $self->{pg_dsn}) {
				$self->logit("FATAL: DEFER_FKEY can not be used with direct import to PostgreSQL, check use of DROP_FKEY instead.\n", 0, 1);
			}
			if ($self->{datadiff} && $self->{pg_dsn}) {
				$self->logit("FATAL: DATADIFF can not be used with direct import to PostgreSQL because direct import may load data in several transactions.\n", 0, 1);
			}
			if ($self->{datadiff} && !$self->{pg_supports_lateral}) {
				$self->logit("FATAL: DATADIFF requires LATERAL support (Pg version 9.3 and above; see config parameter PG_SUPPORTS_LATERAL)\n", 0, 1);
			}
			$self->{dbhdest} = $self->_send_to_pgdb() if ($self->{pg_dsn} && !$self->{dbhdest});
			# In case we will use oracle_fdw creates the foreign tables
			if ($self->{fdw_server} && $self->{pg_dsn})
			{
				# Create the oracle_fdw extension en the foreign server
				$self->_create_foreign_server();
			}
		}
	}

	# Disconnect from the database
	$self->{dbh}->disconnect() if ($self->{dbh});
}

sub _select_output_file_suffix
{
  my ($self, $extension) = @_;
  # If an output file template is defined
  if ($self->{dump_as_file_prefix})
  {
    $self->{fhlog} = undef;
    $self->{fhlog} = new IO::File;
    $self->{fhlog}->open(">>$self->{dump_as_file_prefix}.$extension") or $self->logit("FATAL: can't log to $self->{dump_as_file_prefix}.$extension, $!\n", 0, 1);
    if ($self->{debug}){
        print STDERR  "Saving report to $self->{dump_as_file_prefix}.$extension\n";
    }
  }
}

# use to set encoding
sub _init_environment
{
	my ($self) = @_;

	# Set default Oracle client encoding
	if (!$self->{nls_lang})
	{
		if ($self->{is_mysql}) {
			$self->{nls_lang} = 'utf8';
		} elsif ($self->{is_mssql}) {
			$self->{nls_lang} = 'iso_1';
			$self->{client_encoding} = 'LATIN1' if (!$self->{client_encoding});
		} else {
			$self->{nls_lang} = 'AMERICAN_AMERICA.AL32UTF8';
		}
	}
	if (!$self->{nls_nchar})
	{
		if ($self->{is_mysql}) {
			$self->{nls_nchar} = 'utf8_general_ci';
		} elsif ($self->{is_mssql}) {
			$self->{nls_nchar} = 'SQL_Latin1_General_CP1_CI_AS';
		} else {
			$self->{nls_nchar} = 'AL32UTF8';
		}
	}
	$ENV{NLS_LANG} = $self->{nls_lang};
	$ENV{NLS_NCHAR} = $self->{nls_nchar};

	# Force Perl to use utf8 I/O encoding by default or the
	# encoding given in the BINMODE configuration directive.
	# See http://perldoc.perl.org/5.14.2/open.html for values
	# that can be used. Default is :utf8
	$self->set_binmode();

	# Set default PostgreSQL client encoding to UTF8
	if (!$self->{client_encoding} || $self->{nls_lang} =~ /UTF8/i) {
		$self->{client_encoding} = 'UTF8';
	}
}

sub set_binmode
{
	my $self = shift;

        my ($package, $filename, $line) = caller;

        if ( !$self->{input_file} && (!$self->{'binmode'} || $self->{nls_lang} =~ /UTF8/i) ) {
                use open ':utf8';
        } elsif ($self->{'binmode'} =~ /^:/) {
                eval "use open '$self->{'binmode'}';";
		die "FATAL: can't use open layer $self->{'binmode'}\n" if ($@);
	} elsif ($self->{'binmode'} and $self->{'binmode'} ne 'raw' and $self->{'binmode'} ne 'locale') {
                eval "use open ':encoding($self->{'binmode'})';";
		die "FATAL: can't use open layer :encoding($self->{'binmode'})\n" if ($@);
        }
        # Set default PostgreSQL client encoding to UTF8
        if (!$self->{client_encoding} || ($self->{nls_lang} =~ /UTF8/ && !$self->{input_file}) ) {
                $self->{client_encoding} = 'UTF8';
        }

	if ($#_ == 0)
	{
		my $enc = $self->{'binmode'} || 'utf8';
		$enc =~ s/^://;
		if ($self->{'binmode'} eq 'raw' or $self->{'binmode'} eq 'locale') {
			binmode($_[0], ":$enc");
		} else {
			binmode($_[0], ":encoding($enc)");
		}
	}

}

sub _is_utf8_file
{

	my $file = shift();

	my $utf8 = 0;
	if (open(my $f, '<', $file)) {
		local $/;
		my $data = <$f>;
		close($f);
		if (utf8::decode($data)) {
			$utf8 = 1
		}
	}

	return $utf8;
}

# We provide a DESTROY method so that the autoloader doesn't
# bother trying to find it. We also close the DB connexion
sub DESTROY
{
	my $self = shift;

	#$self->{dbh}->disconnect() if ($self->{dbh});

}


sub set_pg_conn_details
{
	my $self = shift;

	# Init connection details with configuration options
	$self->{pg_dsn} ||= '';
	
	$self->{pg_dsn} =~ /dbname=([^;]*)/;
	$self->{dbname} = $1 || 'testdb';
	$self->{pg_dsn} =~ /host=([^;]*)/;
	$self->{dbhost} = $1 || 'localhost';
	$self->{pg_dsn} =~ /port=([^;]*)/;
	$self->{dbport} = $1 || 5432;
	$self->{dbuser} = $self->{pg_user} || 'pguser';
	$self->{dbpwd} = $self->{pg_pwd} || 'pgpwd';

	if (!$self->{dblink_conn}) {
		#$self->{dblink_conn} = "port=$self->{dbport} dbname=$self->{dbname} host=$self->{dbhost} user=$self->{dbuser} password=$self->{dbpwd}";
		# Use a more generic connection string, the password must be
		# set in .pgpass. Default is to use unix socket to connect.
		$self->{dblink_conn} = "format('port=%s dbname=%s user=%s', current_setting('port'), current_database(), current_user)";
	}
}


=head2 _send_to_pgdb

Open a DB handle to a PostgreSQL database

=cut

sub _send_to_pgdb
{
	my ($self) = @_;

	eval("use DBD::Pg qw(:pg_types);");

	return if ($self->{oracle_speed});
 
	if (!defined $self->{pg_pwd})
	{
		eval("use Term::ReadKey;");
		if (!$@) {
			$self->{pg_user} = $self->_ask_username('PostgreSQL') unless (defined($self->{pg_user}));
			$self->{pg_pwd} = $self->_ask_password('PostgreSQL');
		}
	}
	# Read the password from file each time if the file exists.
	if (-e $self->{pg_pwd})
	{
		open(FH, '<', $self->{pg_pwd}) or $self->logit("FATAL: can't read PG password file: $self->{pg_pwd, $!}\n", 0, 1);
		$self->{pg_pwd} = <FH>;
		chomp($self->{pg_pwd});
		close(FH);
	}

	$ENV{PGAPPNAME} = 'ora2pg ' || $VERSION;

	# Connect the destination database
	my $dbhdest = DBI->connect($self->{pg_dsn}, $self->{pg_user}, $self->{pg_pwd}, {AutoInactiveDestroy => 1, PrintError => 0});

	# Check for connection failure
	if (!$dbhdest) {
		$self->logit("FATAL: $DBI::err ... $DBI::errstr\n", 0, 1);
	}

	# Force execution of initial command
	$self->_pg_initial_command($dbhdest);

	return $dbhdest;
}

=head2 _grants

This function is used to retrieve all privilege information.

It extracts all Oracle's ROLES to convert them to Postgres groups (or roles)
and searches all users associated to these roles.

=cut

sub _grants
{
	my ($self) = @_;

	$self->logit("Retrieving users/roles/grants information...\n", 1);
	($self->{grants}, $self->{roles}) = $self->_get_privilege();
}


=head2 _sequences

This function is used to retrieve all sequences information.

=cut

sub _sequences
{
	my ($self) = @_;

	$self->logit("Retrieving sequences information...\n", 1);
	$self->{sequences} = $self->_get_sequences();
}


=head2 _triggers

This function is used to retrieve all triggers information.

=cut

sub _triggers
{
	my ($self) = @_;

	$self->logit("Retrieving triggers information...\n", 1);
	$self->{triggers} = $self->_get_triggers();
}


=head2 _functions

This function is used to retrieve all functions information.

=cut

sub _functions
{
	my $self = shift;

	$self->logit("Retrieving functions information...\n", 1);

	$self->{functions} = $self->_get_functions();

}

sub start_function_json_config
{
	my ($self, $type) = @_;

	return if (!$self->{json_test});

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});

	unlink("${dirprefix}$type.json");

	$self->{oracle_dsn} =~ /host=([^;]+)/;
	my $ora_host = $1 || 'localhost';
	$self->{oracle_dsn} =~ /port=(\d+)/;
	my $ora_port = $1 || ((!$self->{is_mysql}) ? 1521 : 3306);
	my $sid = '';
	if (!$self->{is_mysql}) {
		$self->{oracle_dsn} =~ /(service_name|sid)=([^;]+)/;
		$sid = $2 || '';
	} else {
		$self->{oracle_dsn} =~ /(database)=([^;]+)/;
		$sid = $2 || '';
	}

	my $pg_host = 'localhost';
	if ($self->{pg_dsn} =~ /host=([^;]+)/) {
		$pg_host = $1;
	}
	my $pg_port = 5432;
	if ($self->{pg_dsn} =~ /port=(\d+)/) {
		$pg_port = $1;
	}
	my $pg_db = '';
	if ($self->{pg_dsn} =~ /dbname=([^;]+)/) {
		$pg_db = $1;
	}

	my $tfh = $self->append_export_file($dirprefix . "$type.json", 1);
	flock($tfh, 2) || die "FATAL: can't lock file ${dirprefix}$type.json\n";
	$tfh->print(qq/{
  "oraConfig": {
    "dsn": "$self->{oracle_dsn}",
    "host": "$ora_host",
    "port": $ora_port,
    "user": "$self->{oracle_user}",
    "password": "$self->{oracle_pwd}",
    "service_name": "$sid",
    "schema": "$self->{schema}"
  },
  "pgConfig": {
    "dsn": "$self->{pg_dsn}",
    "host": "$pg_host",
    "port": $pg_port,
    "user": "$self->{pg_user}",
    "password": "$self->{pg_pwd}",
    "dbname": "$pg_db",
    "schema": "$self->{pg_schema}"
  },
  "procfuncConfig": [
/);
	$self->close_export_file($tfh, 1);
}

sub end_function_json_config
{
	my ($self, $type) = @_;

	return if (!$self->{json_test});

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});

	my $tfh = $self->append_export_file($dirprefix . "$type.json", 1);
	flock($tfh, 2) || die "FATAL: can't lock file ${dirprefix}$type.json\n";
	# Add an empty json entry at end
	$tfh->print(qq/    {
      "routine_type": "",
      "ora": {
        "routine_name": "",
        "return_type": "",
        "args_list": [
          {
            "0": [
              {
                "name": "",
                "mode": "",
                "type": "",
                "default": "",
                "value": ""
              }
              ]
          } ]
      },
      "pg": {
        "routine_name": "",
        "return_type": "",
        "args_list": [
          {
            "0": [
              {
                "name": "",
                "mode": "",
                "type": "",
                "default": "",
                "value": ""
              }
              ]
          } ]
      }
    }
/);

	# terminate the json document
	$tfh->print(qq/  ]
}
/);
	$self->close_export_file($tfh, 1);
}

=head2 _procedures

This function is used to retrieve all procedures information.

=cut

sub _procedures
{
	my $self = shift;

	$self->logit("Retrieving procedures information...\n", 1);

	$self->{procedures} = $self->_get_procedures();

}


=head2 _packages

This function is used to retrieve all packages information.

=cut

sub _packages
{
	my ($self) = @_;

	if ($self->{is_mysql} or $self->{is_mssql}) {
		$self->logit("Action type PACKAGES is not available for $self->{sgbd_name}.\n", 0, 1);
	}
	$self->logit("Retrieving packages information...\n", 1);
	$self->{packages} = $self->_get_packages();

}


=head2 _types

This function is used to retrieve all custom types information.

=cut

sub _types
{
	my ($self) = @_;

	$self->logit("Retrieving user defined types information...\n", 1);
	$self->{types} = $self->_get_types();

}

=head2 _tables

This function is used to retrieve all table information.

Sets the main hash of the database structure $self->{tables}.
Keys are the names of all tables retrieved from the current
database. Each table information is composed of an array associated
to the table_info key as array reference. In other way:

    $self->{tables}{$class_name}{table_info} = [(OWNER,TYPE,COMMENT,NUMROW)];

DBI TYPE can be TABLE, VIEW, SYSTEM TABLE, GLOBAL TEMPORARY, LOCAL TEMPORARY,
ALIAS, SYNONYM or a data source specific type identifier. This only extracts
the TABLE type.

It also calls these other private subroutines to affect the main hash
of the database structure :

    @{$self->{tables}{$class_name}{column_info}} = $self->_column_info($class_name, $owner, 'TABLE');
    %{$self->{tables}{$class_name}{unique_key}}  = $self->_unique_key($class_name, $owner);
    @{$self->{tables}{$class_name}{foreign_key}} = $self->_foreign_key($class_name, $owner);
    %{$self->{tables}{$class_name}{check_constraint}}  = $self->_check_constraint($class_name, $owner);

=cut

sub sort_view_by_iter
{

	if (exists $ordered_views{$a}{iter} || exists $ordered_views{$b}{iter}) {
		return $ordered_views{$a}{iter} <=> $ordered_views{$b}{iter};
	} else {
		return $a cmp $b;
	}
}

sub _tables
{
	my ($self, $nodetail) = @_;

	if ($self->{is_mssql} && $self->{type} eq 'TABLE')
	{
		$self->logit("Retrieving table partitioning information...\n", 1);
		%{ $self->{partitions_list} } = $self->_get_partitioned_table();
	}

	# Get all tables information specified by the DBI method table_info
	$self->logit("Retrieving table information...\n", 1);

	# Retrieve tables informations
	my %tables_infos = $self->_table_info($self->{count_rows});

	# Retrieve column identity information
	if ($self->{type} ne 'FDW')
	{
		%{ $self->{identity_info} } = $self->_get_identities();
	}

	if (scalar keys %tables_infos > 0)
	{
		if ( grep(/^$self->{type}$/, 'TABLE','SHOW_REPORT','COPY','INSERT')
				&& !$self->{skip_indices} && !$self->{skip_indexes})
		{
			$self->logit("Retrieving index information...\n", 1);
			my $autogen = 0;
			$autogen = 1 if (grep(/^$self->{type}$/, 'COPY','INSERT'));
			my ($uniqueness, $indexes, $idx_type, $idx_tbsp) = $self->_get_indexes('',$self->{schema}, $autogen);
			foreach my $tb (keys %{$indexes})
			{
				next if (!exists $tables_infos{$tb});
				%{$self->{tables}{$tb}{indexes}} = %{$indexes->{$tb}};
			}
			foreach my $tb (keys %{$idx_type}) {
				next if (!exists $tables_infos{$tb});
				%{$self->{tables}{$tb}{idx_type}} = %{$idx_type->{$tb}};
			}
			foreach my $tb (keys %{$idx_tbsp}) {
				next if (!exists $tables_infos{$tb});
				%{$self->{tables}{$tb}{idx_tbsp}} = %{$idx_tbsp->{$tb}};
			}
			foreach my $tb (keys %{$uniqueness}) {
				next if (!exists $tables_infos{$tb});
				%{$self->{tables}{$tb}{uniqueness}} = %{$uniqueness->{$tb}};
			}
		}

		# Get detailed informations on each tables
		if (!$nodetail)
		{
			$self->logit("Retrieving columns information...\n", 1);
			# Retrieve all column's details
			my %columns_infos = $self->_column_info('',$self->{schema}, 'TABLE');
			foreach my $tb (keys %columns_infos)
			{
				next if (!exists $tables_infos{$tb});
				foreach my $c (keys %{$columns_infos{$tb}}) {
					push(@{$self->{tables}{$tb}{column_info}{$c}}, @{$columns_infos{$tb}{$c}});
				}
			}
			%columns_infos = ();

			# Retrieve comment of each columns and FK information if not foreign table export
			if ($self->{type} ne 'FDW' and (!$self->{oracle_fdw_data_export} || $self->{drop_fkey} || $self->{drop_indexes}))
			{
				if ($self->{type} eq 'TABLE')
				{
					$self->logit("Retrieving comments information...\n", 1);
					my %columns_comments = $self->_column_comments();
					foreach my $tb (keys %columns_comments)
					{
						next if (!exists $tables_infos{$tb});
						foreach my $c (keys %{$columns_comments{$tb}}) {
							$self->{tables}{$tb}{column_comments}{$c} = $columns_comments{$tb}{$c};
						}
					}
				}

				# Extract foreign keys informations
				if (!$self->{skip_fkeys})
				{
					$self->logit("Retrieving foreign keys information...\n", 1);
					my ($foreign_link, $foreign_key) = $self->_foreign_key('',$self->{schema});
					foreach my $tb (keys %{$foreign_link}) {
						next if (!exists $tables_infos{$tb});
						%{$self->{tables}{$tb}{foreign_link}} =  %{$foreign_link->{$tb}};
					}
					foreach my $tb (keys %{$foreign_key}) {
						next if (!exists $tables_infos{$tb});
						push(@{$self->{tables}{$tb}{foreign_key}}, @{$foreign_key->{$tb}});
					}
				}
			}
		}

		# Retrieve unique keys and check constraint information if not FDW export
		if ($self->{type} ne 'FDW' and !$self->{oracle_fdw_data_export})
		{
			$self->logit("Retrieving unique keys information...\n", 1);
			my %unique_keys = $self->_unique_key('',$self->{schema});
			foreach my $tb (keys %unique_keys)
			{
				next if (!exists $tables_infos{$tb});
				foreach my $c (keys %{$unique_keys{$tb}}) {
					$self->{tables}{$tb}{unique_key}{$c} = $unique_keys{$tb}{$c};
				}
			}
			%unique_keys = ();

			if (!$self->{skip_checks})
			{
				$self->logit("Retrieving check constraints information...\n", 1);
				my %check_constraints = $self->_check_constraint('',$self->{schema});
				foreach my $tb (keys %check_constraints) {
					next if (!exists $tables_infos{$tb});
					%{$self->{tables}{$tb}{check_constraint}} = ( %{$check_constraints{$tb}});
				}
			}

		}
	}

	my @done = ();
	my $id = 0;
	# Set the table information for each class found
	my $i = 1;
	my $num_total_table = scalar keys %tables_infos;
	my $count_table = 0;
	my $PGBAR_REFRESH = set_refresh_count($num_total_table);
	foreach my $t (sort keys %tables_infos)
	{
		if (!$self->{quiet} && !$self->{debug} && ($count_table % $PGBAR_REFRESH) == 0)
		{
			print STDERR $self->progress_bar($i, $num_total_table, 25, '=', 'tables', "scanning table $t" ), "\r";
		}
		$count_table++;

		if (grep(/^\Q$t\E$/, @done)) {
			$self->logit("Duplicate entry found: $t\n", 1);
		} else {
			push(@done, $t);
		} 
		$self->logit("[$i] Scanning table $t ($tables_infos{$t}{num_rows} rows)...\n", 1);
		
		# Check of uniqueness of the table
		if (exists $self->{tables}{$t}{field_name}) {
			$self->logit("Warning duplicate table $t, maybe a SYNONYM ? Skipped.\n", 1);
			next;
		}
		# Try to respect order specified in the TABLES limited extraction array
		if ($#{$self->{limited}{TABLE}} > 0)
		{
			$self->{tables}{$t}{internal_id} = 0;
			for (my $j = 0; $j <= $#{$self->{limited}{TABLE}}; $j++)
			{
				if (uc($self->{limited}{TABLE}->[$j]) eq uc($t))
				{
					$self->{tables}{$t}{internal_id} = $j;
					last;
				}
			}
		}

		# usually TYPE,COMMENT,NUMROW,...
		$self->{tables}{$t}{table_info}{type} = $tables_infos{$t}{type};
		$self->{tables}{$t}{table_info}{comment} = $tables_infos{$t}{comment};
		$self->{tables}{$t}{table_info}{num_rows} = $tables_infos{$t}{num_rows};
		$self->{tables}{$t}{table_info}{owner} = $tables_infos{$t}{owner};
		$self->{tables}{$t}{table_info}{tablespace} = $tables_infos{$t}{tablespace};
		$self->{tables}{$t}{table_info}{nested} = $tables_infos{$t}{nested};
		$self->{tables}{$t}{table_info}{size} = $tables_infos{$t}{size};
		$self->{tables}{$t}{table_info}{auto_increment} = $tables_infos{$t}{auto_increment};
		$self->{tables}{$t}{table_info}{connection} = $tables_infos{$t}{connection};
		$self->{tables}{$t}{table_info}{nologging} = $tables_infos{$t}{nologging};
		$self->{tables}{$t}{table_info}{partitioned} = $tables_infos{$t}{partitioned};
		$self->{tables}{$t}{table_info}{temporary} = $tables_infos{$t}{temporary};
		$self->{tables}{$t}{table_info}{duration} = $tables_infos{$t}{duration};
		$self->{tables}{$t}{table_info}{index_type} = $tables_infos{$t}{index_type};
		if (exists $tables_infos{$t}{fillfactor}) {
		    $self->{tables}{$t}{table_info}{fillfactor} = $tables_infos{$t}{fillfactor};
		}

		# Set the fields information
		if ($self->{type} ne 'SHOW_REPORT')
		{
			my $tmp_tbname = $t;
			if ($self->{is_mysql})
			{
				if ( $t !~ /\./ && $tables_infos{$t}{owner}) {
					$tmp_tbname = "\`$tables_infos{$t}{owner}\`.\`$t\`";
				} else {
					# in case we already have the schema name, add doublequote
					$tmp_tbname =~ s/\./\`.\`/;
					$tmp_tbname = "\`$tmp_tbname\`";
				}
			}
			elsif ($self->{is_mssql})
			{
				if ( $t !~ /\./ && $tables_infos{$t}{owner}) {
					$tmp_tbname = "[$tables_infos{$t}{owner}].[$t]";
				} else {
					# in case we already have the schema name, add doublequote
					$tmp_tbname =~ s/\./\].\[/;
					$tmp_tbname = "[$tmp_tbname]";
				}
			}
			else
			{
				if ( $t !~ /\./ ) {
					$tmp_tbname = "\"$tables_infos{$t}{owner}\".\"$t\"";
				} else {
					# in case we already have the schema name, add doublequote
					$tmp_tbname =~ s/\./"."/;
					$tmp_tbname = "\"$tmp_tbname\"";
				}
			}

			foreach my $k (sort {$self->{tables}{$t}{column_info}{$a}[11] <=> $self->{tables}{$t}{column_info}{$b}[11]} keys %{$self->{tables}{$t}{column_info}})
			{
				$self->{tables}{$t}{type} = 'table';
				push(@{$self->{tables}{$t}{field_name}}, $self->{tables}{$t}{column_info}{$k}[0]);
				push(@{$self->{tables}{$t}{field_type}}, $self->{tables}{$t}{column_info}{$k}[1]);
			}
		}
		$i++;
	}

	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($i - 1, $num_total_table, 25, '=', 'tables', 'end of scanning.'), "\n";
	}
 
	####	
	# Get views definition if it must be exported as table
	####	
	if ($#{$self->{view_as_table}} >= 0)
	{
		my %view_infos = $self->_get_views();
		my @exanped_views = ();
		foreach my $view (sort keys %view_infos)
		{
			foreach my $pattern (@{$self->{view_as_table}}) {
				push(@exanped_views, $view) if ($view =~ /^$pattern$/i);
			}
		}

		# Retrieve comment of each columns
		my %columns_comments = $self->_column_comments();
		foreach my $view (keys %columns_comments)
		{
			next if (!exists $view_infos{$view});
			next if (!grep(/^$view$/i, @exanped_views));
			foreach my $c (keys %{$columns_comments{$view}}) {
				$self->{tables}{$view}{column_comments}{$c} = $columns_comments{$view}{$c};
			}
		}

		foreach my $view (sort keys %view_infos)
		{
			# Set the table information for each class found
			# Jump to desired extraction
			next if (!grep(/^$view$/i, @exanped_views));
			$self->logit("Scanning view $view to export as table...\n", 0);
			$self->{tables}{$view}{type} = 'view';
			$self->{tables}{$view}{text} = $view_infos{$view}{text};
			$self->{tables}{$view}{owner} = $view_infos{$view}{owner};
			$self->{tables}{$view}{iter} = $view_infos{$view}{iter} if (exists $view_infos{$view}{iter});
			$self->{tables}{$view}{alias}= $view_infos{$view}{alias};
			$self->{tables}{$view}{comment} = $view_infos{$view}{comment};
			my $realview = $view;
			$realview =~ s/"//g;
			if (!$self->{is_mysql})
			{
				if ($realview !~ /\./) {
					$realview = "\"$self->{tables}{$view}{owner}\".\"$realview\"";
				} else {
					$realview =~ s/\./"."/;
					$realview = "\"$realview\"";
				}
			}
			# Set the fields information
			my $sth = $self->{dbh}->prepare("SELECT * FROM $realview WHERE 1=0");
			if (!defined($sth))
			{
				warn "Can't prepare statement: $DBI::errstr";
				next;
			}
			$sth->execute;
			if ($sth->err)
			{
				warn "Can't execute statement: $DBI::errstr";
				next;
			}
			$self->{tables}{$view}{field_name} = $sth->{NAME};
			$self->{tables}{$view}{field_type} = $sth->{TYPE};
			my %columns_infos = $self->_column_info($view, $self->{schema}, 'VIEW', @exanped_views);
			foreach my $tb (keys %columns_infos)
			{
				next if ($tb ne $view);
				foreach my $c (keys %{$columns_infos{$tb}}) {
					push(@{$self->{tables}{$view}{column_info}{$c}}, @{$columns_infos{$tb}{$c}});
				}
			}
		}
	}

	####	
	# Get materialized views definition if it must be exported as table
	####	
	if ($#{$self->{mview_as_table}} >= 0)
	{
		my %view_infos = $self->_get_materialized_views();
		my @exanped_views = ();
		foreach my $view (sort keys %view_infos)
		{
			foreach my $pattern (@{$self->{mview_as_table}}) {
				push(@exanped_views, $view) if ($view =~ /^$pattern$/i);
			}
		}

		foreach my $view (sort keys %view_infos)
		{
			# Set the table information for each class found
			# Jump to desired extraction
			next if (!grep(/^$view$/i, @exanped_views));
			$self->logit("Scanning materialized view $view to export as table...\n", 0);
			if (exists $self->{tables}{$view})
			{
				$self->logit("WARNING: cannot export materialized view $view as table, a table with same name already exists...\n", 0);
				next;
			}
			$self->{tables}{$view}{type} = 'mview';
			$self->{tables}{$view}{text} = $view_infos{$view}{text};
			$self->{tables}{$view}{owner} = $view_infos{$view}{owner};
			my $realview = $view;
			$realview =~ s/"//g;
			if (!$self->{is_mysql})
			{
				if ($realview !~ /\./) {
					$realview = "\"$self->{tables}{$view}{owner}\".\"$realview\"";
				} else {
					$realview =~ s/\./"."/;
					$realview = "\"$realview\"";
				}
			}
			# Set the fields information
			my $sth = $self->{dbh}->prepare("SELECT * FROM $realview WHERE 1=0");
			if (!defined($sth))
			{
				warn "Can't prepare statement: $DBI::errstr";
				next;
			}
			$sth->execute;
			if ($sth->err)
			{
				warn "Can't execute statement: $DBI::errstr";
				next;
			}
			$self->{tables}{$view}{field_name} = $sth->{NAME};
			$self->{tables}{$view}{field_type} = $sth->{TYPE};
			my %columns_infos = $self->_column_info($view, $self->{schema}, 'MVIEW', @exanped_views);
			foreach my $tb (keys %columns_infos)
			{
				next if ($tb ne $view);
				foreach my $c (keys %{$columns_infos{$tb}}) {
					push(@{$self->{tables}{$view}{column_info}{$c}}, @{$columns_infos{$tb}{$c}});
				}
			}
		}
	}

	# Look at external tables
	if (!$self->{is_mysql} && ($self->{db_version} !~ /Release 8/)) {
		%{$self->{external_table}} = $self->_get_external_tables();
	}

	if (!$self->{is_mssql} && $self->{type} eq 'TABLE')
	{
		$self->logit("Retrieving table partitioning information...\n", 1);
		%{ $self->{partitions_list} } = $self->_get_partitioned_table();
		%{ $self->{subpartitions_list} } = $self->_get_subpartitioned_table();
	}
}

sub _get_plsql_code
{
	my $str = shift();

	my $ct = '';
	my @parts = split(/\b(BEGIN|DECLARE|END\s*(?!IF|LOOP|CASE|INTO|FROM|,|\))[^;\s]*\s*;)/i, $str);
	my $code = '';
	my $other = '';
	my $i = 0;
	for (; $i <= $#parts; $i++)
	{
		$ct++ if ($parts[$i] =~ /\bBEGIN\b/i);
		$ct-- if ($parts[$i] =~ /\bEND\s*(?!IF|LOOP|CASE|INTO|FROM|,|\))[^;\s]*\s*;/i);
		if ( ($ct ne '') && ($ct == 0) ) {
			$code .= $parts[$i];
			last;
		}
		$code .= $parts[$i];
	}
	$i++;
	for (; $i <= $#parts; $i++) {
		$other .= $parts[$i];
	}

	return ($code, $other);
}

sub _parse_constraint
{
	my ($self, $tb_name, $cur_col_name, $c) = @_;
	if ($c =~ /^([^\s]+)\s+(UNIQUE|PRIMARY KEY)\s*\(([^\)]+)\)/is)
	{
		my $tp = 'U';
		$tp = 'P' if ($2 eq 'PRIMARY KEY');
		$self->{tables}{$tb_name}{unique_key}{$1} = { (
			type => $tp, 'generated' => 0, 'index_name' => $1,
			columns => ()
		) };
		push(@{$self->{tables}{$tb_name}{unique_key}{$1}{columns}}, split(/\s*,\s*/, $3));
	}
	elsif ($c =~ /^([^\s]+)\s+CHECK\s*\((.*)\)/is)
	{
		my $name = $1;
		my $desc = $2;
		if ($desc =~ /^([a-z_\$0-9]+)\b/i) {
			$name .= "_$1";
		}
		my %tmp = ($name => $desc);
		$self->{tables}{$tb_name}{check_constraint}{constraint}{$name}{condition} = $desc;
		if ($c =~ /NOVALIDATE/is) {
			$self->{tables}{$tb_name}{check_constraint}{constraint}{$name}{validate} = 'NOT VALIDATED';
		}
	}
	elsif ($c =~ /^([^\s]+)\s+FOREIGN KEY\s*(\([^\)]+\))?\s*REFERENCES\s*([^\(\s]+)\s*\(([^\)]+)\)/is)
	{
		my $c_name = $1;
		if ($2) {
			$cur_col_name = $2;
		}
		my $f_tb_name = $3;
		my @col_list = split(/,/, $4);
		$c_name =~ s/"//g;
		$f_tb_name =~ s/"//g;
		$cur_col_name =~ s/[\("\)]//g;
		map { s/"//g; } @col_list;
		if (!$self->{export_schema}) {
			$f_tb_name =~ s/^[^\.]+\.//;
			map { s/^[^\.]+\.//; } @col_list;
		}
		push(@{$self->{tables}{$tb_name}{foreign_link}{"\U$c_name\E"}{local}}, $cur_col_name);
		push(@{$self->{tables}{$tb_name}{foreign_link}{"\U$c_name\E"}{remote}{$f_tb_name}}, @col_list);
		my $deferrable = '';
		$deferrable = 'DEFERRABLE' if ($c =~ /DEFERRABLE/);
		my $deferred = '';
		$deferred = 'DEFERRED' if ($c =~ /INITIALLY DEFERRED/);
		my $novalidate = '';
		$novalidate = 'NOT VALIDATED' if ($c =~ /NOVALIDATE/);
		# CONSTRAINT_NAME,R_CONSTRAINT_NAME,SEARCH_CONDITION,DELETE_RULE,$deferrable,DEFERRED,R_OWNER,TABLE_NAME,OWNER,UPDATE_RULE,VALIDATED
		push(@{$self->{tables}{$tb_name}{foreign_key}}, [ ($c_name,'','','',$deferrable,$deferred,'',$tb_name,'','',$novalidate) ]);
	}
}

sub _remove_text_constant_part
{
	my ($self, $str) = @_;

	for (my $i = 0; $i <= $#{$self->{alternative_quoting_regexp}}; $i++) {
		while ($$str =~ s/$self->{alternative_quoting_regexp}[$i]/\?TEXTVALUE$self->{text_values_pos}\?/s) {
			$self->{text_values}{$self->{text_values_pos}} = '$$' . $1 . '$$';
			$self->{text_values_pos}++;
		}
	}

	$$str =~ s/\\'/ORA2PG_ESCAPE1_QUOTE'/gs;
	while ($$str =~ s/''/ORA2PG_ESCAPE2_QUOTE/gs) {}

	while ($$str =~ s/('[^']+')/\?TEXTVALUE$self->{text_values_pos}\?/s) {
		$self->{text_values}{$self->{text_values_pos}} = $1;
		$self->{text_values_pos}++;
	}

	for (my $i = 0; $i <= $#{$self->{string_constant_regexp}}; $i++) {
		while ($$str =~ s/($self->{string_constant_regexp}[$i])/\?TEXTVALUE$self->{text_values_pos}\?/s) {
			$self->{text_values}{$self->{text_values_pos}} = $1;
			$self->{text_values_pos}++;
		}
	}
}

sub _restore_text_constant_part
{
	my ($self, $str) = @_;

	$$str =~ s/\?TEXTVALUE(\d+)\?/$self->{text_values}{$1}/gs;
	$$str =~ s/ORA2PG_ESCAPE2_QUOTE/''/gs;
	$$str =~ s/ORA2PG_ESCAPE1_QUOTE'/\\'/gs;

       if ($self->{type} eq 'TRIGGER') {
	       $$str =~ s/(\s+)(NEW|OLD)\.'([^']+)'/$1$2\.$3/igs;
       }
}

sub _get_ddl_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->read_input_file($self->{input_file});

	$content =~ s/CREATE\s+OR\s+REPLACE/CREATE/gs;
	$content =~ s/CREATE\s+EDITIONABLE/CREATE/gs;
	$content =~ s/CREATE\s+NONEDITIONABLE/CREATE/gs;

	if ($self->{is_mysql})
	{
		$content =~ s/CREATE\s+ALGORITHM=[^\s]+/CREATE/gs;
		$content =~ s/CREATE\s+DEFINER=[^\s]+/CREATE/gs;
		$content =~ s/SQL SECURITY DEFINER VIEW/VIEW/gs;
	}

	return $content;
}

sub read_schema_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->_get_ddl_from_file();
	# Clear content from comment and text constant for better parsing
	if (!$self->{no_clean_comment})
	{
		$self->_remove_comments(\$content, 1);
		$content =~  s/\%ORA2PG_COMMENT\d+\%//gs;
	}
	my $tid = 0; 

	my @statements = split(/\s*;\s*/, $content);

	foreach $content (@statements)
	{
		$content .= ';';

		# Remove some unwanted and unused keywords from the statements
		$content =~ s/\s+(PARALLEL|COMPRESS|CLUSTERED|NONCLUSTERED)\b//igs;
		$content =~ s/\s+WITH\s+CHECK\s+ADD\s+CONSTRAINT\s+/ ADD CONSTRAINT /igs;

		if ($content =~ s/TRUNCATE TABLE\s+([^\s;]+)([^;]*);//is)
		{
			my $tb_name = $1;
			$tb_name =~ s/"//gs;
			if (!exists $self->{tables}{$tb_name}{table_info}{type})
			{
				$self->{tables}{$tb_name}{table_info}{type} = 'TABLE';
				$self->{tables}{$tb_name}{table_info}{num_rows} = 0;
				$tid++;
				$self->{tables}{$tb_name}{internal_id} = $tid;
			}
			$self->{tables}{$tb_name}{truncate_table} = 1;
		}
		elsif ($content =~ s/CREATE\s+(GLOBAL|PRIVATE)?\s*(TEMPORARY)?\s*TABLE[\s]+([^\s]+)\s+AS\s+([^;]+);//is)
		{
			my $tb_name = $3;
			$tb_name =~ s/"//gs;
			my $tb_def = $4;
			$tb_def =~ s/\s+/ /gs;
			$self->{tables}{$tb_name}{table_info}{type} = 'TEMPORARY ' if ($2);
			$self->{tables}{$tb_name}{table_info}{type} .= 'TABLE';
			$self->{tables}{$tb_name}{table_info}{num_rows} = 0;
			$tid++;
			$self->{tables}{$tb_name}{internal_id} = $tid;
			$self->{tables}{$tb_name}{table_as} = $tb_def;
		}
		elsif ($content =~ s/CREATE\s+(GLOBAL|PRIVATE)?\s*(TEMPORARY)?\s*TABLE[\s]+([^\s\(]+)\s*([^;]+);//is)
		{
			my $tb_name = $3;
			my $tb_def  = $4;
			my $tb_param  = '';
			$tb_name =~ s/"//gs;
			$self->{tables}{$tb_name}{table_info}{type} = 'TEMPORARY ' if ($2);
			$self->{tables}{$tb_name}{table_info}{type} .= 'TABLE';
			$self->{tables}{$tb_name}{table_info}{num_rows} = 0;
			$tid++;
			$self->{tables}{$tb_name}{internal_id} = $tid;
 			# Remove goldengate suplemental table logging
 			$tb_def =~ s/SUPPLEMENTAL LOG DATA \(.*?\) COLUMNS//is;
			# For private temporary table extract the ON COMMIT information (18c)
			if ($tb_def =~ s/ON\s+COMMIT\s+PRESERVE\s+DEFINITION//is)
			{
				$self->{tables}{$tb_name}{table_info}{on_commit} = 'ON COMMIT PRESERVE ROWS';
			}
			elsif ($tb_def =~ s/ON\s+COMMIT\s+DROP\s+DEFINITION//is)
			{
				$self->{tables}{$tb_name}{table_info}{on_commit} = 'ON COMMIT DROP';
			}
			elsif ($self->{tables}{$tb_name}{table_info}{type} eq 'TEMPORARY ')
			{
				# Default for PRIVATE TEMPORARY TABLE
				$self->{tables}{$tb_name}{table_info}{on_commit} = 'ON COMMIT DROP';
			}
			# Get table embedded comment
			if ($tb_def =~ s/COMMENT=["']([^"']+)["']//is)
			{
				$self->{tables}{$tb_name}{table_info}{comment} = $1;
			}
			$tb_def =~ s/^\(//;
			my %fct_placeholder = ();
			my $i = 0;
			while ($tb_def =~ s/(\([^\(\)]*\))/\%\%FCT$i\%\%/is)
			{
				$fct_placeholder{$i} = $1;
				$i++;
			};
			($tb_def, $tb_param) = split(/\s*\)\s*/, $tb_def);
			my @column_defs = split(/\s*,\s*/, $tb_def);
			map { s/^\s+//; s/\s+$//; } @column_defs;
			my $pos = 0;
			my $cur_c_name = '';
			foreach my $c (@column_defs)
			{
				next if (!$c);

				# Replace temporary substitution
				while ($c =~ s/\%\%FCT(\d+)\%\%/$fct_placeholder{$1}/is) {
					delete $fct_placeholder{$1};
				}
				# Mysql unique key embedded definition, transform it to special parsing 
				$c =~ s/^UNIQUE KEY/INDEX UNIQUE/is;
				# Remove things that are not possible with postgres
				$c =~ s/(PRIMARY KEY.*)NOT NULL/$1/is;
				# Rewrite some parts for easiest/generic parsing
				my $tbn = $tb_name;
				$tbn =~ s/\./_/gs;
				$c =~ s/^(PRIMARY KEY|UNIQUE)/CONSTRAINT o2pu_$tbn $1/is;
				$c =~ s/^(CHECK[^,;]+)DEFERRABLE\s+INITIALLY\s+DEFERRED/$1/is;
				$c =~ s/^CHECK\b/CONSTRAINT o2pc_$tbn CHECK/is;
				$c =~ s/^FOREIGN KEY/CONSTRAINT o2pf_$tbn FOREIGN KEY/is;
				$c =~ s/\(\s+/\(/gs;

				# register column name between double quote
				my $i = 0;
				my %col_name = ();
				# Get column name
				while ($c =~ s/("[^"]+")/\%\%COLNAME$i\%\%/s)
				{
					$col_name{$i} = $1;
					$i++;
				}
				if ($c =~ s/^\s*([^\s]+)\s*//s)
				{
					my $c_name = $1;
					$c_name =~ s/\%\%COLNAME(\d+)\%\%/$col_name{$1}/sg;
					$c =~ s/\%\%COLNAME(\d+)\%\%/$col_name{$1}/sg;
					if (!$self->{preserve_case}) {
						$c_name =~ s/"//gs;
					}
					# Retrieve all columns information
					if (!grep(/^\Q$c_name\E$/i, 'CONSTRAINT','INDEX'))
					{
						$cur_c_name = $c_name;
						$c_name =~ s/\./_/gs;
						my $c_default = '';
						my $virt_col = 'NO';
						$c =~ s/\s+ENABLE//is;
						if ($c =~ s/\bGENERATED\s+(ALWAYS|BY\s+DEFAULT)\s+(ON\s+NULL\s+)?AS\s+IDENTITY\s*(.*)//is)
						{
							$self->{identity_info}{$tb_name}{$c_name}{generation} = $1;
							my $options = $3;
							$self->{identity_info}{$tb_name}{$c_name}{options} = $3;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/(SCALE|EXTEND|SESSION)_FLAG: .//isg;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/KEEP_VALUE: .//is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/(START WITH):/$1/is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/(INCREMENT BY):/$1/is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/MAX_VALUE:/MAXVALUE/is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/MIN_VALUE:/MINVALUE/is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/CYCLE_FLAG: N/NO CYCLE/is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/NOCYCLE/NO CYCLE/is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/CYCLE_FLAG: Y/CYCLE/is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/CACHE_SIZE:/CACHE/is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/CACHE_SIZE:/CACHE/is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/ORDER_FLAG: .//is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/,//gs;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/\s$//s;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/CACHE\s+0/CACHE 1/is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/\s*NOORDER//is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/\s*NOKEEP//is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/\s*NOSCALE//is;
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/\s*NOT\s+NULL//is;
							# Be sure that we don't exceed the bigint max value,
							# we assume that the increment is always positive
							if ($self->{identity_info}{$tb_name}{$c_name}{options} =~ /MAXVALUE\s+(\d+)/is) {
								$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/(MAXVALUE)\s+\d+/$1 9223372036854775807/is;
							}
							$self->{identity_info}{$tb_name}{$c_name}{options} =~ s/\s+/ /igs;
						}
						elsif ($c =~ s/\b(GENERATED ALWAYS AS|AS)\s+(.*)//is)
						{
							$virt_col = 'YES';
							$c_default = $2;
							$c_default =~ s/\s+VIRTUAL//is;
						}
						my $c_type = '';
						if ($c =~ s/^\s*ENUM\s*(\([^\(\)]+\))\s*//is)
						{
							my %tmp = ();
							$tmp{name} = lc($tb_name . '_' . $c_name . '_t');
							$tmp{pos} = 0;
							$tmp{code} .= "CREATE TYPE " .
								$self->quote_object_name($tmp{name}) .
								" AS ENUM ($1);";
							push(@{$self->{types}}, \%tmp);
							$c_type = $tmp{name};
						} elsif ($c =~ s/^([^\s\(]+)\s*//s) {
							$c_type = $1;
						} elsif ($c_default)
						{
							# Try to guess a type the virtual column was declared without one,
							# but always default to text and always display a warning.
							if ($c_default =~ /ROUND\s*\(/is) {
								$c_type = 'numeric';
							} elsif ($c_default =~ /TO_DATE\s\(/is) {
								$c_type = 'timestamp';
							} else {
								$c_type = 'text';
							}
							print STDERR "WARNING: Virtual column $tb_name.$cur_c_name has no data type defined, using $c_type but you need to check that this is the right type.\n";
						}
						else
						{
							next;
						}
						if (!$self->{preserve_case}) {
							$c_type =~ s/"//gs;
						}
						my $c_length = '';
						my $c_scale = '';
						if ($c =~ s/^\(([^\)]+)\)\s*//s)
						{
							$c_length = $1;
							if ($c_length =~ s/\s*,\s*(\d+)\s*//s) {
								$c_scale = $1;
							}
						}
						my $c_nullable = 1;
						if ($c =~ s/CONSTRAINT\s*([^\s]+)?\s*NOT NULL//is) {
							$c_nullable = 0;
						} elsif ($c !~ /IS\s+NOT\s+NULL/is && $c =~ s/\bNOT\s+NULL//is) {
							$c_nullable = 0;
						}

						if (($c =~ s/(UNIQUE|PRIMARY KEY)\s*\(([^\)]+)\)//is) || ($c =~ s/(UNIQUE|PRIMARY KEY)\s*//is))
						{
							$c_name =~ s/\./_/gs;
							my $pk_name = 'o2pu_' . $c_name; 
							my $cols = $c_name;
							if ($2) {
								$cols = $2;
							}
							$self->_parse_constraint($tb_name, $c_name, "$pk_name $1 ($cols)");

						}
						elsif ( ($c =~ s/CONSTRAINT\s([^\s]+)\sCHECK\s*\(([^\)]+)\)//is) || ($c =~ s/CHECK\s*\(([^\)]+)\)//is) )
						{
							$c_name =~ s/\./_/gs;
							my $pk_name = 'o2pc_' . $c_name; 
							my $chk_search = $1;
							if ($2)
							{
								$pk_name = $1;
								$chk_search = $2;
							}
							$chk_search .= $c if ($c eq ')');
							$self->_parse_constraint($tb_name, $c_name, "$pk_name CHECK ($chk_search)");
						}
						elsif ($c =~ s/REFERENCES\s+([^\(\s]+)\s*\(([^\)]+)\)//is)
						{

							$c_name =~ s/\./_/gs;
							my $pk_name = 'o2pf_' . $c_name; 
							my $chk_search = $1 . "($2)";
							$chk_search =~ s/\s+//gs;
							$self->_parse_constraint($tb_name, $c_name, "$pk_name FOREIGN KEY ($c_name) REFERENCES $chk_search");
						}

						my $auto_incr = 0;
						if ($c =~ s/\s*AUTO_INCREMENT\s*//is) {
							$auto_incr = 1;
						}
						# At this stage only the DEFAULT part might be on the string
						if ($c =~ /\bDEFAULT\s+/is)
						{
							if ($c =~ s/\bDEFAULT\s+('[^']+')\s*//is) {
								$c_default = $1;
							} elsif ($c =~ s/\bDEFAULT\s+([^\s]+)\s*$//is) {
								$c_default = $1;
							} elsif ($c =~ s/\bDEFAULT\s+(.*)$//is) {
								$c_default = $1;
							}
							$c_default =~ s/"//gs;
							if ($self->{plsql_pgsql}) {
								$c_default = Ora2Pg::PLSQL::convert_plsql_code($self, $c_default);
							}
						}
						if ($c_type =~ /date|timestamp/i && $c_default =~ /^'0000-/)
						{
							if ($self->{replace_zero_date}) {
								$c_default = $self->{replace_zero_date};
							} else {
								$c_default =~ s/^'0000-\d+-\d+/'1970-01-01/;
							}
							if ($c_default =~ /^[\-]*INFINITY$/) {
								$c_default .= "::$c_type";
							}
						}
						# COLUMN_NAME,DATA_TYPE,DATA_LENGTH,NULLABLE,DATA_DEFAULT,DATA_PRECISION,DATA_SCALE,CHAR_LENGTH,TABLE_NAME,OWNER,VIRTUAL_COLUMN,POSITION,AUTO_INCREMENT,SRID,SDO_DIM,SDO_GTYPE
						push(@{$self->{tables}{$tb_name}{column_info}{$c_name}}, ($c_name, $c_type, $c_length, $c_nullable, $c_default, $c_length, $c_scale, $c_length, $tb_name, '', $virt_col, $pos, $auto_incr));
					}
					elsif (uc($c_name) eq 'CONSTRAINT')
					{
						$self->_parse_constraint($tb_name, $cur_c_name, $c);
					}
					elsif (uc($c_name) eq 'INDEX')
					{
						if ($c =~ /^\s*UNIQUE\s+([^\s]+)\s+\(([^\)]+)\)/)
						{
							my $idx_name = $1;
							my @cols = ();
							push(@cols, split(/\s*,\s*/, $2));
							map { s/^"//; s/"$//; } @cols;
							$self->{tables}{$tb_name}{unique_key}->{$idx_name}{type} = 'U';
							$self->{tables}{$tb_name}{unique_key}->{$idx_name}{generated} = 0;
							$self->{tables}{$tb_name}{unique_key}->{$idx_name}{index_name} = $idx_name;
							push(@{$self->{tables}{$tb_name}{unique_key}->{$idx_name}{columns}}, @cols);
						}
						elsif ($c =~ /^\s*([^\s]+)\s+\(([^\)]+)\)/)
						{
							my $idx_name = $1;
							my @cols = ();
							push(@cols, split(/\s*,\s*/, $2));
							map { s/^"//; s/"$//; } @cols;
							push(@{$self->{tables}{$tb_name}{indexes}{$idx_name}}, @cols); 
						}
					}
				}
				else
				{
					$c =~ s/\%\%COLNAME(\d+)\%\%/$col_name{$1}/sg;
				}
				$pos++;
			}
			map {s/^/\t/; s/$/,\n/; } @column_defs;
			# look for storage information
			if ($tb_param =~ /TABLESPACE[\s]+([^\s]+)/is) {
				$self->{tables}{$tb_name}{table_info}{tablespace} = $1;
				$self->{tables}{$tb_name}{table_info}{tablespace} =~ s/"//gs;
			}
			if ($tb_param =~ /PCTFREE\s+(\d+)/is) {
				# We only take care of pctfree upper than the default
				if ($1 > 10) {
					# fillfactor must be >= 10
					$self->{tables}{$tb_name}{table_info}{fillfactor} = 100 - &Ora2Pg::Oracle::min(90, $1);
				}
			}
			if ($tb_param =~ /\bNOLOGGING\b/is) {
				$self->{tables}{$tb_name}{table_info}{nologging} = 1;
			}
			if ($tb_param =~ /\bGLOBAL\s+TEMPORARY\b/is) {
				$self->{tables}{$tb_name}{table_info}{temporary} = 'Y';
			}

			if ($tb_param =~ /ORGANIZATION EXTERNAL/is) {
				if ($tb_param =~ /DEFAULT DIRECTORY ([^\s]+)/is) {
					$self->{external_table}{$tb_name}{director} = $1;
				}
				$self->{external_table}{$tb_name}{delimiter} = ',';
				if ($tb_param =~ /FIELDS TERMINATED BY '(.)'/is) {
					$self->{external_table}{$tb_name}{delimiter} = $1;
				}
				if ($tb_param =~ /PREPROCESSOR EXECDIR\s*:\s*'([^']+)'/is) {
					$self->{external_table}{$tb_name}{program} = $1;
				}
				if ($tb_param =~ /LOCATION\s*\(\s*'([^']+)'\s*\)/is) {
					$self->{external_table}{$tb_name}{location} = $1;
				}
			}
		}
		elsif ($content =~ s/CREATE\s+(UNIQUE|BITMAP)?\s*INDEX\s+([^\s]+)\s+ON\s+([^\s\(]+)\s*\((.*)\)//is)
		{
			my $is_unique = $1;
			my $idx_name = $2;
			my $tb_name = $3;
			my $idx_def = $4;
			$idx_name =~ s/"//gs;
			$tb_name =~ s/\s+/ /gs;
			$idx_def =~ s/\s+/ /gs;
			$idx_def =~ s/\s*nologging//is;
			$idx_def =~ s/STORAGE\s*\([^\)]+\)\s*//is;
			$idx_def =~ s/COMPRESS(\s+\d+)?\s*//is;
			$idx_def =~ s/\)\s*WITH\s*\(.*//is;
			$idx_def =~ s/\s+ASC\b//is;
			$idx_def =~ s/^\s+//s;
			$idx_def =~ s/\s+$//is;
			# look include information
			if ($idx_def =~ s/\s*\)\s*(INCLUDE|INCLUDING)\s*\(([^\)]+)//is)
			{
				my $include = $2;
				$include =~ s/\s+//g;
				push(@{$self->{tables}{$tb_name}{idx_type}{$idx_name}{type_include}}, split(/\s*,\s*/, $include));
			}
			# look for storage information
			if ($idx_def =~ s/TABLESPACE\s*([^\s]+)\s*//is)
			{
				$self->{tables}{$tb_name}{idx_tbsp}{$idx_name} = $1;
				$self->{tables}{$tb_name}{idx_tbsp}{$idx_name} =~ s/"//gs;
			}
			if ($idx_def =~ s/ONLINE\s*//is) {
				$self->{tables}{$tb_name}{concurrently}{$idx_name} = 1;
			}
			if ($idx_def =~ s/INDEXTYPE\s+IS\s+.*SPATIAL_INDEX//is)
			{
				$self->{tables}{$tb_name}{spatial}{$idx_name} = 1;
				$self->{tables}{$tb_name}{idx_type}{$idx_name}{type} = 'SPATIAL INDEX';
				$self->{tables}{$tb_name}{idx_type}{$idx_name}{type_name} = 'SPATIAL_INDEX';
			}
			if ($idx_def =~ s/layer_gtype=([^\s,]+)//is) {
				$self->{tables}{$tb_name}{idx_type}{$idx_name}{type_constraint} = uc($1);
			}
			if ($idx_def =~ s/sdo_indx_dims=(\d)//is) {
				$self->{tables}{$tb_name}{idx_type}{$idx_name}{type_dims} = $1;
			}
			if ($is_unique eq 'BITMAP')
			{
				$is_unique = '';
				$self->{tables}{$tb_name}{idx_type}{$idx_name}{type_name} = 'BITMAP';
			}
			$self->{tables}{$tb_name}{uniqueness}{$idx_name} = $is_unique || '';
			$idx_def =~ s/SYS_EXTRACT_UTC\s*\(([^\)]+)\)/$1/isg;
			if ($self->{plsql_pgsql}) {
				$idx_def = Ora2Pg::PLSQL::convert_plsql_code($self, $idx_def);
			}
			push(@{$self->{tables}{$tb_name}{indexes}{$idx_name}}, $idx_def);
			$self->{tables}{$tb_name}{idx_type}{$idx_name}{type} = 'NORMAL';
			if ($idx_def =~ /\(/s) {
				$self->{tables}{$tb_name}{idx_type}{$idx_name}{type} = 'FUNCTION-BASED';
			}

			if (!exists $self->{tables}{$tb_name}{table_info}{type})
			{
				$self->{tables}{$tb_name}{table_info}{type} = 'TABLE';
				$self->{tables}{$tb_name}{table_info}{num_rows} = 0;
				$tid++;
				$self->{tables}{$tb_name}{internal_id} = $tid;
			}
		}
		elsif ($content =~ s/ALTER\s+TABLE\s+([^\s]+)\s+ADD\s*\(*\s*(.*)//is)
		{
			my $tb_name = $1;
			my $tb_def = $2;
			$tb_name =~ s/"//g;
			# Oracle allow multiple constraints declaration inside a single ALTER TABLE
			# CONSTRAINT CK_TblMstCustomerDriver_RentalDepositAmount CHECK  ((RentalDepositAmount>=(0)));
			while ($tb_def =~ s/CONSTRAINT\s+([^\s]+)\s+CHECK\s*([\(]+.*?[\)]+)\s*(ENABLE|DISABLE|VALIDATE|NOVALIDATE|DEFERRABLE|INITIALLY|DEFERRED|USING\s+INDEX|\s+)?([^,]*)//is)
			{
				my $constname = $1;
				my $code = $2;
				my $states = $3;
				my $tbspace_move = $4;
				if (!exists $self->{tables}{$tb_name}{table_info}{type})
				{
					$self->{tables}{$tb_name}{table_info}{type} = 'TABLE';
					$self->{tables}{$tb_name}{table_info}{num_rows} = 0;
					$tid++;
					$self->{tables}{$tb_name}{internal_id} = $tid;
				}
				my $validate = '';
				$validate = ' NOT VALID' if ( $states =~ /NOVALIDATE/is);
				push(@{$self->{tables}{$tb_name}{alter_table}}, "ADD CONSTRAINT \L$constname\E CHECK $code$validate");
				if ( $tbspace_move =~ /USING\s+INDEX\s+TABLESPACE\s+([^\s]+)/is) {
					if ($self->{use_tablespace}) {
						$tbspace_move = "ALTER INDEX $constname SET TABLESPACE " . lc($1);
						push(@{$self->{tables}{$tb_name}{alter_index}}, $tbspace_move);
					}
				} elsif ($tbspace_move =~ /USING\s+INDEX\s+([^\s]+)/is) {
					$self->{tables}{$tb_name}{alter_table}[-1] .= " USING INDEX " . lc($1);
				}
				
			}
			while ($tb_def =~ s/CONSTRAINT\s+([^\s]+)\s+FOREIGN\s+KEY\s*(\(.*?\)\s+REFERENCES\s+[^\s]+\s*\(.*?\))\s*([^,\)]+|$)//is) {
				my $constname = $1;
				my $other_def = $3;
				if (!exists $self->{tables}{$tb_name}{table_info}{type})
				{
					$self->{tables}{$tb_name}{table_info}{type} = 'TABLE';
					$self->{tables}{$tb_name}{table_info}{num_rows} = 0;
					$tid++;
					$self->{tables}{$tb_name}{internal_id} = $tid;
				}
				push(@{$self->{tables}{$tb_name}{alter_table}}, "ADD CONSTRAINT \L$constname\E FOREIGN KEY $2");
				if ($other_def =~ /(ON\s+DELETE\s+(?:NO ACTION|RESTRICT|CASCADE|SET NULL))/is) {
					$self->{tables}{$tb_name}{alter_table}[-1] .= " $1";
				}
				if ($other_def =~ /(ON\s+UPDATE\s+(?:NO ACTION|RESTRICT|CASCADE|SET NULL))/is) {
					$self->{tables}{$tb_name}{alter_table}[-1] .= " $1";
				}
				my $validate = '';
				$validate = ' NOT VALID' if ( $other_def =~ /NOVALIDATE/is);
				$self->{tables}{$tb_name}{alter_table}[-1] .= $validate;
			}
			# We can just have one primary key constraint
			if ($tb_def =~ s/CONSTRAINT\s+([^\s]+)\s+PRIMARY KEY//is) {
				my $constname = lc($1);
				$tb_def =~ s/^[^\(]+//;
				$tb_def =~ s/\);$//s;
				if ( $tb_def =~ s/USING\s+INDEX\s+TABLESPACE\s+([^\s]+).*//s) {
					$tb_def =~ s/\s+$//;
					if ($self->{use_tablespace}) {
						my $tbspace_move = "ALTER INDEX $constname SET TABLESPACE $1";
						push(@{$self->{tables}{$tb_name}{alter_index}}, $tbspace_move);
					}
					push(@{$self->{tables}{$tb_name}{alter_table}}, "ADD CONSTRAINT $constname PRIMARY KEY " . lc($tb_def));
				} elsif ($tb_def =~ s/USING\s+INDEX\s+([^\s]+).*//s) {
					push(@{$self->{tables}{$tb_name}{alter_table}}, "ADD PRIMARY KEY " . lc($tb_def));
					$self->{tables}{$tb_name}{alter_table}[-1] .= " USING INDEX " . lc($1);
				} elsif ($tb_def) {
					push(@{$self->{tables}{$tb_name}{alter_table}}, "ADD CONSTRAINT $constname PRIMARY KEY " . lc($tb_def));
				}
				if (!exists $self->{tables}{$tb_name}{table_info}{type}) {
					$self->{tables}{$tb_name}{table_info}{type} = 'TABLE';
					$self->{tables}{$tb_name}{table_info}{num_rows} = 0;
					$tid++;
					$self->{tables}{$tb_name}{internal_id} = $tid;
				}
			}
		}
		elsif ($content =~ s/ALTER\s+TABLE\s+([^\s]+)\s+ADD\s+(CONSTRAINT\s+[^\s]+\s+.*)//is)
		{
			my $tb_name = $1;
			my $tb_def = $2;
			$tb_name =~ s/"//g;
			# Oracle allow multiple constraints declaration inside a single ALTER TABLE
			while ($tb_def =~ s/CONSTRAINT\s+([^\s]+)\s+CHECK\s*(\(.*?\))\s+(ENABLE|DISABLE|VALIDATE|NOVALIDATE|DEFERRABLE|INITIALLY|DEFERRED|USING\s+INDEX|\s+)+([^,]*)//is)
			{
				my $constname = $1;
				my $code = $2;
				my $states = $3;
				my $tbspace_move = $4;
				if (!exists $self->{tables}{$tb_name}{table_info}{type})
				{
					$self->{tables}{$tb_name}{table_info}{type} = 'TABLE';
					$self->{tables}{$tb_name}{table_info}{num_rows} = 0;
					$tid++;
					$self->{tables}{$tb_name}{internal_id} = $tid;
				}
				my $validate = '';
				$validate = ' NOT VALID' if ( $states =~ /NOVALIDATE/is);
				push(@{$self->{tables}{$tb_name}{alter_table}}, "ADD CONSTRAINT \L$constname\E CHECK $code$validate");
				if ( $tbspace_move =~ /USING\s+INDEX\s+TABLESPACE\s+([^\s]+)/is) {
					if ($self->{use_tablespace}) {
						$tbspace_move = "ALTER INDEX $constname SET TABLESPACE " . lc($1);
						push(@{$self->{tables}{$tb_name}{alter_index}}, $tbspace_move);
					}
				} elsif ($tbspace_move =~ /USING\s+INDEX\s+([^\s]+)/is) {
					$self->{tables}{$tb_name}{alter_table}[-1] .= " USING INDEX " . lc($1);
				}
				
			}

			while ($tb_def =~ s/CONSTRAINT\s+([^\s]+)\s+FOREIGN\s+KEY\s*(\(.*?\)\s+REFERENCES\s+[^\s]+\s*\(.*?\))\s*([^,\)]+|$)//is) {
				my $constname = $1;
				my $other_def = $3;
				if (!exists $self->{tables}{$tb_name}{table_info}{type})
				{
					$self->{tables}{$tb_name}{table_info}{type} = 'TABLE';
					$self->{tables}{$tb_name}{table_info}{num_rows} = 0;
					$tid++;
					$self->{tables}{$tb_name}{internal_id} = $tid;
				}
				push(@{$self->{tables}{$tb_name}{alter_table}}, "ADD CONSTRAINT \L$constname\E FOREIGN KEY $2");
				if ($other_def =~ /(ON\s+DELETE\s+(?:NO ACTION|RESTRICT|CASCADE|SET NULL))/is) {
					$self->{tables}{$tb_name}{alter_table}[-1] .= " $1";
				}
				if ($other_def =~ /(ON\s+UPDATE\s+(?:NO ACTION|RESTRICT|CASCADE|SET NULL))/is) {
					$self->{tables}{$tb_name}{alter_table}[-1] .= " $1";
				}
				my $validate = '';
				$validate = ' NOT VALID' if ( $other_def =~ /NOVALIDATE/is);
				$self->{tables}{$tb_name}{alter_table}[-1] .= $validate;
			}
			# We can just have one primary key constraint
			if ($tb_def =~ s/CONSTRAINT\s+([^\s]+)\s+PRIMARY KEY//is) {
				my $constname = lc($1);
				$tb_def =~ s/^[^\(]+//;
				$tb_def =~ s/\);$//s;
				if ( $tb_def =~ s/USING\s+INDEX\s+TABLESPACE\s+([^\s]+).*//s) {
					$tb_def =~ s/\s+$//;
					if ($self->{use_tablespace}) {
						my $tbspace_move = "ALTER INDEX $constname SET TABLESPACE $1";
						push(@{$self->{tables}{$tb_name}{alter_index}}, $tbspace_move);
					}
					push(@{$self->{tables}{$tb_name}{alter_table}}, "ADD CONSTRAINT $constname PRIMARY KEY " . lc($tb_def));
				} elsif ($tb_def =~ s/USING\s+INDEX\s+([^\s]+).*//s) {
					push(@{$self->{tables}{$tb_name}{alter_table}}, "ADD PRIMARY KEY " . lc($tb_def));
					$self->{tables}{$tb_name}{alter_table}[-1] .= " USING INDEX " . lc($1);
				} elsif ($tb_def) {
					push(@{$self->{tables}{$tb_name}{alter_table}}, "ADD CONSTRAINT $constname PRIMARY KEY " . lc($tb_def));
				}
				if (!exists $self->{tables}{$tb_name}{table_info}{type}) {
					$self->{tables}{$tb_name}{table_info}{type} = 'TABLE';
					$self->{tables}{$tb_name}{table_info}{num_rows} = 0;
					$tid++;
					$self->{tables}{$tb_name}{internal_id} = $tid;
				}
			}
		}
		else
		{
			print STDERR "[DEBUG] unhandled line: $content\n";
		}
	}

	# Extract comments
	$self->read_comment_from_file();
}

sub read_comment_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->_get_ddl_from_file();

	my $tid = 0; 

	while ($content =~ s/COMMENT\s+ON\s+TABLE\s+([^\s]+)\s+IS\s+'([^;]+);//is)
	{
		my $tb_name = $1;
		my $tb_comment = $2;
		$tb_name =~ s/"//g;
		$tb_comment =~ s/'\s*$//g;
		if (exists $self->{tables}{$tb_name}) {
			$self->{tables}{$tb_name}{table_info}{comment} = $tb_comment;
		}
	}

	while ($content =~ s/COMMENT\s+ON\s+COLUMN\s+(.*?)\s+IS\s*'([^;]+);//is)
	{
		my $tb_name = $1;
		my $tb_comment = $2;
		# register column name between double quote
		my $i = 0;
		my %col_name = ();
		# Get column name
		while ($tb_name =~ s/("[^"]+")/\%\%COLNAME$i\%\%/s)
		{
			$col_name{$i} = $1;
			$i++;
		}
		$tb_comment =~ s/'\s*$//g;
		if ($tb_name =~ s/\.([^\.]+)$//)
		{
			my $cname = $1;
			$tb_name =~ s/\%\%COLNAME(\d+)\%\%/$col_name{$1}/sg;
			$tb_name =~ s/"//g;
			$cname =~ s/\%\%COLNAME(\d+)\%\%/$col_name{$1}/sg;
			$cname =~ s/"//g;
			$cname =~ s/\./_/g;
			if (exists $self->{tables}{$tb_name}) {
					$self->{tables}{$tb_name}{column_comments}{"\L$cname\E"} = $tb_comment;
			} elsif (exists $self->{views}{$tb_name}) {
					$self->{views}{$tb_name}{column_comments}{"\L$cname\E"} = $tb_comment;
			}
		}
		else
		{
			$tb_name =~ s/\%\%COLNAME(\d+)\%\%/$col_name{$1}/sg;
		}
	}

}

sub read_view_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->_get_ddl_from_file();

	# Clear content from comment and text constant for better parsing
	$self->_remove_comments(\$content);

	my $tid = 0; 

	$content =~ s/\s+NO\s+FORCE\s+/ /igs;
	$content =~ s/\s+FORCE\s+EDITIONABLE\s+/ /igs;
	$content =~ s/\s+FORCE\s+/ /igs;
	$content =~ s/\s+EDITIONABLE\s+/ /igs;
	$content =~ s/\s+OR\s+REPLACE\s+/ /igs;
	$content =~ s/CREATE\s+VIEW\s+([^\s]+)\s+OF\s+(.*?)\s+AS\s+/CREATE VIEW $1 AS /isg;

	# Views with aliases
	while ($content =~ s/CREATE\s+VIEW\s+([^\s]+)\s*\((.*?)\)\s+AS\s+([^;]+)(;|$)//is)
	{
		my $v_name = $1;
		my $v_alias = $2;
		my $v_def = $3;
		$v_name =~ s/"//g;
		$tid++;
	        $self->{views}{$v_name}{text} = $v_def;
	        $self->{views}{$v_name}{iter} = $tid;
		# Remove constraint
		while ($v_alias =~ s/(,[^,\(]+\(.*)$//) {};
		my @aliases = split(/\s*,\s*/, $v_alias);
		foreach (@aliases)
		{
			s/^\s+//;
			s/\s+$//;
			my @tmp = split(/\s+/);
			push(@{$self->{views}{$v_name}{alias}}, \@tmp);
		}
	}
	# Standard views
	while ($content =~ s/CREATE\s+VIEW\s+([^\s]+)\s+AS\s+([^;]+);//i)
	{
		my $v_name = $1;
		my $v_def = $2;
		$v_name =~ s/"//g;
		$tid++;
	        $self->{views}{$v_name}{text} = $v_def;
	}

	# Extract comments
	$self->read_comment_from_file();
}

sub read_grant_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->_get_ddl_from_file();

	# Clear content from comment and text constant for better parsing
	$self->_remove_comments(\$content);

	my $tid = 0; 

	# Extract grant information
	while ($content =~ s/GRANT\s+(.*?)\s+ON\s+([^\s]+)\s+TO\s+([^;]+)(\s+WITH GRANT OPTION)?;//i) {
		my $g_priv = $1;
		my $g_name = $2;
		$g_name =~ s/"//g;
		my $g_user = $3;
		my $g_option = $4;
		$g_priv =~ s/\s+//g;
		$tid++;
		$self->{grants}{$g_name}{type} = '';
		push(@{$self->{grants}{$g_name}{privilege}{$g_user}}, split(/,/, $g_priv));
		if ($g_priv =~ /EXECUTE/) {
			$self->{grants}{$g_name}{type} = 'PACKAGE BODY';
		} else {
			$self->{grants}{$g_name}{type} = 'TABLE';
		}
	}

}

sub read_trigger_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->_get_ddl_from_file();

	# Clear content from comment and text constant for better parsing
	$self->_remove_comments(\$content);

	my $tid = 0; 
	my $doloop = 1;
	my @triggers_decl = split(/(?:CREATE)?(?:\s+OR\s+REPLACE)?\s*(?:DEFINER=[^\s]+)?\s*\bTRIGGER(\s+|$)/is, $content);
	foreach $content (@triggers_decl)
	{
		my $t_name = '';
		my $t_pos = '';
		my $t_event = '';
		my $tb_name = '';
		my $trigger = '';
		my $t_type = '';
		my $t_referencing = '';
		if ($content =~ s/^([^\s]+)\s+(BEFORE|AFTER|INSTEAD\s+OF)\s+(.*?)\s+ON\s+([^\s]+)\s+(.*)(\bEND\s*(?!IF|LOOP|CASE|INTO|FROM|,)[a-z0-9_\$"]*(?:;|$))//is)
		{
			$t_name = $1;
			$t_pos = $2;
			$t_event = $3;
			$tb_name = $4;
			$trigger = $5 . $6;
			$t_name =~ s/"//g;
			$tb_name =~ s/"//g;
		}
		elsif ($content =~ s/^([^\s]+)\s+(BEFORE|AFTER|INSTEAD|\s+|OF)((?:INSERT|UPDATE|DELETE|OR|\s+|OF)+\s+(?:.*?))*\s+ON\s+([^\s]+)\s+(.*)(\bEND\s*(?!IF|LOOP|CASE|INTO|FROM|,)[a-z0-9_\$"]*(?:;|$))//is)
		{
			$t_name = $1;
			$t_pos = $2;
			$t_event = $3;
			$tb_name = $4;
			$trigger = $5 . $6;
			$t_name =~ s/"//g;
			$tb_name =~ s/"//g;
		}

		next if (!$t_name || ! $tb_name);

		# Remove referencing clause, not supported by PostgreSQL < 10
		if ($self->{pg_version} < 10) {
			$trigger =~ s/REFERENCING\s+(.*?)(FOR\s+EACH\s+)/$2/is;
		} elsif ($trigger =~ s/REFERENCING\s+(.*?)(FOR\s+EACH\s+)/$2/is) {
			$t_referencing = " REFERENCING $1";
		}
		$t_referencing =~ s/REFERENCING\s+(NEW|OLD)\s+AS\s+(NEW|OLD)(\s+(NEW|OLD)\s+AS\s+(NEW|OLD))?//gsi;

		if ($trigger =~ s/^\s*(FOR\s+EACH\s+)(ROW|STATEMENT)\s*//is) {
			$t_type = $1 . $2;
		}
		my $t_when_cond = '';
		if ($trigger =~ s/^\s*WHEN\s+(.*?)\s+((?:BEGIN|DECLARE|CALL).*)//is)
		{
			$t_when_cond = $1;
			$trigger = $2;
			if ($trigger =~ /^(BEGIN|DECLARE)/i) {
				($trigger, $content) = &_get_plsql_code($trigger);
			}
			else
			{
				$trigger =~ s/([^;]+;)\s*(.*)/$1/;
				$content = $2;
			}
		}
		else
		{
			if ($trigger =~ /^(BEGIN|DECLARE)/i) {
				($trigger, $content) = &_get_plsql_code($trigger);
			}
		}
		$tid++;

		# TRIGGER_NAME, TRIGGER_TYPE, TRIGGERING_EVENT, TABLE_NAME, TRIGGER_BODY, WHEN_CLAUSE, DESCRIPTION, ACTION_TYPE, OWNER
		$trigger =~ s/\bEND\s+[^\s]+\s+$/END/is;
		my $t_schema = '';
		if ($t_name =~ s/^([^\.]+)\.//i) {
			if ($self->{export_schema} && ($self->{pg_schema} || $self->{schema} || $1)) {
				$t_schema = $self->{pg_schema} || $self->{schema} || $1;
			}
		}
		if ($tb_name =~ s/^([^\.]+)\.//i) {
			if ($self->{export_schema} && ($self->{pg_schema} || $self->{schema} || $1)) {
				$tb_name = ($self->{pg_schema} || $self->{schema} || $1) . '.' . $tb_name;
			}
		}
		my $when_event = '';
		if ($t_when_cond) {
			$when_event = "$t_name\n$t_pos$t_referencing $t_event ON $tb_name\n$t_type";
		} elsif ($t_referencing) {
			$when_event = $t_referencing;
		}
		$when_event =~ s/REFERENCING\s+(NEW|OLD)\s+AS\s+(NEW|OLD)\s+(NEW|OLD)\s+AS\s+(NEW|OLD)//gsi;
		push(@{$self->{triggers}}, [($t_name, $t_pos, $t_event, $tb_name, $trigger, $t_when_cond, $when_event, $t_type, $t_schema)]);
	}
}

sub read_sequence_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->_get_ddl_from_file();

	# Clear content from comment and text constant for better parsing
	$self->_remove_comments(\$content, 1);
	$content =~  s/\%ORA2PG_COMMENT\d+\%//gs;
	my $tid = 0; 

	# Sequences 
	while ($content =~ s/CREATE\s+SEQUENCE[\s]+([^\s;]+)\s*([^;]+);//i)
	{
		my $s_name = $1;
		my $s_def = $2;
		$s_name =~ s/"//g;
		$s_def =~ s/\s+/ /g;
		$tid++;
		my @seq_info = ();

		# Field of @seq_info
		# SEQUENCE_NAME, MIN_VALUE, MAX_VALUE, INCREMENT_BY, LAST_NUMBER, CACHE_SIZE, CYCLE_FLAG, SEQUENCE_OWNER FROM $self->{prefix}_SEQUENCES";
		push(@seq_info, $s_name);
		if ($s_def =~ /MINVALUE\s+([\-\d]+)/i) {
			push(@seq_info, $1);
		} else {
			push(@seq_info, '');
		}
		if ($s_def =~ /MAXVALUE\s+([\-\d]+)/i)
		{
			if ($1 > 9223372036854775807) {
				push(@seq_info, 9223372036854775807);
			} else {
				push(@seq_info, $1);
			}
		} else {
			push(@seq_info, '');
		}
		if ($s_def =~ /INCREMENT\s*(?:BY)?\s+([\-\d]+)/i) {
			push(@seq_info, $1);
		} else {
			push(@seq_info, 1);
		}

		if ($s_def =~ /START\s+WITH\s+([\-\d]+)/i) {
			push(@seq_info, $1);
		} else {
			push(@seq_info, '');
		}
		if ($s_def =~ /CACHE\s+(\d+)/i) {
			push(@seq_info, $1);
		} else {
			push(@seq_info, '');
		}
		if ($s_def =~ /NOCYCLE/i) {
			push(@seq_info, 'NO');
		} else {
			push(@seq_info, 'YES');
		}
		if ($s_name =~ /^([^\.]+)\./i) {
			push(@seq_info, $1);
		} else {
			push(@seq_info, '');
		}
		push(@{$self->{sequences}{$s_name}}, @seq_info);
	}
}

sub read_tablespace_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->_get_ddl_from_file();

	my @tbsps = split(/\s*;\s*/, $content);
	# tablespace without undo ones
	foreach $content (@tbsps)
	{
		$content .= ';';
		if ($content =~ /CREATE\s+(?:BIGFILE|SMALLFILE)?\s*(?:TEMPORARY)?\s*TABLESPACE\s+([^\s;]+)\s*([^;]*);/is)
		{
			my $t_name = $1;
			my $t_def = $2;
			$t_name =~ s/"//g;
			if ($t_def =~ /(?:DATA|TEMP)FILE\s+'([^']+)'/is)
			{
				my $t_path = $1;
				$t_path =~ s/:/\//g;
				$t_path =~ s/\\/\//g;
				if (dirname($t_path) eq '.') {
					$t_path = 'change_tablespace_dir';
				} else {
					$t_path = dirname($t_path);
				}
				# TYPE - TABLESPACE_NAME - FILEPATH - OBJECT_NAME
				@{$self->{tablespaces}{TABLE}{$t_name}{$t_path}} = ();
			}

		}
	}
}

sub read_directory_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->_get_ddl_from_file();

	# Directory
	while ($content =~ s/CREATE(?: OR REPLACE)?\s+DIRECTORY\s+([^\s]+)\s+AS\s+'([^']+)'\s*;//is) {
		my $d_name = uc($1);
		my $d_def = $2;
		$d_name =~ s/"//g;
		if ($d_def !~ /\/$/) {
			$d_def .= '/';
		}
		$self->{directory}{$d_name}{path} = $d_def;
	}

	# Directory
	while ($content =~ s/GRANT\s+(.*?)ON\s+DIRECTORY\s+([^\s]+)\s+TO\s+([^;\s]+)\s*;//is) {
		my $d_grant = $1;
		my $d_name = uc($2);
		my $d_user = uc($3);
		$d_name =~ s/"//g;
		$d_user =~ s/"//g;
		$self->{directory}{$d_name}{grantee}{$d_user} = $d_grant;
	}
}

sub read_synonym_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->_get_ddl_from_file();

	# Directory
	while ($content =~ s/CREATE(?: OR REPLACE)?(?: PUBLIC)?\s+SYNONYM\s+([^\s]+)\s+FOR\s+([^;\s]+)\s*;//is) {
		my $s_name = uc($1);
		my $s_def = $2;
		$s_name =~ s/"//g;
		$s_def =~ s/"//g;
		if ($s_name =~ s/^([^\.]+)\.//) {
			$self->{synonyms}{$s_name}{owner} = $1;
		} else {
			$self->{synonyms}{$s_name}{owner} = $self->{schema};
		}
		if ($s_def =~ s/@(.*)//) {
			$self->{synonyms}{$s_name}{dblink} = $1;
		}
		if ($s_def =~ s/^([^\.]+)\.//) {
			$self->{synonyms}{$s_name}{table_owner} = $1;
		}
		$self->{synonyms}{$s_name}{table_name} = $s_def;
	}

}

sub read_dblink_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->_get_ddl_from_file();

	# Directory
	while ($content =~ s/CREATE(?: SHARED)?(?: PUBLIC)?\s+DATABASE\s+LINK\s+([^\s]+)\s+CONNECT TO\s+([^\s]+)\s*([^;]+);//is) {
		my $d_name = $1;
		my $d_user = $2;
		my $d_auth = $3;
		$d_name =~ s/"//g;
		$d_user =~ s/"//g;
		$self->{dblink}{$d_name}{owner} = $self->{shema};
		$self->{dblink}{$d_name}{user} = $d_user;
		$self->{dblink}{$d_name}{username} = $self->{pg_user} || $d_user;
		if ($d_auth =~ s/USING\s+([^\s]+)//) {
			$self->{dblink}{$d_name}{host} = $1;
			$self->{dblink}{$d_name}{host} =~ s/'//g;
		}
		if ($d_auth =~ s/IDENTIFIED\s+BY\s+([^\s]+)//) {
			$self->{dblink}{$d_name}{password} = $1;
		}
		if ($d_auth =~ s/AUTHENTICATED\s+BY\s+([^\s]+)\s+IDENTIFIED\s+BY\s+([^\s]+)//) {
			$self->{dblink}{$d_name}{user} = $1;
			$self->{dblink}{$d_name}{password} = $2;
			$self->{dblink}{$d_name}{username} = $self->{pg_user} || $1;
		}
	}

	# Directory
	while ($content =~ s/CREATE(?: SHARED)?(?: PUBLIC)?\s+DATABASE\s+LINK\s+([^\s]+)\s+USING\s+([^;]+);//is) {
		my $d_name = $1;
		my $d_conn = $2;
		$d_name =~ s/"//g;
		$d_conn =~ s/'//g;
		$self->{dblink}{$d_name}{owner} = $self->{shema};
		$self->{dblink}{$d_name}{host} = $d_conn;
	}


}


=head2 _views

This function is used to retrieve all views information.

Sets the main hash of the views definition $self->{views}.
Keys are the names of all views retrieved from the current
database and values are the text definitions of the views.

It then sets the main hash as follows:

    # Definition of the view
    $self->{views}{$table}{text} = $lview_infos{$table};

=cut

sub _views
{
	my ($self) = @_;

	# Get all views information
	$self->logit("Retrieving views information...\n", 1);
	my %view_infos = $self->_get_views();
	# Retrieve comment of each columns
	my %columns_comments = $self->_column_comments();
	foreach my $view (keys %columns_comments) {
		next if (!exists $view_infos{$view});
		foreach my $c (keys %{$columns_comments{$view}}) {
			$self->{views}{$view}{column_comments}{$c} = $columns_comments{$view}{$c};
		}
	}

	my $i = 1;
	foreach my $view (sort keys %view_infos)
	{
		$self->logit("[$i] Scanning $view...\n", 1);
		$self->{views}{$view}{text} = $view_infos{$view}{text};
		$self->{views}{$view}{owner} = $view_infos{$view}{owner};
		$self->{views}{$view}{check_option} = $view_infos{$view}{check_option};
		$self->{views}{$view}{updatable} = $view_infos{$view}{updatable};
		$self->{views}{$view}{iter} = $view_infos{$view}{iter} if (exists $view_infos{$view}{iter});
		$self->{views}{$view}{comment} = $view_infos{$view}{comment};
                # Retrieve also aliases from views
                $self->{views}{$view}{alias} = $view_infos{$view}{alias};
		$i++;
	}
}

=head2 _materialized_views

This function is used to retrieve all materialized views information.

Sets the main hash of the views definition $self->{materialized_views}.
Keys are the names of all materialized views retrieved from the current
database and values are the text definitions of the views.

It then sets the main hash as follows:

    # Definition of the materialized view
    $self->{materialized_views}{text} = $mview_infos{$view};

=cut

sub _materialized_views
{
	my ($self) = @_;

	# Get all views information
	$self->logit("Retrieving materialized views information...\n", 1);
	my %mview_infos = $self->_get_materialized_views();

	my $i = 1;
	foreach my $table (sort keys %mview_infos)
	{
		$self->logit("[$i] Scanning $table...\n", 1);
		$self->{materialized_views}{$table}{text} = $mview_infos{$table}{text};
		$self->{materialized_views}{$table}{updatable}= $mview_infos{$table}{updatable};
		$self->{materialized_views}{$table}{refresh_mode}= $mview_infos{$table}{refresh_mode};
		$self->{materialized_views}{$table}{refresh_method}= $mview_infos{$table}{refresh_method};
		$self->{materialized_views}{$table}{no_index}= $mview_infos{$table}{no_index};
		$self->{materialized_views}{$table}{rewritable}= $mview_infos{$table}{rewritable};
		$self->{materialized_views}{$table}{build_mode}= $mview_infos{$table}{build_mode};
		$self->{materialized_views}{$table}{owner}= $mview_infos{$table}{owner};
		$i++;
	}

	# Retrieve index informations
	if (scalar keys %mview_infos)
	{
		my ($uniqueness, $indexes, $idx_type, $idx_tbsp) = $self->_get_indexes('',$self->{schema});
		foreach my $tb (keys %{$indexes})
		{
			next if (!exists $self->{materialized_views}{$tb});
			%{$self->{materialized_views}{$tb}{indexes}} = %{$indexes->{$tb}};
		}
		foreach my $tb (keys %{$idx_type})
		{
			next if (!exists $self->{materialized_views}{$tb});
			%{$self->{materialized_views}{$tb}{idx_type}} = %{$idx_type->{$tb}};
		}
	}
}

=head2 _tablespaces

This function is used to retrieve all Oracle Tablespaces information.

Sets the main hash $self->{tablespaces}.

=cut

sub _tablespaces
{
	my ($self) = @_;

	$self->logit("Retrieving tablespaces information...\n", 1);
	$self->{tablespaces} = $self->_get_tablespaces();
	$self->{list_tablespaces} = $self->_list_tablespaces();

}

=head2 _partitions

This function is used to retrieve all Oracle partition information.

Sets the main hash $self->{partition}.

=cut

sub _partitions
{
	my ($self) = @_;

	$self->logit("Retrieving partitions information...\n", 1);
	($self->{partitions}, $self->{partitions_default}) = $self->_get_partitions();

	($self->{subpartitions}, $self->{subpartitions_default}) = $self->_get_subpartitions();

	# Get partition list meta information
	%{ $self->{partitions_list} } = $self->_get_partitioned_table();
	%{ $self->{subpartitions_list} } = $self->_get_subpartitioned_table();

	# Look for main table indexes to reproduce them on partition
	my ($uniqueness, $indexes, $idx_type, $idx_tbsp) = $self->_get_indexes('',$self->{schema}, 0);
	foreach my $tb (keys %{$indexes}) {
		%{$self->{tables}{$tb}{indexes}} = %{$indexes->{$tb}};
	}
	foreach my $tb (keys %{$idx_type}) {
		%{$self->{tables}{$tb}{idx_type}} = %{$idx_type->{$tb}};
	}
	foreach my $tb (keys %{$idx_tbsp}) {
		%{$self->{tables}{$tb}{idx_tbsp}} = %{$idx_tbsp->{$tb}};
	}
	foreach my $tb (keys %{$uniqueness}) {
		%{$self->{tables}{$tb}{uniqueness}} = %{$uniqueness->{$tb}};
	}

	# Retrieve all unique keys informations
	my %unique_keys = $self->_unique_key('',$self->{schema});
	foreach my $tb (keys %unique_keys) {
		foreach my $c (keys %{$unique_keys{$tb}}) {
			$self->{tables}{$tb}{unique_key}{$c} = $unique_keys{$tb}{$c};
		}
	}
}

=head2 _dblinks

This function is used to retrieve all Oracle dblinks information.

Sets the main hash $self->{dblink}.

=cut

sub _dblinks
{
	my ($self) = @_;

	$self->logit("Retrieving dblinks information...\n", 1);
	%{$self->{dblink}} = $self->_get_dblink();

}

=head2 _directories

This function is used to retrieve all Oracle directories information.

Sets the main hash $self->{directory}.

=cut

sub _directories
{
	my ($self) = @_;

	$self->logit("Retrieving directories information...\n", 1);
	%{$self->{directory}} = $self->_get_directory();

}


sub get_replaced_tbname
{
	my ($self, $tmptb) = @_;

	if (exists $self->{replaced_tables}{"\L$tmptb\E"} && $self->{replaced_tables}{"\L$tmptb\E"})
	{
		$self->logit("\tReplacing table $tmptb as " . $self->{replaced_tables}{lc($tmptb)} . "...\n", 1);
		$tmptb = $self->{replaced_tables}{lc($tmptb)};
	}

	$tmptb = $self->quote_object_name($tmptb);

	return $tmptb; 
}

sub get_tbname_with_suffix
{
	my ($self, $tmptb, $suffix) = @_;

	return $self->quote_object_name($tmptb . $suffix);
}

sub _export_table_data
{
	my ($self, $table, $part_name, $subpart, $pos, $p, $dirprefix, $sql_header) = @_;

	if ($subpart) {
		$self->logit("(pid: $$) Exporting data of subpartition $subpart of partition $part_name from table $table...\n", 1);
	} elsif ($part_name) {
		$self->logit("(pid: $$) Exporting data of partition $part_name from table $table...\n", 1);
	} else {
		$self->logit("(pid: $$) Exporting data from table $table...\n", 1);
	}

	# Rename table and double-quote it if required
	my $tmptb = $self->get_replaced_tbname($table);

	# register the column list and data type in dedicated structs
	@{$self->{data_cols}{$table}} = ();
	@{$self->{tables}{$table}{field_name}} = ();
	@{$self->{tables}{$table}{field_type}} = ();
	foreach my $k (sort {$self->{tables}{$table}{column_info}{$a}[11] <=> $self->{tables}{$table}{column_info}{$b}[11]} keys %{$self->{tables}{$table}{column_info}})
	{
		push(@{$self->{data_cols}{$table}}, $k);
		push(@{$self->{tables}{$table}{field_name}}, $self->{tables}{$table}{column_info}{$k}[0]);
		push(@{$self->{tables}{$table}{field_type}}, $self->{tables}{$table}{column_info}{$k}[1]);
	}

	# Open output file
	$self->data_dump($sql_header, $table) if (!$self->{pg_dsn} && $self->{file_per_table});

	my $total_record = 0;

	# When copy freeze is required, force a transaction with a truncate
	if ($self->{copy_freeze} && !$self->{pg_dsn})
	{
		$self->{truncate_table} = 1;
		if ($self->{file_per_table}) {
			$self->data_dump("BEGIN;\n",  $table);
		} else {
			$self->dump("\nBEGIN;\n");
		}
	} else {
		$self->{copy_freeze} = '';
	}

	# Open a new connection to PostgreSQL destination with parallel table export 
	my $local_dbh = undef;
	if (($self->{parallel_tables} > 1) && $self->{pg_dsn}) {
		$local_dbh = $self->_send_to_pgdb();
	} else {
		$local_dbh = $self->{dbhdest};
 	}

	if ($self->{global_delete} || exists $self->{delete}{"\L$table\E"})
	{
		my $delete_clause = '';
		my $delete_clause_start = "DELETE";
		if ($self->{datadiff}) {
			$delete_clause_start = "INSERT INTO " . $self->get_tbname_with_suffix($tmptb, $self->{datadiff_del_suffix}) . " SELECT *";
		}
		if (exists $self->{delete}{"\L$table\E"} && $self->{delete}{"\L$table\E"})
		{
			$delete_clause = "$delete_clause_start FROM $tmptb WHERE " . $self->{delete}{"\L$table\E"} . ";";
			$self->logit("\tApplying DELETE clause on table: " . $self->{delete}{"\L$table\E"} . "\n", 1);
		}
		elsif ($self->{global_delete})
		{
			$delete_clause = "$delete_clause_start FROM $tmptb WHERE " . $self->{global_delete} . ";";
			$self->logit("\tApplying DELETE global clause: " . $self->{global_delete} . "\n", 1);

		}
		if ($delete_clause)
		{
			if ($self->{pg_dsn})
			{
				$self->logit("Deleting from table $table...\n", 1);
				my $s = $local_dbh->do("$delete_clause") or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);
			}
			else
			{
				if ($self->{file_per_table}) {
					$self->data_dump("$delete_clause\n",  $table);
				} else {
					$self->dump("\n$delete_clause\n");
				}
			}
		}
	}

	# Set parent table name to compose partition name
	my $ptmptb = $table;
	if (exists $self->{replaced_tables}{"\L$table\E"} && $self->{replaced_tables}{"\L$table\E"})
	{
		$self->logit("\tReplacing table $table as " . $self->{replaced_tables}{lc($table)} . "...\n", 1);
		$ptmptb = $self->{replaced_tables}{lc($table)};
	}

	my $tbpart_name = '';
	if ($part_name)
	{
		$tbpart_name = $part_name;
		$tbpart_name = $ptmptb . '_part' . $pos if ($self->{rename_partition});
		if ($self->{rename_partition} && $part_name eq 'default') {
			$tbpart_name = $table . '_part_default';
		}
	}

	my $sub_tb_name = '';
	if ($subpart)
	{
		$sub_tb_name = $subpart;
		$sub_tb_name =~ s/^[^\.]+\.//; # remove schema part if any
		$sub_tb_name = $ptmptb . '_part' . $pos . '_subpart' . $p if ($self->{rename_partition});
		if ($self->{rename_partition} && $subpart eq 'default') {
			$sub_tb_name = $tbpart_name . '_subpart_default';
		}
	}

	# Set search path
	my $search_path = $self->set_search_path();

	# Add table truncate order if there's no global DELETE clause or one specific to the current table
	if ($self->{truncate_table} && !$self->{global_delete} && !exists $self->{delete}{"\L$table\E"})
	{
		my $truncate_order = "TRUNCATE TABLE ";
		if ($subpart) {
			$truncate_order .= $sub_tb_name;
		} elsif ($part_name) {
			$truncate_order .= $tbpart_name;
		} else {
			$truncate_order .= $tmptb;
		}
		if ($self->{pg_dsn} && !$self->{oracle_speed})
		{
			if ($search_path)
			{
				$self->logit("Setting search_path using: $search_path...\n", 1);
				$local_dbh->do($search_path) or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);
			}
			$self->logit("$truncate_order...\n", 1);
			my $s = $local_dbh->do($truncate_order) or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);
		}
		else
		{
			my $head = "SET client_encoding TO '\U$self->{client_encoding}\E';\n";
			$head .= "SET synchronous_commit TO off;\n" if (!$self->{synchronous_commit});
			if ($self->{file_per_table}) {
				$self->data_dump("$head$search_path\n$truncate_order;\n",  $table);
			} else {
				$self->dump("\n$head$search_path\n$truncate_order;\n");
			}
		}
	}
	else
	{
		my $head = "SET client_encoding TO '\U$self->{client_encoding}\E';\n";
		$head .= "SET synchronous_commit TO off;\n" if (!$self->{synchronous_commit});
		if ($self->{file_per_table}) {
			$self->data_dump("$head$search_path\n",  $table);
		} else {
			$self->dump("\n$head$search_path\n");
		}
	}

	# With partitioned table, load data direct from table partition or subpartition
	if ($subpart)
	{
		if ($self->{file_per_table} && !$self->{pg_dsn})
		{
			# Do not dump data again if the file already exists
			if ($self->file_exists("$dirprefix${sub_tb_name}_$self->{output}"))
			{
				# close the connection with parallel table export
				if (($self->{parallel_tables} > 1) && $self->{pg_dsn}) {
					$local_dbh->disconnect() if (defined $local_dbh);
				}
				return $total_record;
			}
		}

		$self->logit("Dumping sub partition table $table -> $part_name -> $subpart...\n", 1);
		$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $subpart, 1, $tbpart_name, $sub_tb_name);
		# Rename temporary filename into final name
		$self->rename_dump_partfile($dirprefix, $subpart, $table);

	}
	elsif ($part_name)
	{
		if ($self->{file_per_table} && !$self->{pg_dsn})
		{
			# Do not dump data again if the file already exists
			if ($self->file_exists("$dirprefix${tbpart_name}_$self->{output}"))
			{
				# close the connection with parallel table export
				if (($self->{parallel_tables} > 1) && $self->{pg_dsn}) {
					$local_dbh->disconnect() if (defined $local_dbh);
				}
				return $total_record;
			}
		}

		$self->logit("Dumping partition table $table -> $part_name...\n", 1);
		$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $part_name, 0, $tbpart_name);
		# Rename temporary filename into final name
		$self->rename_dump_partfile($dirprefix, $part_name, $table);
	}
	elsif (exists $self->{partitions}{$table})
	{
		foreach my $pos (sort {$self->{partitions}{$table}{$a} <=> $self->{partitions}{$table}{$b}} keys %{$self->{partitions}{$table}})
		{
			my $part_name = $self->{partitions}{$table}{$pos}{name};
			my $tbpart_name = $part_name;
			$tmptb = $table;
			if (exists $self->{replaced_tables}{"\L$table\E"} && $self->{replaced_tables}{"\L$table\E"})
			{
				$self->logit("\tReplacing table $table as " . $self->{replaced_tables}{lc($table)} . "...\n", 1);
				$tmptb = $self->{replaced_tables}{lc($table)};
			}
			$tbpart_name = $tmptb . '_part' . $pos if ($self->{rename_partition});
			next if ($self->{allow_partition} && !grep($_ =~ /^$tbpart_name$/i, @{$self->{allow_partition}}));

			if (exists $self->{subpartitions}{$table}{$part_name})
			{
				foreach my $p (sort {$a <=> $b} keys %{$self->{subpartitions}{$table}{$part_name}})
				{
					my $subpart = $self->{subpartitions}{$table}{$part_name}{$p}{name};
					next if ($self->{allow_partition} && !grep($_ =~ /^$subpart$/i, @{$self->{allow_partition}}));
					my $sub_tb_name = $subpart;
					$sub_tb_name =~ s/^[^\.]+\.//; # remove schema part if any
					$sub_tb_name = $tmptb . '_part' . $pos . '_subpart' . $p if ($self->{rename_partition});
					if ($self->{file_per_table} && !$self->{pg_dsn}) {
						# Do not dump data again if the file already exists
						next if ($self->file_exists("$dirprefix${sub_tb_name}_$self->{output}"));
					}

					if ($#{$self->{tables}{$table}{field_name}} < 0) {
						$self->logit("Table $table has no column defined, skipping...\n", 1);
						next;
					}

					$self->logit("Dumping sub partition table $table ($subpart)...\n", 1);
					$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $subpart, 1, $tbpart_name, $sub_tb_name);
					# Rename temporary filename into final name
					$self->rename_dump_partfile($dirprefix, $subpart, $table);
				}
				# Now load content of the default subpartition table
				if ($self->{subpartitions_default}{$table}{$part_name})
				{
					if (!$self->{allow_partition} || grep($_ =~ /^$self->{subpartitions_default}{$table}{$part_name}{name}$/i, @{$self->{allow_partition}}))
					{
						if ($self->{file_per_table} && !$self->{pg_dsn})
						{
							# Do not dump data again if the file already exists
							if (!$self->file_exists("$dirprefix$self->{subpartitions_default}{$table}{$part_name}{name}_$self->{output}"))
							{
								if ($self->{rename_partition}) {
									$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{subpartitions_default}{$table}{$part_name}{name}, 1, $tbpart_name, $tbpart_name . '_subpart_default');
								} else {
									$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{subpartitions_default}{$table}{$part_name}{name}, 1, $part_name, $self->{subpartitions_default}{$table}{$part_name}{name});
								}
							}
						}
						else
						{
							if ($self->{rename_partition}) {
								$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{subpartitions_default}{$table}{$part_name}{name}, 1, $tbpart_name, $tbpart_name . '_subpart_default');
							} else {
								$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{subpartitions_default}{$table}{$part_name}{name}, 1, $part_name, $self->{subpartitions_default}{$table}{$part_name}{name});
							}
						}
					}
					# Rename temporary filename into final name
					$self->rename_dump_partfile($dirprefix, $self->{subpartitions_default}{$table}{$part_name}{name}, $table);
				}
			}
			else
			{
				if ($self->{file_per_table} && !$self->{pg_dsn})
				{
					# Do not dump data again if the file already exists
					next if ($self->file_exists("$dirprefix${tbpart_name}_$self->{output}"));
				}

				$self->logit("Dumping partition table $table ($part_name)...\n", 1);
				$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $part_name, 0, $tbpart_name);
				# Rename temporary filename into final name
				$self->rename_dump_partfile($dirprefix, $part_name, $table);
			}
		}

		# Now load content of the default partition table
		if (exists $self->{partitions_default}{$table})
		{
			if (!$self->{allow_partition} || grep($_ =~ /^$self->{partitions_default}{$table}{name}$/i, @{$self->{allow_partition}}))
			{
				my $tbpart_name = $self->{partitions_default}{$table}{name};
				my $tmptb = $table;
				if (exists $self->{replaced_tables}{"\L$table\E"} && $self->{replaced_tables}{"\L$table\E"})
				{
					$self->logit("\tReplacing table $table as " . $self->{replaced_tables}{lc($table)} . "...\n", 1);
					$tmptb = $self->{replaced_tables}{lc($table)};
				}
				$tbpart_name = $tmptb . '_part_default' if ($self->{rename_partition});
				if ($self->{file_per_table} && !$self->{pg_dsn})
				{
					# Do not dump data again if the file already exists
					if (!$self->file_exists("$dirprefix$self->{partitions_default}{$table}{name}_$self->{output}"))
					{
						$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{partitions_default}{$table}{name}, 0, $tbpart_name);
					}
				}
				else
				{
					$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{partitions_default}{$table}{name}, 0, $tbpart_name);
				}
				# Rename temporary filename into final name
				$self->rename_dump_partfile($dirprefix, $self->{partitions_default}{$table}{name}, $table);
			}
		}
	}
	else
	{

		# Do not dump data if the table has no column
		if ($#{$self->{tables}{$table}{field_name}} < 0) {
			$self->logit("Table $table has no column defined, skipping...\n", 1);
		} else {
			$total_record = $self->_dump_table($dirprefix, $sql_header, $table);
		}
	}

 	# close the connection with parallel table export
 	if (($self->{parallel_tables} > 1) && $self->{pg_dsn}) {
 		$local_dbh->disconnect() if (defined $local_dbh);
 	}

	# Rename temporary filename into final name
	$self->rename_dump_partfile($dirprefix, $table) if (!$self->{oracle_speed});

	return $total_record;
}

sub _export_fdw_table_data
{
	my ($self, $table, $dirprefix, $sql_header) = @_;

	$self->logit("FATAL: foreign data export requires that PG_DSN to be set \n", 0, 1) if (!$self->{pg_dsn});

	$self->logit("Exporting data of table $table using foreign table...\n", 1);

	# Rename table and double-quote it if required
	my $tmptb = $self->get_replaced_tbname($table);
	my $total_record = 0;

	$self->{copy_freeze} = '';

	# Open a new connection to PostgreSQL destination with parallel table export
	my $local_dbh = undef;
	if ($self->{parallel_tables} > 1 && $self->{pg_dsn}) {
		$local_dbh = $self->_send_to_pgdb();
	} else {
		$local_dbh = $self->{dbhdest};
	}

	if ($self->{global_delete} || exists $self->{delete}{"\L$table\E"})
	{
		my $delete_clause = '';
		my $delete_clause_start = "DELETE";
		if (exists $self->{delete}{"\L$table\E"} && $self->{delete}{"\L$table\E"})
		{
			$delete_clause = "$delete_clause_start FROM $tmptb WHERE " . $self->{delete}{"\L$table\E"} . ";";
			$self->logit("\tApplying DELETE clause on table: " . $self->{delete}{"\L$table\E"} . "\n", 1);
		}
		elsif ($self->{global_delete})
		{
			$delete_clause = "$delete_clause_start FROM $tmptb WHERE " . $self->{global_delete} . ";";
			$self->logit("\tApplying DELETE global clause: " . $self->{global_delete} . "\n", 1);
		}

		if ($delete_clause)
		{
			$self->logit("Deleting from table $table...\n", 1);
			my $s = $local_dbh->do("$delete_clause") or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);
		}
	}

	# Add table truncate order if there's no global DELETE clause or one specific to the current table
	if ($self->{truncate_table} && !$self->{global_delete} && !exists $self->{delete}{"\L$table\E"})
	{
		# Set search path
		my $search_path = $self->set_search_path();
		if (!$self->{oracle_speed})
		{
			if ($search_path)
			{
				$self->logit("Setting search_path using: $search_path...\n", 1);
				$local_dbh->do($search_path) or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);
			}
			$self->logit("Truncating table $tmptb...\n", 1);

			my $s = $local_dbh->do("TRUNCATE TABLE $tmptb;") or $self->logit("FATAL: " . $local_dbh->errstr . ". SQL: TRUNCATE TABLE $tmptb;\n", 0, 1);
		}
	}

	$total_record = $self->_dump_fdw_table($dirprefix, $sql_header, $table, $local_dbh);

	# close the connection with parallel table export
	if ($self->{parallel_tables} > 1) {
		$local_dbh->disconnect() if (defined $local_dbh);
	}

	return $total_record;
}

sub rename_dump_partfile
{
	my ($self, $dirprefix, $partname, $tbl) = @_;

        my $filename = "${dirprefix}tmp_${tbl}_${partname}_$self->{output}";
        my $filedest = "${dirprefix}${tbl}_${partname}_$self->{output}";
        if (!$tbl)
        {
                $filename = "${dirprefix}tmp_${partname}_$self->{output}";
                $filedest = "${dirprefix}${partname}_$self->{output}";
	}
	if (-e $filename) {
		$self->logit("Renaming temporary file $filename into $filedest\n", 1);
		rename($filename, $filedest);
	}
}

sub set_refresh_count
{
	my $count = shift;

	return 500 if ($count > 10000);
	return 100 if ($count > 1000);
	return 10 if ($count > 100);
	return 1;
}

sub translate_function
{
	my ($self, $i, $num_total_function, %functions) = @_;

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});

	# Clear memory in multiprocess mode
	if ($self->{jobs} > 1)
	{
		$self->{functions} = (); 
		$self->{procedures} = (); 
	}

	my $t0 = Benchmark->new;

	my $sql_output = '';
	my $lsize = 0;
	my $lcost = 0;
	my $fct_count = 0;
	my $PGBAR_REFRESH = set_refresh_count($num_total_function);
	foreach my $fct (sort keys %functions)
	{
		if (!$self->{quiet} && !$self->{debug} && ($fct_count % $PGBAR_REFRESH) == 0)
		{
			print STDERR $self->progress_bar($i+1, $num_total_function, 25, '=', 'functions', "generating $fct" ), "\r";
		}
		$fct_count++;
		$self->logit("Dumping function $fct...\n", 1);
		if ($self->{file_per_function})
		{
			my $f = "$dirprefix${fct}_$self->{output}";
			$f = "${fct}_$self->{output}" if ($self->{psql_relative_path});
			$f =~ s/\.(?:gz|bz2)$//i;
			$self->dump("\\i$self->{psql_relative_path} $f\n");
			$self->save_filetoupdate_list("ORA2PG_$self->{type}", lc($fct), "$dirprefix${fct}_$self->{output}");
		}
		else {
			$self->save_filetoupdate_list("ORA2PG_$self->{type}", lc($fct), "$dirprefix$self->{output}");
		}

		my $fhdl = undef;

		$self->_remove_comments(\$functions{$fct}{text});
		$lsize = length($functions{$fct}{text});

		if ($self->{file_per_function})
		{
			$self->logit("Dumping to one file per function : ${fct}_$self->{output}\n", 1);
			$fhdl = $self->open_export_file("${fct}_$self->{output}");
			$self->set_binmode($fhdl) if (!$self->{compress});
		}
		if ($self->{plsql_pgsql})
		{
			my $sql_f = '';
			if ($self->{is_mysql}) {
				$sql_f = $self->_convert_function($functions{$fct}{owner}, $functions{$fct}{text}, $fct);
			} else {
				$sql_f = $self->_convert_function($functions{$fct}{owner}, $functions{$fct}{text});
			}
			if ( $sql_f )
			{
				$sql_output .= $sql_f . "\n\n";
				if ($self->{estimate_cost})
				{
					my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $sql_f, 'FUNCTION');
					$cost += $Ora2Pg::PLSQL::OBJECT_SCORE{'FUNCTION'};
					$lcost += $cost;
					$self->logit("Function ${fct} estimated cost: $cost\n", 1);
					$sql_output .= "-- Function ${fct} estimated cost: $cost\n";
					foreach (sort { $cost_detail{$b} <=> $cost_detail{$a} } keys %cost_detail)
					{
						next if (!$cost_detail{$_});
						$sql_output .= "\t-- $_ => $cost_detail{$_}";
						if (!$self->{is_mysql}) {
							$sql_output .= " (cost: $Ora2Pg::PLSQL::UNCOVERED_SCORE{$_})" if ($Ora2Pg::PLSQL::UNCOVERED_SCORE{$_});
						} else {
							$sql_output .= " (cost: $Ora2Pg::PLSQL::UNCOVERED_MYSQL_SCORE{$_})" if ($Ora2Pg::PLSQL::UNCOVERED_MYSQL_SCORE{$_});
						}
						$sql_output .= "\n";
					}
					if ($self->{jobs} > 1)
					{
						my $tfh = $self->append_export_file($dirprefix . 'temp_cost_file.dat', 1);
						flock($tfh, 2) || die "FATAL: can't lock file temp_cost_file.dat\n";
						$tfh->print("${fct}:$lsize:$lcost\n");
						$self->close_export_file($tfh, 1);
					}
				}
			}
		}
		else
		{
			$sql_output .= $functions{$fct}{text} . "\n\n";
		}
		$self->_restore_comments(\$sql_output);
		if ($self->{plsql_pgsql}) {
			$sql_output =~ s/(-- REVOKE ALL ON (?:FUNCTION|PROCEDURE) [^;]+ FROM PUBLIC;)/&remove_newline($1)/sge;
		}

		my $sql_header = "-- Generated by Ora2Pg, the Oracle database Schema converter, version $VERSION\n";
		$sql_header .= "-- Copyright 2000-2025 Gilles DAROLD. All rights reserved.\n";
		$sql_header .= "-- DATASOURCE: $self->{oracle_dsn}\n\n";
		if ($self->{client_encoding}) {
			$sql_header .= "SET client_encoding TO '\U$self->{client_encoding}\E';\n\n";
		}
		if ($self->{type} ne 'TABLE') {
			$sql_header .= $self->set_search_path();
		}
		$sql_header .= "\\set ON_ERROR_STOP ON\n\n" if ($self->{stop_on_error});
		$sql_header .= "SET check_function_bodies = false;\n\n" if (!$self->{function_check});
		$sql_header = '' if ($self->{no_header});

		if ($self->{file_per_function}) {
			$self->dump($sql_header . $sql_output, $fhdl);
			$self->close_export_file($fhdl);
			$sql_output = '';
		}
	}

	my $t1 = Benchmark->new;
	my $td = timediff($t1, $t0);
	$self->logit("Translating of $fct_count functions took: " . timestr($td) . "\n", 1);

	return ($sql_output, $lsize, $lcost);
}

sub _replace_declare_var
{
	my ($self, $code) = @_;

	if ($$code =~ s/\b(DECLARE\s+(?:.*?)\s+BEGIN)/\%DECLARE\%/is) {
		my $declare = $1;
		# Collect user defined exception
		while ($declare =~ s/\b([^\s]+)\s+EXCEPTION\s*;//i) {
			my $e = lc($1);
			if (!exists $Ora2Pg::PLSQL::EXCEPTION_MAP{"\U$e\L"} && !grep(/^$e$/, values %Ora2Pg::PLSQL::EXCEPTION_MAP) && !exists $self->{custom_exception}{$e}) {
				$self->{custom_exception}{$e} = $self->{exception_id}++;
			}
		}
		$declare =~ s/PRAGMA\s+EXCEPTION_INIT[^;]*;//igs;
		if ($self->{is_mysql}) {
			($$code, $declare) = Ora2Pg::MySQL::replace_mysql_variables($self, $$code, $declare);
		}
		$$code =~ s/\%DECLARE\%/$declare/is;
	} elsif ($self->{is_mysql}) {
		($$code, $declare) = Ora2Pg::MySQL::replace_mysql_variables($self, $$code, $declare);
		$$code = "DECLARE\n" . $declare . "\n" . $$code if ($declare);
	}

	# Replace call to raise exception
	foreach my $e (keys %{$self->{custom_exception}})
	{
		$$code =~ s/\bRAISE\s+$e\b/RAISE EXCEPTION '$e' USING ERRCODE = '$self->{custom_exception}{$e}'/igs;
		$$code =~ s/(\s+(?:WHEN|OR)\s+)$e\s+/$1SQLSTATE '$self->{custom_exception}{$e}' /igs;
	}

}

# Routine used to save the file to update in pass2 of translation
sub save_filetoupdate_list
{
	my ($self, $pname, $ftcname, $file_name) = @_;

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});

	my $tfh = $self->append_export_file($dirprefix . 'temp_pass2_file.dat', 1);
	flock($tfh, 2) || die "FATAL: can't lock file temp_pass2_file.dat\n";
	$tfh->print("${pname}:${ftcname}:$file_name\n");
	$self->close_export_file($tfh, 1);
}

=head2 _set_file_header

Returns a string containing the common header of each output file.

=cut

sub _set_file_header
{
	my $self = shift();

	return '' if ($self->{no_header});

	my $sql_header = "-- Generated by Ora2Pg, the Oracle database Schema converter, version $VERSION\n";
	$sql_header .= "-- Copyright 2000-2025 Gilles DAROLD. All rights reserved.\n";
	$sql_header .= "-- DATASOURCE: $self->{oracle_dsn}\n\n";
	if ($self->{client_encoding})
	{
		$sql_header .= "SET client_encoding TO '\U$self->{client_encoding}\E';\n\n";
	}
	if ($self->{type} ne 'TABLE')
	{
		$sql_header .= $self->set_search_path();
	}
	$sql_header .= "\\set ON_ERROR_STOP ON\n\n" if ($self->{stop_on_error});
	$sql_header .= "SET check_function_bodies = false;\n\n" if (!$self->{function_check});

	return $sql_header;
}

=head2 export_view

Export Oracle view into PostgreSQL compatible SQL statements.

=cut

sub export_view
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Add views definition...\n", 1);

	# Read DML from file if any
	if ($self->{input_file}) {
		$self->read_view_from_file();
	}
	my $nothing = 0;
	$self->dump($sql_header);
	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	my $i = 1;
	my $num_total_view = scalar keys %{$self->{views}};
	%ordered_views = %{$self->{views}};
	my $count_view = 0;
	my $PGBAR_REFRESH = set_refresh_count($num_total_view);
	foreach my $view (sort sort_view_by_iter keys %ordered_views)
	{
		$self->logit("\tAdding view $view...\n", 1);
		if (!$self->{quiet} && !$self->{debug} && ($count_view % $PGBAR_REFRESH) == 0)
		{
			print STDERR $self->progress_bar($i, $num_total_view, 25, '=', 'views', "generating $view" ), "\r";
		}
		$count_view++;
		my $fhdl = undef;
		if ($self->{file_per_table})
		{
			my $file_name = "$dirprefix${view}_$self->{output}";
			$file_name = "${view}_$self->{output}" if ($self->{psql_relative_path});
			$file_name =~ s/\.(gz|bz2)$//;
			$self->dump("\\i$self->{psql_relative_path} '$file_name'\n");
			$self->logit("Dumping to one file per view : ${view}_$self->{output}\n", 1);
			$fhdl = $self->open_export_file("${view}_$self->{output}");
			$self->set_binmode($fhdl) if (!$self->{compress});
			$self->save_filetoupdate_list("ORA2PG_$self->{type}", lc($view), $file_name);
		} else {
			$self->save_filetoupdate_list("ORA2PG_$self->{type}", lc($view), "$dirprefix$self->{output}");
		}
		$self->_remove_comments(\$self->{views}{$view}{text});
		if (!$self->{pg_supports_checkoption}) {
			$self->{views}{$view}{text} =~ s/\s*WITH\s+CHECK\s+OPTION//is;
		}
		# Remove unsupported definitions from the ddl statement
		$self->{views}{$view}{text} =~ s/\s*WITH\s+READ\s+ONLY//is;
		$self->{views}{$view}{text} =~ s/\s*OF\s+([^\s]+)\s+(WITH|UNDER)\s+[^\)]+\)//is;
		$self->{views}{$view}{text} =~ s/\s*OF\s+XMLTYPE\s+[^\)]+\)//is;
		$self->{views}{$view}{text} = $self->_format_view($view, $self->{views}{$view}{text});
		my $tmpv = $view;
		if (exists $self->{replaced_tables}{"\L$tmpv\E"} && $self->{replaced_tables}{"\L$tmpv\E"})
		{
			$self->logit("\tReplacing table $tmpv as " . $self->{replaced_tables}{lc($tmpv)} . "...\n", 1);
			$tmpv = $self->{replaced_tables}{lc($tmpv)};
		}
		if ($self->{export_schema} && !$self->{schema} && ($tmpv =~ /^([^\.]+)\./) ) {
			$sql_output .= $self->set_search_path($1) . "\n";
		}
		$tmpv = $self->quote_object_name($tmpv);

		if (!@{$self->{views}{$view}{alias}})
		{
			$sql_output .= "CREATE$self->{create_or_replace} VIEW $tmpv AS ";
			$sql_output .= $self->{views}{$view}{text};
			$sql_output .= ';' if ($sql_output !~ /;\s*$/s);
			$sql_output .= "\n";
			if ($self->{estimate_cost}) {
				my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $self->{views}{$view}{text}, 'VIEW');
				$cost += $Ora2Pg::PLSQL::OBJECT_SCORE{'VIEW'};
				$cost_value += $cost;
				$sql_output .= "\n-- Estimed cost of view [ $view ]: " . sprintf("%2.2f", $cost);
			}
			$sql_output .= "\n";
		}
		else
		{
			$sql_output .= "CREATE$self->{create_or_replace} VIEW $tmpv (";
			my $count = 0;
			my %col_to_replace = ();
			foreach my $d (@{$self->{views}{$view}{alias}})
			{
				if ($count == 0) {
					$count = 1;
				} else {
					$sql_output .= ", ";
				}
				# Change column names
				my $fname = $d->[0];
				if (exists $self->{replaced_cols}{"\L$view\E"}{"\L$fname\E"} && $self->{replaced_cols}{"\L$view\E"}{"\L$fname\E"})
				{
					$self->logit("\tReplacing column \L$d->[0]\E as " . $self->{replaced_cols}{"\L$view\E"}{"\L$fname\E"} . "...\n", 1);
					$fname = $self->{replaced_cols}{"\L$view\E"}{"\L$fname\E"};
				}
				$sql_output .= $self->quote_object_name($fname);
			}
			$sql_output .= ") AS " . $self->{views}{$view}{text};
			$sql_output .= ';' if ($sql_output !~ /;\s*$/s);
			$sql_output .= "\n";
			if ($self->{estimate_cost})
			{
				my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $self->{views}{$view}{text}, 'VIEW');
				$cost += $Ora2Pg::PLSQL::OBJECT_SCORE{'VIEW'};
				$cost_value += $cost;
				$sql_output .= "\n-- Estimed cost of view [ $view ]: " . sprintf("%2.2f", $cost);
			}
			$sql_output .= "\n";
		}

		if ($self->{force_owner})
		{
			my $owner = $self->{views}{$view}{owner};
			$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
			$sql_output .= "ALTER VIEW $tmpv OWNER TO " . $self->quote_object_name($owner) . ";\n";
		}

		# Add comments on view and columns
		if (!$self->{disable_comment})
		{
			if ($self->{views}{$view}{comment})
			{
				$sql_output .= "COMMENT ON VIEW $tmpv ";
				$self->{views}{$view}{comment} =~ s/'/''/gs;
				$sql_output .= " IS E'" . $self->{views}{$view}{comment} . "';\n\n";
			}

			foreach my $f (sort { lc{$a} cmp lc($b) } keys %{$self->{views}{$view}{column_comments}})
			{
				next unless $self->{views}{$view}{column_comments}{$f};
				$self->{views}{$view}{column_comments}{$f} =~ s/'/''/gs;
				# Change column names
				my $fname = $f;
				if (exists $self->{replaced_cols}{"\L$view\E"}{"\L$f\E"} && $self->{replaced_cols}{"\L$view\E"}{"\L$f\E"}) {
					$fname = $self->{replaced_cols}{"\L$view\E"}{"\L$f\E"};
				}
				$sql_output .= "COMMENT ON COLUMN " . $self->quote_object_name($tmpv) . '.'
						. $self->quote_object_name($fname)
						. " IS E'" . $self->{views}{$view}{column_comments}{$f} .  "';\n";
			}
		}

		if ($self->{file_per_table})
		{
			$self->dump($sql_header . $sql_output, $fhdl);
			$self->_restore_comments(\$sql_output);
			$self->close_export_file($fhdl);
			$sql_output = '';
		}
		$nothing++;
		$i++;

	}
	%ordered_views = ();

	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($i - 1, $num_total_view, 25, '=', 'views', 'end of output.'), "\n";
	}

	if (!$nothing) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	} else {
		$sql_output .= "\n";
	}

	$self->dump($sql_output);

	return;
}

=head2 export_mview

Export Oracle materialized view into PostgreSQL compatible SQL statements.

=cut

sub export_mview
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Add materialized views definition...\n", 1);

	my $nothing = 0;
	$self->dump($sql_header) if ($self->{file_per_table} && !$self->{pg_dsn});
	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	if ($self->{plsql_pgsql} && !$self->{pg_supports_mview})
	{
		$sql_header .= "DROP TABLE $self->{pg_supports_ifexists} materialized_views;\n" if ($self->{drop_if_exists});
		my $sqlout = qq{
$sql_header

CREATE TABLE materialized_views (
mview_name text NOT NULL PRIMARY KEY,
view_name text NOT NULL,
iname text,
last_refresh TIMESTAMP WITH TIME ZONE
);

CREATE OR REPLACE FUNCTION create_materialized_view(text, text, text)
RETURNS VOID
AS \$\$
DECLARE
mview ALIAS FOR \$1; -- name of the materialized view to create
vname ALIAS FOR \$2; -- name of the related view
iname ALIAS FOR \$3; -- name of the colum of mview to used as unique key
entry materialized_views%ROWTYPE;
BEGIN
EXECUTE 'SELECT * FROM materialized_views WHERE mview_name = ' || quote_literal(mview) || '' INTO entry;
IF entry.iname IS NOT NULL THEN
RAISE EXCEPTION 'Materialized view % already exist.', mview;
END IF;

EXECUTE 'REVOKE ALL ON ' || quote_ident(vname) || ' FROM PUBLIC';
EXECUTE 'GRANT SELECT ON ' || quote_ident(vname) || ' TO PUBLIC';
EXECUTE 'CREATE TABLE ' || quote_ident(mview) || ' AS SELECT * FROM ' || quote_ident(vname);
EXECUTE 'REVOKE ALL ON ' || quote_ident(mview) || ' FROM PUBLIC';
EXECUTE 'GRANT SELECT ON ' || quote_ident(mview) || ' TO PUBLIC';
INSERT INTO materialized_views (mview_name, view_name, iname, last_refresh)
VALUES (
quote_literal(mview), 
quote_literal(vname),
quote_literal(iname),
CURRENT_TIMESTAMP
);
IF iname IS NOT NULL THEN
EXECUTE 'CREATE INDEX ' || quote_ident(mview) || '_' || quote_ident(iname)  || '_idx ON ' || quote_ident(mview) || '(' || quote_ident(iname) || ')';
END IF;

RETURN;
END
\$\$
SECURITY DEFINER
LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION drop_materialized_view(text) RETURNS VOID
AS
\$\$
DECLARE
mview ALIAS FOR \$1;
entry materialized_views%ROWTYPE;
BEGIN
EXECUTE 'SELECT * FROM materialized_views WHERE mview_name = ''' || quote_literal(mview) || '''' INTO entry;
IF entry.iname IS NULL THEN
RAISE EXCEPTION 'Materialized view % does not exist.', mview;
END IF;

IF entry.iname IS NOT NULL THEN
EXECUTE 'DROP INDEX ' || quote_ident(mview) || '_' || entry.iname  || '_idx';
END IF;
EXECUTE 'DROP TABLE ' || quote_ident(mview);
EXECUTE 'DELETE FROM materialized_views WHERE mview_name=''' || quote_literal(mview) || '''';

RETURN;
END
\$\$
SECURITY DEFINER
LANGUAGE plpgsql ;

CREATE OR REPLACE FUNCTION refresh_full_materialized_view(text) RETURNS VOID
AS \$\$
DECLARE
mview ALIAS FOR \$1;
entry materialized_views%ROWTYPE;
BEGIN
EXECUTE 'SELECT * FROM materialized_views WHERE mview_name = ''' || quote_literal(mview) || '''' INTO entry;
IF entry.iname IS NULL THEN
RAISE EXCEPTION 'Materialized view % does not exist.', mview;
END IF;

IF entry.iname IS NOT NULL THEN
EXECUTE 'DROP INDEX ' || quote_ident(mview) || '_' || entry.iname  || '_idx';
END IF;
EXECUTE 'TRUNCATE ' || quote_ident(mview);
EXECUTE 'INSERT INTO ' || quote_ident(mview) || ' SELECT * FROM ' || entry.view_name;
EXECUTE 'UPDATE materialized_views SET last_refresh=CURRENT_TIMESTAMP WHERE mview_name=''' || quote_literal(mview) || '''';

IF entry.iname IS NOT NULL THEN
EXECUTE 'CREATE INDEX ' || quote_ident(mview) || '_' || entry.iname  || '_idx ON ' || quote_ident(mview) || '(' || entry.iname || ')';
END IF;

RETURN;
END
\$\$
SECURITY DEFINER
LANGUAGE plpgsql ;

};
		$self->dump($sqlout);
	}
	my $i = 1;
	my $num_total_mview = scalar keys %{$self->{materialized_views}};
	my $count_mview = 0;
	my $PGBAR_REFRESH = set_refresh_count($num_total_mview);
	foreach my $view (sort { $a cmp $b } keys %{$self->{materialized_views}})
	{
		$self->logit("\tAdding materialized view $view...\n", 1);
		if (!$self->{quiet} && !$self->{debug} && ($count_mview % $PGBAR_REFRESH) == 0) {
			print STDERR $self->progress_bar($i, $num_total_mview, 25, '=', 'materialized views', "generating $view" ), "\r";
		}
		$count_mview++;
		my $fhdl = undef;
		if ($self->{file_per_table} && !$self->{pg_dsn}) {
			my $file_name = "$dirprefix${view}_$self->{output}";
			$file_name = "${view}_$self->{output}" if ($self->{psql_relative_path});
			$file_name =~ s/\.(gz|bz2)$//;
			$self->dump("\\i$self->{psql_relative_path} '$file_name'\n");
			$self->logit("Dumping to one file per materialized view : ${view}_$self->{output}\n", 1);
			$fhdl = $self->open_export_file("${view}_$self->{output}");
			$self->set_binmode($fhdl) if (!$self->{compress});
			$self->save_filetoupdate_list("ORA2PG_$self->{type}", lc($view), $file_name);
		} else {
			$self->save_filetoupdate_list("ORA2PG_$self->{type}", lc($view), "$dirprefix$self->{output}");
		}
		if (!$self->{plsql_pgsql})
		{
			$sql_output .= "DROP MATERIALIZED VIEW $self->{pg_supports_ifexists} $view;\n" if ($self->{drop_if_exists});
			$sql_output .= "CREATE MATERIALIZED VIEW $view\n";
			if (!$self->{is_mysql} && !$self->{is_mssql})
			{
				$sql_output .= "BUILD $self->{materialized_views}{$view}{build_mode}\n";
				$sql_output .= "REFRESH $self->{materialized_views}{$view}{refresh_method} ON $self->{materialized_views}{$view}{refresh_mode}\n";
				$sql_output .= "ENABLE QUERY REWRITE" if ($self->{materialized_views}{$view}{rewritable});
				$sql_output .= "AS ";
			}
			$sql_output .= "$self->{materialized_views}{$view}{text}";
			if (!$self->{is_mysql} && !$self->{is_mssql})
			{
				$sql_output .= " USING INDEX" if ($self->{materialized_views}{$view}{no_index});
				$sql_output .= " USING NO INDEX" if (!$self->{materialized_views}{$view}{no_index});
			}
			$sql_output .= ";\n\n";

			# Set the index definition
			my ($idx, $fts_idx) = $self->_create_indexes($view, 0, %{$self->{materialized_views}{$view}{indexes}});
			$sql_output .= "$idx$fts_idx\n\n";
		} else {
			$self->{materialized_views}{$view}{text} = $self->_format_view($view, $self->{materialized_views}{$view}{text});
			if (!$self->{preserve_case}) {
				$self->{materialized_views}{$view}{text} =~ s/"//gs;
			}
			if ($self->{export_schema} && !$self->{schema} && ($view =~ /^([^\.]+)\./) ) {
				$sql_output .= $self->set_search_path($1) . "\n";
			}
			$self->{materialized_views}{$view}{text} =~ s/^PERFORM/SELECT/;
			if (!$self->{pg_supports_mview})
			{
				$sql_output .= "DROP VIEW $self->{pg_supports_ifexists} \L$view\E_mview;\n" if ($self->{drop_if_exists});
				$sql_output .= "CREATE VIEW \L$view\E_mview AS\n";
				if ($self->{is_mssql}) {
					$self->{materialized_views}{$view}{text} =~ s/^(.*?)\s+AS\s+//is;
				}
				$sql_output .= $self->{materialized_views}{$view}{text};
				$sql_output .= ";\n\n";
				$sql_output .= "SELECT create_materialized_view('\L$view\E','\L$view\E_mview', change with the name of the colum to used for the index);\n\n\n";

				if ($self->{force_owner})
				{
					my $owner = $self->{materialized_views}{$view}{owner};
					$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
					$sql_output .= "ALTER VIEW " . $self->quote_object_name($view . '_mview')
								. " OWNER TO " . $self->quote_object_name($owner) . ";\n";
				}
			}
			else
			{
				$sql_output .= "DROP MATERIALIZED VIEW $self->{pg_supports_ifexists} \L$view\E;\n" if ($self->{drop_if_exists});
				$sql_output .= "CREATE MATERIALIZED VIEW \L$view\E AS\n";
				if ($self->{is_mssql}) {
					$self->{materialized_views}{$view}{text} =~ s/^(.*?)\s+AS\s+//is;
				}
				$sql_output .= $self->{materialized_views}{$view}{text};
				if ($self->{materialized_views}{$view}{build_mode} eq 'DEFERRED') {
					$sql_output .= " WITH NO DATA";
				}
				$sql_output .= ";\n";
				# Set the index definition
				my ($idx, $fts_idx) = $self->_create_indexes($view, 0, %{$self->{materialized_views}{$view}{indexes}});
				$sql_output .= "$idx$fts_idx\n\n";
			}
		}
		if ($self->{force_owner})
		{
			my $owner = $self->{materialized_views}{$view}{owner};
			$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
			$sql_output .= "ALTER MATERIALIZED VIEW " . $self->quote_object_name($view)
						. " OWNER TO " . $self->quote_object_name($owner) . ";\n";
		}

		if ($self->{file_per_table} && !$self->{pg_dsn})
		{
			$self->dump($sql_header . $sql_output, $fhdl);
			$self->close_export_file($fhdl);
			$sql_output = '';
		}
		$nothing++;
		$i++;
	}
	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($i - 1, $num_total_mview, 25, '=', 'materialized views', 'end of output.'), "\n";
	}
	if (!$nothing) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}

	$self->dump($sql_output);

	return;
}

=head2 export_grant

Export Oracle user grants into PostgreSQL compatible SQL statements.

=cut

sub export_grant
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Add users/roles/grants privileges...\n", 1);

	my $grants = '';
	my $users = '';

	# Read DML from file if any
	if ($self->{input_file}) {
		$self->read_grant_from_file();
	}
	
	# Do not create privilege defintiion if object type is USER
	delete $self->{grants} if ($self->{grant_object} && $self->{grant_object} eq 'USER');

	# Add privilege definition
	foreach my $table (sort {"$self->{grants}{$a}{type}.$a" cmp "$self->{grants}{$b}{type}.$b" } keys %{$self->{grants}}) {
		my $realtable = lc($table);
		my $obj = $self->{grants}{$table}{type} || 'TABLE';
		$obj =~ s/ (PARTITION|SUBPARTITION)//i;
		if ($self->{export_schema} && $self->{schema}) {
			$realtable = $self->quote_object_name("$self->{schema}.$table");
		} elsif ($self->{preserve_case}) {
			$realtable =  $self->quote_object_name($table);
		}
		$grants .= "-- Set priviledge on $self->{grants}{$table}{type} $table\n";

		my $ownee = $self->quote_object_name($self->{grants}{$table}{owner});

		my $wgrantoption = '';
		if ($self->{grants}{$table}{grantable}) {
			$wgrantoption = ' WITH GRANT OPTION';
		}
		if ($self->{grants}{$table}{type} ne 'PACKAGE BODY')
		{
			if ($self->{grants}{$table}{owner})
			{
				if (grep(/^$self->{grants}{$table}{owner}$/, @{$self->{roles}{roles}}))
				{
					$grants .= "ALTER $obj $realtable OWNER TO ROLE $ownee;\n";
					$obj = '' if (!grep(/^$obj$/, 'FUNCTION', 'PROCEDURE', 'SEQUENCE','SCHEMA','TABLESPACE'));
					$grants .= "GRANT ALL ON $obj $realtable TO ROLE $ownee$wgrantoption;\n";
				}
				else
				{
					$grants .= "ALTER $obj $realtable OWNER TO $ownee;\n";
					$obj = '' if (!grep(/^$obj$/, 'FUNCTION', 'PROCEDURE', 'SEQUENCE','SCHEMA','TABLESPACE'));
					$grants .= "GRANT ALL ON $obj $realtable TO $ownee$wgrantoption;\n";
				}

				if ($realtable =~ /^([^\.]+)\./)
				{
					$grants .= "GRANT USAGE ON SCHEMA $1 TO $ownee;\n";
				}
			}
			if (grep(/^$self->{grants}{$table}{type}$/, 'FUNCTION', 'PROCEDURE', 'SEQUENCE','SCHEMA','TABLESPACE')) {
				$grants .= "REVOKE ALL ON $self->{grants}{$table}{type} $realtable FROM PUBLIC;\n";
			} else {
				$grants .= "REVOKE ALL ON $realtable FROM PUBLIC;\n";
			}
		}
		else
		{
			$realtable =~ s/^[^\.]+\.//;
			if ($self->{grants}{$table}{owner})
			{
				if (grep(/^$self->{grants}{$table}{owner}$/, @{$self->{roles}{roles}}))
				{
					$grants .= "ALTER SCHEMA $realtable OWNER TO ROLE $ownee;\n";
					$grants .= "GRANT EXECUTE ON ALL ROUTINES IN SCHEMA $realtable TO ROLE $ownee$wgrantoption;\n";
				}
				else
				{
					$grants .= "ALTER SCHEMA $realtable OWNER TO $ownee;\n";
					$grants .= "GRANT EXECUTE ON ALL ROUTINES IN SCHEMA $realtable TO $ownee$wgrantoption;\n";
				}
			}
			$grants .= "REVOKE ALL ON SCHEMA $realtable FROM PUBLIC;\n";
		}

		foreach my $usr (sort keys %{$self->{grants}{$table}{privilege}})
		{
			my $agrants = '';
			foreach my $g (@GRANTS) {
				$agrants .= "$g," if (grep(/^$g$/i, @{$self->{grants}{$table}{privilege}{$usr}}));
			}
			$agrants =~ s/,$//;
			$usr = $self->quote_object_name($usr);
			if ($self->{grants}{$table}{type} ne 'PACKAGE BODY')
			{
				if (grep(/^$self->{grants}{$table}{type}$/, 'FUNCTION', 'PROCEDURE', 'SEQUENCE','SCHEMA','TABLESPACE', 'TYPE')) {
					$grants .= "GRANT $agrants ON $obj $realtable TO $usr$wgrantoption;\n";
				} else {
					$grants .= "GRANT $agrants ON $realtable TO $usr$wgrantoption;\n";
				}

				if ($realtable =~ /^([^\.]+)\./)
				{
					$grants .= "GRANT USAGE ON SCHEMA $1 TO $usr;\n";
				}
			}
			else
			{
				$realtable =~ s/^[^\.]+\.//;
				$grants .= "GRANT USAGE ON SCHEMA $realtable TO $usr$wgrantoption;\n";
				$grants .= "GRANT EXECUTE ON ALL ROUTINES IN SCHEMA $realtable TO $usr$wgrantoption;\n";
			}
		}
		$grants .= "\n";
	}

	# Do not create user when privilege on an object type is asked
	delete $self->{roles} if ($self->{grant_object} && $self->{grant_object} ne 'USER');

	foreach my $r (@{$self->{roles}{owner}}, @{$self->{roles}{grantee}})
	{
		my $secret = 'change_my_secret';
		if ($self->{gen_user_pwd}) {
			$secret = &randpattern("CccnCccn");
		}
		$sql_header .= "CREATE " . ($self->{roles}{type}{$r} ||'USER') . " $r";
		$sql_header .= " WITH PASSWORD '$secret'" if ($self->{roles}{password_required}{$r} ne 'NO');
		# It's difficult to parse all oracle privilege. So if one admin option is set we set all PG admin option.
		if (grep(/YES|1/, @{$self->{roles}{$r}{admin_option}})) {
			$sql_header .= " CREATEDB CREATEROLE CREATEUSER INHERIT";
		}
		if ($self->{roles}{type}{$r} eq 'USER') {
			$sql_header .= " LOGIN";
		}
		if (exists $self->{roles}{role}{$r}) {
			$users .= " IN ROLE " . join(',', @{$self->{roles}{role}{$r}});
		}
		$sql_header .= ";\n";
	}
	if (!$grants) {
		$grants = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}

	$sql_output .= "\n" . $grants . "\n" if ($grants);

	$self->_restore_comments(\$grants);
	$self->dump($sql_header . $sql_output);

	return;
}

=head2 export_sequence

Export Oracle sequence into PostgreSQL compatible SQL statements.

=cut

sub export_sequence
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Add sequences definition...\n", 1);

	# Read DML from file if any
	if ($self->{input_file}) {
		$self->read_sequence_from_file();
	}
	my $i = 1;
	my $num_total_sequence = scalar keys %{$self->{sequences}};
	my $count_seq = 0;
	my $PGBAR_REFRESH = set_refresh_count($num_total_sequence);
	if ($self->{export_schema} && ($self->{schema} || $self->{pg_schema}))
	{
		if ($self->{create_schema}) {
			$sql_output .= "CREATE SCHEMA IF NOT EXISTS " . $self->quote_object_name($self->{pg_schema} || $self->{schema}) . ";\n";
		}
	}
	foreach my $seq (sort keys %{$self->{sequences}})
	{
		if (!$self->{quiet} && !$self->{debug} && ($count_seq % $PGBAR_REFRESH) == 0) {
			print STDERR $self->progress_bar($i, $num_total_sequence, 25, '=', 'sequences', "generating $seq" ), "\r";
		}
		$count_seq++;
		my $cache = '';
		$cache = $self->{sequences}{$seq}->[5] if ($self->{sequences}{$seq}->[5]);
		my $cycle = '';
		$cycle = ' CYCLE' if ($self->{sequences}{$seq}->[6] eq 'Y');
		$sql_output .= "DROP SEQUENCE $self->{pg_supports_ifexists} " . $self->quote_object_name($seq) . ";\n" if ($self->{drop_if_exists});
		$sql_output .= "CREATE SEQUENCE " . $self->quote_object_name($seq) . " INCREMENT $self->{sequences}{$seq}->[3]";
		if ($self->{sequences}{$seq}->[1] eq '' || $self->{sequences}{$seq}->[1] <= (-2**63/2)) {
			$sql_output .= " NO MINVALUE";
		# MSSQL has 32 bit sequences
		} elsif ($self->{sequences}{$seq}->[1] eq '' || $self->{sequences}{$seq}->[1] <= (-2**32/2)) {
			$sql_output .= " NO MINVALUE";
		} else {
			$sql_output .= " MINVALUE $self->{sequences}{$seq}->[1]";
		}
		# Max value lower than start value are not allowed
		if (($self->{sequences}{$seq}->[2] > 0) && ($self->{sequences}{$seq}->[2] < $self->{sequences}{$seq}->[4])) {
			if (!$cycle) {
				$self->{sequences}{$seq}->[2] = $self->{sequences}{$seq}->[4];
			} else {
				$self->{sequences}{$seq}->[4] = $self->{sequences}{$seq}->[1];
			}
		}
		if ($self->{sequences}{$seq}->[2] eq '' || $self->{sequences}{$seq}->[2] >= (2**63/2)-1) {
			$sql_output .= " NO MAXVALUE";
		# MSSQL has 32 bit sequences
		} elsif ($self->{sequences}{$seq}->[2] eq '' || $self->{sequences}{$seq}->[2] >= (2**32/2)-1) {
			$sql_output .= " NO MAXVALUE";
		} else {
			$self->{sequences}{$seq}->[2] = 9223372036854775807 if ($self->{sequences}{$seq}->[2] > 9223372036854775807);
			$sql_output .= " MAXVALUE $self->{sequences}{$seq}->[2]";
		}
		$sql_output .= " START $self->{sequences}{$seq}->[4]";
		$sql_output .= " CACHE $cache" if ($cache ne '');
		$sql_output .= "$cycle;\n";

		if ($self->{force_owner})
		{
			my $owner = $self->{sequences}{$seq}->[7];
			$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
			$sql_output .= "ALTER SEQUENCE " . $self->quote_object_name($seq)
						. " OWNER TO " . $self->quote_object_name($owner) . ";\n";
		}
		$i++;
	}
	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($i - 1, $num_total_sequence, 25, '=', 'sequences', 'end of output.'), "\n";
	}
	if (!$sql_output) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}

	$self->dump($sql_header . $sql_output);

	return;
}

=head2 export_sequence_values

Export Oracle sequence last values into PostgreSQL compatible SQL statements.

=cut

sub export_sequence_values
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = $self->set_search_path() . "\n";

	$self->logit("Add sequences last values...\n", 1);

	# Read DML from file if any
	if ($self->{input_file}) {
		$self->read_sequence_from_file();
	}
	my $i = 1;
	my $num_total_sequence = scalar keys %{$self->{sequences}};
	my $count_seq = 0;
	my $PGBAR_REFRESH = set_refresh_count($num_total_sequence);
	foreach my $seq (sort keys %{$self->{sequences}})
	{
		if (!$self->{quiet} && !$self->{debug} && ($count_seq % $PGBAR_REFRESH) == 0) {
			print STDERR $self->progress_bar($i, $num_total_sequence, 25, '=', 'sequences', "generating $seq" ), "\r";
		}
		$count_seq++;
		$sql_output .= "ALTER SEQUENCE " . $self->quote_object_name($seq) . " START WITH $self->{sequences}{$seq}->[4];\n";
		$i++;
	}
	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($i - 1, $num_total_sequence, 25, '=', 'sequences', 'end of output.'), "\n";
	}
	if (!$sql_output) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}

	$self->dump($sql_header . $sql_output);

	return;
}


=head2 export_dblink

Export Oracle sequence into PostgreSQL compatible SQL statements.

=cut

sub export_dblink
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Add dblink definition...\n", 1);

	# Read DML from file if any
	if ($self->{input_file}) {
		$self->read_dblink_from_file();
	}
	my $i = 1;
	my $num_total_dblink = scalar keys %{$self->{dblink}};

	foreach my $db (sort { $a cmp $b } keys %{$self->{dblink}})
	{
		if (!$self->{quiet} && !$self->{debug}) {
			print STDERR $self->progress_bar($i, $num_total_dblink, 25, '=', 'dblink', "generating $db" ), "\r";
		}
		my $srv_name = $self->quote_object_name($db);
		$srv_name =~ s/^.*\.//;
		$sql_output .= "CREATE SERVER $srv_name";
		if ($self->{is_mysql}) {
			$sql_output .= " FOREIGN DATA WRAPPER mysql_fdw OPTIONS (host '$self->{dblink}{$db}{host}'";
			$sql_output .= ", port '$self->{dblink}{$db}{port}'" if ($self->{dblink}{$db}{port});
			$sql_output .= ");\n";
		}
		elsif ($self->{is_mssql})
		{
			$self->{oracle_dsn} =~ /driver=([^;]+);/;
			my $driver = $1 || 'msodbcsql18';
			$sql_output .= " FOREIGN DATA WRAPPER odbc_fdw OPTIONS (odbc_driver '$driver', odbc_server '$self->{dblink}{$db}{host}'";
			$sql_output .= ", odbc_port '$self->{dblink}{$db}{port}'" if ($self->{dblink}{$db}{port});
			$sql_output .= ");\n";
		}
		else
		{
			$sql_output .= " FOREIGN DATA WRAPPER oracle_fdw OPTIONS (dbserver '$self->{dblink}{$db}{host}');\n";
		}
		if ($self->{dblink}{$db}{password} ne 'NONE')
		{
			$self->{dblink}{$db}{password} ||= 'secret';
			$self->{dblink}{$db}{password} = ", password '$self->{dblink}{$db}{password}'";
		}
		if ($self->{dblink}{$db}{username})
		{
			my $usr_name = $self->quote_object_name($self->{dblink}{$db}{username});
			$usr_name =~ s/^.*\.//;
			$sql_output .= "CREATE USER MAPPING FOR \"$usr_name\" SERVER $srv_name";
			$usr_name = $self->quote_object_name($self->{dblink}{$db}{user});
			$usr_name =~ s/^.*\.//;
			$sql_output .= " OPTIONS (user '$usr_name' $self->{dblink}{$db}{password});\n";
		}
		
		if ($self->{force_owner})
		{
			my $owner = $self->{dblink}{$db}{owner};
			$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
			$sql_output .= "ALTER SERVER $srv_name"
						. " OWNER TO " . $self->quote_object_name($owner) . ";\n";
		}
		$i++;
	}
	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($i - 1, $num_total_dblink, 25, '=', 'dblink', 'end of output.'), "\n";
	}
	if (!$sql_output) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}

	$self->dump($sql_header . $sql_output);

	return;
}

=head2 export_directory

Export Oracle directory into PostgreSQL compatible SQL statements.

=cut

sub export_directory
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Add directory definition...\n", 1);

	# Read DML from file if any
	if ($self->{input_file}) {
		$self->read_directory_from_file();
	}
	my $i = 1;
	my $num_total_directory = scalar keys %{$self->{directory}};

	foreach my $db (sort { $a cmp $b } keys %{$self->{directory}}) {

		if (!$self->{quiet} && !$self->{debug}) {
			print STDERR $self->progress_bar($i, $num_total_directory, 25, '=', 'directory', "generating $db" ), "\r";
		}
		$sql_output .= "INSERT INTO external_file.directories (directory_name,directory_path) VALUES ('$db', '$self->{directory}{$db}{path}');\n";
		foreach my $owner (keys %{$self->{directory}{$db}{grantee}}) {
			my $write = 'false';
			$write = 'true' if ($self->{directory}{$db}{grantee}{$owner} =~ /write/i);
			$sql_output .= "INSERT INTO external_file.directory_roles(directory_name,directory_role,directory_read,directory_write) VALUES ('$db','" . $self->quote_object_name($owner) . "', true, $write);\n";
		}
		$i++;
	}
	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($i - 1, $num_total_directory, 25, '=', 'directory', 'end of output.'), "\n";
	}
	if (!$sql_output) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}

	$self->dump($sql_header . $sql_output);

	return;
}

sub _replace_sql_type
{
	my ($self, $str) = @_;

	if ($self->{is_mysql}) {
		$str = Ora2Pg::MySQL::replace_sql_type($self, $str);
	} elsif ($self->{is_mssql}) {
		$str = Ora2Pg::MSSQL::replace_sql_type($self, $str);
	} else {
		$str = Ora2Pg::PLSQL::replace_sql_type($self, $str);
	}

	return $str;
}

=head2 export_trigger

Export Oracle trigger into PostgreSQL compatible SQL statements.

=cut

sub export_trigger
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Add triggers definition...\n", 1);

	$self->dump($sql_header);
	# Read DML from file if any
	if ($self->{input_file}) {
		$self->read_trigger_from_file();
	}
	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	my $nothing = 0;
	my $i = 1;      
	my $num_total_trigger = $#{$self->{triggers}} + 1;
	my $count_trig = 0;
	my $PGBAR_REFRESH = set_refresh_count($num_total_trigger);
	foreach my $trig (sort {$a->[0] cmp $b->[0]} @{$self->{triggers}})
	{
		if (!$self->{quiet} && !$self->{debug} && ($count_trig % $PGBAR_REFRESH) == 0) {
			print STDERR $self->progress_bar($i, $num_total_trigger, 25, '=', 'triggers', "generating $trig->[0]" ), "\r";
		}
		$count_trig++;
		my $fhdl = undef;
		if ($self->{file_per_function})
		{
			my $schm = '';
			$schm = $trig->[8] . '-' if ($trig->[8] && $self->{export_schema} && !$self->{schema});
			my $f = "$dirprefix$schm$trig->[0]_$self->{output}";
			$f = "$schm$trig->[0]_$self->{output}" if ($self->{psql_relative_path});
			$f =~ s/\.(?:gz|bz2)$//i;
			$self->dump("\\i$self->{psql_relative_path} $f\n");
			$self->logit("Dumping to one file per trigger : $schm$trig->[0]_$self->{output}\n", 1);
			$fhdl = $self->open_export_file("$schm$trig->[0]_$self->{output}");
			$self->set_binmode($fhdl) if (!$self->{compress});
			$self->save_filetoupdate_list("ORA2PG_$self->{type}", lc($trig->[0]), "$dirprefix$schm$trig->[0]_$self->{output}");
		}
		else
		{
			$self->save_filetoupdate_list("ORA2PG_$self->{type}", lc($trig->[0]), "$dirprefix$self->{output}");
		}
		$trig->[1] =~ s/\s*EACH ROW//is;
		chomp($trig->[4]);

		$trig->[4] =~ s/([^\*])[;\/]$/$1/;

		# reordering of event when there is a OF keyword to specify columns
		$trig->[2] =~ s/(\s+OR\s+UPDATE)(\s+.*)(\s+OF\s+)/$2$1$3/i;
		$self->logit("\tDumping trigger $trig->[0] defined on table $trig->[3]...\n", 1);
		my $tbname = $self->get_replaced_tbname($trig->[3]);

		# Store current trigger table name for possible use in outer join translation
		$self->{current_trigger_table} = $trig->[3];

		# Replace column name in function code
		if (exists $self->{replaced_cols}{"\L$trig->[3]\E"})
		{
			foreach my $coln (sort keys %{$self->{replaced_cols}{"\L$trig->[3]\E"}})
			{
				$self->logit("\tReplacing column \L$coln\E as " . $self->{replaced_cols}{"\L$trig->[3]\E"}{"\L$coln\E"} . "...\n", 1);
				my $cname = $self->{replaced_cols}{"\L$trig->[3]\E"}{"\L$coln\E"};
				$cname = $self->quote_object_name($cname);
				$trig->[4] =~ s/(OLD|NEW)\.$coln\b/$1\.$cname/igs;
				$trig->[6] =~ s/\b$coln\b/$self->{replaced_cols}{"\L$trig->[3]\E"}{"\L$coln\E"}/is;
			}
		}
		# Extract columns specified in the UPDATE OF ... ON clause
		my $cols = '';
		if ($trig->[2] =~ /UPDATE/ && $trig->[6] =~ /UPDATE\s+OF\s+(.*?)\s+ON/i)
		{
			my @defs = split(/\s*,\s*/, $1);
			$cols = ' OF ';
			foreach my $c (@defs) {
				$cols .= $self->quote_object_name($c) . ',';
			}
			$cols =~ s/,$//;
		}

		if ($self->{export_schema} && !$self->{schema}) {
			$sql_output .= $self->set_search_path($trig->[8]) . "\n";
		}
		# Check if it's like a pg rule
		$self->_remove_comments(\$trig->[4]);
		if (!$self->{pg_supports_insteadof} && $trig->[1] =~ /INSTEAD OF/)
		{
			if ($self->{plsql_pgsql})
			{
				$trig->[4] = Ora2Pg::PLSQL::convert_plsql_code($self, $trig->[4]);
				$self->_replace_declare_var(\$trig->[4]);
			}
			$sql_output .= "CREATE$self->{create_or_replace} RULE " . $self->quote_object_name($trig->[0])
						. " AS\n\tON " . $self->quote_object_name($trig->[2])
						. " TO " . $self->quote_object_name($tbname)
						. "\n\tDO INSTEAD\n(\n\t$trig->[4]\n);\n\n";
			if ($self->{force_owner})
			{
				my $owner = $trig->[8];
				$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
				$sql_output .= "ALTER RULE " . $self->quote_object_name($trig->[0])
							. " OWNER TO " . $self->quote_object_name($owner) . ";\n";
			}
		}
		else
		{
			# Replace direct call of a stored procedure in triggers
			if ($trig->[7] eq 'CALL')
			{
				if ($self->{plsql_pgsql})
				{
					$trig->[4] = Ora2Pg::PLSQL::convert_plsql_code($self, $trig->[4]);
					$self->_replace_declare_var(\$trig->[4]);
				}
				$trig->[4] = "BEGIN\nPERFORM $trig->[4];\nEND;";
			}
			else
			{
				my $ret_kind = 'RETURN NEW;';
				if (uc($trig->[2]) eq 'DELETE') {
					$ret_kind = 'RETURN OLD;';
				} elsif (uc($trig->[2]) =~ /DELETE/) {
					$ret_kind = "IF TG_OP = 'DELETE' THEN\n\tRETURN OLD;\nELSE\n\tRETURN NEW;\nEND IF;\n";
				}
				if ($self->{plsql_pgsql})
				{
					# Add a semi colon if none
					if ($trig->[4] !~ /\bBEGIN\b/i)
					{
						chomp($trig->[4]);
						$trig->[4] .= ';' if ($trig->[4] !~ /;\s*$/s);
						$trig->[4] = "BEGIN\n$trig->[4]\n$ret_kind\nEND;";
					}
					$trig->[4] = Ora2Pg::PLSQL::convert_plsql_code($self, $trig->[4]);
					$self->_replace_declare_var(\$trig->[4]);

					# When an exception statement is used enclosed everything
					# in a block before returning NEW
					if ($trig->[4] =~ /EXCEPTION(.*?)\b(END[;]*)[\s\/]*$/is)
					{
						$trig->[4] =~ s/^\s*BEGIN/BEGIN\n  BEGIN/ism;
						$trig->[4] =~ s/\b(END[;]*)[\s\/]*$/  END;\n$1/is;
					}
					# Add return statement.
					$trig->[4] =~ s/(?:$ret_kind\s+)?\b(END[;]*)(\s*\%ORA2PG_COMMENT\d+\%\s*)?[\s\/]*$/$ret_kind\n$1$2/igs;
					# Look at function header to convert sql type
					my @parts = split(/BEGIN/i, $trig->[4]);
					if ($#parts > 0) {
						$parts[0] = $self->_replace_sql_type($parts[0]);
					}
					$trig->[4] = join('BEGIN', @parts);
					$trig->[4] =~ s/\bRETURN\s*;/$ret_kind/igs;
				}
			}
			$sql_output .= "DROP TRIGGER $self->{pg_supports_ifexists} " . $self->quote_object_name($trig->[0])
						. " ON " . $self->quote_object_name($tbname) . " CASCADE;\n";
			my $security = '';
			my $revoke = '';
			my $trig_fctname = $self->quote_object_name("trigger_fct_\L$trig->[0]\E");
			if ($self->{export_schema} && ($self->{pg_schema} || $self->{schema} || $trig->[8])) {
				$trig_fctname = $self->quote_object_name($self->{pg_schema} || $self->{schema} || $trig->[8]) . ".$trig_fctname";
			}

			if ($self->{security}{"\U$trig->[0]\E"}{security} eq 'DEFINER')
			{
				$security = " SECURITY DEFINER";
				$revoke = "-- REVOKE ALL ON FUNCTION $trig_fctname() FROM PUBLIC;\n";
			}
			$security = " SECURITY INVOKER" if ($self->{force_security_invoker});
			$trig->[4] =~ s/CREATE TRIGGER (.*?)\sAS\s+//s;
			if ($self->{pg_supports_when} && $trig->[5])
			{
				if (!$self->{preserve_case})
				{
					$trig->[4] =~ s/"([^"]+)"/\L$1\E/gs;
					$trig->[4] =~ s/ALTER TRIGGER\s+[^\s]+\s+ENABLE(;)?//;
				}
				$sql_output .= "CREATE$self->{create_or_replace} FUNCTION $trig_fctname() RETURNS trigger AS \$BODY\$\n$trig->[4]\n\$BODY\$\n LANGUAGE 'plpgsql'$security;\n$revoke\n";
				if ($self->{force_owner})
				{
					my $owner = $trig->[8];
					$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
					$sql_output .= "ALTER FUNCTION $trig_fctname() OWNER TO " . $self->quote_object_name($owner) . ";\n\n";
				}
				$self->_remove_comments(\$trig->[6]);
				$trig->[6] =~ s/\n+$//s;
				$trig->[6] =~ s/^[^\.\s]+\.//;
				if (!$self->{preserve_case}) {
					$trig->[6] =~ s/"([^"]+)"/\L$1\E/gs;
				}
				chomp($trig->[6]);
				# Remove referencing clause, not supported by PostgreSQL < 10
				if ($self->{pg_version} < 10) {
					$trig->[6] =~ s/REFERENCING\s+(.*?)(FOR\s+EACH\s+)/$2/is;
				}
				$trig->[6] =~ s/REFERENCING\s+(NEW|OLD)\s+AS\s+(NEW|OLD)\s+(NEW|OLD)\s+AS\s+(NEW|OLD)//gsi;
				$trig->[6] =~ s/^\s*["]*(?:$trig->[0])["]*//is;
				$trig->[6] =~ s/\s+ON\s+([^"\s]+)\s+/" ON " . $self->quote_object_name($1) . " "/ies;
				$sql_output .= "DROP TRIGGER $self->{pg_supports_ifexists} " . $self->quote_object_name($trig->[0]) . " ON " . $self->quote_object_name($1) . ";\n" if ($self->{drop_if_exists});
				$sql_output .= "CREATE TRIGGER " . $self->quote_object_name($trig->[0]) . "$trig->[6]\n";

				if ($trig->[5])
				{
					$self->_remove_comments(\$trig->[5]);
					$trig->[5] =~ s/"([^"]+)"/\L$1\E/gs if (!$self->{preserve_case});
					if ($self->{plsql_pgsql})
					{
						$trig->[5] = Ora2Pg::PLSQL::convert_plsql_code($self, $trig->[5]);
						$self->_replace_declare_var(\$trig->[5]);
					}
					$sql_output .= "\tWHEN ($trig->[5])\n";
				}
				if ($trig->[6] =~ /REFERENCING/)
				{
					$trig->[6] =~ s/REFERENCING\s+(NEW|OLD)\s+AS\s+(NEW|OLD)\s+(NEW|OLD)\s+AS\s+(NEW|OLD)//gsi;
					$sql_output .= "$trig->[6] ";
				}
				$sql_output .= "\tEXECUTE PROCEDURE $trig_fctname();\n\n";
			}
			else
			{
				if (!$self->{preserve_case})
				{
					$trig->[4] =~ s/"([^"]+)"/\L$1\E/gs;
					$trig->[4] =~ s/ALTER TRIGGER\s+[^\s]+\s+ENABLE(;)?//;
				}
				$sql_output .= "CREATE$self->{create_or_replace} FUNCTION $trig_fctname() RETURNS trigger AS \$BODY\$\n$trig->[4]\n\$BODY\$\n LANGUAGE 'plpgsql'$security;\n$revoke\n";
				if ($self->{force_owner})
				{
					my $owner = $trig->[8];
					$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
					$sql_output .= "ALTER FUNCTION $trig_fctname() OWNER TO " . $self->quote_object_name($owner) . ";\n\n";
				}
				$sql_output .= "DROP TRIGGER $self->{pg_supports_ifexists} " . $self->quote_object_name($trig->[0]) . " ON " . $self->quote_object_name($tbname) . ";\n" if ($self->{drop_if_exists});
				$sql_output .= "CREATE TRIGGER " . $self->quote_object_name($trig->[0]) . "\n\t";
				my $statement = 0;
				$statement = 1 if ($trig->[1] =~ s/ STATEMENT//);
				$sql_output .= "$trig->[1] $trig->[2]$cols ON " . $self->quote_object_name($tbname) . " ";
				if ($trig->[6] =~ s/.*(REFERENCING\s+.*)/$1/is) {
					$trig->[6] =~ s/REFERENCING\s+(NEW|OLD)\s+AS\s+(NEW|OLD)(\s+(NEW|OLD)\s+AS\s+(NEW|OLD))?//gsi;
					$trig->[6] =~ s/\s+FOR EACH ROW//gsi;
					$sql_output .= "$trig->[6] ";
				}
				if ($self->{is_mssql}) {
					my $reftb = "REFERENCING OLD TABLE AS Deleted NEW TABLE AS Inserted";
					$reftb =~ s/OLD TABLE AS Deleted // if ($trig->[2] eq 'INSERT');
					$reftb =~ s/NEW TABLE AS Inserted // if ($trig->[2] eq 'DELETE');
					$sql_output .= "$reftb FOR EACH STATEMENT\n" if ($trig->[1] !~ /INSTEAD OF/is);
				} elsif ($statement) {
					$sql_output .= "FOR EACH STATEMENT\n" if ($trig->[1] !~ /INSTEAD OF/is);
				} else {
					$sql_output .= "FOR EACH ROW\n" if ($trig->[1] !~ /INSTEAD OF/is);
				}
				$sql_output .= "\tEXECUTE PROCEDURE $trig_fctname();\n\n";
			}
		}
		$self->_restore_comments(\$sql_output);
		if ($self->{file_per_function})
		{
			$self->dump($sql_header . $sql_output, $fhdl);
			$self->close_export_file($fhdl);
			$sql_output = '';
		}
		$nothing++;
		$i++;
	}
	delete $self->{current_trigger_table};

	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($i - 1, $num_total_trigger, 25, '=', 'triggers', 'end of output.'), "\n";
	}
	if (!$nothing) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}

	$self->dump($sql_output);

	return;
}

=head2 parallelize_statements

Parallelize SQL statements to import into PostgreSQL.

=cut

sub parallelize_statements
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Parse SQL orders to load...\n", 1);

	my $nothing = 0;
	#---------------------------------------------------------
	# Load a file containing SQL code to load into PostgreSQL
	#---------------------------------------------------------
	my %comments = ();
	my @settings = ();
	if ($self->{input_file})
	{
		$self->{functions} = ();
		$self->logit("Reading input SQL orders from file $self->{input_file}...\n", 1);
		my $content = $self->read_input_file($self->{input_file});
		# remove comments only, text constants are preserved
		$self->_remove_comments(\$content, 1);
		$content =~  s/\%ORA2PG_COMMENT\d+\%//gs;
		my $query = 1;
		foreach my $l (split(/\n/, $content))
		{
			chomp($l);
			next if ($l =~ /^\s*$/);
			# do not parse interactive or session command
			next if ($l =~ /^(\\set|\\pset|\\i)/is);
			# Put setting change in header to apply them on all parallel session
			# This will help to set a special search_path or encoding
			if ($l =~ /^SET\s+/i)
			{
				push(@settings, $l);
				next;
			}
			if ($old_line)
			{
				$l = $old_line .= ' ' . $l;
				$old_line = '';
			}
			if ($l =~ /;\s*$/)
			{
					$self->{queries}{$query} .= "$l\n";
					$query++;
			} else {
				$self->{queries}{$query} .= "$l\n";
			}
		}
	} else {
		$self->logit("No input file, aborting...\n", 0, 1);
	}

	#--------------------------------------------------------
	my $total_queries = scalar keys %{$self->{queries}};
	$self->{child_count} = 0;
	foreach my $q (sort {$a <=> $b} keys %{$self->{queries}})
	{
		chomp($self->{queries}{$q});
		next if (!$self->{queries}{$q});
		if ($self->{jobs} > 1)
		{
			while ($self->{child_count} >= $self->{jobs})
			{
				my $kid = waitpid(-1, WNOHANG);
				if ($kid > 0)
				{
					$self->{child_count}--;
					delete $RUNNING_PIDS{$kid};
				}
				usleep(50000);
			}
			spawn sub {
				$self->_pload_to_pg($q, $self->{queries}{$q}, @settings);
			};
			$self->{child_count}++;
		} else {
			$self->_pload_to_pg($q, $self->{queries}{$q}, @settings);
		}
		if (!$self->{quiet} && !$self->{debug}) {
			print STDERR $self->progress_bar($q, $total_queries, 25, '=', 'queries', "dispatching query #$q" ), "\r";
		}
		$nothing++;
	}
	$self->{queries} = ();

	if (!$total_queries) {
		$self->logit("No query to load...\n", 0);
	} else {
		# Wait for all child end
		while ($self->{child_count} > 0)
		{
			my $kid = waitpid(-1, WNOHANG);
			if ($kid > 0)
			{
				$self->{child_count}--;
				delete $RUNNING_PIDS{$kid};
			}
			usleep(50000);
		}
		if (!$self->{quiet} && !$self->{debug}) {
			print STDERR "\n";
		}
	}
	return;
}

=head2 translate_query

Translate Oracle's queries into PostgreSQL compatible statement.

=cut

sub translate_query
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Parse queries definition...\n", 1);
	$self->dump($sql_header);

	my $nothing = 0;
	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	#---------------------------------------------------------
	# Code to use to find queries parser issues, it load a file
	# containing the untouched SQL code from Oracle queries
	#---------------------------------------------------------
	if ($self->{input_file})
	{
		$self->{functions} = ();
		$self->logit("Reading input code from file $self->{input_file}...\n", 1);
		my $content = $self->read_input_file($self->{input_file});
		$self->_remove_comments(\$content);
		my $query = 1;
		foreach my $l (split(/(?:^\/$|;\s*$)/m, $content))
		{
			chomp($l);
			next if ($l =~ /^\s*$/s);
			$self->{queries}{$query}{code} = "$l\n";
			$query++;
		}
		$content = '';
		foreach my $q (keys %{$self->{queries}}) {
			$self->_restore_comments(\$self->{queries}{$q}{code});
		}
	}
	
	foreach my $q (sort { $a <=> $b } keys %{$self->{queries}})
	{
		if ($self->{queries}{$q}{code} !~ /(SELECT|UPDATE|DELETE|INSERT|DROP|TRUNCATE|CREATE(?:UNIQUE)? INDEX|ALTER)/is) {
			$self->{queries}{$q}{to_be_parsed} = 0;
		} else {
			$self->{queries}{$q}{to_be_parsed} = 1;
		}
	}

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

	my $total_size = 0;
	my $cost_value = 0;
	foreach my $q (sort {$a <=> $b} keys %{$self->{queries}})
	{
		$total_size += length($self->{queries}{$q}{code});
		$self->logit("Dumping query $q...\n", 1);
		if ($self->{queries}{$q}{to_be_parsed})
		{
			if ($self->{plsql_pgsql}) {
				$self->_remove_comments(\$self->{queries}{$q}{code});
				if (!$self->{preserve_case}) {
					$self->{queries}{$q}{code} =~ s/"//gs;
				}
				my $sql_q = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{queries}{$q}{code});
				my $estimate = '';
				if ($self->{estimate_cost})
				{
					my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $sql_q, 'QUERY');
					$cost += $Ora2Pg::PLSQL::OBJECT_SCORE{'QUERY'};
					$cost_value += $cost;
					$estimate = "\n-- Estimed cost of query [ $q ]: " . sprintf("%2.2f", $cost);
				}
				$self->_restore_comments(\$sql_q);
				$sql_output .= $sql_q;
				$sql_output .= ';' if ($sql_q !~ /;\s*$/);
				$sql_output .= $estimate;
			}
			else
			{
				$sql_output .= $self->{queries}{$q}{code};
			}
		}
		else
		{
			$sql_output .= $self->{queries}{$q}{code};
			$sql_output .= ';' if ($self->{queries}{$q}{code} !~ /;\s*$/);
		}
		$sql_output .= "\n";
		$nothing++;
	}
	if ($self->{estimate_cost}) {
		$cost_value = sprintf("%2.2f", $cost_value);
		my @infos = ( "Total number of queries: ".(scalar keys %{$self->{queries}}).".",
			"Total size of queries code: $total_size bytes.",
			"Total estimated cost: $cost_value units, ".$self->_get_human_cost($cost_value)."."
		);
		$self->logit(join("\n", @infos) . "\n", 1);
		map { s/^/-- /; } @infos;
		$sql_output .= join("\n", @infos);
	}
	if (!$nothing) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}
	$self->dump($sql_output);

	$self->{queries} = ();

	return;
}

=head2 translate_script

Translate Oracle's SQL script into PostgreSQL compatible scripts.

=cut

sub translate_script
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Parse whole script...\n", 1);
	$self->dump($sql_header);

	my $nothing = 0;
	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	#---------------------------------------------------------
	# Code to use to find queries parser issues, it load a file
	# containing the untouched SQL code from Oracle queries
	#---------------------------------------------------------
	if ($self->{input_file})
	{
		$self->{functions} = ();
		$self->logit("Reading input code from file $self->{input_file}...\n", 1);
		$self->{script}{code} = $self->read_input_file($self->{input_file});
		#$self->_remove_comments(\$self->{script}{code});
		#$self->_restore_comments(\$self->{script}{code});
		chomp($self->{script}{code});
		if (length($self->{script}{code}) > 0) {
			$nothing = 1;
		}
	}
	else
	{
		$self->logit("FATAL: the SCRIPT action need an input file, see -i option\n", 0, 1);
	}
	
	#--------------------------------------------------------

	my $cost_value = 0;
	my $total_size = length($self->{script}{code}) || 0;
	$self->logit("Dumping script...\n", 1);
	
	$self->_remove_comments(\$self->{script}{code});
	if (!$self->{preserve_case}) {
		$self->{script}{code} =~ s/"//gs;
	}
	my $sql_q = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{script}{code});
	my $estimate = '';
	if ($self->{estimate_cost})
	{
		my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $sql_q, 'SCRIPT');
		$cost += $Ora2Pg::PLSQL::OBJECT_SCORE{'QUERY'};
		$cost_value += $cost;
		$estimate = "\n-- Estimed cost of script [ $self->{input_file} ]: " . sprintf("%2.2f", $cost);
	}
	$self->_restore_comments(\$sql_q);

	# replace script parameters
	$sql_q =~ s/'\&(\d+)'/:'param$1'/gs;
	$sql_q =~ s/\&(\d+)/:param$1/gs;

	$sql_output .= $sql_q;
	$sql_output .= ';' if ($sql_q !~ /;\s*$/);
	$sql_output .= $estimate;

	if ($self->{estimate_cost})
	{
		$cost_value = sprintf("%2.2f", $cost_value);
		my @infos = ("Total size of queries code: $total_size bytes.",
			"Total estimated cost: $cost_value units, ".$self->_get_human_cost($cost_value)."."
		);
		$self->logit(join("\n", @infos) . "\n", 1);
		map { s/^/-- /; } @infos;
		$sql_output .= join("\n", @infos);
	}
	if (!$nothing) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}
	$self->dump($sql_output);

	$self->{script} = ();

	return;
}


=head2 export_function

Export Oracle functions into PostgreSQL compatible statement.

=cut

sub export_function
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	use constant SQL_DATATYPE => 2;
	$self->logit("Add functions definition...\n", 1);
	$self->dump($sql_header);
	my $nothing = 0;
	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	#---------------------------------------------------------
	# Code to use to find function parser issues, it load a file
	# containing the untouched PL/SQL code from Oracle Function
	#---------------------------------------------------------
	if ($self->{input_file})
	{
		$self->{functions} = ();
		$self->logit("Reading input code from file $self->{input_file}...\n", 1);
		my $content = $self->read_input_file($self->{input_file});
		$self->_remove_comments(\$content);
		my @allfct = split(/\n/, $content);
		my $fcnm = '';
		my $old_line = '';
		my $language = '';
		foreach my $l (@allfct) {
			chomp($l);
			$l =~ s/\r//g;
			next if ($l =~ /^\s*$/);
			if ($old_line) {
				$l = $old_line .= ' ' . $l;
				$old_line = '';
			}
			if ($l =~ /^\s*CREATE\s*(?:OR REPLACE)?\s*(?:EDITIONABLE|NONEDITIONABLE|DEFINER=[^\s]+)?\s*$/i) {
				$old_line = $l;
				next;
			}
			if ($l =~ /^\s*(?:EDITIONABLE|NONEDITIONABLE|DEFINER=[^\s]+)?\s*(FUNCTION|PROCEDURE)$/i) {
				$old_line = $l;
				next;
			}
			if ($l =~ /^\s*CREATE\s*(?:OR REPLACE)?\s*(?:EDITIONABLE|NONEDITIONABLE|DEFINER=[^\s]+)?\s*(FUNCTION|PROCEDURE)\s*$/i) {
				$old_line = $l;
				next;
			}
			$l =~ s/^\s*CREATE (?:OR REPLACE)?\s*(?:EDITIONABLE|NONEDITIONABLE|DEFINER=[^\s]+)?\s*(FUNCTION|PROCEDURE)/$1/i;
			$l =~ s/^\s*(?:EDITIONABLE|NONEDITIONABLE)?\s*(FUNCTION|PROCEDURE)/$1/i;
			if ($l =~ /^(FUNCTION|PROCEDURE)\s+([^\s\(]+)/i) {
				$fcnm = $2;
				$fcnm =~ s/"//g;
			}
			next if (!$fcnm);
			if ($l =~ /LANGUAGE\s+([^\s="'><\!\(\)]+)/is) {
				$language = $1;
			}
			$self->{functions}{$fcnm}{text} .= "$l\n";

			if (!$language) {
				if ($l =~ /^END\s+$fcnm(_atx)?\s*;/i) {
					$fcnm = '';
				}
			} else {
				if ($l =~ /;/i) {
					$fcnm = '';
					$language = '';
				}
			}
		}
		# Get all metadata from all functions when we are
		# reading a file, otherwise it has already been done
		foreach my $fct (sort keys %{$self->{functions}})
		{
			$self->{functions}{$fct}{text} =~ s/(.*?\b(?:FUNCTION|PROCEDURE)\s+(?:[^\s\(]+))(\s*\%ORA2PG_COMMENT\d+\%\s*)+/$2$1 /is;
			my %fct_detail = $self->_lookup_function($self->{functions}{$fct}{text}, ($self->{is_mysql}) ? $fct : undef);
			if (!exists $fct_detail{name}) {
				delete $self->{functions}{$fct};
				next;
			}
			$self->{functions}{$fct}{type} = uc($fct_detail{type});
			delete $fct_detail{code};
			delete $fct_detail{before};
			my $sch = 'unknown';
			my $fname = $fct;
			if ($fname =~ s/^([^\.\s]+)\.([^\s]+)$/$2/is) {
				$sch = $1;
			}
			$fname =~ s/"//g;
			%{$self->{function_metadata}{$sch}{'none'}{$fname}{metadata}} = %fct_detail;
			$self->_restore_comments(\$self->{functions}{$fct}{text});
		}
	}

	#--------------------------------------------------------
	my $total_size = 0;
	my $cost_value = 0;
	my $num_total_function = scalar keys %{$self->{functions}};
	my $fct_cost = '';
	my $parallel_fct_count = 0;
	unlink($dirprefix . 'temp_cost_file.dat') if ($self->{parallel_tables} > 1 && $self->{estimate_cost});

	my $t0 = Benchmark->new;

	# Group functions by chunk in multiprocess mode
	my $num_chunk = $self->{jobs} || 1;
	my @fct_group = ();
	my $i = 0;
	foreach my $key ( sort keys %{$self->{functions}} )
	{
		if ($self->{print_dependencies} && $self->{plsql_pgsql} && !$self->{no_function_metadata})
		{
			my $plsql_code = $self->{functions}{$key}{text};
			$plsql_code =~ s/FUNCTION $key//i;
			$self->_remove_comments(\$plsql_code);
			# look for other routines call in the stored function
			foreach my $sch (sort keys %{ $self->{function_metadata} })
			{
				foreach my $pkg_name (sort keys %{ $self->{function_metadata}{$sch} })
				{
					foreach my $fname (sort keys %{ $self->{function_metadata}{$sch}{$pkg_name} })
					{
						next if ($key =~ /^$fname$/i || $key =~ /^.*\.$fname$/i);
						if ($plsql_code =~ /\b$fname\b/is)
						{
							push(@{ $self->{object_dependencies}{uc("$self->{functions}{$key}{owner}.$key")}{routines} }, uc("$sch.$fname"));
						}
					}
				}
			}
			# Look for merge/insert/update/delete
			@{ $self->{object_dependencies}{uc("$self->{functions}{$key}{owner}.$key")}{merge} } = $plsql_code =~ /\bMERGE\s+INTO\s+([^\(\s;,]+)/igs;
			@{ $self->{object_dependencies}{uc("$self->{functions}{$key}{owner}.$key")}{insert} } = $plsql_code =~ /\bINSERT\s+INTO\s+([^\(\s;,]+)/igs;
			@{ $self->{object_dependencies}{uc("$self->{functions}{$key}{owner}.$key")}{update} } = $plsql_code =~ /(?:(?!FOR).)*?\s*\bUPDATE\s+([^\s;,]+)\s+/igs;
			@{ $self->{object_dependencies}{uc("$self->{functions}{$key}{owner}.$key")}{delete} } = $plsql_code =~ /\b(?:DELETE\s+FROM|TRUNCATE\s+TABLE)\s+([^\s;,]+)\s+/igs;
		}
		$fct_group[$i++]{$key} = $self->{functions}{$key};
		$i = 0 if ($i == $num_chunk);
	}
	my $num_cur_fct = 0;
	for ($i = 0; $i <= $#fct_group; $i++)
	{
		if ($self->{jobs} > 1) {
			$self->logit("Creating a new process to translate functions...\n", 1);
			spawn sub {
				$self->translate_function($num_cur_fct, $num_total_function, %{$fct_group[$i]});
			};
			$parallel_fct_count++;
		} else {
			my ($code, $lsize, $lcost) = $self->translate_function($num_cur_fct, $num_total_function, %{$fct_group[$i]});
			$sql_output .= $code;
			$total_size += $lsize;
			$cost_value += $lcost;
		}
		$num_cur_fct += scalar keys %{$fct_group[$i]};
		$nothing++;
	}
	# Wait for all oracle connection terminaison
	if ($self->{jobs} > 1)
	{
		while ($parallel_fct_count)
		{
			my $kid = waitpid(-1, WNOHANG);
			if ($kid > 0) {
				$parallel_fct_count--;
				delete $RUNNING_PIDS{$kid};
			}
			usleep(50000);
		}
		if ($self->{estimate_cost}) {
			my $tfh = $self->read_export_file($dirprefix . 'temp_cost_file.dat');
			flock($tfh, 2) || die "FATAL: can't lock file temp_cost_file.dat\n";
			while (my $l = <$tfh>) {
				chomp($l);
				my ($fname, $fsize, $fcost) = split(/:/, $l);
				$total_size += $fsize;
				$cost_value += $fcost;
			}
			$self->close_export_file($tfh, 1);
			unlink($dirprefix . 'temp_cost_file.dat');
		}
	}
	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($num_cur_fct, $num_total_function, 25, '=', 'functions', 'end of functions export.'), "\n";
	}
	if ($self->{estimate_cost}) {
		my @infos = ( "Total number of functions: ".(scalar keys %{$self->{functions}}).".",
			"Total size of function code: $total_size bytes.",
			"Total estimated cost: $cost_value units, ".$self->_get_human_cost($cost_value)."."
		);
		$self->logit(join("\n", @infos) . "\n", 1);
		map { s/^/-- /; } @infos;
		$sql_output .= "\n" .  join("\n", @infos);
		$sql_output .= "\n" . $fct_cost;
	}
	if (!$nothing) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}

	$self->dump($sql_output);

	if (scalar keys %{ $self->{object_dependencies} } > 0)
	{
		my $sp_tree = "object_type;object_name;routines_called;insert;update;delete;merge\n";
		foreach my $caller ( sort keys %{ $self->{object_dependencies} } )
		{
			$sp_tree .= "FUNCTION;$caller";
			$sp_tree .= ";";
			foreach my $sp (@{ $self->{object_dependencies}{$caller}{routines} }) {
				my $star = ($#{ $self->{object_dependencies}{$sp}{routines} } >= 0) ? '*' : '';
				$sp_tree .= "$sp$star,";
			}
			$sp_tree =~ s/,$//;
			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{insert} });
			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{update} });
			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{delete} });
			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{merge} });
			$sp_tree .= "\n";
		}
		my $fhdl = $self->open_export_file("functions_dependencies.csv");
		$self->dump($sp_tree, $fhdl);
		$self->close_export_file($fhdl);
	}

	$self->{functions} = ();

	my $t1 = Benchmark->new;
	my $td = timediff($t1, $t0);
	$self->logit("Total time to translate all functions with $num_chunk process: " . timestr($td) . "\n", 1);

	return;
}

=head2 export_procedure

Export Oracle procedures into PostgreSQL compatible statement.

=cut

sub export_procedure
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	use constant SQL_DATATYPE => 2;
	$self->logit("Add procedures definition...\n", 1);
	my $nothing = 0;
	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	$self->dump($sql_header);
	#---------------------------------------------------------
	# Code to use to find procedure parser issues, it load a file
	# containing the untouched PL/SQL code from Oracle Procedure
	#---------------------------------------------------------
	if ($self->{input_file})
	{
		$self->{procedures} = ();
		$self->logit("Reading input code from file $self->{input_file}...\n", 1);
		my $content = $self->read_input_file($self->{input_file});
		$self->_remove_comments(\$content);
		my @allfct = split(/\n/, $content);
		my $fcnm = '';
		my $old_line = '';
		my $language = '';
		my $first_comment = '';
		foreach my $l (@allfct)
		{
			$l =~ s/\r//g;
			next if ($l =~ /^\/$/);
			next if ($l =~ /^\s*$/);
			if ($old_line)
			{
				$l = $old_line .= ' ' . $l;
				$old_line = '';
			}
			$comment .= $l if ($l =~ /^\%ORA2PG_COMMENT\d+\%$/);
			if ($l =~ /^\s*CREATE\s*(?:OR REPLACE)?\s*(?:EDITIONABLE|NONEDITIONABLE)?\s*$/i)
			{
				$old_line = $comment . $l;
				$comment = '';
				next;
			}
			if ($l =~ /^\s*(?:EDITIONABLE|NONEDITIONABLE)?\s*(FUNCTION|PROCEDURE)$/i)
			{
				$old_line = $comment . $l;
				$comment = '';
				next;
			}
			if ($l =~ /^\s*CREATE\s*(?:OR REPLACE)?\s*(?:EDITIONABLE|NONEDITIONABLE)?\s*(FUNCTION|PROCEDURE)\s*$/i)
			{
				$old_line = $comment . $l;
				$comment = '';
				next;
			}
			$l =~ s/^\s*CREATE (?:OR REPLACE)?\s*(?:EDITIONABLE|NONEDITIONABLE)?\s*(FUNCTION|PROCEDURE)/$1/i;
			$l =~ s/^\s*(?:EDITIONABLE|NONEDITIONABLE)?\s*(FUNCTION|PROCEDURE)/$1/i;
			if ($l =~ /^(FUNCTION|PROCEDURE)\s+([^\s\(]+)/i)
			{
				$fcnm = $2;
				$fcnm =~ s/"//g;
			}
			next if (!$fcnm);
			if ($l =~ /LANGUAGE\s+([^\s="'><\!\(\)]+)/is) {
				$language = $1;
			}
			if ($comment)
			{
				$self->{procedures}{$fcnm}{text} .= "$comment";
				$comment = '';
			}
			$self->{procedures}{$fcnm}{text} .= "$l\n";
			if (!$language)
			{
				if ($l =~ /^END\s+$fcnm(_atx)?\s*;/i) {
					$fcnm = '';
				}
			}
			else
			{
				if ($l =~ /;/i)
				{
					$fcnm = '';
					$language = '';
				}
			}
		}

		# Get all metadata from all procedures when we are
		# reading a file, otherwise it has already been done
		foreach my $fct (sort keys %{$self->{procedures}})
		{
			$self->{procedures}{$fct}{text} =~ s/(.*?\b(?:FUNCTION|PROCEDURE)\s+(?:[^\s\(]+))(\s*\%ORA2PG_COMMENT\d+\%\s*)+/$2$1 /is;
			my %fct_detail = $self->_lookup_function($self->{procedures}{$fct}{text}, ($self->{is_mysql}) ? $fct : undef);
			if (!exists $fct_detail{name})
			{
				delete $self->{procedures}{$fct};
				next;
			}
			$self->{procedures}{$fct}{type} = $fct_detail{type};
			delete $fct_detail{code};
			delete $fct_detail{before};
			my $sch = 'unknown';
			my $fname = $fct;
			if ($fname =~ s/^([^\.\s]+)\.([^\s]+)$/$2/is) {
				$sch = $1;
			}
			$fname =~ s/"//g;
			%{$self->{function_metadata}{$sch}{'none'}{$fname}{metadata}} = %fct_detail;
			$self->_restore_comments(\$self->{procedures}{$fct}{text});
		}
	}

	#--------------------------------------------------------
	my $total_size = 0;
	my $cost_value = 0;
	my $num_total_function = scalar keys %{$self->{procedures}};
	my $fct_cost = '';
	my $parallel_fct_count = 0;
	unlink($dirprefix . 'temp_cost_file.dat') if ($self->{parallel_tables} > 1 && $self->{estimate_cost});

	my $t0 = Benchmark->new;

	# Group functions by chunk in multiprocess mode
	my $num_chunk = $self->{jobs} || 1;
	my @fct_group = ();
	my $i = 0;
	foreach my $key (sort keys %{$self->{procedures}} )
	{
		if ($self->{print_dependencies} && $self->{plsql_pgsql} && !$self->{no_function_metadata})
		{
			my $plsql_code = $self->{procedures}{$key}{text};
			$plsql_code =~ s/FUNCTION $key//i;
			$self->_remove_comments(\$plsql_code);
			# look for other routines call in the stored procedure
			foreach my $sch (sort keys %{ $self->{function_metadata} })
			{
				foreach my $pkg_name (sort keys %{ $self->{function_metadata}{$sch} })
				{
					foreach my $fname (sort keys %{ $self->{function_metadata}{$sch}{$pkg_name} })
					{
						next if ($key =~ /^$fname$/i || $key =~ /^.*\.$fname$/i);
						if ($plsql_code =~ /\b$fname\b/is) {
							push(@{ $self->{object_dependencies}{uc("$self->{procedures}{$key}{owner}.$key")}{routines} }, uc("$sch.$fname"));
						}
					}
				}
			}
			# Look for merge/insert/update/delete
			@{ $self->{object_dependencies}{uc("$self->{procedures}{$key}{owner}.$key")}{merge} } = $plsql_code =~ /\bMERGE\s+INTO\s+([^\(\s;,]+)/igs;
			@{ $self->{object_dependencies}{uc("$self->{procedures}{$key}{owner}.$key")}{insert} } = $plsql_code =~ /\bINSERT\s+INTO\s+([^\(\s;,]+)/igs;
			@{ $self->{object_dependencies}{uc("$self->{procedures}{$key}{owner}.$key")}{update} } = $plsql_code =~ /(?:(?!FOR).)*?\s*\bUPDATE\s+([^\s;,]+)\s+/igs;
			@{ $self->{object_dependencies}{uc("$self->{procedures}{$key}{owner}.$key")}{delete} } = $plsql_code =~ /\b(?:DELETE\s+FROM|TRUNCATE\s+TABLE)\s+([^\s;,]+)\s+/igs;
		}
		$fct_group[$i++]{$key} = $self->{procedures}{$key};
		$i = 0 if ($i == $num_chunk);
	}
	my $num_cur_fct = 0;
	for ($i = 0; $i <= $#fct_group; $i++)
	{
		if ($self->{jobs} > 1)
		{
			$self->logit("Creating a new process to translate procedures...\n", 1);
			spawn sub {
				$self->translate_function($num_cur_fct, $num_total_function, %{$fct_group[$i]});
			};
			$parallel_fct_count++;
		} else {
			my ($code, $lsize, $lcost) = $self->translate_function($num_cur_fct, $num_total_function, %{$fct_group[$i]});
			$sql_output .= $code;
			$total_size += $lsize;;
			$cost_value += $lcost;
		}
		$num_cur_fct += scalar keys %{$fct_group[$i]};
		$nothing++;
	}

	# Wait for all oracle connection terminaison
	if ($self->{jobs} > 1)
	{
		while ($parallel_fct_count)
		{
			my $kid = waitpid(-1, WNOHANG);
			if ($kid > 0)
			{
				$parallel_fct_count--;
				delete $RUNNING_PIDS{$kid};
			}
			usleep(50000);
		}
		if ($self->{estimate_cost})
		{
			my $tfh = $self->read_export_file($dirprefix . 'temp_cost_file.dat');
			flock($tfh, 2) || die "FATAL: can't lock file temp_cost_file.dat\n";
			if (defined $tfh)
			{
				while (my $l = <$tfh>)
				{
					chomp($l);
					my ($fname, $fsize, $fcost) = split(/:/, $l);
					$total_size += $fsize;
					$cost_value += $fcost;
				}
				$self->close_export_file($tfh, 1);
			}
			unlink($dirprefix . 'temp_cost_file.dat');
		}
	}
	if (!$self->{quiet} && !$self->{debug})
	{
		print STDERR $self->progress_bar($num_cur_fct, $num_total_function, 25, '=', 'procedures', 'end of procedures export.'), "\n";
	}
	if ($self->{estimate_cost})
	{
		my @infos = ( "Total number of procedures: ".(scalar keys %{$self->{procedures}}).".",
			"Total size of procedures code: $total_size bytes.",
			"Total estimated cost: $cost_value units, ".$self->_get_human_cost($cost_value)."."
		);
		$self->logit(join("\n", @infos) . "\n", 1);
		map { s/^/-- /; } @infos;
		$sql_output .= "\n" .  join("\n", @infos);
		$sql_output .= "\n" . $fct_cost;
	}
	if (!$nothing) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}

	$self->dump($sql_output);

	if (scalar keys %{ $self->{object_dependencies} } > 0)
	{
		my $sp_tree = "object_type;object_name;routines_called;insert;update;delete;merge\n";
		foreach my $caller ( sort keys %{ $self->{object_dependencies} } )
		{
			$sp_tree .= "PROCEDURE;$caller";
			$sp_tree .= ";";
			foreach my $sp (@{ $self->{object_dependencies}{$caller}{routines} }) {
				my $star = ($#{ $self->{object_dependencies}{$sp}{routines} } >= 0) ? '*' : '';
				$sp_tree .= "$sp$star,";
			}
			$sp_tree =~ s/,$//;
			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{insert} });
			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{update} });
			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{delete} });
			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{merge} });
			$sp_tree .= "\n";
		}
		my $fhdl = $self->open_export_file("procedures_dependencies.csv");
		$self->dump($sp_tree, $fhdl);
		$self->close_export_file($fhdl);
	}

	$self->{procedures} = ();

	my $t1 = Benchmark->new;
	my $td = timediff($t1, $t0);
	$self->logit("Total time to translate all functions with $num_chunk process: " . timestr($td) . "\n", 1);

	return;
}

=head2 export_package

Export Oracle package into PostgreSQL compatible statement.

=cut

sub export_package
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->{current_package} = '';
	$self->logit("Add packages definition...\n", 1);
	my $nothing = 0;
	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	$self->dump($sql_header);

	#---------------------------------------------------------
	# Code to use to find package parser bugs, it load a file
	# containing the untouched PL/SQL code from Oracle Package
	#---------------------------------------------------------
	if ($self->{input_file})
	{
		$self->{plsql_pgsql} = 1;
		$self->{packages} = ();
		$self->logit("Reading input code from file $self->{input_file}...\n", 1);
		my $content = $self->read_input_file($self->{input_file});
		my $pknm = '';
		my $before = '';
		my $old_line = '';
		my $skip_pkg_header = 0;
		$self->_remove_comments(\$content);
		# Normalise start of package declaration
		$content =~ s/CREATE(?:\s+OR\s+REPLACE)?(?:\s+EDITIONABLE|\s+NONEDITIONABLE)?\s+PACKAGE\s+/CREATE OR REPLACE PACKAGE /igs;
		# Preserve header
		$content =~ s/^(.*?)(CREATE OR REPLACE PACKAGE)/$2/s;
		my $start = $1 || '';
		my @pkg_content = split(/CREATE OR REPLACE PACKAGE\s+/is, $content);
		for (my $i = 0; $i <= $#pkg_content; $i++)
		{
			# package declaration
			if ($pkg_content[$i] !~ /^BODY\s+/is)
			{
				if ($pkg_content[$i] =~ /^([^\s]+)/is)
				{
					my $pname = lc($1);
					$pname =~ s/"//g;
					$pname =~ s/^[^\.]+\.//g;
					$self->{packages}{$pname}{desc} = 'CREATE OR REPLACE PACKAGE ' . $pkg_content[$i];
					$self->{packages}{$pname}{text} =  $start if ($start);
					$start = '';
				}
			}
			else
			{
				if ($pkg_content[$i] =~ /^BODY\s+([^\s]+)\s+/is)
				{
					my $pname = lc($1);
					$pname =~ s/"//g;
					$pname =~ s/^[^\.]+\.//g;
					$self->{packages}{$pname}{text} .= 'CREATE OR REPLACE PACKAGE ' . $pkg_content[$i];
				}
			}
		}
		@pkg_content = ();

		foreach my $pkg (sort keys %{$self->{packages}})
		{
			my $tmp_txt = '';
			if (exists $self->{packages}{$pkg}{desc})
			{
				# Move comments at end of package declaration before package definition
				while ($self->{packages}{$pkg}{desc} =~ s/(\%ORA2PG_COMMENT\d+\%\s*)$//) {
					$self->{packages}{$pkg}{text} = $1 . $self->{packages}{$pkg}{text};
				}
			}
			# Get all metadata from all procedures/function when we are
			# reading from a file, otherwise it has already been done
			$tmp_txt = $self->{packages}{$pkg}{text};
			$tmp_txt =~ s/^.*CREATE OR REPLACE PACKAGE\s+/CREATE OR REPLACE PACKAGE /s;
			my %infos = $self->_lookup_package($tmp_txt);
			my $sch = 'unknown';
			my $pname = $pkg;
			if ($pname =~ s/^([^\.\s]+)\.([^\s]+)$/$2/is) {
				$sch = $1;
			}
			foreach my $f (sort keys %infos)
			{
				next if (!$f);
				my $name = lc($f);
				delete $infos{$f}{code};
				delete $infos{$f}{before};
				$pname =~ s/"//g;
				$name =~ s/"//g;
				%{$self->{function_metadata}{$sch}{$pname}{$name}{metadata}} = %{$infos{$f}};
			}
			$self->_restore_comments(\$self->{packages}{$pkg}{text});
		}
	}

	#--------------------------------------------------------
	my $default_global_vars = '';

	my $number_fct = 0;
	my $i = 1;
	my $num_total_package = scalar keys %{$self->{packages}};
	foreach my $pkg (sort keys %{$self->{packages}})
	{
		my $total_size = 0;
		my $total_size_no_comment = 0;
		my $cost_value = 0;
		if (!$self->{quiet} && !$self->{debug}) {
			print STDERR $self->progress_bar($i, $num_total_package, 25, '=', 'packages', "generating $pkg" ), "\r";
		}
		$i++, next if (!$self->{packages}{$pkg}{text} && !$self->{packages}{$pkg}{desc});

		# Save and cleanup previous global variables defined in other package
		if (scalar keys %{$self->{global_variables}})
		{
			foreach my $n (sort keys %{$self->{global_variables}})
			{
				if (exists $self->{global_variables}{$n}{constant} || exists $self->{global_variables}{$n}{default}) {
					$default_global_vars .= "$n = '$self->{global_variables}{$n}{default}'\n";
				} else {
					$default_global_vars .= "$n = ''\n";
				}
			}
		}
		%{$self->{global_variables}} = ();
		my $pkgbody = '';
		my $fct_cost = '';
		if (!$self->{plsql_pgsql})
		{
			$self->logit("Dumping package $pkg...\n", 1);
			if ($self->{file_per_function})
			{
				my $f = "$dirprefix\L${pkg}\E_$self->{output}";
				$f = "\L${pkg}\E_$self->{output}" if ($self->{psql_relative_path});
				$f =~ s/\.(?:gz|bz2)$//i;
				$pkgbody = "\\i$self->{psql_relative_path} $f\n";
				my $fhdl = $self->open_export_file("$dirprefix\L${pkg}\E_$self->{output}", 1);
				$self->set_binmode($fhdl) if (!$self->{compress});
				$self->dump($sql_header . $self->{packages}{$pkg}{desc} . "\n\n" . $self->{packages}{$pkg}{text}, $fhdl);
				$self->close_export_file($fhdl);
			} else {
				$pkgbody = $self->{packages}{$pkg}{desc} . "\n\n" . $self->{packages}{$pkg}{text};
			}

		}
		else
		{
			$self->{current_package} = $pkg;

			# If there is a declaration only do not go further than looking at global var and packages type
			if (!$self->{packages}{$pkg}{text})
			{
				my $t = $self->_convert_package($pkg);
				$self->_restore_comments(\$t) if (!$self->{file_per_function});

				$sql_output .= "\n\n-- Oracle package '$pkg' declaration, please edit to match PostgreSQL syntax.\n";
				$sql_output .= $t . "\n";
				$sql_output .= "-- End of Oracle package '$pkg' declaration\n\n";
				if ($self->{estimate_cost}) {
					$sql_output .= "-- Total size of package code: $total_size bytes.\n";
					$sql_output .= "-- Total size of package code without comments and header: $total_size_no_comment bytes.\n";
					$sql_output .= "-- Detailed cost per function:\n" . $fct_cost;
				}
				$nothing++;
				$i++;
				next;
			}

			if ($self->{estimate_cost}) {
				$total_size += length($self->{packages}->{$pkg}{text});
			}
			$self->_remove_comments(\$self->{packages}{$pkg}{text});

			# Remove trailing comment and space
			$self->{packages}{$pkg}{text} =~ s/(\s*\%ORA2PG_COMMENT\d+\%)\s*$//s;
			$self->{packages}{$pkg}{text} =~ s/\s*$//s;

			# Normalyse package creation call
			$self->{packages}{$pkg}{text} =~ s/CREATE(?:\s+OR\s+REPLACE)?(?:\s+EDITIONABLE|\s+NONEDITIONABLE)?\s+PACKAGE\s+/CREATE OR REPLACE PACKAGE /is;
			if ($self->{estimate_cost})
			{
				my %infos = $self->_lookup_package($self->{packages}{$pkg}{text});
				foreach my $f (sort keys %infos)
				{
					next if (!$f);
					my @cnt = $infos{$f}{code} =~ /(\%ORA2PG_COMMENT\d+\%)/i;
					$total_size_no_comment += (length($infos{$f}{code}) - (17 * length(join('', @cnt))));
					my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $infos{$f}{code}, $infos{$f}{type});
					$self->logit("Function $f estimated cost: $cost\n", 1);
					$cost_value += $cost;
					$number_fct++;
					$fct_cost .= "\t-- Function $f total estimated cost: $cost\n";
					foreach (sort { $cost_detail{$b} <=> $cost_detail{$a} } keys %cost_detail) {
						next if (!$cost_detail{$_});
						$fct_cost .= "\t\t-- $_ => $cost_detail{$_}";
						if (!$self->{is_mysql}) {
							$fct_cost .= " (cost: $Ora2Pg::PLSQL::UNCOVERED_SCORE{$_})" if ($Ora2Pg::PLSQL::UNCOVERED_SCORE{$_});
						} else {
							$fct_cost .= " (cost: $Ora2Pg::PLSQL::UNCOVERED_MYSQL_SCORE{$_})" if ($Ora2Pg::PLSQL::UNCOVERED_MYSQL_SCORE{$_});
						}
						$fct_cost .= "\n";
					}
				}
				$cost_value += $Ora2Pg::PLSQL::OBJECT_SCORE{'PACKAGE BODY'};
				$fct_cost .= "-- Total estimated cost for package $pkg: $cost_value units, " . $self->_get_human_cost($cost_value) . "\n";
			}
			$txt = $self->_convert_package($pkg);
			$self->_restore_comments(\$txt) if (!$self->{file_per_function});
			$txt =~ s/(-- REVOKE ALL ON (?:FUNCTION|PROCEDURE) [^;]+ FROM PUBLIC;)/&remove_newline($1)/sge;
			if (!$self->{file_per_function}) {
				$self->normalize_function_call(\$txt);
			}
			$pkgbody .= $txt;
			$pkgbody =~ s/[\r\n]*\bEND;\s*$//is;
			$pkgbody =~ s/(\s*;)\s*$/$1/is;
		}
		if ($self->{estimate_cost})
		{
			$self->logit("Total size of package code: $total_size bytes.\n", 1);
			$self->logit("Total size of package code without comments and header: $total_size_no_comment bytes.\n", 1);
			$self->logit("Total estimated cost for package $pkg: $cost_value units, " . $self->_get_human_cost($cost_value) . ".\n", 1);
		}
		if ($pkgbody && ($pkgbody =~ /[a-z]/is))
		{
			$sql_output .= "\n\n-- Oracle package '$pkg' declaration, please edit to match PostgreSQL syntax.\n";
			$sql_output .= $pkgbody . "\n";
			$sql_output .= "-- End of Oracle package '$pkg' declaration\n\n";
			if ($self->{estimate_cost}) {
				$sql_output .= "-- Total size of package code: $total_size bytes.\n";
				$sql_output .= "-- Total size of package code without comments and header: $total_size_no_comment bytes.\n";
				$sql_output .= "-- Detailed cost per function:\n" . $fct_cost;
			}
			$nothing++;
		}
		$self->{total_pkgcost} += ($number_fct*$Ora2Pg::PLSQL::OBJECT_SCORE{'FUNCTION'});
		$self->{total_pkgcost} += $Ora2Pg::PLSQL::OBJECT_SCORE{'PACKAGE BODY'};
		$i++;
	}
	if ($self->{estimate_cost} && $number_fct) {
		$self->logit("Total number of functions found inside all packages: $number_fct.\n", 1);
	}
	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($i - 1, $num_total_package, 25, '=', 'packages', 'end of output.'), "\n";
	}
	if (!$nothing) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}

	$self->dump($sql_output);

	if (scalar keys %{ $self->{object_dependencies} } > 0)
	{
		my $sp_tree = "object_type;object_name;routines_called;insert;update;delete;merge\n";
		foreach my $caller ( sort keys %{ $self->{object_dependencies} } )
		{
			$sp_tree .= "PACKAGE;$caller";
			$sp_tree .= ";";
			foreach my $sp (@{ $self->{object_dependencies}{$caller}{routines} }) {
				my $star = ($#{ $self->{object_dependencies}{$sp}{routines} } >= 0) ? '*' : '';
				$sp_tree .= "$sp$star,";
			}
			$sp_tree =~ s/,$//;
			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{insert} });
			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{update} });
			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{delete} });
			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{merge} });
			$sp_tree .= "\n";
		}
		my $fhdl = $self->open_export_file("packages_dependencies.csv");
		$self->dump($sp_tree, $fhdl);
		$self->close_export_file($fhdl);
	}

	$self->{packages} = ();
	$sql_output = '';

	# Create file to load custom variable initialization into postgresql.conf
	if (scalar keys %{$self->{global_variables}})
	{
		foreach my $n (sort keys %{$self->{global_variables}})
		{
			if (exists $self->{global_variables}{$n}{constant} || exists $self->{global_variables}{$n}{default}) {
				$default_global_vars .= "$n = '$self->{global_variables}{$n}{default}'\n";
			} else {
				$default_global_vars .= "$n = ''\n";
			}
		}
	}
	%{$self->{global_variables}} = ();

	# Save global variable that need to be initialized at startup
	if ($default_global_vars)
	{
		my $dirprefix = '';
		$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
		open(OUT, ">${dirprefix}global_variables.conf");
		print OUT "# Global variables with default values used in packages.\n";
		print OUT $default_global_vars;
		close(OUT);
	}

	return;
}

=head2 export_type

Export Oracle type into PostgreSQL compatible statement.

=cut

sub export_type
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Add custom types definition...\n", 1);
	#---------------------------------------------------------
	# Code to use to find type parser issues, it load a file
	# containing the untouched PL/SQL code from Oracle type
	#---------------------------------------------------------
	if ($self->{input_file})
	{
		$self->{types} = ();
		$self->logit("Reading input code from file $self->{input_file}...\n", 1);
		my $content = $self->read_input_file($self->{input_file});
		$self->_remove_comments(\$content);
		my $i = 0;
		foreach my $l (split(/;/, $content))
		{
			chomp($l);
			next if ($l =~ /^[\s\/]*$/s);
			my $cmt = '';
			while ($l =~ s/(\%ORA2PG_COMMENT\d+\%)//s) {
				$cmt .= "$1";
			}
			$self->_restore_comments(\$cmt);
			$l =~ s/^\s+//;
			$l =~ s/^CREATE\s+(?:OR REPLACE)?\s*(?:NONEDITIONABLE|EDITIONABLE)?\s*//is;
			$l .= ";\n";
			if ($l =~ /^(SUBTYPE|TYPE)\s+([^\s\(]+)/is) {
				push(@{$self->{types}}, { ('name' => $2, 'code' => $l, 'comment' => $cmt, 'pos' => $i) });
			}
			$i++;
		}
	}
	#--------------------------------------------------------
	my $i = 1;
	foreach my $tpe (sort {$a->{pos} <=> $b->{pos} } @{$self->{types}})
	{
		$self->logit("Dumping type $tpe->{name}...\n", 1);
		if (!$self->{quiet} && !$self->{debug}) {
			print STDERR $self->progress_bar($i, $#{$self->{types}}+1, 25, '=', 'types', "generating $tpe->{name}" ), "\r";
		}
		if ($self->{plsql_pgsql}) {
			$tpe->{code} = $self->_convert_type($tpe->{code}, $tpe->{owner});
		} else {
			$tpe->{code} =~ s/^CREATE TYPE/TYPE/i;
			if ($tpe->{code} !~ /^SUBTYPE\s+/) {
				$tpe->{code} = "CREATE$self->{create_or_replace} $tpe->{code}\n";
			}
		}
		$tpe->{code} =~ s/REPLACE type/REPLACE TYPE/;
		$sql_output .= $tpe->{comment} . $tpe->{code} . "\n";
		$i++;
	}
	$self->_restore_comments(\$sql_output);
	$self->{comment_values} = ();

	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($i - 1, $#{$self->{types}}+1, 25, '=', 'types', 'end of output.'), "\n";
	}
	if (!$sql_output) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}
	if ($self->{export_schema} && ($self->{pg_schema} || $self->{schema})) {
		$sql_header .= "CREATE SCHEMA IF NOT EXISTS " . $self->quote_object_name($self->{pg_schema} || $self->{schema}) . ";\n";
	}
	$self->dump($sql_header . $sql_output);

	return;
}

=head2 export_tablespace

Export Oracle tablespace into PostgreSQL compatible statement.

=cut

sub export_tablespace
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	$sql_header .= "-- Oracle tablespaces export, please edit path to match your filesystem.\n";
	$sql_header .= "-- In PostgreSQl the path must be a directory and is expected to already exists\n";
	my $sql_output = "";

	$self->logit("Add tablespaces definition...\n", 1);

	my $create_tb = '';
	my @done = ();
	# Read DML from file if any
	if ($self->{input_file}) {
		$self->read_tablespace_from_file();
	}
	my $dirprefix = '';
	foreach my $tb_type (sort keys %{$self->{tablespaces}})
	{
		#next if ($tb_type eq 'INDEX PARTITION' || $tb_type eq 'TABLE PARTITION');
		# TYPE - TABLESPACE_NAME - FILEPATH - OBJECT_NAME
		foreach my $tb_name (sort keys %{$self->{tablespaces}{$tb_type}})
		{
			foreach my $tb_path (sort keys %{$self->{tablespaces}{$tb_type}{$tb_name}})
			{
				# Replace Oracle tablespace filename
				my $loc = $tb_name;
				if ($tb_path =~ /^(.*[^\\\/]+)/) {
					$loc = $1 . '/' . $loc;
				}
				if (!grep(/^$tb_name$/, @done))
				{
					$create_tb .= "CREATE TABLESPACE \L$tb_name\E LOCATION '$loc';\n";
					my $owner = $self->{list_tablespaces}{$tb_name}{owner} || '';
					$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
					if ($owner)
					{
						$create_tb .= "ALTER TABLESPACE " . $self->quote_object_name($tb_name)
								. " OWNER TO " . $self->quote_object_name($owner) . ";\n";
					}
				}
				push(@done, $tb_name);
				foreach my $obj (@{$self->{tablespaces}{$tb_type}{$tb_name}{$tb_path}}) {
					$tb_type =~ s/ PARTITION//;
					next if ($self->{file_per_index} && ($tb_type eq 'INDEX'));
					$sql_output .= "ALTER $tb_type " . $self->quote_object_name($obj)
							. " SET TABLESPACE " . $self->quote_object_name($tb_name) . ";\n";
				}
			}
		}
	}

	$sql_output = "$create_tb\n" . $sql_output if ($create_tb);
	if (!$sql_output) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}
	
	$self->dump($sql_header . $sql_output);
	
	if ($self->{file_per_index} && (scalar keys %{$self->{tablespaces}} > 0))
	{
		my $fhdl = undef;
		$self->logit("Dumping tablespace alter indexes to one separate file : TBSP_INDEXES_$self->{output}\n", 1);
		$fhdl = $self->open_export_file("TBSP_INDEXES_$self->{output}");
		$self->set_binmode($fhdl) if (!$self->{compress});
		$sql_output = '';
		foreach my $tb_type (sort keys %{$self->{tablespaces}})
		{
			# TYPE - TABLESPACE_NAME - FILEPATH - OBJECT_NAME
			foreach my $tb_name (sort keys %{$self->{tablespaces}{$tb_type}})
			{
				foreach my $tb_path (sort keys %{$self->{tablespaces}{$tb_type}{$tb_name}})
				{
					# Replace Oracle tablespace filename
					my $loc = $tb_name;
					$tb_path =~ /^(.*)[^\\\/]+$/;
					$loc = $1 . $loc;
					foreach my $obj (@{$self->{tablespaces}{$tb_type}{$tb_name}{$tb_path}})
					{
						$tb_type =~ s/ PARTITION//;
						next if ($tb_type eq 'TABLE');
						$sql_output .= "ALTER $tb_type \L$obj\E SET TABLESPACE \L$tb_name\E;\n";
					}
				}
			}
		}
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$sql_output && !$self->{no_header});
		$self->dump($sql_header . $sql_output, $fhdl);
		$self->close_export_file($fhdl);
	}
	return;
}

=head2 export_kettle

Export Oracle table into Kettle script to load data into PostgreSQL.

=cut

sub export_kettle
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	# Remove external table from data export
	if (scalar keys %{$self->{external_table}} ) {
		foreach my $table (keys %{$self->{tables}}) {
			if ( grep(/^$table$/i, keys %{$self->{external_table}}) ) {
				delete $self->{tables}{$table};
			}
		}
	}

	# Ordering tables by name by default
	my @ordered_tables = sort { $a cmp $b } keys %{$self->{tables}};
	if (lc($self->{data_export_order}) eq 'size') {
		@ordered_tables = sort {
			($self->{tables}{$b}{table_info}{num_rows} || $self->{tables}{$a}{table_info}{num_rows}) ?
				$self->{tables}{$b}{table_info}{num_rows} <=> $self->{tables}{$a}{table_info}{num_rows} :
					$a cmp $b 
		} keys %{$self->{tables}};
	}
	# User provide the ordered list of table from a file
	elsif (-e $self->{data_export_order})
	{
		if (open(my $tfh, '<', $self->{data_export_order}))
		{
			@ordered_tables = ();
			while (my $l = <$tfh>)
			{
				chomp($l);
				next if (!exists $self->{tables}{$!});
				push(@ordered_tables, $l);
			}
			close($tfh);
		}
		else
		{
			$self->logit("FATAL: can't read file $self->{data_export_order} for ordering table export. $!\n", 0, 1);
		}
	}

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	foreach my $table (@ordered_tables) {
		$shell_commands .= $self->create_kettle_output($table, $dirprefix);
	}
	$self->dump("#!/bin/sh\n\n", $fhdl);
	$self->dump("KETTLE_TEMPLATE_PATH='.'\n\n", $fhdl);
	$self->dump($shell_commands, $fhdl);

	return;
}

=head2 export_partition

Export Oracle partition into PostgreSQL compatible statement.

=cut

sub export_partition
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Add partitions definition...\n", 1);

	my $total_partition = 0;
	foreach my $t (sort keys %{ $self->{partitions} })
	{
		$total_partition += scalar keys %{$self->{partitions}{$t}};
	}
	foreach my $t (sort keys %{ $self->{subpartitions_list} })
	{
		foreach my $p (sort keys %{ $self->{subpartitions_list}{$t} }) {
			$total_partition += $self->{subpartitions_list}{$t}{$p}{count};
		}
	}

	# Extract partition definition from partitioned tables
	my $nparts = 1;
	my $partition_indexes = '';
	foreach my $table (sort keys %{$self->{partitions}})
	{
		my $function = '';
		$function = qq{
CREATE$self->{create_or_replace} FUNCTION ${table}_insert_trigger()
RETURNS TRIGGER AS \$\$
BEGIN
} if (!$self->{pg_supports_partition});

		my $cond = 'IF';
		my $funct_cond = '';
		my %create_table = ();
		my $idx = 0;
		my $old_pos = '';
		my $old_part = '';
		my $owner = '';
		my $PGBAR_REFRESH = set_refresh_count($total_partition);
		# Extract partitions in their position order
		foreach my $pos (sort {$a <=> $b} keys %{$self->{partitions}{$table}})
		{
			next if (!$self->{partitions}{$table}{$pos}{name});
			my $part = $self->{partitions}{$table}{$pos}{name};
			if (!$self->{quiet} && !$self->{debug} && ($nparts % $PGBAR_REFRESH) == 0)
			{
				print STDERR $self->progress_bar($nparts, $total_partition, 25, '=', 'partitions', "generating $table/$part" ), "\r";
			}
			$nparts++;
			my $create_table_tmp = '';
			my $create_table_index_tmp = '';
			my $tb_name = '';
			if (exists $self->{replaced_tables}{"\L$table\E"} && $self->{replaced_tables}{"\L$table\E"})
			{
				$self->logit("\tReplacing table $table as " . $self->{replaced_tables}{lc($table)} . "...\n", 1);
				$tb_name = $self->{replaced_tables}{lc($table)};
			}
			if ($self->{rename_partition}) {
				$tb_name = ($tb_name||$table) . '_part' . $pos;
			} else {
				if ($self->{export_schema} && !$self->{schema} && ($table =~ /^([^\.]+)\./)) {
					$tb_name =  $1 . '.' . $part;
				} else {
					$tb_name =  $part;
				}
			}
			$tb_name = $table . '_default' if (!$tb_name);
			$create_table_tmp .= "DROP TABLE $self->{pg_supports_ifexists} " . $self->quote_object_name($tb_name) . ";\n" if ($self->{drop_if_exists});
			if (!$self->{pg_supports_partition})
			{
				if (!exists $self->{subpartitions}{$table}{$part}) {
					$create_table_tmp .= "CREATE TABLE " . $self->quote_object_name($tb_name)
									. " ( CHECK (\n";
				}
			} else {
				$create_table_tmp .= "CREATE TABLE " . $self->quote_object_name($tb_name)
								. " PARTITION OF " . $self->quote_object_name($table) . "\n";
				$create_table_tmp .= "FOR VALUES";
			}

			my @condition = ();
			my @ind_col = ();
			my $check_cond = '';
			my $reftable = '';
			for (my $i = 0; $i <= $#{$self->{partitions}{$table}{$pos}{info}}; $i++)
			{
				$reftable = $table;
				if ($self->{partition_by_reference} eq 'duplicate' && $self->{partitions}{$table}{$pos}{info}[$i]->{refrtable})
				{
					$reftable = $self->{partitions}{$table}{$pos}{info}[$i]->{refrtable};
				}

				# We received all values for partitonning on multiple column, so get the one at the right indice
				my $value = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$reftable}{$pos}{info}[$i]->{value});
				if ($#{$self->{partitions}{$reftable}{$pos}{info}} >= 0)
				{
					my @values = split(/,\s/, Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$reftable}{$pos}{info}[$i]->{value}));
					$value = $values[$i];
				}
				my $old_value = '';
				if ($old_part)
				{
					$old_value = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$reftable}{$old_pos}{info}[$i]->{value});
					if ($#{$self->{partitions}{$reftable}{$pos}{info}} == 0)
					{
						my @values = split(/,\s/, Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$reftable}{$old_pos}{info}[$i]->{value}));
						$old_value = $values[$i];
					}
				}
				if ($self->{partitions}{$reftable}{$pos}{info}[$i]->{type} eq 'LIST')
				{
					$value = $self->{partitions}{$reftable}{$pos}{info}[$i]->{value};
					if (!$self->{pg_supports_partition}) {
						$check_cond .= "\t$self->{partitions}{$reftable}{$pos}{info}[$i]->{column} IN ($value)";
					} else {
						$check_cond .= " IN ($value)";
					}
				}
				elsif ($self->{partitions}{$reftable}{$pos}{info}[$i]->{type} eq 'RANGE')
				{
					if (!$self->{pg_supports_partition})
					{
						if ($old_part eq '') {
							$check_cond .= "\t$self->{partitions}{$reftable}{$pos}{info}[$i]->{column} < $value";
						}
						else
						{
							$check_cond .= "\t$self->{partitions}{$reftable}{$pos}{info}[$i]->{column} >= $old_value"
								. " AND $self->{partitions}{$reftable}{$pos}{info}[$i]->{column} < $value";
						}
					}
					else
					{
						if ($old_part eq '')
						{
							my $val = 'MINVALUE,' x ($#{$self->{partitions}{$reftable}{$pos}{info}}+1);
							$val =~ s/,$//;
							$check_cond .= " FROM ($val) TO ($value)";
						} else {
							$check_cond .= " FROM ($old_value) TO ($value)";
						}
						$i += $#{$self->{partitions}{$reftable}{$pos}{info}};
					}
				}
				elsif ($self->{partitions}{$reftable}{$pos}{info}[$i]->{type} eq 'HASH')
				{
					if ($self->{pg_version} < 11)
					{
						print STDERR "WARNING: Hash partitioning not supported, skipping partitioning of table $reftable\n";
						$function = '';
						$create_table_tmp = '';
						$create_table_index_tmp = '';
						next;
					}
					else
					{
						my $part_clause = " WITH (MODULUS " . (scalar keys %{$self->{partitions}{$reftable}}) . ", REMAINDER " . ($pos-1) . ")";
						$check_cond .= $part_clause if ($check_cond !~ /\Q$part_clause\E$/);
					}
				}
				elsif ($reftable eq $table)
				{
					print STDERR "WARNING: Unknown partitioning type $self->{partitions}{$reftable}{$pos}{info}[$i]->{type}, skipping partitioning of table $reftable\n";
					$create_table_tmp = '';
					$create_table_index_tmp = '';
					next;
				} 

				if (!$self->{pg_supports_partition})
				{
					$check_cond .= " AND" if ($i < $#{$self->{partitions}{$reftable}{$pos}{info}});
				}
				my $fct = '';
				my $colname = $self->{partitions}{$reftable}{$pos}{info}[$i]->{column};
				if ($colname =~ s/([^\(]+)\(([^\)]+)\)/$2/)
				{
					$fct = $1;
				}
				my $cindx = $self->{partitions}{$reftable}{$pos}{info}[$i]->{column} || '';
				$cindx = lc($cindx) if (!$self->{preserve_case});
				$cindx = Ora2Pg::PLSQL::convert_plsql_code($self, $cindx);
				my $has_hash_subpartition = 0;
				if (exists $self->{subpartitions}{$reftable}{$part})
				{
					foreach my $p (sort {$a <=> $b} keys %{$self->{subpartitions}{$reftable}{$part}})
					{
						for (my $j = 0; $j <= $#{$self->{subpartitions}{$reftable}{$part}{$p}{info}}; $j++)
						{
							if ($self->{subpartitions}{$reftable}{$part}{$p}{info}[$j]->{type} eq 'HASH')
							{
								$has_hash_subpartition = 1;
								last;
							}
						}
						last if ($has_hash_subpartition);
					}
				}

				if (!exists $self->{subpartitions}{$reftable}{$part} || (!$self->{pg_supports_partition} && $has_hash_subpartition))
				{
					# Reproduce indexes definition from the main table before PG 11
					# after they are automatically created on partition tables
					if ($self->{pg_version} < 11)
					{
						my ($idx, $fts_idx) = $self->_create_indexes($reftable, 0, %{$self->{tables}{$reftable}{indexes}});
						my $tb_name2 = $self->quote_object_name($tb_name);
						if ($cindx)
						{
							$create_table_index_tmp .= "CREATE INDEX "
								. $self->quote_object_name("${tb_name}_$colname$pos")
								. " ON " . $self->quote_object_name($tb_name) . " ($cindx);\n";
						}
						if ($idx || $fts_idx)
						{
							$idx =~ s/ $reftable/ $tb_name2/igs;
							$fts_idx =~ s/ $reftable/ $tb_name2/igs;
							# remove indexes already created
							$idx =~ s/CREATE [^;]+ \($cindx\);//;
							$fts_idx =~ s/CREATE [^;]+ \($cindx\);//;
							if ($idx || $fts_idx)
							{
								# fix index name to avoid duplicate index name
								$idx =~ s/(CREATE(?:.*?)INDEX ([^\s]+)) /$1$pos /gs;
								$fts_idx =~ s/(CREATE(?:.*?)INDEX ([^\s]+)) /$1$pos /gs;
								$create_table_index_tmp .= "-- Reproduce partition indexes that was defined on the parent table\n";
							}
							$create_table_index_tmp .= "$idx\n" if ($idx);
							$create_table_index_tmp .= "$fts_idx\n" if ($fts_idx);
						}

						# Set the unique (and primary) key definition 
						$idx = $self->_create_unique_keys($reftable, $self->{tables}{$reftable}{unique_key});
						if ($idx)
						{
							$idx =~ s/ $reftable/ $tb_name2/igs;
							# remove indexes already created
							$idx =~ s/CREATE [^;]+ \($cindx\);//;
							if ($idx)
							{
								# fix index name to avoid duplicate index name
								$idx =~ s/(CREATE(?:.*?)INDEX ([^\s]+)) /$1$pos /gs;
								$create_table_index_tmp .= "-- Reproduce partition unique indexes / pk that was defined on the parent table\n";
								$create_table_index_tmp .= "$idx\n";
								# Remove duplicate index with this one
								if ($idx =~ /ALTER TABLE $tb_name2 ADD PRIMARY KEY (.*);/s)
								{ 
									my $collist = quotemeta($1);
									$create_table_index_tmp =~ s/CREATE INDEX [^;]+ ON $tb_name2 $collist;//s;
								}
							}
						}
					}
				}
				my $deftb = $self->{partitions_default}{$reftable}{name};
				$deftb = $reftable . '_part_default' if ($self->{rename_partition});
				# Reproduce indexes definition from the main table before PG 11
				# after they are automatically created on partition tables
				if ($self->{pg_version} < 11)
				{
					if (exists $self->{partitions_default}{$reftable} && ($create_table{$reftable}{index} !~ /ON $deftb /))
					{
						$cindx = $self->{partitions}{$reftable}{$pos}{info}[$i]->{column} || '';
						$cindx = lc($cindx) if (!$self->{preserve_case});
						$cindx = Ora2Pg::PLSQL::convert_plsql_code($self, $cindx);
						if ($cindx)
						{
							$create_table_index_tmp .= "CREATE INDEX " . $self->quote_object_name($deftb . '_' . $colname) . " ON " . $self->quote_object_name($deftb) . " ($cindx);\n";
						}
					}
					push(@ind_col, $self->{partitions}{$reftable}{$pos}{info}[$i]->{column}) if (!grep(/^$self->{partitions}{$reftable}{$pos}{info}[$i]->{column}$/, @ind_col));
				}
				if ($self->{partitions}{$reftable}{$pos}{info}[$i]->{type} eq 'LIST')
				{
					if (!$fct) {
						push(@condition, "NEW.$self->{partitions}{$reftable}{$pos}{info}[$i]->{column} IN (" . Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$reftable}{$pos}{info}[$i]->{value}) . ")");
					} else {
						push(@condition, "$fct(NEW.$colname) IN (" . Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$reftable}{$pos}{info}[$i]->{value}) . ")");
					}
				}
				elsif ($self->{partitions}{$reftable}{$pos}{info}[$i]->{type} eq 'RANGE')
				{
					if (!$fct) {
						push(@condition, "NEW.$self->{partitions}{$reftable}{$pos}{info}[$i]->{column} < " . Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$reftable}{$pos}{info}[$i]->{value}));
					} else {
						push(@condition, "$fct(NEW.$colname) < " . Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$reftable}{$pos}{info}[$i]->{value}));
					}
				}
				$owner = $self->{partitions}{$reftable}{$pos}{info}[$i]->{owner} || '';
			}
			if (!$self->{pg_supports_partition})
			{
				if ($self->{partitions}{$table}{$pos}{info}[$i]->{type} ne 'HASH')
				{
					if (!exists $self->{subpartitions}{$table}{$part})
					{
						$create_table_tmp .= $check_cond . "\n";
						$create_table_tmp .= ") ) INHERITS ($table);\n";
					}
					$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
					if ($owner) {
						$create_table_tmp .= "ALTER TABLE " . $self->quote_object_name($tb_name)
											. " OWNER TO " . $self->quote_object_name($owner) . ";\n";
					}
				}
			}
			else
			{
				$create_table_tmp .= $check_cond;
				if (exists $self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{type})
				{
					$create_table_tmp .= "\nPARTITION BY " . $self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{type} . " (";
					my $expr = '';
					if (exists $self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{columns})
					{
						my $len = $#{$self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{columns}};
						for (my $j = 0; $j <= $len; $j++)
						{
							if ($self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{type} eq 'LIST') {
								$expr .= ' || ' if ($j > 0);
							} else {
								$expr .= ', ' if ($j > 0);
							}
							$expr .= $self->quote_object_name($self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{columns}[$j]);
							if ($self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{type} eq 'LIST' && $len > 0) {
								$expr .= '::text';
							}
						}
						if ($self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{type} eq 'LIST' && $len >= 0) {
							$expr = '(' . $expr . ')';
						}
					}
					else
					{
						if ($self->{plsql_pgsql}) {
							$self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{expression} = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{expression});
						}
						$expr .= $self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{expression};
					}
					$expr = '(' . $expr . ')' if ($expr =~ /[\(\+\-\*\%:]/ && $expr !~ /^\(.*\)$/);
					$create_table_tmp .= 	"$expr)";
				}
				$create_table_tmp .= ";\n" if ($create_table_tmp);
			}

			# Add subpartition if any defined on Oracle
			my $sub_funct_cond = '';
			my $sub_old_part = '';
			if (exists $self->{subpartitions}{$table}{$part})
			{
				my $sub_cond = 'IF';
				my $sub_funct_cond_tmp = '';
				my $create_subtable_tmp = '';
				my $total_subpartition = scalar %{$self->{subpartitions}{$table}{$part}} || 0;
				foreach my $p (sort {$a <=> $b} keys %{$self->{subpartitions}{$table}{$part}})
				{
					my $subpart = $self->{subpartitions}{$table}{$part}{$p}{name};
					my $sub_tb_name = $subpart;
					$sub_tb_name =~ s/^[^\.]+\.//; # remove schema part if any
					if ($self->{rename_partition})
					{
						$sub_tb_name = $tb_name . '_subpart' . $p;
					} else {
						$sub_tb_name = "${table}_$sub_tb_name";
					}
					if (!$self->{quiet} && !$self->{debug} && ($nparts % $PGBAR_REFRESH) == 0)
					{
						print STDERR $self->progress_bar($nparts, $total_partition, 25, '=', 'partitions', "generating $table/$part/$subpart" ), "\r";
					}
					$nparts++;
					$create_subtable_tmp .= "DROP TABLE $self->{pg_supports_ifexists} " . $self->quote_object_name($sub_tb_name) . ";\n" if ($self->{drop_if_exists});
					$create_subtable_tmp .= "CREATE TABLE " . $self->quote_object_name($sub_tb_name);
					if (!$self->{pg_supports_partition}) {
						$create_subtable_tmp .= " ( CHECK (\n";
					}
					else
					{
						$create_subtable_tmp .= " PARTITION OF " . $self->quote_object_name($tb_name) . " -- $reftable -> $table\n";
						$create_subtable_tmp .= "FOR VALUES";
					}
					my $sub_check_cond_tmp = '';
					my @subcondition = ();
					for (my $i = 0; $i <= $#{$self->{subpartitions}{$table}{$part}{$p}{info}}; $i++)
					{
						# We received all values for partitonning on multiple column, so get the one at the right indice
						my $value = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{value});
						if ($#{$self->{subpartitions}{$table}{$part}{$p}{info}} == 0)
						{
							my @values = split(/,\s/, Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{value}));
							$value = $values[$i];
						}
						my $old_value = '';
						if ($sub_old_part)
						{
							$old_value = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$sub_old_pos}{info}[$i]->{value});
							if ($#{$self->{subpartitions}{$table}{$part}{$p}{info}} == 0)
							{
								my @values = split(/,\s/, Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$sub_old_pos}{info}[$i]->{value}));
								$old_value = $values[$i];
							}
						}

						if ($self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{type} eq 'LIST')
						{
							if (!$self->{pg_supports_partition}) {
								$sub_check_cond_tmp .= "$self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{column} IN ($value)";
							} else {
								$sub_check_cond_tmp .= " IN ($value)";
							}
						}
						elsif ($self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{type} eq 'RANGE')
						{
							if (!$self->{pg_supports_partition})
							{
								if ($old_part eq '') {
									$sub_check_cond_tmp .= "\t$self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{column} < $value";
								}
								else
								{
									$sub_check_cond_tmp .= "\t$self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{column} >= $old_value"
										. " AND $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{column} < $value";
								}
							}
							else
							{
								if ($old_part eq '')
								{
									my $val = 'MINVALUE,' x ($#{$self->{subpartitions}{$table}{$part}{$p}{info}}+1);
									$val =~ s/,$//;
									$sub_check_cond_tmp .= " FROM ($val) TO ($value)";
								} else {
									$sub_check_cond_tmp .= " FROM ($old_value) TO ($value)";
								}
								$i += $#{$self->{subpartitions}{$table}{$part}{$p}{info}};
							}
						}
						elsif ($self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{type} eq 'HASH')
						{
							if ($self->{pg_version} < 11)
							{
								print STDERR "WARNING: Hash partitioning not supported, skipping subpartitioning of table $table\n";
								$create_subtable_tmp = '';
								$sub_funct_cond_tmp = '';
								next;
							}
							else
							{
								my $part_clause = " WITH (MODULUS " . (scalar keys %{$self->{subpartitions}{$table}{$part}}) . ", REMAINDER " . ($p-1) . ")";
								$sub_check_cond_tmp .= $part_clause if ($sub_check_cond_tmp !~ /\Q$part_clause\E$/);
							}
						}
						else
						{
							print STDERR "WARNING: Unknown partitioning type $self->{partitions}{$table}{$pos}{info}[$i]->{type}, skipping partitioning of table $table\n";
							$create_subtable_tmp = '';
							$sub_funct_cond_tmp = '';
							next;
						}
						if (!$self->{pg_supports_partition}) {
							$sub_check_cond_tmp .= " AND " if ($i < $#{$self->{subpartitions}{$table}{$part}{$p}{info}});
						}
						# Reproduce indexes definition from the main table before PG 11
						# after they are automatically created on partition tables
						if ($self->{pg_version} < 11)
						{
							push(@ind_col, $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{column}) if (!grep(/^$self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{column}$/, @ind_col));
							my $fct = '';
							my $colname = $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{column};
							if ($colname =~ s/([^\(]+)\(([^\)]+)\)/$2/) {
								$fct = $1;
							}
							$cindx = join(',', @ind_col);
							$cindx = lc($cindx) if (!$self->{preserve_case});
							$cindx = Ora2Pg::PLSQL::convert_plsql_code($self, $cindx);
							if ($cindx)
							{
								$create_table_index_tmp .= "CREATE INDEX " . $self->quote_object_name("${sub_tb_name}_$colname$p")
									 . " ON " . $self->quote_object_name("$sub_tb_name") . " ($cindx);\n";
							}
							my $tb_name2 = $self->quote_object_name("$sub_tb_name");
							# Reproduce indexes definition from the main table
							my ($idx, $fts_idx) = $self->_create_indexes($table, 0, %{$self->{tables}{$table}{indexes}});
							if ($idx || $fts_idx) {
								$idx =~ s/ $table/ $tb_name2/igs;
								$fts_idx =~ s/ $table/ $tb_name2/igs;
								# remove indexes already created
								$idx =~ s/CREATE [^;]+ \($cindx\);//;
								$fts_idx =~ s/CREATE [^;]+ \($cindx\);//;
								if ($idx || $fts_idx) {
									# fix index name to avoid duplicate index name
									$idx =~ s/(CREATE(?:.*?)INDEX ([^\s]+)) /$1${pos}_$p /gs;
									$fts_idx =~ s/(CREATE(?:.*?)INDEX ([^\s]+)) /$1${pos}_$p /gs;
									$create_table_index_tmp .= "-- Reproduce subpartition indexes that was defined on the parent table\n";
								}
								$create_table_index_tmp .= "$idx\n" if ($idx);
								$create_table_index_tmp .= "$fts_idx\n" if ($fts_idx);
							}

							# Set the unique (and primary) key definition 
							$idx = $self->_create_unique_keys($table, $self->{tables}{$table}{unique_key}, $part);
							if ($idx)
							{
								$create_table_index_tmp .= "-- Reproduce subpartition unique indexes / pk that was defined on the parent table\n";
								$idx =~ s/ $table/ $tb_name2/igs;
								# remove indexes already created
								$idx =~ s/CREATE [^;]+ \($cindx\);//;
								if ($idx) {
									# fix index name to avoid duplicate index name
									$idx =~ s/(CREATE(?:.*?)INDEX ([^\s]+)) /$1${pos}_$p /gs;
									$create_table_index_tmp .= "$idx\n";
									# Remove duplicate index with this one
									if ($idx =~ /ALTER TABLE $tb_name2 ADD PRIMARY KEY (.*);/s) { 
										my $collist = quotemeta($1);
										$create_table_index_tmp =~ s/CREATE INDEX [^;]+ ON $tb_name2 $collist;//s;
									}
								}
							}
						}
						if ($self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{type} eq 'LIST') {
							if (!$fct) {
								push(@subcondition, "NEW.$self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{column} IN (" . Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{value}) . ")");
							} else {
								push(@subcondition, "$fct(NEW.$colname) IN (" . Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{value}) . ")");
							}
						} elsif ($self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{type} eq 'RANGE') {
							if (!$fct) {
								push(@subcondition, "NEW.$self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{column} < " . Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{value}));
							} else {
								push(@subcondition, "$fct(NEW.$colname) < " . Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{value}));
							}
						}
						$owner = $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{owner} || '';
					}
					if ($self->{pg_supports_partition}) {
						$sub_check_cond_tmp .= ';';
						$create_subtable_tmp .= "$sub_check_cond_tmp\n";
					} else {
						$create_subtable_tmp .= $check_cond;
						$create_subtable_tmp .= " AND $sub_check_cond_tmp" if ($sub_check_cond_tmp);
						$create_subtable_tmp .= "\n) ) INHERITS ($table);\n";
					}
					$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
					if ($owner) {
						$create_subtable_tmp .= "ALTER TABLE " . $self->quote_object_name("$sub_tb_name")
										. " OWNER TO " . $self->quote_object_name($owner) . ";\n";
					}
					if ($#subcondition >= 0) {
						$sub_funct_cond_tmp .= "\t\t$sub_cond ( " . join(' AND ', @subcondition) . " ) THEN INSERT INTO "
									. $self->quote_object_name("$sub_tb_name") . " VALUES (NEW.*);\n";
						$sub_cond = 'ELSIF';
					}
					$sub_old_part = $part;
					$sub_old_pos = $p;
				}
				if ($create_subtable_tmp) {
					$create_table_tmp .= $create_subtable_tmp;
					$sub_funct_cond = $sub_funct_cond_tmp;
				}
			}
			$check_cond = '';

			# Fix case where default partition is taken as a value
			$create_table_tmp =~ s/FOR VALUES IN \(default\)/DEFAULT/igs;

			if ($#condition >= 0)
			{
				if (!$sub_funct_cond) {
					$funct_cond .= "\t$cond ( " . join(' AND ', @condition) . " ) THEN INSERT INTO " . $self->quote_object_name($tb_name) . " VALUES (NEW.*);\n";
				}
				else
				{
					my $sub_old_pos = 0;
					if (!$self->{pg_supports_partition})
					{
						$sub_funct_cond = Ora2Pg::PLSQL::convert_plsql_code($self, $sub_funct_cond);
						$funct_cond .= "\t$cond ( " . join(' AND ', @condition) . " ) THEN \n";
						$funct_cond .= $sub_funct_cond;
						if (exists $self->{subpartitions_default}{$table}{$part}{name})
						{
							my $deftb = $self->{subpartitions_default}{$table}{$part}{name};
							$deftb = $table . '_part'. $pos . '_subpart_default' if ($self->{rename_partition});
							$funct_cond .= "\t\tELSE INSERT INTO " . $self->quote_object_name($deftb)
										. " VALUES (NEW.*);\n\t\tEND IF;\n";
							$create_table_tmp .= "DROP TABLE $self->{pg_supports_ifexists} " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}{name}") . ";\n" if ($self->{drop_if_exists});
							$create_table_tmp .= "CREATE TABLE " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}{name}")
										. " () INHERITS ($table);\n";
							if ($cindx)
							{
								$create_table_index_tmp .= "CREATE INDEX " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}{name}_$pos")
									. " ON " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}{name}") . " ($cindx);\n";
							}
						}
						else
						{
							$funct_cond .= qq{		ELSE
				-- Raise an exception
				RAISE EXCEPTION 'Value out of range in subpartition. Fix the ${table}_insert_trigger() function!';
	};
							$funct_cond .= "\t\tEND IF;\n";
						}
					}
					# With default partition just add default and continue
					elsif (exists $self->{subpartitions_default}{$table}{$part})
					{
						my $tb_name = $self->{subpartitions_default}{$table}{$part}{name};
						if ($self->{rename_partition}) {
							$tb_name = $table . '_part' . $pos . '_subpart_default';
						} elsif ($self->{export_schema} && !$self->{schema} && ($table =~ /^([^\.]+)\./)) {
							$tb_name =  $1 . '.' . $self->{subpartitions_default}{$table}{$part}{name};
						}
						$tb_name = $table . '_part' . $pos . '_subpart_default' if (!$tb_name);
						$create_table_tmp .= "DROP TABLE $self->{pg_supports_ifexists} " . $self->quote_object_name($tb_name) . ";\n" if ($self->{drop_if_exists});
						if ($self->{pg_version} >= 11) {
							$create_table_tmp .= "CREATE TABLE " . $self->quote_object_name($tb_name)
									. " PARTITION OF " . $self->quote_object_name($table) . " DEFAULT;\n";
						} elsif ($self->{subpartitions}{$table}{$part}{$sub_old_pos}{info}[$i]->{type} eq 'RANGE') {
							$create_table_tmp .= "CREATE TABLE " . $self->quote_object_name($tb_name)
									. " PARTITION OF " . $self->quote_object_name($table) . " FOR VALUES FROM ($self->{subpartitions}{$table}{$part}{$sub_old_pos}{info}[-1]->{value}) TO (MAX_VALUE);\n";
						}
					}
				}
				$cond = 'ELSIF';
			}
			$old_part = $part;
			$old_pos = $pos;
			$create_table{$table}{table} .= $create_table_tmp;
			$create_table{$table}{index} .= $create_table_index_tmp;
		}

		if (exists $create_table{$table})
		{
			if (!$self->{pg_supports_partition})
			{
				if (exists $self->{partitions_default}{$table} && scalar keys %{$self->{partitions_default}{$table}} > 0)
				{
					my $deftb = $self->{partitions_default}{$table}{name};
					$deftb = $table . '_part_default' if ($self->{rename_partition});
					my $pname = $self->quote_object_name($deftb);
					$function .= $funct_cond . qq{	ELSE
	INSERT INTO $pname VALUES (NEW.*);
};
				}
				elsif ($function)
				{
					$function .= $funct_cond . qq{	ELSE
	-- Raise an exception
	RAISE EXCEPTION 'Value out of range in partition. Fix the ${table}_insert_trigger() function!';
};
				}
				$function .= qq{
END IF;
RETURN NULL;
END;
\$\$
LANGUAGE plpgsql;
} if ($function);
				$function = Ora2Pg::PLSQL::convert_plsql_code($self, $function);
			}
			else
			{
				# With default partition just add default and continue
				if (exists $self->{partitions_default}{$table} && scalar keys %{$self->{partitions_default}{$table}} > 0)
				{
					my $tb_name = '';

					if (exists $self->{replaced_tables}{"\L$table\E"} && $self->{replaced_tables}{"\L$table\E"})
					{
						$self->logit("\tReplacing table $table as " . $self->{replaced_tables}{lc($table)} . "...\n", 1);
						$tb_name = $self->{replaced_tables}{lc($table)};
					}
					if ($self->{rename_partition}) {
						$tb_name = ($tb_name||$table) . '_part_default';
					}
					else
					{
						if ($self->{export_schema} && !$self->{schema} && ($table =~ /^([^\.]+)\./)) {
							$tb_name =  $1 . '.' . $self->{partitions_default}{$table}{name};
						} else {
							$tb_name =  $self->{partitions_default}{$table}{name};
						}
					}
					$tb_name = $table . '_part_default' if (!$tb_name);
					$create_table{$table}{table} .= "DROP TABLE $self->{pg_supports_ifexists} " . $self->quote_object_name($tb_name) . ";\n" if ($self->{drop_if_exists});
					if ($self->{pg_version} >= 11) {
						$create_table{$table}{table} .= "CREATE TABLE " . $self->quote_object_name($tb_name)
								. " PARTITION OF " . $self->quote_object_name($table) . " DEFAULT;\n";
					} else {
						$create_table{$table}{table} .= "CREATE TABLE " . $self->quote_object_name($tb_name)
								. " PARTITION OF " . $self->quote_object_name($table) . " FOR VALUES FROM ($self->{partitions}{$table}{$old_pos}{info}[-1]->{value}) TO (MAX_VALUE);\n";
					}
				}
			}
		}

		if (exists $create_table{$table})
		{
			$partition_indexes .= qq{
-- Create indexes on each partition of table $table
$create_table{$table}{index}

} if ($create_table{$table}{index});
			$sql_output .= qq{
$create_table{$table}{table}
};
			my $tb = $self->quote_object_name($table);
			my $trg = $self->quote_object_name("${table}_insert_trigger");
			my $defname = $self->{partitions_default}{$table}{name};
			$defname = $table . '_part_default' if ($self->{rename_partition});
			$defname = $self->quote_object_name($defname);
			if (!$self->{pg_supports_partition} && $function)
			{
				$sql_output .= qq{
-- Create default table, where datas are inserted if no condition match
CREATE TABLE $defname () INHERITS ($tb);
} if ($self->{partitions_default}{$table}{name});
				$sql_output .= qq{

$function

CREATE TRIGGER ${table}_trigger_insert
BEFORE INSERT ON $table
FOR EACH ROW EXECUTE PROCEDURE $trg();

-------------------------------------------------------------------------------
};

				$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
				if ($owner)
				{
					$sql_output .= "ALTER TABLE " . $self->quote_object_name($self->{partitions_default}{$table}{name})
								. " OWNER TO " . $self->quote_object_name($owner) . ";\n"
						if ($self->{partitions_default}{$table}{name});
					$sql_output .= "ALTER FUNCTION " . $self->quote_object_name("${table}_insert_trigger")
								. "() OWNER TO " . $self->quote_object_name($owner) . ";\n";
				}
			}
		}
	}
	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($nparts - 1, $total_partition, 25, '=', 'partitions', 'end of output.'), "\n";
	}
	if (!$sql_output) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}
	$sql_output =~ s/\n{3,}/\n\n/g;
	$self->dump($sql_header . $sql_output);

	if ($partition_indexes)
	{
		my $fhdl = undef;
		$self->logit("Dumping partition indexes to file : PARTITION_INDEXES_$self->{output}\n", 1);
		$sql_header = "-- Generated by Ora2Pg, the Oracle database Schema converter, version $VERSION\n";
		$sql_header .= "-- Copyright 2000-2025 Gilles DAROLD. All rights reserved.\n";
		$sql_header .= "-- DATASOURCE: $self->{oracle_dsn}\n\n";
		$sql_header = ''  if ($self->{no_header});
		$fhdl = $self->open_export_file("PARTITION_INDEXES_$self->{output}");
		$self->set_binmode($fhdl) if (!$self->{compress});
		$self->dump($sql_header . $partition_indexes, $fhdl);
		$self->close_export_file($fhdl);
	}

	return;
}

=head2 export_synonym

Export Oracle synonym into PostgreSQL compatible statement.

=cut

sub export_synonym
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Add synonyms definition...\n", 1);
	# Read DML from file if any
	if ($self->{input_file}) {
		$self->read_synonym_from_file();
	}
	my $i = 1;
	my $num_total_synonym = scalar keys %{$self->{synonyms}};
	my $count_syn = 0;
	my $PGBAR_REFRESH = set_refresh_count($num_total_synonym);
	foreach my $syn (sort { $a cmp $b } keys %{$self->{synonyms}})
	{
		if (!$self->{quiet} && !$self->{debug} && ($count_syn % $PGBAR_REFRESH) == 0) {
			print STDERR $self->progress_bar($i, $num_total_synonym, 25, '=', 'synonyms', "generating $syn" ), "\r";
		}
		$count_syn++;
		if ($self->{synonyms}{$syn}{dblink}) {
			$sql_output .= "-- You need to create foreign table $self->{synonyms}{$syn}{table_owner}.$self->{synonyms}{$syn}{table_name} using foreign server: '$self->{synonyms}{$syn}{dblink}'\n -- see DBLINK export type to export the server definition\n";
		}
		$sql_output .= "CREATE$self->{create_or_replace} VIEW " . $self->quote_object_name($syn)
			. " AS SELECT * FROM ";
		if ($self->{synonyms}{$syn}{table_owner} && !$self->{schema} && $self->{export_schema}) {
			$sql_output .= $self->quote_object_name("$self->{synonyms}{$syn}{table_owner}.$self->{synonyms}{$syn}{table_name}") . ";\n";
		} else {
			$sql_output .= $self->quote_object_name($self->{synonyms}{$syn}{table_name}) . ";\n";
		}
		if ($self->{force_owner})
		{
			my $owner = $self->{synonyms}{$syn}{owner};
			$owner = $self->{force_owner} if ($self->{force_owner} && ($self->{force_owner} ne "1"));
			$sql_output .= "ALTER VIEW " . $self->quote_object_name("$self->{synonyms}{$syn}{owner}.$syn")
						. " OWNER TO " . $self->quote_object_name($owner) . ";\n";
		}
		$i++;
	}
	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($i - 1, $num_total_synonym, 25, '=', 'synonyms', 'end of output.'), "\n";
	}
	if (!$sql_output) {
		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
	}

	$self->dump($sql_header . $sql_output);

	return;
}

=head2 export_table

Export Oracle table into PostgreSQL compatible statement.

=cut

sub export_table
{
	my $self = shift;

	my $sql_header = $self->_set_file_header();
	my $sql_output = "";

	$self->logit("Exporting tables...\n", 1);

	if (!$self->{oracle_fdw_data_export})
	{
		if ($self->{export_schema} && ($self->{schema} || $self->{pg_schema}))
		{
			if ($self->{create_schema})
			{
				if ($self->{pg_schema} && $self->{pg_schema} =~ /,/) {
					$self->logit("FATAL: with export type TABLE you can not set multiple schema to PG_SCHEMA when EXPORT_SCHEMA is enabled.\n", 0, 1);
				}
				$sql_output .= "CREATE SCHEMA IF NOT EXISTS " . $self->quote_object_name($self->{pg_schema} || $self->{schema}) . ";\n";
			}
			my $owner = '';
			$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
			$owner ||= $self->{schema};
			if ($owner && $self->{create_schema}) {
				$sql_output .= "ALTER SCHEMA " . $self->quote_object_name($self->{pg_schema} || $self->{schema}) . " OWNER TO " .  $self->quote_object_name($owner) . ";\n";
			}
			$sql_output .= "\n";
		}
		elsif ($self->{export_schema})
		{
			if ($self->{create_schema})
			{
				my $current_schema = '';
				foreach my $table (sort keys %{$self->{tables}})
				{
					if ($table =~ /^([^\.]+)\..*/)
					{
						if ($1 ne $current_schema)
						{
							$current_schema = $1;
							$sql_output .= "CREATE SCHEMA IF NOT EXISTS " . $self->quote_object_name($1) . ";\n";
						}
					}
				}
			}
		}
		$sql_output .= $self->set_search_path();
	}

	# Read DML from file if any
	if ($self->{input_file}) {
		$self->read_schema_from_file();
	}

	my $constraints = '';
	if ($self->{file_per_constraint}) {
		$constraints .= $self->set_search_path();
	}
	my $indices = '';
	my $fts_indices = '';

	# Find first the total number of tables
	my $num_total_table = scalar keys %{$self->{tables}};

	# Hash that will contains virtual column information to build triggers
	my %virtual_trigger_info = ();

	# Stores DDL to restart autoincrement sequences
	my $sequence_output = '';

	if ($self->{is_mssql})
	{
		if (lc($self->{case_insensitive_search}) eq 'citext') {
			$sql_output .= "CREATE EXTENSION IF NOT EXISTS citext;\n";
		}
	} else {
		$self->{case_insensitive_search} = 'none';
	}

	# Dump all table/index/constraints SQL definitions
	my $ib = 1;
	my $count_table = 0;
	my $PGBAR_REFRESH = set_refresh_count($num_total_table);
	foreach my $table (sort {
			if (exists $self->{tables}{$a}{internal_id}) {
				$self->{tables}{$a}{internal_id} <=> $self->{tables}{$b}{internal_id};
			} else {
				$a cmp $b;
			}
		} keys %{$self->{tables}})
	{
		# Foreign table can not be temporary
		next if (($self->{type} eq 'FDW' || $self->{oracle_fdw_data_export})
				and $self->{tables}{$table}{table_info}{type} =~/ TEMPORARY/);

		$self->logit("Dumping table $table...\n", 1);
		if (!$self->{quiet} && !$self->{debug} && ($count_table % $PGBAR_REFRESH) == 0) {
			print STDERR $self->progress_bar($ib, $num_total_table, 25, '=', 'tables', "exporting $table" ), "\r";
		}
		$count_table++;

		# Create FDW server if required
		if ( $self->{external_to_fdw} && grep(/^$table$/i, keys %{$self->{external_table}}) )
		{
			my $srv_name = "\L$self->{external_table}{$table}{directory}\E";
			$srv_name =~ s/^.*\.//;
			$sql_header .= "CREATE EXTENSION IF NOT EXISTS file_fdw;\n\n" if ($sql_header !~ /CREATE EXTENSION .* file_fdw;/is);
			$sql_header .= "CREATE SERVER $srv_name FOREIGN DATA WRAPPER file_fdw;\n\n" if ($sql_header !~ /CREATE SERVER $srv_name FOREIGN DATA WRAPPER file_fdw;/is);
		}

		# MySQL ON UPDATE clause
		my %trigger_update = ();

		my $tbname = $self->get_replaced_tbname($table);
		my $foreign = '';
		if ( ($self->{type} eq 'FDW') || $self->{oracle_fdw_data_export} || ($self->{external_to_fdw} && (grep(/^$table$/i, keys %{$self->{external_table}}) || $self->{tables}{$table}{table_info}{connection})) ) {
			$foreign = ' FOREIGN ';
		}
		my $obj_type = $self->{tables}{$table}{table_info}{type} || 'TABLE';
		if ( ($obj_type eq 'TABLE') && $self->{tables}{$table}{table_info}{nologging} && !$self->{disable_unlogged} ) {
			$obj_type = 'UNLOGGED ' . $obj_type;
		}
		if ($self->{export_gtt} && !$foreign && $self->{tables}{$table}{table_info}{temporary} eq 'Y') {
			if ($sql_output !~ /LOAD '.*pgtt';/s) {
				if (!$self->{pgtt_nosuperuser}) {
					$sql_output .= "\nLOAD 'pgtt';\n";
				} else {
					$sql_output .= "\nLOAD '\$libdir/plugins/pgtt';\n";
				}
			}
			$obj_type = ' /*GLOBAL*/ TEMPORARY TABLE' if ($obj_type =~ /TABLE/);
		}
		if (exists $self->{tables}{$table}{table_as})
		{
			if ($self->{plsql_pgsql}) {
				$self->{tables}{$table}{table_as} = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{tables}{$table}{table_as});
			}
			$sql_output .= "\nDROP${foreign} TABLE $self->{pg_supports_ifexists} $tbname;" if ($self->{drop_if_exists});

			my $withoid = _make_WITH($self->{with_oid}, $self->{tables}{$tbname}{table_info});
			$sql_output .= "\nCREATE $obj_type $tbname $withoid AS $self->{tables}{$table}{table_as};\n";
			next;
		}
		if (exists $self->{tables}{$table}{truncate_table}) {
			$sql_output .= "\nTRUNCATE TABLE $tbname;\n";
		}
		my $serial_sequence = '';
		my $enum_str = '';
		my @skip_column_check = ();
		$sql_output .= "#ORA2PGENUM#\n"; # used to insert any enum data type before the table that will use it
		if (exists $self->{tables}{$table}{column_info})
		{
			my $schem = '';

			# Add the destination schema
			if ($self->{oracle_fdw_data_export} && ($self->{type} eq 'INSERT' || $self->{type} eq 'COPY')) {
				 $sql_output .= "\nCREATE FOREIGN TABLE $self->{fdw_import_schema}.$tbname (\n";
			} else {
				$sql_output .= "\nDROP${foreign} TABLE $self->{pg_supports_ifexists} $tbname;" if ($self->{drop_if_exists});
				$sql_output .= "\nCREATE$foreign $obj_type $tbname (\n";
			}

			# get column name list.
			my @collist = ();
			foreach my $k (sort {$self->{tables}{$table}{column_info}{$a}[11] <=> $self->{tables}{$table}{column_info}{$b}[11]} keys %{$self->{tables}{$table}{column_info}}) {
				push(@collist, $self->{tables}{$table}{column_info}{$k}[0]);
			}

			# Extract column information following the position order
			foreach my $k (sort { 
					if (!$self->{reordering_columns}) {
						$self->{tables}{$table}{column_info}{$a}[11] <=> $self->{tables}{$table}{column_info}{$b}[11];
					} else {
						my $tmpa = $self->{tables}{$table}{column_info}{$a};
						$tmpa->[2] =~ s/\D//g;
						my $typa = $self->_sql_type($tmpa->[1], $tmpa->[2], $tmpa->[5], $tmpa->[6], $tmpa->[4]);
						$typa =~ s/\(.*//;
						my $tmpb = $self->{tables}{$table}{column_info}{$b};
						$tmpb->[2] =~ s/\D//g;
						my $typb = $self->_sql_type($tmpb->[1], $tmpb->[2], $tmpb->[5], $tmpb->[6], $tmpb->[4]);
						$typb =~ s/\(.*//;
						if($TYPALIGN{$typa} != $TYPALIGN{$typb}){
							# sort by field size asc
							$TYPALIGN{$typa} <=> $TYPALIGN{$typb};
						} else {
							# if same size sort by original position
							$self->{tables}{$table}{column_info}{$a}[11] <=> $self->{tables}{$table}{column_info}{$b}[11];
						}
					}
				} keys %{$self->{tables}{$table}{column_info}})
			{

				# COLUMN_NAME,DATA_TYPE,DATA_LENGTH,NULLABLE,DATA_DEFAULT,DATA_PRECISION,DATA_SCALE,CHAR_LENGTH,TABLE_NAME,OWNER,VIRTUAL_COLUMN,POSITION,AUTO_INCREMENT,SRID,SDO_DIM,SDO_GTYPE
				my $f = $self->{tables}{$table}{column_info}{$k};
				$f->[2] =~ s/[^0-9\-\.]//g;
				my $type = $self->_sql_type($f->[1], $f->[2], $f->[5], $f->[6], $f->[4], 1);
				$type = "$f->[1], $f->[2]" if (!$type);
				# Change column names
				my $fname = $f->[0];
				if (exists $self->{replaced_cols}{"\L$table\E"}{"\L$fname\E"} && $self->{replaced_cols}{"\L$table\E"}{"\L$fname\E"})
				{
					$self->logit("\tReplacing column \L$f->[0]\E as " . $self->{replaced_cols}{"\L$table\E"}{"\L$fname\E"} . "...\n", 1);
					$fname = $self->{replaced_cols}{"\L$table\E"}{"\L$fname\E"};
				}

				# Check if we need auto increment
				if ($f->[12] eq 'auto_increment' || $f->[12] eq '1')
				{
					if ($type !~ s/bigint/bigserial/)
					{
						if ($type !~ s/smallint/smallserial/) {
							$type =~ s/integer/serial/;
							$type =~ s/numeric.*/bigserial/;
						}
					}
					if ($type =~ /serial/)
					{
						my $seqname = lc($tbname) . '_' . lc($fname) . '_seq';
						if ($self->{preserve_case}) {
							$seqname = $tbname . '_' . $fname . '_seq';
						}
						my $tobequoted = 0;
						if ($seqname =~ s/"//g) {
							$tobequoted = 1;
						}
						
						if (length($seqname) > 63)
						{
							if (length($tbname) > 29) {
								$seqname = substr(lc($tbname), 0, 29);
							} else {
								$seqname = lc($tbname);
							}
							if (length($fname) > 29) {
								$seqname .= '_' . substr(lc($fname), 0, 29);
							} else {
								$seqname .= '_' . lc($fname);
							}
							$seqname .= '_seq';
						}
						if ($tobequoted) {
							$seqname = '"' . $seqname . '"';
						}
						if (exists $self->{tables}{$table}{table_info}{auto_increment})
						{
							$self->{tables}{$table}{table_info}{auto_increment} = 1 if ($self->{is_mysql} && !$self->{tables}{$table}{table_info}{auto_increment});
							$serial_sequence .= "ALTER SEQUENCE $seqname RESTART WITH $self->{tables}{$table}{table_info}{auto_increment};\n";
						}
					}
				}

				# Check if this column should be replaced by a boolean following table/column name
				my $was_enum = 0;
				if ($f->[1] =~ s/^\s*ENUM\s*\(//i)
				{
					$was_enum = 1;
					$f->[1] =~ s/\)$//;
					my $keyname = lc($tbname . '_' . $fname . '_t');
					$keyname =~ s/["\`]//g;
					$enum_str .= "\nCREATE TYPE " .  $self->quote_object_name($keyname) .
								" AS ENUM ($f->[1]);";
					$type = $self->quote_object_name($keyname);
				}
				my $typlen = $f->[5];
				$typlen ||= $f->[2];
				if (!$self->{oracle_fdw_data_export})
				{
					if (grep(/^$f->[0]$/i, @{$self->{'replace_as_boolean'}{uc($table)}})) {
						$type = 'boolean';
						push(@skip_column_check, $fname);
					}
					# Check if this column should be replaced by a boolean following type/precision
					elsif (exists $self->{'replace_as_boolean'}{uc($f->[1])} && ($self->{'replace_as_boolean'}{uc($f->[1])}[0] == $typlen)) {
						$type = 'boolean';
						push(@skip_column_check, $fname);
					}
				}
				if ($f->[1] =~ /SDO_GEOMETRY/)
				{
					# 12:SRID,13:SDO_DIM,14:SDO_GTYPE
					# Set the dimension, array is (srid, dims, gtype)
					my $suffix = '';
					if ($f->[13] == 3) {
						$suffix = 'Z';
					} elsif ($f->[13] == 4) {
						$suffix = 'ZM';
					}
					my $gtypes = '';
					if (!$f->[14] || ($f->[14] =~  /,/) ) {
						$gtypes = $Ora2Pg::Oracle::ORA2PG_SDO_GTYPE{0};
					} else {
						$gtypes = $f->[14];
					}
					$type = "geometry($gtypes$suffix";
					if ($f->[12]) {
						$type .= ",$f->[12]";
					}
					$type .= ")";
				}
				$type = $self->{'modify_type'}{"\L$table\E"}{"\L$f->[0]\E"} if (exists $self->{'modify_type'}{"\L$table\E"}{"\L$f->[0]\E"});
				$fname = $self->quote_object_name($fname);
				my $citext_constraint = '';
				if ($self->{case_insensitive_search} =~ /^citext$/i && $type =~ /^(?:char|varchar|text)/)
				{
					if ($type =~ /^(?:char|varchar|text)\s*\((\d+)\)/) {
						$constraints .= "ALTER TABLE " . $self->quote_object_name($table) . " ADD CHECK (char_length($fname) <= $1);\n";
					}
					$type = 'citext';
				}
				elsif ($self->{case_insensitive_search} !~ /^none$/i && $type =~ /^(?:char|varchar|text)/)
				{
					$type .= ' COLLATE ' . $self->{case_insensitive_search};
				}
				$sql_output .= "\t$fname $type";
				if ($foreign && $self->is_primary_key_column($table, $f->[0])) {
					 $sql_output .= " OPTIONS (key 'true')";
				}
				if (!$f->[3] || ($f->[3] =~ /^N/))
				{
					# smallserial, serial and bigserial use a NOT NULL sequence as default value,
					# so we don't need to add it here
					if ($type !~ /serial/) {
						push(@{$self->{tables}{$table}{check_constraint}{notnull}}, $f->[0]);
						$sql_output .= " NOT NULL";
					}
				}

				# Autoincremented columns
				if (!$self->{schema} && $self->{export_schema} && $f->[8] !~ /\./) {
					$f->[8] = "$f->[9].$f->[8]";
				}
				if (exists $self->{identity_info}{$f->[8]}{$f->[0]} and $self->{type} ne 'FDW' and !$self->{oracle_fdw_data_export})
				{
					$sql_output =~ s/ NOT NULL\s*$//s; # IDENTITY or serial column are NOT NULL by default
					if ($self->{pg_supports_identity})
					{
						$sql_output =~ s/ [^\s]+$/ bigint/ if ($self->{force_identity_bigint}) ; # Force bigint
						$sql_output .= " GENERATED $self->{identity_info}{$f->[8]}{$f->[0]}{generation} AS IDENTITY";
						if (exists $self->{identity_info}{$f->[8]}{$f->[0]}{options}
							&& $self->{identity_info}{$f->[8]}{$f->[0]}{options} ne '')
						{
							# Adapt automatically the max value following the data type
							if ($sql_output =~ / (integer|int4|int) GENERATED/i) {
								$self->{identity_info}{$f->[8]}{$f->[0]}{options} =~ s/ 9223372036854775807/ 2147483647/s;
							}
							$sql_output .= " (" . $self->{identity_info}{$f->[8]}{$f->[0]}{options} . ')';
						}
					}
					else
					{
						$sql_output =~ s/bigint\s*$/bigserial/s;
						$sql_output =~ s/smallint\s*$/smallserial/s;
						$sql_output =~ s/(integer|int)\s*$/serial/s;
					}
					$sql_output .= ",\n";
					if ($self->{preserve_case}) {
						$sequence_output .= "SELECT ora2pg_upd_autoincrement_seq('$f->[8]','$f->[0]');\n";
					} else {
						$sequence_output .= "SELECT ora2pg_upd_autoincrement_seq('\L$f->[8]\E','\L$f->[0]\E');\n";
					}
					next;
				}

				# Default value
				if ($f->[4] ne "" && uc($f->[4]) ne 'NULL')
				{
					$f->[4] =~ s/^\s+//;
					$f->[4] =~ s/\s+$//;
					$f->[4] =~ s/"//gs;
					if ($self->{plsql_pgsql}) {
						$f->[4] = Ora2Pg::PLSQL::convert_plsql_code($self, $f->[4]);
					}
					# Check if the column make reference to an other column
					#my $use_other_col = 0;
					#foreach my $c (@collist) {
					#	$use_other_col = 1 if ($f->[4] =~ /\b$c\b/i);
					#}

					if ($f->[10] eq 'YES' && $self->{pg_version} >= 12)
					{
						$sql_output .= " GENERATED ALWAYS AS (" . $f->[4] . ") STORED";
					}
					# Check if this is a virtual column before proceeding to default value export
					elsif ($self->{tables}{$table}{column_info}{$k}[10] eq 'YES')
					{
						$virtual_trigger_info{$table}{$k} = $f->[4];
						$virtual_trigger_info{$table}{$k} =~ s/"//gs;
						foreach my $c (keys %{$self->{tables}{$table}{column_info}}) {
							$virtual_trigger_info{$table}{$k} =~ s/\b$c\b/NEW.$c/gs;
						}
					}
					else
					{
						if (($f->[4] ne '') && ($self->{type} ne 'FDW') && !$self->{oracle_fdw_data_export})
						{
							if ($type eq 'boolean')
							{
								my $found = 0;
								foreach my $k (sort {$b cmp $a} keys %{ $self->{ora_boolean_values} })
								{
									if ($f->[4] =~ /\b$k\b/i)
									{
										$sql_output .= " DEFAULT '" . $self->{ora_boolean_values}{$k} . "'";
										$found = 1;
										last;
									}
								}
								$sql_output .= " DEFAULT " . $f->[4] if (!$found);
							}
							else
							{
								if (($f->[4] !~ /^'/) && ($f->[4] =~ /[^\d\.]/))
								{
									if ($type =~ /CHAR|TEXT/i || ($was_enum && $f->[1] =~ /'/i)) {
										$f->[4] = "'$f->[4]'" if ($f->[4] !~ /[']/ && $f->[4] !~ /\(.*\)/ && uc($f->[4]) ne 'NULL');
									}
									elsif ($type =~ /DATE|TIME/i)
									{
										# Case of MSSQL datetime default value to 0, must be converted to '1900-01-01 00:00:00'
										if ($self->{is_mssql} && $f->[4] eq "(0)")
										{
											if ($type =~ /TIME/i) {
												$f->[4] = '1900-01-01 00:00:00';
											} else {
												$f->[4] = '1900-01-01';
											}
										}
										# All other cases
										if ($f->[4] =~ /^0000-/)
										{
											if ($self->{replace_zero_date}) {
												$f->[4] = $self->{replace_zero_date};
											} else {
												$f->[4] =~ s/^0000-\d+-\d+/1970-01-01/;
											}
										}
										if ($f->[4] =~ /^\d+/) {
											$f->[4] = "'$f->[4]'";
										} elsif ($f->[4] =~ /^[\-]*INFINITY$/) {
											$f->[4] = "'$f->[4]'::$type";
										} elsif ($f->[4] =~ /AT TIME ZONE/i) {
											$f->[4] = "($f->[4])";
										}
									}
								}
								else
								{
									my @c =  $f->[4] =~ /\./g;
									if ($#c >= 1)
									{
										if ($type =~ /CHAR|TEXT/i || ($was_enum && $f->[1] =~ /'/i)) {
											$f->[4] = "'$f->[4]'" if ($f->[4] !~ /[']/ && $f->[4] !~ /\(.*\)/ && uc($f->[4]) ne 'NULL');
										}
										elsif ($type =~ /DATE|TIME/i)
										{
											# Case of MSSQL datetime default value to 0, must be converted to '1900-01-01 00:00:00'
											if ($self->{is_mssql} && $f->[4] eq "(0)")
											{
												if ($type =~ /TIME/i) {
													$f->[4] = '1900-01-01 00:00:00';
												} else {
													$f->[4] = '1900-01-01';
												}
											}
											# All other cases
											if ($f->[4] =~ /^0000-/)
											{
												if ($self->{replace_zero_date}) {
													$f->[4] = $self->{replace_zero_date};
												} else {
													$f->[4] =~ s/^0000-\d+-\d+/1970-01-01/;
												}
											}
											if ($f->[4] =~ /^\d+/) {
												$f->[4] = "'$f->[4]'";
											} elsif ($f->[4] =~ /^[\-]*INFINITY$/) {
												$f->[4] = "'$f->[4]'::$type";
											} elsif ($f->[4] =~ /AT TIME ZONE/i) {
												$f->[4] = "($f->[4])";
											}
										} elsif (uc($f->[4]) ne 'NULL') {
											$f->[4] = "'$f->[4]'";
										}
									}
									elsif ($type =~ /(char|text)/i && $f->[4] !~ /^'/)
									{
										$f->[4] = "'$f->[4]'";
									}
								}
								$f->[4] = 'NULL' if ($f->[4] eq "''" && $type =~ /int|double|numeric/i);
								$f->[4] =~ s/'((?:session|current)_[^']+)'/$1/;
								$sql_output .= " DEFAULT $f->[4]";
							}
						}
					}
				}
				$sql_output .= ",\n";

				# Replace default generated value on update by a trigger
				if ($f->[12] =~ /^DEFAULT_GENERATED on update (.*)/i) {
					$trigger_update{$f->[0]} = $1;
				}
			}
			if ($self->{pkey_in_create}) {
				$sql_output .= $self->_get_primary_keys($table, $self->{tables}{$table}{unique_key});
			}
			$sql_output =~ s/,$//;
			$sql_output .= ')';

			if (exists $self->{tables}{$table}{table_info}{on_commit})
			{
				$sql_output .= ' ' . $self->{tables}{$table}{table_info}{on_commit};
			}
			if ($self->{tables}{$table}{table_info}{partitioned} && $self->{pg_supports_partition} && !$self->{disable_partition})
			{
				if (grep(/^$self->{partitions_list}{"\L$table\E"}{type}$/, 'HASH', 'RANGE', 'LIST') && !exists $self->{partitions_list}{"\L$table\E"}{refrtable})
				{
					$sql_output .= " PARTITION BY " . $self->{partitions_list}{"\L$table\E"}{type} . " (";
					my $expr = '';
					if (exists $self->{partitions_list}{"\L$table\E"}{columns})
					{
						my $len = $#{$self->{partitions_list}{"\L$table\E"}{columns}};
						for (my $j = 0; $j <= $len; $j++)
						{
							if ($self->{partitions_list}{"\L$table\E"}{type} eq 'LIST') {
								$expr .= ' || ' if ($j > 0);
							} else {
								$expr .= ', ' if ($j > 0);
							}
							$expr .= $self->quote_object_name($self->{partitions_list}{"\L$table\E"}{columns}[$j]);
							if ($self->{partitions_list}{"\L$table\E"}{type} eq 'LIST' && $len > 0) {
								$expr .= '::text';
							}
						}
						if ($self->{partitions_list}{"\L$table\E"}{type} eq 'LIST' && $len >= 0) {
							$expr = '(' . $expr . ')';
						}
					}
					else
					{
						if ($self->{plsql_pgsql}) {
							$self->{partitions_list}{"\L$table\E"}{expression} = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions_list}{"\L$table\E"}{expression});
						}
						$expr .= $self->{partitions_list}{"\L$table\E"}{expression};
					}
					$expr = '(' . $expr . ')' if ($expr =~ /[\(\+\-\*\%:]/ && $expr !~ /^\(.*\)$/);
					$sql_output .= 	"$expr)";
				}
				elsif ($self->{partition_by_reference} eq 'none')
				{
					print STDERR "WARNING: unsupported partition type on table '$table'\n";
					$sql_output .=  " -- Unsupported partition type '" . $self->{partitions_list}{"\L$table\E"}{type} . "', please check constraint: " . $self->{partitions_list}{"\L$table\E"}{refconstraint} . "\n";
				} 
				elsif ($self->{partition_by_reference} eq 'duplicate')
				{
					$sql_output .= "DUPLICATE_EMPLACEMENT";
					my $reftable = $self->{partitions_list}{"\L$table\E"}{refrtable};
					$sql_output .= " PARTITION BY " . $self->{partitions_list}{"\L$reftable\E"}{type} . " (";
					my $expr = '';
					if (exists $self->{partitions_list}{"\L$reftable\E"}{columns})
					{
						my $len = $#{$self->{partitions_list}{"\L$reftable\E"}{columns}};
						for (my $j = 0; $j <= $len; $j++)
						{
							if ($self->{partitions_list}{"\L$reftable\E"}{type} eq 'LIST') {
								$expr .= ' || ' if ($j > 0);
							} else {
								$expr .= ', ' if ($j > 0);
							}
							$expr .= $self->quote_object_name($self->{partitions_list}{"\L$reftable\E"}{columns}[$j]);
							if ($self->{partitions_list}{"\L$reftable\E"}{type} eq 'LIST' && $len > 0) {
								$expr .= '::text';
							}
						}
						if ($self->{partitions_list}{"\L$reftable\E"}{type} eq 'LIST' && $len >= 0) {
							$expr = '(' . $expr . ')';
						}
					}
					else
					{
						if ($self->{plsql_pgsql}) {
							$self->{partitions_list}{"\L$reftable\E"}{expression} = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions_list}{"\L$reftable\E"}{expression});
						}
						$expr .= $self->{partitions_list}{"\L$reftable\E"}{expression};
					}
					$expr = '(' . $expr . ')' if ($expr =~ /[\(\+\-\*\%:]/ && $expr !~ /^\(.*\)$/);
					$sql_output .= 	"$expr)";

					foreach my $k (keys %{ $self->{tables}{"$reftable"}{column_info} })
					{
						next if (!grep(/^$k$/i, @{$self->{partitions_list}{"\L$reftable\E"}{columns}}));
						my $f = $self->{tables}{"$reftable"}{column_info}{$k};
						$f->[2] =~ s/[^0-9\-\.]//g;
						# COLUMN_NAME,DATA_TYPE,DATA_LENGTH,NULLABLE,DATA_DEFAULT,DATA_PRECISION,DATA_SCALE,CHAR_LENGTH,TABLE_NAME,OWNER,VIRTUAL_COLUMN,POSITION,AUTO_INCREMENT,SRID,SDO_DIM,SDO_GTYPE
						my $type = $self->_sql_type($f->[1], $f->[2], $f->[5], $f->[6], $f->[4], 1);
						$type = "$f->[1], $f->[2]" if (!$type);
						# Change column names
						my $fname = $f->[0];
						if (exists $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"} && $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"})
						{
							$self->logit("\tReplacing column \L$f->[0]\E as " . $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"} . "...\n", 1);
							$fname = $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"};
						}
						$sql_output =~ s/\s\)DUPLICATE_EMPLACEMENT/,\n\t\L$fname\E $type NOT NULL -- partition by reference from $table to $reftable\n)/s;
					}
				}
				elsif ($self->{partition_by_reference} =~ /^\d+$/)
				{
					my $reftable = $table;
					$sql_output .= " PARTITION BY HASH (";
					my $expr = '';
					if (exists $self->{partitions_list}{"\L$reftable\E"}{columns})
					{
						my $len = $#{$self->{partitions_list}{"\L$reftable\E"}{columns}};
						for (my $j = 0; $j <= $len; $j++)
						{
							if ($self->{partitions_list}{"\L$reftable\E"}{type} eq 'LIST') {
								$expr .= ' || ' if ($j > 0);
							} else {
								$expr .= ', ' if ($j > 0);
							}
							$expr .= $self->quote_object_name($self->{partitions_list}{"\L$reftable\E"}{columns}[$j]);
							if ($self->{partitions_list}{"\L$reftable\E"}{type} eq 'LIST' && $len > 0) {
								$expr .= '::text';
							}
						}
						if ($self->{partitions_list}{"\L$reftable\E"}{type} eq 'LIST' && $len >= 0) {
							$expr = '(' . $expr . ')';
						}
					}
					else
					{
						if ($self->{plsql_pgsql}) {
							$self->{partitions_list}{"\L$reftable\E"}{expression} = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions_list}{"\L$reftable\E"}{expression});
						}
						$expr .= $self->{partitions_list}{"\L$reftable\E"}{expression};
					}
					$expr = '(' . $expr . ')' if ($expr =~ /[\(\+\-\*\%:]/ && $expr !~ /^\(.*\)$/);
					$sql_output .= 	"$expr)";
				}
			}
			if ($obj_type =~ /\bTEMPORARY TABLE\b/)
			{
				if ($self->{tables}{$table}{table_info}{duration} eq 'SYS$TRANSACTION') {
					$sql_output .= ' ON COMMIT DELETE ROWS';
				} elsif ($self->{tables}{$table}{table_info}{duration} eq 'SYS$SESSION') {
					$sql_output .= ' ON COMMIT PRESERVE ROWS';
				}
			}
			if ( ($self->{type} ne 'FDW') && !$self->{oracle_fdw_data_export}
				&& (!$self->{external_to_fdw} || (!grep(/^$table$/i, keys %{$self->{external_table}})
						&& !$self->{tables}{$table}{table_info}{connection})) )
			{
				my $withoid = _make_WITH($self->{with_oid}, $self->{tables}{$table}{table_info});
				if ($self->{use_tablespace} && $self->{tables}{$table}{table_info}{tablespace} && !grep(/^$self->{tables}{$table}{table_info}{tablespace}$/i, @{$self->{default_tablespaces}})) {
					$sql_output .= " $withoid TABLESPACE $self->{tables}{$table}{table_info}{tablespace};\n";
				} else {
					$sql_output .= " $withoid;\n";
				}
			}
			elsif ( grep(/^$table$/i, keys %{$self->{external_table}}) )
			{
				my $program = '';
				$program = ", program '$self->{external_table}{$table}{program}'";
				$sql_output .= " SERVER \L$self->{external_table}{$table}{directory}\E OPTIONS(filename '$self->{external_table}{$table}{directory_path}$self->{external_table}{$table}{location}', format 'csv', delimiter '$self->{external_table}{$table}{delimiter}'$program);\n";
			}
			elsif ($self->{is_mysql})
			{
				$schem = "dbname '$self->{schema}'," if ($self->{schema});
				my $r_server = $self->{fdw_server};
				my $r_table = $table;
				if ($self->{tables}{$table}{table_info}{connection} =~ /([^'\/]+)\/([^']+)/)
				{
					$r_server = $1;
					$r_table = $2;
				}
				$sql_output .= " SERVER $r_server OPTIONS($schem table_name '$r_table');\n";
			}
			elsif ($self->{is_mssql})
			{
				$schem = "$self->{schema}." if ($self->{schema});
				my $r_server = $self->{fdw_server};
				my $r_table = $table;
				if ($self->{tables}{$table}{table_info}{connection} =~ /([^'\/]+)\/([^']+)/)
				{
					$r_server = $1;
					$r_table = $2;
				}
				$sql_output .= " SERVER $r_server OPTIONS(table_name '$schem$r_table');\n";
			}
			else
			{
				my $tmptb = $table;
				if ($self->{schema}) {
					$schem = "schema '$self->{schema}',";
				} elsif ($tmptb =~ s/^([^\.]+)\.//) {
					$schem = "schema '$1',";
				}
				$sql_output .= " SERVER $self->{fdw_server} OPTIONS($schem table '$tmptb', ";
				if ($self->{oracle_fdw_prefetch}) {
					$sql_output .= "prefetch '$self->{oracle_fdw_prefetch}', ";
			        }
				$sql_output .= "readonly 'true');\n";
			}
		}
		$sql_output =~ s/#ORA2PGENUM#/$enum_str/s;

		$sql_header .= "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\n" if ($self->{use_uuid} && $sql_header !~ /CREATE EXTENSION .*uuid-ossp/is);

		# For data export from foreign table, go to next table
		if ($self->{oracle_fdw_data_export})
		{
			$ib++;
			next;
		}

		$sql_output .= $serial_sequence;

		# clean up partition list
		foreach my $t (keys %{$self->{partitions_list}})
		{
			my $nb = keys %{$self->{partitions_list}{$t}};
			delete $self->{partitions_list}{$t} if ($nb == 0);
		}

		# Add comments on table
		if (!$self->{disable_comment} && $self->{tables}{$table}{table_info}{comment})
		{
			$self->{tables}{$table}{table_info}{comment} =~ s/'/''/gs;
			$sql_output .= "COMMENT ON$foreign TABLE " . $self->quote_object_name($tbname) . " IS E'$self->{tables}{$table}{table_info}{comment}';\n";
		}

		# Add comments on columns
		if (!$self->{disable_comment})
		{
			foreach my $f (sort { lc($a) cmp lc($b) } keys %{$self->{tables}{$table}{column_comments}})
			{
				next unless $self->{tables}{$table}{column_comments}{$f};
				$self->{tables}{$table}{column_comments}{$f} =~ s/'/''/gs;
				# Change column names
				my $fname = $f;
				if (exists $self->{replaced_cols}{"\L$table\E"}{lc($fname)} && $self->{replaced_cols}{"\L$table\E"}{lc($fname)}) {
					$self->logit("\tReplacing column $f as " . $self->{replaced_cols}{"\L$table\E"}{lc($fname)} . "...\n", 1);
					$fname = $self->{replaced_cols}{"\L$table\E"}{lc($fname)};
				}
				$sql_output .= "COMMENT ON COLUMN " .  $self->quote_object_name($tbname) . '.'
								.  $self->quote_object_name($fname)
								. " IS E'" . $self->{tables}{$table}{column_comments}{$f} .  "';\n";
			}
		}

		# Change ownership
		if ($self->{force_owner} && $sql_output =~ /$tbname/is )
		{
			my $owner = $self->{tables}{$table}{table_info}{owner};
			$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
			$sql_output .= "ALTER $foreign " . ($self->{tables}{$table}{table_info}{type} || 'TABLE')
		       				.  " " .$self->quote_object_name($tbname)
						. " OWNER TO " .  $self->quote_object_name($owner) . ";\n";
		}
		if (exists $self->{tables}{$table}{alter_index} && $self->{tables}{$table}{alter_index})
		{
			foreach (@{$self->{tables}{$table}{alter_index}}) {
				$sql_output .= "$_;\n";
			}
		}
		my $export_indexes = 1;

		if ((!$self->{tables}{$table}{table_info}{partitioned} || $self->{pg_version} >= 11
				|| $self->{disable_partition}) && $self->{type} ne 'FDW')
		{
			# Set the indexes definition
			my ($idx, $fts_idx) = $self->_create_indexes($table, 0, %{$self->{tables}{$table}{indexes}});
			$indices .= "$idx\n" if ($idx);
			$fts_indices .= "$fts_idx\n" if ($fts_idx);
			if (!$self->{file_per_index})
			{
				$sql_output .= $indices;
				$indices = '';
				$sql_output .= $fts_indices;
				$fts_indices = '';
			}

			# Set the unique (and primary) key definition 
			$constraints .= $self->_create_unique_keys($table, $self->{tables}{$table}{unique_key});
			# Set the check constraint definition 
			$constraints .= $self->_create_check_constraint($table, $self->{tables}{$table}{check_constraint},$self->{tables}{$table}{field_name}, @skip_column_check);
			if (!$self->{file_per_constraint})
			{
				$sql_output .= $constraints;
				$constraints = '';
			}
		}

		if (exists $self->{tables}{$table}{alter_table} && !$self->{disable_unlogged} )
		{
			$obj_type =~ s/UNLOGGED //;
			foreach (@{$self->{tables}{$table}{alter_table}}) {
				$sql_output .= "\nALTER $obj_type $tbname $_;\n";
			}
		}
		$ib++;

		# Add the MySQL ON UPDATE trigger
		if (scalar keys %trigger_update)
		{
			my $objname = $table . '_default_';
			$objname =~ s/[^a-z0-9_]//ig;
			$sql_output .= qq{
DROP TRIGGER IF EXISTS ${objname}_trg ON $tbname;
CREATE OR REPLACE FUNCTION ${objname}_fct() RETURNS trigger AS \$\$
BEGIN
};
			foreach my $c (sort keys %trigger_update)
			{	
				my $colname = $self->quote_object_name($c);
				$sql_output .= qq{
    NEW.$colname = $trigger_update{$c};};
			}
			$sql_output .= qq{
    RETURN NEW;
END;
\$\$ LANGUAGE plpgsql;

CREATE TRIGGER ${objname}_trg
    BEFORE UPDATE ON $tbname
    FOR EACH ROW
    EXECUTE FUNCTION ${objname}_fct();
};
		}
	}

	if (!$self->{quiet} && !$self->{debug})
	{
		print STDERR $self->progress_bar($ib - 1, $num_total_table, 25, '=', 'tables', 'end of table export.'), "\n";
	}

	# When exporting data with oracle_fdw there is no more to do
	return $sql_output if ($self->{oracle_fdw_data_export});

	if ($sequence_output && $self->{type} ne 'FDW')
	{
		my $fhdl = undef;
		my $qt = '';
		$qt = '"' if ($self->{preserve_case});
		my $fct_sequence = qq{
CREATE OR REPLACE FUNCTION ora2pg_upd_autoincrement_seq (tbname text, colname text) RETURNS VOID AS \$body\$
DECLARE
        query text;
        maxval bigint;
        seqname text;
BEGIN
        query := 'SELECT max($qt' || colname || '$qt)+1 FROM $qt' || tbname || '$qt';
        EXECUTE query INTO maxval;
        IF (maxval IS NOT NULL) THEN
		query := \$\$SELECT pg_get_serial_sequence ('$qt\$\$|| tbname || \$\$$qt', '\$\$ || colname || \$\$');\$\$;
                EXECUTE query INTO seqname;
                IF (seqname IS NOT NULL) THEN
                        query := 'ALTER SEQUENCE ' || seqname || ' RESTART WITH ' || maxval;
                        EXECUTE query;
                END IF;
        ELSE
                RAISE NOTICE 'Table % is empty, you must load the AUTOINCREMENT file after data import.', tbname;
        END IF;
END;
\$body\$
LANGUAGE PLPGSQL;

};
		$sequence_output = $fct_sequence . $sequence_output;
		$sequence_output .= "DROP FUNCTION ora2pg_upd_autoincrement_seq(text, text);\n";
		$self->logit("Dumping DDL to restart autoincrement sequences into separate file : AUTOINCREMENT_$self->{output}\n", 1);
		$fhdl = $self->open_export_file("AUTOINCREMENT_$self->{output}");
		$self->set_binmode($fhdl) if (!$self->{compress});
		$sequence_output = $self->set_search_path() . $sequence_output;
		$self->dump($sql_header . $sequence_output, $fhdl);
		$self->close_export_file($fhdl);
	}

	if ($self->{file_per_index} && ($self->{type} ne 'FDW'))
	{
		my $fhdl = undef;
		$self->logit("Dumping indexes to one separate file : INDEXES_$self->{output}\n", 1);
		$fhdl = $self->open_export_file("INDEXES_$self->{output}");
		$self->set_binmode($fhdl) if (!$self->{compress});
		$indices = "-- Nothing found of type indexes\n" if (!$indices && !$self->{no_header});
		$indices =~ s/\n+/\n/gs;
		$self->_restore_comments(\$indices);
		$indices = $self->set_search_path() . $indices;
		$self->dump($sql_header . $indices, $fhdl);
		$self->close_export_file($fhdl);
		$indices = '';
		if ($fts_indices) {
			$fts_indices =~ s/\n+/\n/gs;
			my $unaccent = '';
			if ($self->{use_lower_unaccent}) {
				$unaccent = qq{
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE OR REPLACE FUNCTION unaccent_immutable(text)
RETURNS text AS
\$\$
  SELECT lower(public.unaccent('public.unaccent', \$1));
\$\$ LANGUAGE sql IMMUTABLE;

};
			} elsif ($self->{use_unaccent}) {
				$unaccent = qq{
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE OR REPLACE FUNCTION unaccent_immutable(text)
RETURNS text AS
\$\$
  SELECT public.unaccent('public.unaccent', \$1);
\$\$ LANGUAGE sql IMMUTABLE;

};
			}
			# FTS TRIGGERS are exported in a separated file to be able to parallelize index creation
			$self->logit("Dumping triggers for FTS indexes to one separate file : FTS_INDEXES_$self->{output}\n", 1);
			$fhdl = $self->open_export_file("FTS_INDEXES_$self->{output}");
			$self->set_binmode($fhdl) if (!$self->{compress});
			$self->_restore_comments(\$fts_indices);
			$fts_indices = $self->set_search_path() . $fts_indices;
			$self->dump($sql_header. $unaccent . $fts_indices, $fhdl);
			$self->close_export_file($fhdl);
			$fts_indices = '';
		}
	}

	# Dumping foreign key constraints
	my $fkeys = '';
	foreach my $table (sort keys %{$self->{tables}})
	{
		next if ($#{$self->{tables}{$table}{foreign_key}} < 0);
		$self->logit("Dumping RI $table...\n", 1);
		# Add constraint definition
		if ($self->{type} ne 'FDW')
		{
			my $create_all = $self->_create_foreign_keys($table);
			if ($create_all)
			{
				if ($self->{file_per_fkeys}) {
					$fkeys .= $create_all;
				} else {
					if ($self->{file_per_constraint}) {
						$constraints .= $create_all;
					} else {
						$sql_output .= $create_all;
					}
				}
			}
		}
	}

	if ($self->{file_per_constraint} && ($self->{type} ne 'FDW'))
	{
		my $fhdl = undef;
		$self->logit("Dumping constraints to one separate file : CONSTRAINTS_$self->{output}\n", 1);
		$fhdl = $self->open_export_file("CONSTRAINTS_$self->{output}");
		$self->set_binmode($fhdl) if (!$self->{compress});
		$constraints = "-- Nothing found of type constraints\n" if (!$constraints && !$self->{no_header});
		$self->_restore_comments(\$constraints);
		$self->dump($sql_header . $constraints, $fhdl);
		$self->close_export_file($fhdl);
		$constraints = '';
	}

	if ($fkeys)
	{
		my $fhdl = undef;
		$self->logit("Dumping foreign keys to one separate file : FKEYS_$self->{output}\n", 1);
		$fhdl = $self->open_export_file("FKEYS_$self->{output}");
		$self->set_binmode($fhdl) if (!$self->{compress});
		$fkeys = "-- Nothing found of type foreign keys\n" if (!$fkeys && !$self->{no_header});
		$self->_restore_comments(\$fkeys);
		$fkeys = $self->set_search_path() . $fkeys;
		$self->dump($sql_header . $fkeys, $fhdl);
		$self->close_export_file($fhdl);
		$fkeys = '';
	}

	if (!$sql_output)
	{
		$sql_output = "-- Nothing found of type TABLE\n" if (!$self->{no_header});
	}
	else
	{
		$self->_restore_comments(\$sql_output);
	}

	$self->dump($sql_header . $sql_output);

	# Some virtual column have been found
	if ($self->{type} ne 'FDW' and scalar keys %virtual_trigger_info > 0)
	{
		my $trig_out = '';
		foreach my $tb (sort keys %virtual_trigger_info)
		{
			my $tname = "virt_col_${tb}_trigger";
			$tname =~ s/\./_/g;
			$tname = $self->quote_object_name($tname);
			my $fname = "fct_virt_col_${tb}_trigger";
			$fname =~ s/\./_/g;
			$fname = $self->quote_object_name($fname);
			$trig_out .= "DROP TRIGGER $self->{pg_supports_ifexists} $tname ON " . $self->quote_object_name($tb) . " CASCADE;\n\n";
			$trig_out .= "CREATE$self->{create_or_replace} FUNCTION $fname() RETURNS trigger AS \$BODY\$\n";
			$trig_out .= "BEGIN\n";
			foreach my $c (sort keys %{$virtual_trigger_info{$tb}}) {
				$trig_out .= "\tNEW.$c = $virtual_trigger_info{$tb}{$c};\n";
			}
			$tb = $self->quote_object_name($tb);
			$trig_out .= qq{
RETURN NEW;
end
\$BODY\$
 LANGUAGE 'plpgsql' SECURITY DEFINER;

CREATE TRIGGER $tname
        BEFORE INSERT OR UPDATE ON $tb FOR EACH ROW
        EXECUTE PROCEDURE $fname();

};
		}
		$self->_restore_comments(\$trig_out);
		if (!$self->{file_per_constraint})
		{
			$self->dump($trig_out);
		}
		else
		{
			my $fhdl = undef;
			$self->logit("Dumping virtual column triggers to one separate file : VIRTUAL_COLUMNS_$self->{output}\n", 1);
			$fhdl = $self->open_export_file("VIRTUAL_COLUMNS_$self->{output}");
			$self->set_binmode($fhdl) if (!$self->{compress});
			$self->dump($sql_header . $trig_out, $fhdl);
			$self->close_export_file($fhdl);
		}
	}
}

=head2 _get_sql_statements

Returns a string containing the PostgreSQL compatible SQL Schema
definition.

=cut

sub _get_sql_statements
{
	my $self = shift;

	# Process view
	if ($self->{type} eq 'VIEW')
	{
		$self->export_view();
	}

	# Process materialized view
	elsif ($self->{type} eq 'MVIEW')
	{
		$self->export_mview();
	}

	# Process grant
	elsif ($self->{type} eq 'GRANT')
	{
		$self->export_grant();
	}

	# Process sequences
	elsif ($self->{type} eq 'SEQUENCE')
	{
		$self->export_sequence();
	}

	# Process sequences values
	elsif ($self->{type} eq 'SEQUENCE_VALUES')
	{
		$self->export_sequence_values();
	}

	# Process dblink
	elsif ($self->{type} eq 'DBLINK')
	{
		$self->export_dblink();
	}

	# Process dblink
	elsif ($self->{type} eq 'DIRECTORY')
	{
		$self->export_directory();
	}

	# Process triggers
	elsif ($self->{type} eq 'TRIGGER')
	{
		$self->export_trigger();
	}

	# Process queries to parallelize
	elsif ($self->{type} eq 'LOAD')
	{
		$self->parallelize_statements();
	}

	# Process queries only
	elsif ($self->{type} eq 'QUERY')
	{
		$self->translate_query();
	}

	# Process SQL script
	elsif ($self->{type} eq 'SCRIPT')
	{
		$self->translate_script();
	}

	# Process functions only
	elsif ($self->{type} eq 'FUNCTION')
	{
		$self->start_function_json_config($self->{type});

		$self->export_function();

		$self->end_function_json_config($self->{type});
	}

	# Process procedures only
	elsif ($self->{type} eq 'PROCEDURE')
	{
		$self->start_function_json_config($self->{type});

		$self->export_procedure();

		$self->end_function_json_config($self->{type});
	}

	# Process packages only
	elsif ($self->{type} eq 'PACKAGE')
	{
		$self->start_function_json_config($self->{type});

		$self->export_package();

		$self->end_function_json_config($self->{type});
	}

	# Process types only
	elsif ($self->{type} eq 'TYPE')
	{
		$self->export_type();
	}

	# Process TABLESPACE only
	elsif ($self->{type} eq 'TABLESPACE')
	{
		$self->export_tablespace();
	}

	# Export as Kettle XML file
	elsif ($self->{type} eq 'KETTLE')
	{
		$self->export_kettle();
	}

	# Process PARTITION only
	elsif ($self->{type} eq 'PARTITION')
	{
		$self->export_partition();
	}

	# Process synonyms only
	elsif ($self->{type} eq 'SYNONYM')
	{
		$self->export_synonym();
	}

	# Dump the database structure: tables, constraints, indexes, etc.
	elsif ($self->{type} eq 'TABLE' or $self->{type} eq 'FDW')
	{
		$self->export_table();
	}

	# Extract data only
	elsif (($self->{type} eq 'INSERT') || ($self->{type} eq 'COPY'))
	{
		if ($self->{oracle_fdw_data_export} && $self->{pg_dsn} && $self->{drop_foreign_schema})
		{
			# Temporarily disable partitioning (if set) to obtain appropriate DDL for the oracle_fdw foreign table
			my $original_disable_partition = $self->{disable_partition};
			$self->{disable_partition} = 1;
			my $fdw_definition = $self->export_table();
			$self->{disable_partition} = $original_disable_partition;
			$self->{dbhdest}->do("DROP SCHEMA $self->{pg_supports_ifexists} $self->{fdw_import_schema} CASCADE") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
			$self->{dbhdest}->do("CREATE SCHEMA $self->{fdw_import_schema}") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
			$self->{dbhdest}->do($fdw_definition) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . ", SQL: $fdw_definition\n", 0, 1);
		}

		my $sql_output = "";
		my $dirprefix = '';
		$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});

                my $t0 = Benchmark->new;

		# Connect the Oracle database to gather information
		if ($self->{oracle_dsn} =~ /dbi:mysql/i) {
			$self->{is_mysql} = 1;
		} elsif ($self->{oracle_dsn} =~ /dbi:ODBC:driver=msodbcsql/i) {
			$self->{is_mssql} = 1;
		}
		$self->{dbh} = $self->_db_connection();

		# Remove external table from data export
		if (scalar keys %{$self->{external_table}} )
		{
			foreach my $table (keys %{$self->{tables}})
			{
				if ( grep(/^$table$/i, keys %{$self->{external_table}}) ) {
					delete $self->{tables}{$table};
				}
			}
		}

		# Get current SCN to get data at a fix point in time
		if (!$self->{no_start_scn} && !$self->{start_scn} && !$self->{is_mysql} && !$self->{is_mssql})
		{
			my $sth = $self->{dbh}->prepare("SELECT CURRENT_SCN FROM v\$database") or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
			$sth->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
			my @row = $sth->fetchrow();
			$self->{start_scn} = $row[0];
			$sth->finish;
			$self->logit("Automatic storing of current SCN for table export: $self->{start_scn}\n", 1);
		}

		# Remove remote table from export, they must be exported using FDW export type
		foreach my $table (sort keys %{$self->{tables}})
		{
			if ( $self->{tables}{$table}{table_info}{connection} ) {
				delete $self->{tables}{$table};
			}
		}

		# Get partition information
		$self->_partitions() if (!$self->{disable_partition});

		# Ordering tables by name by default
		my @ordered_tables = sort { $a cmp $b } keys %{$self->{tables}};
		if (lc($self->{data_export_order}) eq 'size')
		{
			@ordered_tables = sort {
				($self->{tables}{$b}{table_info}{num_rows} || $self->{tables}{$a}{table_info}{num_rows}) ?
					$self->{tables}{$b}{table_info}{num_rows} <=> $self->{tables}{$a}{table_info}{num_rows} :
						$a cmp $b 
		       	} keys %{$self->{tables}};
		}
		# User provide the ordered list of table from a file
		elsif (-e $self->{data_export_order})
		{
			if (open(my $tfh, '<', $self->{data_export_order}))
			{
				@ordered_tables = ();
				while (my $l = <$tfh>)
				{
					chomp($l);
					next if (!exists $self->{tables}{$l});
					push(@ordered_tables, $l);
				}
				close($tfh);
			}
			else
			{
				$self->logit("FATAL: can't read file $self->{data_export_order} for ordering table export. $!\n", 0, 1);
			}
		}

		# Set SQL orders that should be in the file header
		# (before the COPY or INSERT commands)
		my $first_header = "$sql_header\n";
		# Add search path and constraint deferring
		my $search_path = $self->set_search_path();
		if (!$self->{pg_dsn} && !$self->{oracle_speed})
		{
			# Set search path
			if ($search_path) {
				$first_header .= $self->set_search_path();
			}
			# Open transaction
			$first_header .= "BEGIN;\n";
			# Defer all constraints
			if ($self->{defer_fkey}) {
				$first_header .= "SET CONSTRAINTS ALL DEFERRED;\n\n";
			}
		}
		elsif (!$self->{oracle_speed})
		{
			# Set search path
			if ($search_path) {
				$self->{dbhdest}->do($search_path) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
			}
			$self->{dbhdest}->do("BEGIN;") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		}

		#### Defined all SQL commands that must be executed before and after data loading
		if (!$self->{oracle_speed})
		{
			my $load_file = "\n";
			foreach my $table (@ordered_tables)
			{
				# Rename table and double-quote it if required
				my $tmptb = $self->get_replaced_tbname($table);

				# Check that the destination table exists
				if ($self->{pg_dsn} && !$self->{on_error_stop})
				{
					my $rv = $self->{dbhdest}->do("SELECT relname FROM pg_class WHERE relname = '$tmptb'");
					if ($rv eq '0E0')
					{
						$self->logit("WARNING: destination table $table doesn't exists, aborting data export for this table.\n", 0);
						next;
					}
				}
				# Do not process nested table
				if (!$self->{is_mysql} && exists $self->{tables}{$table}{table_info}{nested} && $self->{tables}{$table}{table_info}{nested} ne 'NO')
				{
					$self->logit("WARNING: nested table $table will not be exported.\n", 1);
					next;
				}

				# Remove main table partition (for MySQL "SELECT * FROM emp PARTITION (p1);" is supported from 5.6)
				delete $self->{partitions}{$table} if (exists $self->{partitions}{$table} && $self->{is_mysql} && ($self->{db_version} =~ /^5\.[012345]/));
				# Remove main table partition if we have a where clause for the table,
				# in this case lookup for PARTITION (p1) must not be done.
				delete $self->{partitions}{$table} if (exists $self->{partitions}{$table} && ($self->{global_where} or (exists $self->{where}{"\L$table\E"} && $self->{where}{"\L$table\E"})));

				if (-e "${dirprefix}tmp_${table}_$self->{output}") {
					$self->logit("Removing incomplete export file ${dirprefix}tmp_${table}_$self->{output}\n", 1);
					unlink("${dirprefix}tmp_${table}_$self->{output}");
				}

				#### Set SQL commands that must be executed before data loading

				# Drop foreign keys if required
				if ($self->{drop_fkey})
				{
					$self->logit("Dropping foreign keys of table $table...\n", 1);
					my @drop_all = $self->_drop_foreign_keys($table, @{$self->{tables}{$table}{foreign_key}});
					foreach my $str (@drop_all)
					{
						chomp($str);
						next if (!$str);
						if ($self->{pg_dsn}) {
							my $s = $self->{dbhdest}->do($str) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
						} else {
							$first_header .= "$str\n";
						}
					}
				}

				# Drop indexes if required
				if ($self->{drop_indexes})
				{
					$self->logit("Dropping indexes of table $table...\n", 1);
					my @drop_all = $self->_drop_indexes($table, %{$self->{tables}{$table}{indexes}}) . "\n";
					foreach my $str (@drop_all)
					{
						chomp($str);
						next if (!$str);
						if ($self->{pg_dsn}) {
							my $s = $self->{dbhdest}->do($str) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
						} else {
							$first_header .= "$str\n";
						}
					}
				}

				# Disable triggers of current table if requested
				if ($self->{disable_triggers} && !$self->{oracle_speed})
				{
					my $trig_type = 'USER';
					$trig_type = 'ALL' if (uc($self->{disable_triggers}) eq 'ALL');
					$self->logit("Disabling \L$trig_type\E triggers...\n", 1);
					if ($self->{pg_dsn}) {
						my $s = $self->{dbhdest}->do("ALTER TABLE $tmptb DISABLE TRIGGER $trig_type;") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
					} else {
						$first_header .=  "ALTER TABLE $tmptb DISABLE TRIGGER $trig_type;\n";
					}
				}

				#### Add external data file loading if file_per_table is enable
				if ($self->{file_per_table} && !$self->{pg_dsn})
				{
					my $file_name = "$dirprefix${table}_$self->{output}";
					$file_name = "${table}_$self->{output}" if ($self->{psql_relative_path});
					$file_name =~ s/\.(gz|bz2)$//;
					$load_file .=  "\\i$self->{psql_relative_path} '$file_name'\n";
				}

				# With partitioned table, load data direct from table partition
				if (exists $self->{partitions}{$table})
				{
					foreach my $pos (sort {$a <=> $b} keys %{$self->{partitions}{$table}})
					{
						my $part_name = $self->{partitions}{$table}{$pos}{name};
						my $tb_name = '';
						if (!exists $self->{subpartitions}{$table}{$part_name}) {
							$tb_name = $part_name;
						}
						$tb_name = $table . '_part' . $pos if ($self->{rename_partition});
						next if ($self->{allow_partition} && !grep($_ =~ /^$part_name$/i, @{$self->{allow_partition}}));

						if (exists $self->{subpartitions}{$table}{$part_name})
						{
							foreach my $p (sort {$a <=> $b} keys %{$self->{subpartitions}{$table}{$part_name}})
							{
								my $subpart = $self->{subpartitions}{$table}{$part_name}{$p}{name};
								next if ($self->{allow_partition} && !grep($_ =~ /^$subpart$/i, @{$self->{allow_partition}}));
								my $sub_tb_name = $subpart;
								$sub_tb_name =~ s/^[^\.]+\.//; # remove schema part if any
								if ($self->{file_per_table} && !$self->{pg_dsn})
								{
									my $file_name = "$dirprefix${table}_${sub_tb_name}_$self->{output}";
									$file_name = "${table}_${sub_tb_name}_$self->{output}" if ($self->{psql_relative_path});
									$file_name =~ s/\.(gz|bz2)$//;
									$load_file .=  "\\i$self->{psql_relative_path} '$file_name'\n";
								}
								$sub_tb_name = $tb_name . '_subpart' . $p if ($self->{rename_partition});
							}
							# Now load content of the default partition table
							if ($self->{subpartitions_default}{$table}{$part_name})
							{
								if (!$self->{allow_partition} || grep($_ =~ /^$self->{subpartitions_default}{$table}{$part_name}{name}$/i, @{$self->{allow_partition}}))
								{
									if ($self->{file_per_table} && !$self->{pg_dsn})
									{
										my $part_name = $self->{subpartitions_default}{$table}{$part_name}{name};
										my $file_name = "$dirprefix${table}_${part_name}_$self->{output}";
										$file_name = "${table}_${part_name}_$self->{output}" if ($self->{psql_relative_path});
										$file_name =~ s/\.(gz|bz2)$//;
										$load_file .=  "\\i$self->{psql_relative_path} '$file_name'\n";
										$part_name = $tb_name . '_default' if ($self->{rename_partition});
									}
								}
							}
						}
						else
						{
							if ($self->{file_per_table} && !$self->{pg_dsn})
							{
								my $part_name = $self->{partitions}{$table}{$pos}{name};
								my $file_name = "$dirprefix${table}_${part_name}_$self->{output}";
								$file_name = "${table}_${part_name}_$self->{output}" if ($self->{psql_relative_path});
								$file_name =~ s/\.(gz|bz2)$//;
								$load_file .=  "\\i$self->{psql_relative_path} '$file_name'\n";
							}
						}
					}
					# Now load content of the default partition table
					if (exists $self->{partitions_default}{$table})
					{
						if (!$self->{allow_partition} || grep($_ =~ /^$self->{partitions_default}{$table}{name}$/i, @{$self->{allow_partition}}))
						{
							if ($self->{file_per_table} && !$self->{pg_dsn})
							{
								my $part_name = $self->{partitions_default}{$table}{name};
								my $file_name = "$dirprefix${table}_${part_name}_$self->{output}";
								$file_name = "${table}_${part_name}_$self->{output}" if ($self->{psql_relative_path});
								$file_name =~ s/\.(gz|bz2)$//;
								$load_file .=  "\\i$self->{psql_relative_path} '$file_name'\n";
								$part_name = $table . '_part_default' if ($self->{rename_partition});
							}
						}
					}
				}

				# Create temporary tables for DATADIFF
				if ($self->{datadiff})
				{
					my $tmptb_del = $self->get_tbname_with_suffix($tmptb, $self->{datadiff_del_suffix});
					my $tmptb_ins = $self->get_tbname_with_suffix($tmptb, $self->{datadiff_ins_suffix});
					my $tmptb_upd = $self->get_tbname_with_suffix($tmptb, $self->{datadiff_upd_suffix});
					if ($self->{datadiff_work_mem}) {
						$first_header .= "SET work_mem TO '" . $self->{datadiff_work_mem} . "';\n";
					}
					if ($self->{datadiff_temp_buffers}) {
						$first_header .= "SET temp_buffers TO '" . $self->{datadiff_temp_buffers} . "';\n";
					}
					$first_header .= "LOCK TABLE $tmptb IN EXCLUSIVE MODE;\n";
					$first_header .= "CREATE TEMPORARY TABLE $tmptb_del";
					$first_header .= " (LIKE $tmptb INCLUDING DEFAULTS INCLUDING CONSTRAINTS INCLUDING INDEXES)";
					$first_header .= " ON COMMIT DROP;\n";
					$first_header .= "CREATE TEMPORARY TABLE $tmptb_ins";
					$first_header .= " (LIKE $tmptb INCLUDING DEFAULTS INCLUDING CONSTRAINTS INCLUDING INDEXES)";
					$first_header .= " ON COMMIT DROP;\n";
					$first_header .= "CREATE TEMPORARY TABLE $tmptb_upd";
					$first_header .= " (old $tmptb_del, new $tmptb_ins, changed_columns TEXT[])";
					$first_header .= " ON COMMIT DROP;\n";
				}

			}

			# When copy freeze is required, close the current transaction
			if ($self->{copy_freeze} && !$self->{pg_dsn}) {
				$first_header .= ("\nCOMMIT;\n");
			}

			if (!$self->{pg_dsn})
			{
				# Write header to file
				$self->dump($first_header);

				if ($self->{file_per_table}) {
					# Write file loader
					$self->dump($load_file);
				}
			}

			# Commit transaction
			if ($self->{pg_dsn}) {
				my $s = $self->{dbhdest}->do("COMMIT;") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
			}
		}

		####
		#### Proceed to data export
		####

		# Set total number of rows
		$self->{global_rows} = 0;
		foreach my $table (keys %{$self->{tables}})
		{
                        if ($self->{global_where})
			{
                                if ($self->{is_mysql} && ($self->{global_where} =~ /\s+LIMIT\s+\d+,(\d+)/)) {
					$self->{tables}{$table}{table_info}{num_rows} = $1 if ($i < $self->{tables}{$table}{table_info}{num_rows});
                                } elsif ($self->{global_where} =~ /\s+ROWNUM\s+[<=>]+\s+(\d+)/) {
					$self->{tables}{$table}{table_info}{num_rows} = $1 if ($i < $self->{tables}{$table}{table_info}{num_rows});
                                }
                        }
			elsif (exists $self->{where}{"\L$table\E"})
			{
                                if ($self->{is_mysql} && ($self->{where}{"\L$table\E"} =~ /\s+LIMIT\s+\d+,(\d+)/)) {
					$self->{tables}{$table}{table_info}{num_rows} = $1 if ($i < $self->{tables}{$table}{table_info}{num_rows});
                                } elsif ($self->{where}{"\L$table\E"} =~ /\s+ROWNUM\s+[<=>]+\s+(\d+)/) {
					$self->{tables}{$table}{table_info}{num_rows} = $1 if ($i < $self->{tables}{$table}{table_info}{num_rows});
                                }
                        }
			$self->{global_rows} += $self->{tables}{$table}{table_info}{num_rows};
		}

		# Open a pipe for interprocess communication
		my $reader = new IO::Handle;
		my $writer = new IO::Handle;

		# Fork the logger process
		if (!$self->{quiet} && !$self->{debug})
		{
			if ( ($self->{jobs} > 1) || ($self->{oracle_copies} > 1) || ($self->{parallel_tables} > 1))
			{
				$pipe = IO::Pipe->new($reader, $writer);
				$writer->autoflush(1);
				spawn sub {
					$self->multiprocess_progressbar();
				};
			}
		}
		$dirprefix = '';
		$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});

		my $first_start_time = time();
		my $global_count = 0;
		my $parallel_tables_count = 1;
		$self->{oracle_copies} = 1 if ($self->{parallel_tables} > 1);

		# Send global startup information to pipe
		if (defined $pipe)
		{
			$pipe->writer();
			$pipe->print("GLOBAL EXPORT START TIME: $first_start_time\n");
			$pipe->print("GLOBAL EXPORT ROW NUMBER: $self->{global_rows}\n");
		}
		$self->{global_start_time} = time();
		foreach my $table (@ordered_tables)
		{
			# Rename table and double-quote it if required
			my $tmptb = $self->get_replaced_tbname($table);

			# Check that the destination table exists
			if ($self->{pg_dsn} && !$self->{on_error_stop})
			{
				my $rv = $self->{dbhdest}->do("SELECT relname FROM pg_class WHERE relname = '$tmptb'");
				if ($rv eq '0E0')
				{
					$self->logit("WARNING: destination table $table doesn't exists, aborting data export for this table.\n", 0);
					next;
				}
			}

			# Do not process nested table
			if (!$self->{is_mysql} && exists $self->{tables}{$table}{table_info}{nested} && $self->{tables}{$table}{table_info}{nested} ne 'NO')
			{
				$self->logit("WARNING: nested table $table will not be exported.\n", 1);
				next;
			}

			if ($self->{file_per_table} && !$self->{pg_dsn})
			{
				# Do not dump data again if the file already exists
				next if ($self->file_exists("$dirprefix${table}_$self->{output}"));
			}

			# Set global count
			$global_count += $self->{tables}{$table}{table_info}{num_rows};

			# Extract all column information used to determine data export.
			# This hash will be used in function _howto_get_data()
			%{$self->{colinfo}} = $self->_column_attributes($table, $self->{schema}, 'TABLE');

			# Get the current SCN before getting data for this table
			if ($self->{cdc_ready})
			{
				my $sth = $self->{dbh}->prepare("SELECT CURRENT_SCN FROM v\$database") or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
				$sth->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
				my @row = $sth->fetchrow();
				$self->{current_oracle_scn}{$table} = $row[0];
				$sth->finish;
				$self->logit("Storing SCN for table $table: $self->{current_oracle_scn}{$table}\n", 1);
			}

			my $total_record = 0;
			if ($self->{parallel_tables} > 1)
			{

				# With partitioned table, load data direct from table partition
				if (!$self->{disable_parallel_partition} && !$self->{fdw_server} && exists $self->{partitions}{$table})
				{
					foreach my $pos (sort {$self->{partitions}{$table}{$a} <=> $self->{partitions}{$table}{$b}} keys %{$self->{partitions}{$table}})
					{
						my $part_name = $self->{partitions}{$table}{$pos}{name};
						next if ($self->{allow_partition} && !grep($_ =~ /^$part_name$/i, @{$self->{allow_partition}}));

						if (exists $self->{subpartitions}{$table}{$part_name})
						{
							foreach my $p (sort {$a <=> $b} keys %{$self->{subpartitions}{$table}{$part_name}})
							{
								my $subpart = $self->{subpartitions}{$table}{$part_name}{$p}{name};
								next if ($self->{allow_partition} && !grep($_ =~ /^$subpart$/i, @{$self->{allow_partition}}));
								spawn sub {
									if (!$self->{fdw_server} || !$self->{pg_dsn}) {
										$self->_export_table_data($table, $part_name, $subpart, $pos, $p, $dirprefix, $sql_header);
									} else {
										$self->_export_fdw_table_data($table, $dirprefix, $sql_header);
									}
								};

								$parallel_tables_count++;

								# Wait for oracle connection terminaison
								while ($parallel_tables_count > $self->{parallel_tables})
								{
									my $kid = waitpid(-1, WNOHANG);
									if ($kid > 0)
									{
										$parallel_tables_count--;
										delete $RUNNING_PIDS{$kid};
									}
									usleep(50000);
								}
							}
							# Now load content of the default subpartition table
							if ($self->{subpartitions_default}{$table}{$part_name})
							{
								if (!$self->{allow_partition} || grep($_ =~ /^$self->{subpartitions_default}{$table}{$part_name}{name}$/i, @{$self->{allow_partition}}))
								{
									spawn sub {
										if (!$self->{fdw_server} || !$self->{pg_dsn}) {
											$self->_export_table_data($table, $part_name, $subpart, $pos, 'default', $dirprefix, $sql_header);
										} else {
											$self->_export_fdw_table_data($table, $dirprefix, $sql_header);
										}
									};

									$parallel_tables_count++;

									# Wait for oracle connection terminaison
									while ($parallel_tables_count > $self->{parallel_tables})
									{
										my $kid = waitpid(-1, WNOHANG);
										if ($kid > 0)
										{
											$parallel_tables_count--;
											delete $RUNNING_PIDS{$kid};
										}
										usleep(50000);
									}
								}
							}
						}
						spawn sub {
							if (!$self->{fdw_server} || !$self->{pg_dsn}) {
								$self->_export_table_data($table, $part_name, $subpart, $pos, undef, $dirprefix, $sql_header);
							} else {
								$self->_export_fdw_table_data($table, $dirprefix, $sql_header);
							}
						};

						$parallel_tables_count++;

						# Wait for oracle connection terminaison
						while ($parallel_tables_count > $self->{parallel_tables})
						{
							my $kid = waitpid(-1, WNOHANG);
							if ($kid > 0)
							{
								$parallel_tables_count--;
								delete $RUNNING_PIDS{$kid};
							}
							usleep(50000);
						}
					}

					# Now load content of the default partition table
					if (exists $self->{partitions_default}{$table})
					{
						if (!$self->{allow_partition} || grep($_ =~ /^$self->{partitions_default}{$table}{name}$/i, @{$self->{allow_partition}}))
						{
							my $tbpart_name = $self->{partitions_default}{$table}{name};
							spawn sub {
								if (!$self->{fdw_server} || !$self->{pg_dsn}) {
									$self->_export_table_data($table, $tbpart_name, $subpart, 'default', undef, $dirprefix, $sql_header);
								} else {
									$self->_export_fdw_table_data($table, $dirprefix, $sql_header);
								}
							};

							$parallel_tables_count++;

							# Wait for oracle connection terminaison
							while ($parallel_tables_count > $self->{parallel_tables})
							{
								my $kid = waitpid(-1, WNOHANG);
								if ($kid > 0)
								{
									$parallel_tables_count--;
									delete $RUNNING_PIDS{$kid};
								}
								usleep(50000);
							}
						}
					}
				}
				else
				{
					spawn sub {
						if (!$self->{fdw_server} || !$self->{pg_dsn}) {
							$self->_export_table_data($table, undef, undef, undef, undef, $dirprefix, $sql_header);
						} else {
							$self->_export_fdw_table_data($table, $dirprefix, $sql_header);
						}
					};

					$parallel_tables_count++;

					# Wait for oracle connection terminaison
					while ($parallel_tables_count > $self->{parallel_tables})
					{
						my $kid = waitpid(-1, WNOHANG);
						if ($kid > 0)
						{
							$parallel_tables_count--;
							delete $RUNNING_PIDS{$kid};
						}
						usleep(50000);
					}
				}
			}
			else
			{
				if (!$self->{fdw_server} || !$self->{pg_dsn}) {
					$total_record = $self->_export_table_data($table, undef, undef, undef, undef, $dirprefix, $sql_header);
				} else {
					$total_record = $self->_export_fdw_table_data($table, $dirprefix, $sql_header);
				}
			}

			# Display total export position
			if (!$self->{quiet} && !$self->{debug})
			{
				if ( ($self->{jobs} <= 1) && ($self->{oracle_copies} <= 1) && ($self->{parallel_tables} <= 1) )
				{
					my $last_end_time = time();
					my $dt = $last_end_time - $first_start_time;
					$dt ||= 1;
					my $rps = int(($total_record || $global_count) / $dt);
					print STDERR $self->progress_bar(($total_record || $global_count), $self->{global_rows}, 25, '=', 'rows', "on total estimated data ($dt sec., avg: $rps recs/sec)"), "\r";
				}
			}
		}

		if (!$self->{quiet} && !$self->{debug})
		{
			if ( ($self->{jobs} <= 1) && ($self->{oracle_copies} <= 1) && ($self->{parallel_tables} <= 1) ) {
				print "\n";
			}
		}

		# Wait for all child die
		if ( ($self->{oracle_copies} > 1) || ($self->{parallel_tables} > 1) )
		{
			# Wait for all child dies less the logger
			my $minnumchild = 1; # will not wait for progressbar process
			$minnumchild = 0 if ($self->{debug} || $self->{quiet}); # in debug or quiet there is no progressbar
			while (scalar keys %RUNNING_PIDS > $minnumchild)
			{
				my $kid = waitpid(-1, WNOHANG);
				if ($kid > 0) {
					delete $RUNNING_PIDS{$kid};
				}
				usleep(50000);
			}
			# Terminate the process logger
			foreach my $k (keys %RUNNING_PIDS)
			{
				kill(10, $k);
				%RUNNING_PIDS = ();
			}
			# Reopen a new database handler
			$self->{dbh}->disconnect() if (defined $self->{dbh});
			if ($self->{oracle_dsn} =~ /dbi:mysql/i) {
				$self->{is_mysql} = 1;
			} elsif ($self->{oracle_dsn} =~ /dbi:ODBC:driver=msodbcsql/i) {
				$self->{is_mssql} = 1;
			}
			$self->{dbh} = $self->_db_connection();
		}
		
		# Start a new transaction
		if ($self->{pg_dsn} && !$self->{oracle_speed}) {
			my $s = $self->{dbhdest}->do("BEGIN;") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		}

		# Remove function created to export external table
		if ($self->{bfile_found} eq 'text')
		{
			$self->logit("Removing function ora2pg_get_bfilename() used to retrieve path from BFILE.\n", 1);
			my $bfile_function = "DROP FUNCTION ora2pg_get_bfilename";
			my $sth2 = $self->{dbh}->do($bfile_function);
		}
		elsif ($self->{bfile_found} eq 'efile')
		{
			$self->logit("Removing function ora2pg_get_efile() used to retrieve EFILE from BFILE.\n", 1);
			my $efile_function = "DROP FUNCTION ora2pg_get_efile";
			my $sth2 = $self->{dbh}->do($efile_function);
		}
		elsif ($self->{bfile_found} eq 'bytea')
		{
			$self->logit("Removing function ora2pg_get_bfile() used to retrieve BFILE content.\n", 1);
			my $efile_function = "DROP FUNCTION ora2pg_get_bfile";
			my $sth2 = $self->{dbh}->do($efile_function);
		}

		#### Set SQL commands that must be executed after data loading
		if (!$self->{oracle_speed})
		{
			my $footer = '';

			# When copy freeze is required, start a new the transaction
			if ($self->{copy_freeze} && !$self->{pg_dsn})
			{
				$footer .= "\nBEGIN;\n";
			}

			my (@datadiff_tbl, @datadiff_del, @datadiff_upd, @datadiff_ins);
			foreach my $table (@ordered_tables)
			{
				# Rename table and double-quote it if required
				my $tmptb = $self->get_replaced_tbname($table);

				# Check that the destination table exists
				if ($self->{pg_dsn} && !$self->{on_error_stop})
				{
					my $rv = $self->{dbhdest}->do("SELECT relname FROM pg_class WHERE relname = '$tmptb'");
					if ($rv eq '0E0')
					{
						$self->logit("WARNING: destination table $table doesn't exists, aborting data export for this table.\n", 0);
						next;
					}
				}

				# Do not process nested table
				if (!$self->{is_mysql} && exists $self->{tables}{$table}{table_info}{nested} && $self->{tables}{$table}{table_info}{nested} ne 'NO')
				{
					$self->logit("WARNING: nested table $table will not be exported.\n", 1);
					next;
				}

				# DATADIFF reduction (annihilate identical deletions and insertions) and execution
				if ($self->{datadiff})
				{
					my $tmptb_del = $self->get_tbname_with_suffix($tmptb, $self->{datadiff_del_suffix});
					my $tmptb_upd = $self->get_tbname_with_suffix($tmptb, $self->{datadiff_upd_suffix});
					my $tmptb_ins = $self->get_tbname_with_suffix($tmptb, $self->{datadiff_ins_suffix});
					my @pg_colnames_nullable = @{$self->{tables}{$table}{pg_colnames_nullable}};
					my @pg_colnames_notnull = @{$self->{tables}{$table}{pg_colnames_notnull}};
					my @pg_colnames_pkey = @{$self->{tables}{$table}{pg_colnames_pkey}};
					# reduce by deleting matching (i.e. quasi "unchanged") entries from $tmptb_del and $tmptb_ins
					$footer .= "WITH del AS (SELECT t, row_number() OVER (PARTITION BY t.*) rownum, ctid FROM $tmptb_del t), ";
					$footer .= "ins AS (SELECT t, row_number() OVER (PARTITION BY t.*) rownum, ctid FROM $tmptb_ins t), ";
					$footer .= "paired AS (SELECT del.ctid ctid1, ins.ctid ctid2 FROM del JOIN ins ON del.t IS NOT DISTINCT FROM ins.t ";
					foreach my $col (@pg_colnames_nullable) {
						$footer .= "AND (((del.t).$col IS NULL AND (ins.t).$col IS NULL) OR ((del.t).$col = (ins.t).$col)) ";
					}
					foreach my $col (@pg_colnames_notnull, @pg_colnames_pkey) {
						$footer .= "AND ((del.t).$col = (ins.t).$col) ";
					}
					$footer .= "AND del.rownum = ins.rownum), ";
					$footer .= "del_del AS (DELETE FROM $tmptb_del WHERE ctid = ANY(ARRAY(SELECT ctid1 FROM paired))), ";
					$footer .= "del_ins AS (DELETE FROM $tmptb_ins WHERE ctid = ANY(ARRAY(SELECT ctid2 FROM paired))) ";
					$footer .= "SELECT 1;\n";
					# convert matching delete+insert into update if configured and primary key exists
					if ($self->{datadiff_update_by_pkey} && $#pg_colnames_pkey >= 0) {
						$footer .= "WITH upd AS (SELECT old, new, old.ctid ctid1, new.ctid ctid2, ARRAY(";
						for my $col (@pg_colnames_notnull) {
							$footer .= "SELECT '$col'::TEXT WHERE old.$col <> new.$col UNION ALL "; 
						}
						for my $col (@pg_colnames_nullable) {
							$footer .= "SELECT '$col'::TEXT WHERE old.$col <> new.$col OR ((old.$col IS NULL) <> (new.$col IS NULL)) UNION ALL "; 
						}
						$footer .= "SELECT ''::TEXT WHERE FALSE) changed_columns FROM $tmptb_del old ";
						$footer .= "JOIN $tmptb_ins new USING (" . join(', ', @pg_colnames_pkey) . ")), ";
						$footer .= "del_del AS (DELETE FROM $tmptb_del WHERE ctid = ANY(ARRAY(SELECT ctid1 FROM upd))), ";
						$footer .= "del_ins AS (DELETE FROM $tmptb_ins WHERE ctid = ANY(ARRAY(SELECT ctid2 FROM upd))) ";
						$footer .= "INSERT INTO $tmptb_upd (old, new, changed_columns) SELECT old, new, changed_columns FROM upd;\n";
					}
					# call optional function specified in config to be called before actual deletion/insertion
					$footer .= "SELECT " . $self->{datadiff_before} . "('" . $tmptb . "', '" . $tmptb_del . "', '" . $tmptb_upd . "', '" . $tmptb_ins . "');\n"
						if ($self->{datadiff_before});
					# do actual delete
					$footer .= "WITH del AS (SELECT d.delctid FROM (SELECT t, COUNT(*) c FROM $tmptb_del t GROUP BY t) s ";
					$footer .= "LEFT JOIN LATERAL (SELECT ctid delctid FROM $tmptb tbl WHERE tbl IS NOT DISTINCT FROM s.t ";
					foreach my $col (@pg_colnames_nullable) {
						$footer .= "AND (((s.t).$col IS NULL AND tbl.$col IS NULL) OR ((s.t).$col = tbl.$col)) ";
					}
					foreach my $col (@pg_colnames_notnull, @pg_colnames_pkey) {
						$footer .= "AND ((s.t).$col = tbl.$col) ";
					}
					$footer .= "LIMIT s.c) d ON TRUE) ";
					$footer .= "DELETE FROM $tmptb WHERE ctid = ANY(ARRAY(SELECT delctid FROM del));\n";
					# do actual update
					if ($self->{datadiff_update_by_pkey} && $#pg_colnames_pkey >= 0 && ($#pg_colnames_nullable >= 0 || $#pg_colnames_notnull >= 0)) {
						$footer .= "UPDATE $tmptb SET ";
						$footer .= join(', ', map { $_ . ' = (upd.new).' . $_ } @pg_colnames_notnull, @pg_colnames_nullable);
						$footer .= " FROM $tmptb_upd upd WHERE ";
						$footer .= join(' AND ', map { $_ . ' = (upd.old).' . $_ } @pg_colnames_pkey);
						$footer .= ";\n";
					}
					# do actual insert
					$footer .= "INSERT INTO $tmptb SELECT * FROM $tmptb_ins;\n";
					# call optional function specified in config to be called after actual deletion/insertion
					$footer .= "SELECT " . $self->{datadiff_after} . "('" . $tmptb . "', '" . $tmptb_del . "', '" . $tmptb_upd . "', '" . $tmptb_ins . "');\n"
						if ($self->{datadiff_after});
					# push table names in array for bunch function call in the end
					push @datadiff_tbl, $tmptb;
					push @datadiff_del, $tmptb_del;
					push @datadiff_upd, $tmptb_upd;
					push @datadiff_ins, $tmptb_ins;
				}

				# disable triggers of current table if requested
				if ($self->{disable_triggers} && !$self->{oracle_speed})
				{
					my $trig_type = 'USER';
					$trig_type = 'ALL' if (uc($self->{disable_triggers}) eq 'ALL');
					my $str = "ALTER TABLE $tmptb ENABLE TRIGGER $trig_type;";
					if ($self->{pg_dsn}) {
						my $s = $self->{dbhdest}->do($str) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
					} else {
						$footer .= "$str\n";
					}
				}

				# Recreate all foreign keys of the concerned tables
				if ($self->{drop_fkey} && !$self->{oracle_speed})
				{
					my @create_all = ();
					$self->logit("Restoring foreign keys of table $table...\n", 1);
					push(@create_all, $self->_create_foreign_keys($table));
					foreach my $str (@create_all)
					{
						chomp($str);
						next if (!$str);
						if ($self->{pg_dsn}) {
							my $s = $self->{dbhdest}->do($str) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . ", SQL: $str\n", 0, 1);
						} else {
							$footer .= "$str\n";
						}
					}
				}

				# Recreate all indexes
				if ($self->{drop_indexes} && !$self->{oracle_speed})
				{
					my @create_all = ();
					$self->logit("Restoring indexes of table $table...\n", 1);
					push(@create_all, $self->_create_indexes($table, 1, %{$self->{tables}{$table}{indexes}}));
					if ($#create_all >= 0)
					{
						foreach my $str (@create_all)
						{
							chomp($str);
							next if (!$str);
							if ($self->{pg_dsn}) {
								my $s = $self->{dbhdest}->do($str) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . ", SQL: $str\n", 0, 1);
							} else {
								$footer .= "$str\n";
							}
						}
					}
				}
			}

			# Insert restart sequences orders
			if (($#ordered_tables >= 0) && !$self->{disable_sequence} && !$self->{oracle_speed})
			{
				$self->logit("Restarting sequences\n", 1);
				my @restart_sequence = $self->_extract_sequence_info();
				foreach my $str (@restart_sequence)
				{
					if ($self->{pg_dsn}) {
						my $s = $self->{dbhdest}->do($str) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
					} else {
						$footer .= "$str\n";
					}
				}
			}

			# DATADIFF: call optional function specified in config to be called with all table names right before commit
			if ($self->{datadiff} && $self->{datadiff_after_all} && $#datadiff_tbl >= 0)
			{
				$footer .= "SELECT " . $self->{datadiff_after_all} . "(ARRAY['";
				$footer .= join("', '", @datadiff_tbl) . "'], ARRAY['";
				$footer .= join("', '", @datadiff_del) . "'], ARRAY['";
				$footer .= join("', '", @datadiff_upd) . "'], ARRAY['";
				$footer .= join("', '", @datadiff_ins) . "']);\n";
			}

		}
		# Commit transaction
		if ($self->{pg_dsn} && !$self->{oracle_speed}) {
			my $s = $self->{dbhdest}->do("COMMIT;") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		} else {
			$footer .= "COMMIT;\n\n";
		}

		# Recreate constraint and indexes if required
		$self->dump("\n$footer") if (!$self->{pg_dsn} && $footer);

		my $npart = 0;
		my $nsubpart = 0;
		foreach my $t (sort keys %{ $self->{partitions} }) {
			$npart += scalar keys %{$self->{partitions}{$t}};
		}
		foreach my $t (sort keys %{ $self->{subpartitions_list} })
		{
			foreach my $p (sort keys %{ $self->{subpartitions_list}{$t} }) {
				$nsubpart += scalar keys %{ $self->{subpartitions_list}{$t}{$p}};
			}
		}

                my $t1 = Benchmark->new;
                my $td = timediff($t1, $t0);
		my $timestr = timestr($td);
		my $title = 'Total time to export data';
		if ($self->{ora2pg_speed}) {
			$title = 'Total time to process data from Oracle';
		} elsif ($self->{oracle_speed}) {
			$title = 'Total time to extract data from Oracle';
		}
                $self->logit("$title from " . (scalar keys %{$self->{tables}}) . " tables ($npart partitions, $nsubpart sub-partitions) and $self->{global_rows} total rows: $timestr\n", 1);
		if ($timestr =~ /^(\d+) wallclock secs/)
		{
			my $mean = sprintf("%.2f", $self->{global_rows}/($1 || 1));
			$self->logit("Speed average: $mean rows/sec\n", 1);
		}

		####
		# Save SCN registered before exporting tables
		####
		if (scalar keys %{$self->{current_oracle_scn}}) {
			my $dirprefix = '';
			$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
			open(OUT, ">${dirprefix}TABLES_SCN.log");
			print OUT "# SCN per table\n";
			foreach my $t (sort keys %{$self->{current_oracle_scn}}) {
				print OUT "$t:$self->{current_oracle_scn}{$t}\n";
			}
			close(OUT);
		}
	}
}

sub fix_function_call
{
	my $self = shift;


	$self->logit("Fixing function calls in output files...\n", 0);

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});

	return unless(open(my $tfh, '<', $dirprefix . 'temp_pass2_file.dat'));
	while (my $l = <$tfh>) {
		chomp($l);
		my ($pname, $fname, $file_name) = split(/:/, $l);
		$file_to_update{$pname}{$fname} = $file_name;
	}
	close($tfh);

	my $child_count = 0;
	# Fix call to package function in files
	foreach my $pname (sort keys %file_to_update ) {
		next if ($pname =~ /^ORA2PG_/);
		foreach my $fname (sort keys %{ $file_to_update{$pname} } ) {
			if ($self->{jobs} > 1) {
				while ($child_count >= $self->{jobs}) {
					my $kid = waitpid(-1, WNOHANG);
					if ($kid > 0) {
						$child_count--;
						delete $RUNNING_PIDS{$kid};
					}
					usleep(50000);
				}
				spawn sub {
					$self->requalify_package_functions($file_to_update{$pname}{$fname});
				};
				$child_count++;
			} else {
				$self->requalify_package_functions($file_to_update{$pname}{$fname});
			}
		}
	}

	# Wait for all child end
	while ($child_count > 0) {
		my $kid = waitpid(-1, WNOHANG);
		if ($kid > 0) {
			$child_count--;
			delete $RUNNING_PIDS{$kid};
		}
		usleep(50000);
	}
}

# Requalify function call by using double quoted if necessary and by replacing
# dot with an undescore when PACKAGE_AS_SCHEMA is disabled.
sub requalify_package_functions
{
	my ($self, $filename) = @_;

	if (open(my $fh, '<', $filename)) {
		$self->set_binmode($fh);
		my $content = '';
		while (<$fh>) { $content .= $_; };
		close($f);
		$self->requalify_function_call(\$content);
		if (open(my $fh, '>', $filename)) {
			$self->set_binmode($fh);
			print $fh $content;
			close($fh);
		} else {
			print STDERR "ERROR: requalify package functions can't write to $filename, $!\n";
			return;
		}
	} else {
		print STDERR "ERROR: requalify package functions can't read file $filename, $!\n";
		return;
	}
}

# Routine used to read input file and return content as string,
# Character / is replaces by a ; and \r are removed
sub read_input_file
{
	my ($self, $file) = @_;

			
	my $content = '';
	if (open(my $fin, '<', $file))
	{
		$self->set_binmode($fin) if (_is_utf8_file( $file));
		while (<$fin>) { next if /^\/$/; $content .= $_; };
		close($fin);
	} else {
		die "FATAL: can't read file $file, $!\n";
	}

	$content =~ s/[\r\n]\/([\r\n]|$)/;$2/gs;
	$content =~ s/\r//gs;
	$content =~ s/[\r\n]SHOW\s+(?:ERRORS|ERR|BTITLE|BTI|LNO|PNO|RECYCLEBIN|RECYC|RELEASE|REL|REPFOOTER|REPF|REPHEADER|REPH|SPOOL|SPOO|SGA|SQLCODE|TTITLE|TTI|USER|XQUERY|SPPARAMETERS|PARAMETERS)[^\r\n]*([\r\n]|$)/;$2/igs;

        if ($self->{is_mysql})
	{
                $content =~ s/"/'/gs;
                $content =~ s/`/"/gs;
                $content =~ s/;\s*\/\//;/gs;
        }

	return $content;
}

sub file_exists
{
	my ($self, $file) = @_;

	return 0 if ($self->{oracle_speed});

	if ($self->{file_per_table} && !$self->{pg_dsn}) {
		if (-e "$file") {
			$self->logit("WARNING: Skipping dumping data to file $file, file already exists.\n", 0);
			return 1;
		}
	}
	return 0;
}

####
# dump table content
####
sub _dump_table
{
	my ($self, $dirprefix, $sql_header, $table, $part_name, $is_subpart, $tbpart_name, $sub_tb_name) = @_;

	my @cmd_head = ();
	my @cmd_foot = ();

	# Set search path
	my $search_path = $self->set_search_path();
	if ((!$self->{truncate_table} || $self->{pg_dsn}) && $search_path) {
		push(@cmd_head,$search_path);
	}

	# Rename table and double-quote it if required
	my $tmptb = '';

	if (exists $self->{replaced_tables}{"\L$table\E"} && $self->{replaced_tables}{"\L$table\E"})
	{
		$self->logit("\tReplacing table $table as " . $self->{replaced_tables}{lc($table)} . "...\n", 1);
		$tmptb = $self->{replaced_tables}{lc($table)};
	}


	# Prefix partition name with tablename, if pg_supports_partition is enabled
	# direct import to partition is not allowed so import to main table.
	if (!$self->{pg_supports_partition} && $part_name && $self->{rename_partition}) {
		$tmptb = $self->get_replaced_tbname($table . '_' . $part_name);
	} elsif (!$self->{pg_supports_partition} && $part_name) {
		$tmptb = $self->get_replaced_tbname($part_name || $table);
	} elsif ($tbpart_name) {
		$tmptb = $self->get_replaced_tbname($tbpart_name);
		$tmptb = $self->get_replaced_tbname($sub_tb_name) if ($sub_tb_name);
	} else {
		$tmptb = $self->get_replaced_tbname($table);
	}

	# Replace Tablename by temporary table for DATADIFF (data will be inserted in real table at the end)
	# !!! does not work correctly for partitions yet !!!
	if ($self->{datadiff}) {
		$tmptb = $self->get_tbname_with_suffix($tmptb, $self->{datadiff_ins_suffix});
	}

	# Build the header of the query
	my @tt = ();
	my @stt = ();
	my @nn = ();
	my $has_geometry = 0;
	my $has_identity = 0;
	$has_identity = 1 if (exists $self->{identity_info}{$table});

	%{ $self->{tables}{$table}{pk_columns} } = ();

	# Extract column information following the Oracle position order
	my @fname = ();
	my (@pg_colnames_nullable, @pg_colnames_notnull, @pg_colnames_pkey);
	@{ $self->{tables}{$table}{dest_column_name} } = ();
	foreach my $i ( 0 .. $#{$self->{tables}{$table}{field_name}} )
	{
		my $fieldname = ${$self->{tables}{$table}{field_name}}[$i];
		next if (!$self->is_in_struct($table, $fieldname));

		next if (!exists $self->{tables}{"$table"}{column_info}{"$fieldname"});

		my $f = $self->{tables}{"$table"}{column_info}{"$fieldname"};
		$f->[2] =~ s/\D//g;

		if (!$self->{enable_blob_export} && $f->[1] =~ /blob/i)
		{
			# user don't want to export blob
			next;
		}
		if (!$self->{enable_clob_export} && $f->[1] =~ /clob/i)
		{
			# user don't want to export clob
			next;
		}

		my $is_pk = $self->is_primary_key_column($table, $fieldname);

		# When lo_import is used we only want the PK colmuns and the BLOB
		if ($self->{lo_import} && $f->[1] !~ /blob/i && !$is_pk) {
			next;
		}

		# Get the indices and column name of the primary
		# for possible use for blob_to_lo data export.
		if ($is_pk) {
			$self->{tables}{$table}{pk_columns}{$i} = $fieldname;
		}

		# A virtual column must not be part of the target list
		next if ($f->[10] eq 'YES' and $self->{pg_supports_virtualcol});

		next if (grep(/^\Q$fieldname\E$/i, @nn));

		if (!$self->{preserve_case}) {
			push(@fname, lc($fieldname));
		} else {
			push(@fname, $fieldname);
		}

		if ($f->[1] =~ /SDO_GEOMETRY/i)
		{
			$self->{local_type} = $self->{type} if (!$self->{local_type});
			$has_geometry = 1;
		}

		my $type = $self->_sql_type($f->[1], $f->[2], $f->[5], $f->[6], $f->[4]);
		$type = "$f->[1], $f->[2]" if (!$type);

		if (uc($f->[1]) eq 'ENUM')
		{
			my $keyname = lc($table . '_' . $fieldname . '_t');
			$f->[1] = $keyname;
		}
		# Check if this column should be replaced by a boolean following table/column name
		if (grep(/^\Q$fieldname\E$/i, @{$self->{'replace_as_boolean'}{uc($table)}})) {
			$type = 'boolean';
		}
		# Check if this column should be replaced by a boolean following type/precision
		elsif (exists $self->{'replace_as_boolean'}{uc($f->[1])} && $#{ $self->{'replace_as_boolean'}{uc($f->[1])} } >= 0)
		{
			if ($self->{'replace_as_boolean'}{uc($f->[1])}[0] == $f->[5] ||
				(!$f->[5] && $self->{'replace_as_boolean'}{uc($f->[1])}[0] == $f->[2]))
			{
				$type = 'boolean';
			}
		}
		$type = $self->{'modify_type'}{lc($table)}{lc($f->[0])} if (exists $self->{'modify_type'}{lc($table)}{lc($f->[0])});

		push(@stt, uc($f->[1]));
		push(@tt, $type);
		push(@nn,  $fieldname);
		# Change column names
		my $colname = $f->[0];
		if ($self->{replaced_cols}{lc($table)}{lc($f->[0])})
		{
			$self->logit("\tReplacing column $f->[0] as " . $self->{replaced_cols}{lc($table)}{lc($f->[0])} . "...\n", 1);
			$colname = $self->{replaced_cols}{lc($table)}{lc($f->[0])};
		}
		$colname = $self->quote_object_name($colname);
		if ($colname !~ /"/ && $self->is_reserved_words($colname)) {
			$colname = '"' . $colname . '"';
		}
		push(@{ $self->{tables}{$table}{dest_column_name} }, $colname) if (!grep(/^\Q$colname\E$/i, @{ $self->{tables}{$table}{dest_column_name} }));;
		if ($self->is_primary_key_column($table, $fieldname)) {
			push @pg_colnames_pkey, "$colname";
		} elsif ($f->[3] =~ m/^Y/) {
			push @pg_colnames_nullable, "$colname";
		} else {
			push @pg_colnames_notnull, "$colname";
		}
	}

	if ($self->{partition_by_reference} eq 'duplicate' && exists $self->{partitions_list}{"\L$table\E"}{refrtable})
	{
		my $reftable = $self->{partitions_list}{"\L$table\E"}{refrtable};
		foreach my $k (keys %{ $self->{tables}{"$reftable"}{column_info} })
		{
			next if (!grep(/^\Q$k\E$/i, @{$self->{partitions_list}{"\L$reftable\E"}{columns}}));
			my $f = $self->{tables}{"$reftable"}{column_info}{$k};
			$f->[2] =~ s/[^0-9\-\.]//g;
			# COLUMN_NAME,DATA_TYPE,DATA_LENGTH,NULLABLE,DATA_DEFAULT,DATA_PRECISION,DATA_SCALE,CHAR_LENGTH,TABLE_NAME,OWNER,VIRTUAL_COLUMN,POSITION,AUTO_INCREMENT,SRID,SDO_DIM,SDO_GTYPE
			my $type = $self->_sql_type($f->[1], $f->[2], $f->[5], $f->[6], $f->[4], 1);
			$type = "$f->[1], $f->[2]" if (!$type);
			# Change column names
			my $fname = $f->[0];
			if (exists $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"} && $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"})
			{
				$self->logit("\tReplacing column \L$f->[0]\E as " . $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"} . "...\n", 1);
				$fname = $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"};
			}

			next if (grep(/^\Q$fname\E$/i, @nn));

			push(@stt, uc($f->[1]));
			push(@tt, $type);
			push(@nn,  $fname);
			# Change column names
			my $colname = $fname;
			if ($self->{replaced_cols}{lc($table)}{lc($f->[0])})
			{
				$self->logit("\tReplacing column $f->[0] as " . $self->{replaced_cols}{lc($table)}{lc($f->[0])} . "...\n", 1);
				$colname = $self->{replaced_cols}{lc($table)}{lc($f->[0])};
			}
			$colname = $self->quote_object_name($colname);
			if ($colname !~ /"/ && $self->is_reserved_words($colname)) {
				$colname = '"' . $colname . '"';
			}
			push(@{ $self->{tables}{$table}{dest_column_name} }, $colname) if (!grep(/^\Q$colname\E$/i, @{ $self->{tables}{$table}{dest_column_name} }));
		}
	}


	# No column => ERROR
	$self->logit("FATAL: no column to export for table $table, aborting\n", 0, 1) if ($#fname < 0);

	$self->{tables}{$table}{pg_colnames_nullable} = \@pg_colnames_nullable;
	$self->{tables}{$table}{pg_colnames_notnull} = \@pg_colnames_notnull;
	$self->{tables}{$table}{pg_colnames_pkey} = \@pg_colnames_pkey;

	my $overriding_system = '';
	if ($self->{pg_supports_identity}) {
		$overriding_system = ' OVERRIDING SYSTEM VALUE' if ($has_identity);
	}

	my $s_out = "INSERT INTO $tmptb (" . join(',', @{ $self->{tables}{$table}{dest_column_name} });
	if ($self->{type} eq 'COPY') {
		$s_out = "\nCOPY $tmptb (" . join(',', @{ $self->{tables}{$table}{dest_column_name} });
	}

	if ($self->{type} eq 'COPY') {
		$s_out .= ") FROM STDIN$self->{copy_freeze};\n";
	} else {
		$s_out .= ")$overriding_system VALUES (";
	}

	# Prepare statements might work in binary mode but not WKT
	# and INTERNAL because they use the call to ST_GeomFromText()
	$has_geometry = 0 if ($self->{geometry_extract_type} eq 'WKB');

	# Use prepared statement in INSERT mode and only if
	# we are not exporting a row with a spatial column
	my $sprep = '';
	if ($self->{pg_dsn} && !$has_geometry)
	{
		if ($self->{type} ne 'COPY')
		{
			for (my $i = 0; $i <= $#fname; $i++)
			{
				if ($stt[$i] eq 'BLOB' && $tt[$i] eq 'oid') {
					$s_out .= "lo_from_bytea(0, decode(?, 'hex')),";
				} else {
					$s_out .= '?,';
				}
			}
			$s_out =~ s/,$//;
			$s_out .= ")";
			if ($self->{insert_on_conflict}) {
				$s_out .= " ON CONFLICT DO NOTHING";
			}
			$sprep = $s_out;
		}
	}

	# Extract all data from the current table
	my $total_record = $self->ask_for_data($table, \@cmd_head, \@cmd_foot, $s_out, \@nn, \@tt, $sprep, \@stt, $part_name, $is_subpart);

	$self->{type} = $self->{local_type} if ($self->{local_type});
	$self->{local_type} = '';
}

####
# dump FDW table content
####
sub _dump_fdw_table
{
	my ($self, $dirprefix, $sql_header, $table, $local_dbh) = @_;

	my @cmd_head = ();
	my @cmd_foot = ();

	# Set search path
	my $search_path = $self->set_search_path();
	if (!$self->{truncate_table} && $search_path) {
		push(@cmd_head,$search_path);
	}

	# Rename table and double-quote it if required
	my $tmptb = $self->get_replaced_tbname($table);

	# Replace Tablename by temporary table for DATADIFF (data will be inserted in real table at the end)
	# !!! does not work correctly for partitions yet !!!
	if ($self->{datadiff}) {
		$tmptb = $self->get_tbname_with_suffix($tmptb, $self->{datadiff_ins_suffix});
	}

	# Build the header of the query
	my $col_list = '';
	my $fdw_col_list = '';
	my $has_geometry = 0;
	my $has_identity = 0;
	$has_identity = 1 if (exists $self->{identity_info}{$table});

	# Extract column information following the Oracle position order
	my @fname = ();
	my (@pg_colnames_nullable, @pg_colnames_notnull, @pg_colnames_pkey);
	foreach my $i ( 0 .. $#{$self->{tables}{$table}{field_name}} )
	{
		my $fieldname = ${$self->{tables}{$table}{field_name}}[$i];
		next if (!$self->is_in_struct($table, $fieldname));

		my $f = $self->{tables}{"$table"}{column_info}{"$fieldname"};
		$f->[2] =~ s/\D//g;
		if (!$self->{enable_blob_export} && $f->[1] =~ /blob/i)
		{
			# user don't want to export blob
			next;
		}
		if (!$self->{enable_clob_export} && $f->[1] =~ /clob/i)
		{
			# user don't want to export clob
			next;
		}

		if (!$self->{preserve_case}) {
			push(@fname, lc($fieldname));
		} else {
			push(@fname, $fieldname);
		}

		if ($f->[1] =~ /SDO_GEOMETRY/i)
		{
			$self->{local_type} = $self->{type} if (!$self->{local_type});
			$has_geometry = 1;
		}

		my $type = $self->_sql_type($f->[1], $f->[2], $f->[5], $f->[6], $f->[4]);
		$type = "$f->[1], $f->[2]" if (!$type);

		# Check for boolean rewritting
		my $typlen = $f->[5];
		$typlen ||= $f->[2];
		# Check if this column should be replaced by a boolean following table/column name
		if (grep(/^\L$fieldname\E$/i, @{$self->{'replace_as_boolean'}{uc($table)}})) {
			$type = 'boolean';
		}
		# Check if this column should be replaced by a boolean following type/precision
		elsif (exists $self->{'replace_as_boolean'}{uc($f->[1])} && ($self->{'replace_as_boolean'}{uc($f->[1])}[0] == $typlen)) {
			$type = 'boolean';
		}
		# check if destination column type must be changed
		my $colname = $fieldname;
		$colname =~ s/["`]//g;
		$type = $self->{'modify_type'}{"\L$table\E"}{"\L$colname\E"} if (exists $self->{'modify_type'}{"\L$table\E"}{"\L$colname\E"});

		if (uc($f->[1]) eq 'ENUM') {
			my $keyname = lc($table . '_' . $colname . '_t');
			$type = $keyname;
		}
		# Change column names
		$colname = $f->[0];
		if ($self->{replaced_cols}{lc($table)}{lc($f->[0])})
		{
			$self->logit("\tReplacing column $f->[0] as " . $self->{replaced_cols}{lc($table)}{lc($f->[0])} . "...\n", 1);
			$colname = $self->{replaced_cols}{lc($table)}{lc($f->[0])};
		}
		# If there is any transformation to apply replace the column name with the clause
		if (exists $self->{transform_value}{lc($table)}{lc($colname)}) {
			$fdw_col_list .= $self->{transform_value}{lc($table)}{lc($colname)} . ",";
		}
		else
		{
			# If this column is translated into boolean apply the CASE clause
			# except for MSSQL export through TDS_FDW because the foreign table
			# has already converted BIT to boolean
			if ($type eq 'boolean' && (uc($f->[1]) ne 'BIT' || !$self->{is_mssql} || !$self->{fdw_server}))
			{
				$fdw_col_list .= "(CASE WHEN " . $self->quote_object_name($colname) . " IS NULL THEN NULL";
				my $true_list = '';
				foreach my $k (keys %{$self->{ora_boolean_values}})
				{
					if ($self->{ora_boolean_values}{$k} eq 't') {
						if ($f->[1] =~ /char/i) {
							$true_list .= " lower(" . $self->quote_object_name($colname) .") = '$k' OR";
						} elsif ($k !~ /\D/) {
							# we only take care of numeric values
							$true_list .= " " . $self->quote_object_name($colname) ." = $k OR";
						}
					}
				}
				$true_list =~ s/ OR$//;
				$fdw_col_list .= " WHEN ($true_list) THEN 't' ELSE 'f' END)::boolean,";
			}
			elsif ($type eq 'oid' && $self->{blob_to_lo})
			{
				$fdw_col_list  .= "lo_from_bytea(0, " . $self->quote_object_name($colname) . "::bytea),";
			}
			else
			{
				$fdw_col_list .= $self->quote_object_name($colname) . ",";
			}
		}
		$colname = $self->quote_object_name($colname);
		if ($colname !~ /"/ && $self->is_reserved_words($colname)) {
			$colname = '"' . $colname . '"';
		}
		$col_list .= "$colname,";
		if ($self->is_primary_key_column($table, $fieldname)) {
			push @pg_colnames_pkey, "$colname";
		} elsif ($f->[3] =~ m/^Y/) {
			push @pg_colnames_nullable, "$colname";
		} else {
			push @pg_colnames_notnull, "$colname";
		}
	}
	$col_list =~ s/,$//;
	$fdw_col_list =~ s/,$//;
	$self->{tables}{$table}{pg_colnames_nullable} = \@pg_colnames_nullable;
	$self->{tables}{$table}{pg_colnames_notnull} = \@pg_colnames_notnull;
	$self->{tables}{$table}{pg_colnames_pkey} = \@pg_colnames_pkey;

	my $overriding_system = '';
	if ($self->{pg_supports_identity} && $has_identity) {
		$overriding_system = ' OVERRIDING SYSTEM VALUE';
	}

	my $s_out = '';
	$fdwtb = $tmptb;
	$fdwtb = '"' . $tmptb . '"' if ($tmptb !~ /"/);
	if ($self->{type} eq 'INSERT')
	# Build INSERT statement
	{
		$s_out = "INSERT INTO $tmptb ($col_list";
		$s_out .= ")$overriding_system SELECT $fdw_col_list FROM $self->{fdw_import_schema}.$fdwtb";
	}
	if ($self->{type} eq 'COPY')
	# Build COPY statement
	{
		if ($self->{oracle_fdw_copy_mode} eq 'local') {
			$ENV{PGPASSWORD} = $self->{dbpwd};
			# Need to escape the quotation marks in $fdwtb
			my $fdwtb_escaped = $fdwtb =~ s/"/\"/gr;
			$s_out = "\\copy (select $fdw_col_list from $self->{fdw_import_schema}.$fdwtb_escaped) TO PROGRAM 'psql -X -h $self->{dbhost} -p $self->{dbport} -d $self->{dbname} -U $self->{dbuser} -c \\\"\\copy $self->{schema}.$tmptb FROM STDIN " . uc($self->{oracle_fdw_copy_format}) . "\\\"' " . uc($self->{oracle_fdw_copy_format});
		}
		if ($self->{oracle_fdw_copy_mode} eq 'server') {
			#$s_out = "COPY (select $fdw_col_list from $self->{fdw_import_schema}.$fdwtb) TO PROGRAM 'PGPASSWORD=$self->{dbpwd} psql -h $self->{dbhost} -p $self->{dbport} -d $self->{dbname} -U $self->{dbuser} -c \"\\copy $self->{schema}.$tmptb FROM STDIN BINARY\"' BINARY";
			$s_out = "COPY (select $fdw_col_list from $self->{fdw_import_schema}.$fdwtb) TO PROGRAM 'PGPASSWORD=$self->{dbpwd} psql -h $self->{dbhost} -p $self->{dbport} -d $self->{dbname} -U $self->{dbuser} -c \"\\copy $self->{schema}.$tmptb FROM STDIN " . uc($self->{oracle_fdw_copy_format}) . "\"' " . uc($self->{oracle_fdw_copy_format});
		}
	}

	$0 = "ora2pg - exporting table $self->{fdw_import_schema}.$fdwtb";

	####
	# Overwrite the query if REPLACE_QUERY is defined for this table
	####
	if ($self->{replace_query}{"\L$table\E"})
	{
		$s_out = $self->{replace_query}{"\L$table\E"};
	}

	# Prepare statements might work in binary mode but not WKT
	# and INTERNAL because they use the call to ST_GeomFromText()
	$has_geometry = 0 if ($self->{geometry_extract_type} eq 'WKB');

	# Append WHERE clause defined in the configuration file that must be applied
	if ($s_out !~ / WHERE /)
	{
		if (exists $self->{where}{"\L$table\E"} && $self->{where}{"\L$table\E"})
		{
			if (!$self->{is_mysql} || ($self->{where}{"\L$table\E"} !~ /\s+LIMIT\s+\d/)) {
				if ($self->{type} eq 'INSERT')
				# When using INSERT the WHERE clause needs to be added to the end
				{
					($s_out =~ / WHERE /) ? $s_out .= ' AND ' : $s_out .= ' WHERE ';
				        $s_out .= '(' . $self->{where}{"\L$table\E"} . ')';
				}
				if ($self->{type} eq 'COPY')
				# When using COPY the WHERE clause needs to be inserted into the SELECT from FDW
				{
					# ") TO PROGRAM" needs to become "$self->{where}) TO PROGRAM"
                                        $s_out =~ s/\) TO PROGRAM/$self->{where}) TO PROGRAM/;
				}
			} else {
				if ($self->{type} eq 'INSERT')
				# When using INSERT the WHERE clause needs to be added to the end
				{
					($s_out =~ / WHERE /) ? $s_out .= ' AND ' : $s_out .= ' WHERE ';
					$s_out .= $self->{where}{"\L$table\E"};
				}
				if ($self->{type} eq 'COPY')
				# When using COPY the WHERE clause needs to be inserted into the SELECT from FDW
				{
					# ") TO PROGRAM" needs to become "$self->{where}) TO PROGRAM"
                                        $s_out =~ s/\) TO PROGRAM/$self->{where}) TO PROGRAM/;
				}
			}
			$self->logit("\tApplying WHERE clause on foreign table: " . $self->{where}{"\L$table\E"} . "\n", 1);
		}
		elsif ($self->{global_where})
		{
			if (!$self->{is_mysql} || ($self->{global_where} !~ /\s+LIMIT\s+\d/)) {
				if ($self->{type} eq 'INSERT')
				# When using INSERT the WHERE clause needs to be added to the end
                                {
			                ($s_out =~ / WHERE /) ? $s_out .= ' AND ' : $s_out .= ' WHERE ';
				        $s_out .= '(' . $self->{global_where} . ')';
                                }
                                if ($self->{type} eq 'COPY')
                                # When using COPY the WHERE clause needs to be inserted into the SELECT from FDW
                                {
                                        # ") TO PROGRAM" needs to become "$self->{where}) TO PROGRAM"
                                        $s_out =~ s/\) TO PROGRAM/ WHERE ($self->{global_where})) TO PROGRAM/;
				}
			} else {
				if ($self->{type} eq 'INSERT')
				# When using INSERT the WHERE clause needs to be added to the end
                                {
			                ($s_out =~ / WHERE /) ? $s_out .= ' AND ' : $s_out .= ' WHERE ';
					$s_out .= $self->{global_where};
                                }
                                if ($self->{type} eq 'COPY')
                                # When using COPY the WHERE clause needs to be inserted into the SELECT from FDW
                                {
                                        # ") TO PROGRAM" needs to become "WHERE $self->{where}) TO PROGRAM"
                                        $s_out =~ s/\) TO PROGRAM/WHERE $self->{global_where}) TO PROGRAM/;
                                }
			}
			$self->logit("\tApplying WHERE global clause: " . $self->{global_where} . "\n", 1);
		}
	}

	if ( ($self->{oracle_copies} > 1) && $self->{defined_pk}{"\L$table\E"} )
	{
		my $colpk = $self->{defined_pk}{"\L$table\E"};
		if ($self->{preserve_case}) {
			$colpk = '"' . $colpk . '"';
		}
		my $cond = " ABS(MOD($colpk, $self->{oracle_copies})) = ?";
		$cond = " ABS($colpk % $self->{oracle_copies}) = ?" if ($self->{is_mssql});
		if ($s_out !~ s/\bWHERE\s+/WHERE $cond AND /)
		{
			if ($s_out !~ s/\b(ORDER\s+BY\s+.*)/WHERE $cond $1/) {
				if ($self->{type} eq 'INSERT')
                                # When using INSERT the WHERE clause needs to be added to the end
                                {
				        $s_out .= " WHERE $cond";
                                }
                                if ($self->{type} eq 'COPY')
                                # When using COPY the WHERE clause needs to be inserted into the SELECT from FDW
                                {
                                        # ") TO PROGRAM" needs to become "WHERE $cond) TO PROGRAM"
                                        $s_out =~ s/\) TO PROGRAM/ WHERE $cond) TO PROGRAM/;
                                }
			}
		}
		$self->{ora_conn_count} = 0;
		while ($self->{ora_conn_count} < $self->{oracle_copies})
		{
			spawn sub {
				if ($self->{type} eq 'INSERT')
				{
					$self->logit("Creating new connection to extract data in parallel...\n", 1);
					my $dbh = $local_dbh->clone();
					my $search_path = $self->set_search_path();
					if ($search_path) {
						$dbh->do($search_path) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
					}
					my $sth = $dbh->prepare($s_out) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
					$self->logit("Parallelizing on core #$self->{ora_conn_count} with query: $s_out\n", 1);
					$self->logit("Exporting foreign table data for $table, #$self->{ora_conn_count}\n", 1);
					$sth->execute($self->{ora_conn_count}) or $self->logit("FATAL: " . $dbh->errstr . ", SQL: $s_out\n", 0, 1);
					$sth->finish();
					if (defined $pipe)
					{
						my $t_time = time();
						$pipe->print("TABLE EXPORT ENDED: $table, end: $t_time, rows $self->{tables}{$table}{table_info}{num_rows}\n");
					}
					$dbh->disconnect() if ($dbh);
				}
				if ($self->{type} eq 'COPY' and $self->{oracle_fdw_copy_mode} eq 'local')
				{
					# Need to replace the "?" in $s_out with the relevant integer ("$self->{ora_conn_count}")
					$s_out =~ s/\?/$self->{ora_conn_count}/;
					$self->logit("Parallelizing on core #$self->{ora_conn_count} using psql command: $s_out\n", 1);
					$ENV{PGPASSWORD} = $self->{dbpwd};
			                my $psql_cmd = "psql -X -h $self->{dbhost} -p $self->{dbport} -d $self->{dbname} -U $self->{dbuser} -c \"$s_out\"";
					$self->logit("Exporting foreign table data for $table, #$self->{ora_conn_count}\n", 1);
					my $cmd_output = `$psql_cmd` or $self->logit("FATAL: " . $cmd_output . "\n", 0, 1);
					if (defined $pipe)
					{
						my $t_time = time();
						$pipe->print("TABLE EXPORT ENDED: $table, end: $t_time, rows $self->{tables}{$table}{table_info}{num_rows}\n");
					}
				}
				if ($self->{type} eq 'COPY' and $self->{oracle_fdw_copy_mode} eq 'server')
				{
                                	$self->logit("Creating new connection to extract data in parallel...\n", 1);
                                	my $dbh = $local_dbh->clone();
                                	my $search_path = $self->set_search_path();
                                	if ($search_path) {
                                        	$dbh->do($search_path) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
                                	}
                                	my $sth = $dbh->prepare($s_out) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
                                	my $s_out_no_password = $s_out =~ s/PGPASSWORD=[^\s]+\s/PGPASSWORD=********** /r;
                                	$self->logit("Parallelizing on core #$self->{ora_conn_count} with query: $s_out_no_password\n", 1);
                                	$self->logit("Exporting foreign table data for $table, #$self->{ora_conn_count}\n", 1);
                                	$sth->execute($self->{ora_conn_count}) or $self->logit("FATAL: " . $dbh->errstr . ", SQL: $s_out\n", 0, 1);
                                	$sth->finish();
					if (defined $pipe)
					{
						my $t_time = time();
						$pipe->print("TABLE EXPORT ENDED: $table, end: $t_time, rows $self->{tables}{$table}{table_info}{num_rows}\n");
					}
					$dbh->disconnect() if ($dbh);
				}
			};
			$self->{ora_conn_count}++;
		}
		# Wait for oracle connection terminaison
		while ($self->{ora_conn_count} > 0)
		{
			my $kid = waitpid(-1, WNOHANG);
			if ($kid > 0)
			{
				$self->{ora_conn_count}--;
				delete $RUNNING_PIDS{$kid};
			}
			usleep(50000);
		}
	}
	else
	{
		if ($self->{type} eq 'INSERT')
		{
			if ($search_path)  {
				$local_dbh->do($search_path) or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);
			}
			$self->logit("Exporting foreign table data for $table using query: $s_out\n", 1);
			$local_dbh->do($s_out) or $self->logit("ERROR: " . $local_dbh->errstr . ", SQL: $s_out\n", 0);
		}
		if ($self->{type} eq 'COPY' and $self->{oracle_fdw_copy_mode} eq 'local')
		{
			$ENV{PGPASSWORD} = $self->{dbpwd};
			my $psql_cmd = "psql -X -h $self->{dbhost} -p $self->{dbport} -d $self->{dbname} -U $self->{dbuser} -c \"$s_out\"";
			$self->logit("Exporting foreign table data for $table using psql command: $s_out\n", 1);
			my $cmd_output = `$psql_cmd` or $self->logit("FATAL: " . $cmd_output . "\n", 0, 1);
		}
		if ($self->{type} eq 'COPY' and $self->{oracle_fdw_copy_mode} eq 'server')
		{
			$self->logit("Exporting foreign table data for $table using query: $s_out\n", 1);
			$local_dbh->do($s_out) or $self->logit("ERROR: " . $local_dbh->errstr . ", SQL: $s_out\n", 0);
		}
	}

	$self->{type} = $self->{local_type} if ($self->{local_type});
	$self->{local_type} = '';
}

sub exclude_mviews
{
	my ($self, $cols) = @_;

	my $sql = " AND ($cols) NOT IN (SELECT OWNER, TABLE_NAME FROM $self->{prefix}_OBJECT_TABLES)";
	$sql .= " AND ($cols) NOT IN (SELECT OWNER, MVIEW_NAME FROM $self->{prefix}_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM $self->{prefix}_MVIEW_LOGS)" if ($self->{type} ne 'FDW');
	return $sql;
}

=head2 _column_comments

This function return comments associated to columns

=cut
sub _column_comments
{
	my ($self, $table) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_column_comments($self, $table);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_column_comments($self, $table);
	} else {
		return Ora2Pg::Oracle::_column_comments($self, $table);
	}
}

sub get_indexname
{
	my ($self, $table, $idx, @collist) = @_;


	my $schm = '';
	my $idxname = '';
	if ($idx =~ /^([^\.]+)\.(.*)$/)
	{
		$schm = $1;
		$idxname = $2;
	} else {
		$idxname = $idx;
	}
	if ($self->{indexes_renaming})
	{
		if ($table =~ /^([^\.]+)\.(.*)$/) {
			$schm = $1;
			$idxname = $2;
		} else {
			$idxname = $table;
		}
		$idxname =~ s/"//g;
		# Remove double quote, DESC and parenthesys
		map { s/"//g; s/.*\(([^\)]+)\).*/$1/; s/\s+DESC//i; s/::.*//; } @collist;
		$idxname .= '_' . join('_', @collist);
		$idxname =~ s/\s+//g;
		if ($self->{indexes_suffix}) {
			$idxname = substr($idxname,0,59);
		} else {
			$idxname = substr($idxname,0,63);
		}
	}
	# Remove non alphanumeric character
	#$idxname =~ s/[^a-z0-9_]+//ig;

	$idxname = $self->quote_object_name("$idxname$self->{indexes_suffix}");

	return $idxname;
}


=head2 _create_indexes

This function return SQL code to create indexes of a table
and triggers to create for FTS indexes.

- $indexonly mean no FTS index output

=cut

sub _create_indexes
{
	my ($self, $table, $indexonly, %indexes) = @_;

	my $tbsaved = $table;
	# The %indexes hash can be passed from table or materialized views definition
	my $objtyp = 'tables';
	if (!exists $self->{tables}{$tbsaved} && exists $self->{materialized_views}{$tbsaved}) {
		$objtyp = 'materialized_views';
	}

	my %pkcollist = ();
	# Save the list of column for PK to check unique index that must be removed
	foreach my $consname (keys %{$self->{$objtyp}{$tbsaved}{unique_key}})
	{
		next if ($self->{$objtyp}{$tbsaved}{unique_key}->{$consname}{type} ne 'P');
		my @conscols = grep(!/^\d+$/, @{$self->{$objtyp}{$tbsaved}{unique_key}->{$consname}{columns}});
		# save the list of column for PK to check unique index that must be removed
		$pkcollist{$tbsaved} = join(", ", @conscols);
	}
	$pkcollist{$tbsaved} =~ s/\s+/ /g;

	$table = $self->get_replaced_tbname($table);
	my @out = ();
	my @fts_out = ();
	my $has_to_char = 0;
	# Set the index definition
	foreach my $idx (sort keys %indexes)
	{
		# Remove cols than have only digit as name
		@{$indexes{$idx}} = grep(!/^\d+$/, @{$indexes{$idx}});

		# Cluster, bitmap join, reversed and IOT indexes will not be exported at all
		# Hash indexes will be exported as btree if PG < 10
		next if ($self->{tables}{$tbsaved}{idx_type}{$idx}{type} =~ /JOIN|IOT|CLUSTER|REV/i);

		if (exists $self->{replaced_cols}{"\L$tbsaved\E"} && $self->{replaced_cols}{"\L$tbsaved\E"})
		{
			foreach my $c (keys %{$self->{replaced_cols}{"\L$tbsaved\E"}}) {
				map { s/\b$c\b/$self->{replaced_cols}{"\L$tbsaved\E"}{"\L$c\E"}/i } @{$indexes{$idx}};
			}
		}

		my @strings = ();
		my $i = 0;
		for (my $j = 0; $j <= $#{$indexes{$idx}}; $j++)
		{
			$indexes{$idx}->[$j] =~ s/''/%%ESCAPED_STRING%%/g;
			while ($indexes{$idx}->[$j] =~ s/'([^']+)'/%%string$i%%/)
			{
				push(@strings, $1);
				$i++;
			}
			if ($self->{plsql_pgsql}) {
				$indexes{$idx}->[$j] = Ora2Pg::PLSQL::convert_plsql_code($self, $indexes{$idx}->[$j], @strings);
			}
			$indexes{$idx}->[$j] =~ s/%%ESCAPED_STRING%%/''/ig;
			$has_to_char = 1 if ($indexes{$idx}->[$j] =~ s/TO_CHAR\s*\(/immutable_to_char\(/ig);
		}

		# Add index opclass if required and type allow it
		my %opclass_type = ();
		if ($self->{use_index_opclass})
		{
			my $i = 0;
			for (my $j = 0; $j <= $#{$indexes{$idx}}; $j++)
			{
				if (exists $self->{$objtyp}{$tbsaved}{column_info}{uc($indexes{$idx}->[$j])})
				{
					my $d = $self->{$objtyp}{$tbsaved}{column_info}{uc($indexes{$idx}->[$j])};
					$d->[2] =~ s/\D//g;
					if ( (($self->{use_index_opclass} == 1) || ($self->{use_index_opclass} <= $d->[2])) && ($d->[1] =~ /VARCHAR/)) {
						my $typ = $self->_sql_type($d->[1], $d->[2], $d->[5], $d->[6], $f->[4]);
						$typ =~ s/\(.*//;
						if ($typ =~ /varchar/) {
							$typ = ' varchar_pattern_ops';
						} elsif ($typ =~ /text/) {
							$typ = ' text_pattern_ops';
						} elsif ($typ =~ /char/) {
							$typ = ' bpchar_pattern_ops';
						}
						$opclass_type{$indexes{$idx}->[$j]} = "$indexes{$idx}->[$j] $typ";
					}
				}
			}
		}

		# Add parentheses to index column definition when a space or arithmetic operators are found
		if (!$self->{input_file})
		{
			for ($i = 0; $i <= $#{$indexes{$idx}}; $i++)
			{
				if ( $indexes{$idx}->[$i] =~ /[\s\-\+\/\*]/ && $indexes{$idx}->[$i] !~ /^[^\.\s]+\s+(ASC|DESC)$/i
				       			&& $indexes{$idx}->[$i] !~ /\s+collate\s+/i ) {
					$indexes{$idx}->[$i] = '(' . $indexes{$idx}->[$i] . ')';
				}
				$indexes{$idx}->[$i] =~ s/"//g;
			}
		}
		else
		{
			for ($i = 0; $i <= $#{$indexes{$idx}}; $i++)
			{
				my @tmp_col = split(/\s*,\s*/, $indexes{$idx}->[$i]);
				for (my $j = 0; $j <= $#tmp_col; $j++)
				{
					if ( $tmp_col[$j] =~ /[\s\-\+\/\*]/ && $tmp_col[$j] !~ /^[^\.\s]+\s+(ASC|DESC)$/i
				       			&& $tmp_col[$j] !~ /\s+collate\s+/i ) {
						$tmp_col[$j] = '(' . $tmp_col[$j] . ')';
					}
				}
				$indexes{$idx}->[$i] = join(', ', @tmp_col);
			}
		}
		my $columns = '';
		foreach my $s (@{$indexes{$idx}})
		{
			$s = '"' . $s . '"' if ($self->is_reserved_words($s));
			if ($s =~ /\|\|/i) {
				$columns .= '(' . $s . ')';
			} else {
				if ($s =~ /^CASE\s+.*END/i) {
					$s = "($s)";
				}
				$columns .= ((exists $opclass_type{$s}) ? $opclass_type{$s} : $s) . ", ";
			}
			# Add double quotes on column name if PRESERVE_CASE is enabled
			foreach my $c (keys %{$self->{tables}{$tbsaved}{column_info}})
			{
				$columns =~ s/\b$c\b/"$c"/g if ($self->{preserve_case} && $columns !~ /"$c"/);
			}
		}
		$columns =~ s/, $//s;
		$columns =~ s/\s+/ /gs;
		my $colscompare = $columns;
		$colscompare =~ s/"//g;
		$colscompare =~ s/ //g;
		my $columnlist = '';
		my $skip_index_creation = 0;
		my %pk_hist = ();

		foreach my $consname (keys %{$self->{$objtyp}{$tbsaved}{unique_key}})
		{
			my $constype =  $self->{$objtyp}{$tbsaved}{unique_key}->{$consname}{type};
			next if (($constype ne 'P') && ($constype ne 'U'));
			my @conscols = grep(!/^\d+$/, @{$self->{$objtyp}{$tbsaved}{unique_key}->{$consname}{columns}});
			for ($i = 0; $i <= $#conscols; $i++)
			{
				# Change column names
				if (exists $self->{replaced_cols}{"\L$tbsaved\E"}{"\L$conscols[$i]\E"} && $self->{replaced_cols}{"\L$tbsaved\E"}{"\L$conscols[$i]\E"}) {
					$conscols[$i] = $self->{replaced_cols}{"\L$tbsaved\E"}{"\L$conscols[$i]\E"};
				}
			}
			$columnlist = join(',', @conscols);
			$columnlist =~ s/"//gs;
			$columnlist =~ s/\s+//gs;
			if ($constype eq 'P')
			{
				$pk_hist{$table} = $columnlist;
			}
			if (lc($columnlist) eq lc($colscompare))
			{
				$skip_index_creation = 1;
				last;
			}
		}

		# Do not create the index if there already a constraint on the same column list
		# or there a primary key defined on the same columns as a unique index, in both cases
		# the index will be automatically created by PostgreSQL at constraint import time.
		if (!$skip_index_creation)
		{
			my $unique = '';
			$unique = ' UNIQUE' if ($self->{$objtyp}{$tbsaved}{uniqueness}{$idx} eq 'UNIQUE');
			my $str = '';
			my $fts_str = '';
			my $concurrently = '';
			if ($self->{$objtyp}{$tbsaved}{concurrently}{$idx}) {
				$concurrently = ' CONCURRENTLY';
			}
			$columns = lc($columns) if (!$self->{preserve_case});
			next if ( lc($columns) eq lc($pkcollist{$tbsaved}) );

			for ($i = 0; $i <= $#strings; $i++) {
				$columns =~ s/\%\%string$i\%\%/'$strings[$i]'/;
			}

			# Replace call of schema.package.function() into package.function()
			$columns =~ s/\b[^\s\.]+\.([^\s\.]+\.[^\s\.]+)\s*\(/$1\(/is;

			# Do not create indexes if they are already defined as constraints
			if ($self->{type} eq 'TABLE')
			{
				my $col_list = $columns;
				$col_list =~ s/"//g;
				$col_list =~ s/, /,/g;
				next if (exists $pk_hist{$table} && uc($pk_hist{$table}) eq uc($col_list));
			}

			my $idxname = $self->get_indexname($table, $idx, @{$indexes{$idx}});

			$str .= "DROP INDEX $self->{pg_supports_ifexists} $idxname;\n" if ($self->{drop_if_exists});
			my $tb = $self->quote_object_name($table);
			if ($self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_name} =~ /SPATIAL_INDEX/)
			{
				$str .= "CREATE INDEX$concurrently " . $idxname
						. " ON $tb USING gist($columns)";
			}
			elsif ($self->{bitmap_as_gin} && ($self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_name} eq 'BITMAP' || $self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type} eq 'BITMAP'))
			{
				$str .= "CREATE INDEX$concurrently " . $idxname
						. " ON $tb USING gin($columns)";
			}
			elsif ( ($self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_name} =~ /CTXCAT/) ||
				($self->{context_as_trgm} && ($self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_name} =~ /FULLTEXT|CONTEXT/)) )
			{
				# use pg_trgm
				my @cols = split(/\s*,\s*/, $columns);
				map { s/^(.*)$/unaccent_immutable($1)/; } @cols if ($self->{use_unaccent});
				$columns = join(" gin_trgm_ops, ", @cols);
				$columns .= " gin_trgm_ops";
				$str .= "CREATE INDEX$concurrently " . $idxname
						. " ON $tb USING gin($columns)";
			}
			elsif (($self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_name} =~ /FULLTEXT|CONTEXT/) && $self->{fts_index_only})
			{
				my $stemmer = $self->{fts_config} || lc($self->{$objtyp}{$tbsaved}{idx_type}{$idx}{stemmer}) || 'pg_catalog.english';
				my $dico = $stemmer;
				$dico =~ s/^pg_catalog\.//;
				if ($self->{use_unaccent}) {
					$dico =~ s/^(..).*/$1/;
					if ($fts_str !~ /CREATE TEXT SEARCH CONFIGURATION $dico (COPY = $stemmer);/s) {
						$fts_str .= "CREATE TEXT SEARCH CONFIGURATION $dico (COPY = $stemmer);\n";
						$stemmer =~ s/pg_catalog\.//;
						$fts_str .= "ALTER TEXT SEARCH CONFIGURATION $dico ALTER MAPPING FOR hword, hword_part, word WITH unaccent, ${stemmer}_stem;\n\n";
					}
				}
				# use function-based index"
				my @cols = split(/\s*,\s*/, $columns);
				$columns = "to_tsvector('$dico', " . join("||' '||", @cols) . ")";
				$fts_str .= "CREATE INDEX$concurrently " . $idxname
						. " ON $tb USING gin($columns);\n";
			}
			elsif (($self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_name} =~ /FULLTEXT|CONTEXT/) && !$self->{fts_index_only})
			{
				# use Full text search, then create dedicated column and trigger before the index.
				map { s/"//g; } @{$indexes{$idx}};
				my $newcolname = join('_', @{$indexes{$idx}});
				$fts_str .= "\n-- Append the FTS column to the table\n";
				$fts_str .= "\nALTER TABLE $tb ADD COLUMN tsv_" . substr($newcolname,0,59) . " tsvector;\n";
				my $fctname = "tsv_${table}_" . substr($newcolname,0,59-(length($table)+1));
				my $trig_name = "trig_tsv_${table}_" . substr($newcolname,0,54-(length($table)+1));
				my $contruct_vector =  '';
				my $update_vector =  '';
				my $weight = 'A';
				my $stemmer = $self->{fts_config} || lc($self->{$objtyp}{$tbsaved}{idx_type}{$idx}{stemmer}) || 'pg_catalog.english';
				my $dico = $stemmer;
				$dico =~ s/^pg_catalog\.//;
				if ($self->{use_unaccent})
				{
					$dico =~ s/^(..).*/$1/;
					if ($fts_str !~ /CREATE TEXT SEARCH CONFIGURATION $dico (COPY = $stemmer);/s)
					{
						$fts_str .= "CREATE TEXT SEARCH CONFIGURATION $dico (COPY = $stemmer);\n";
						$stemmer =~ s/pg_catalog\.//;
						$fts_str .= "ALTER TEXT SEARCH CONFIGURATION $dico ALTER MAPPING FOR hword, hword_part, word WITH unaccent, ${stemmer}_stem;\n\n";
					}
				}
				if ($#{$indexes{$idx}} > 0)
				{
					foreach my $col (@{$indexes{$idx}})
					{
						$contruct_vector .= "\t\tsetweight(to_tsvector('$dico', coalesce(new.$col,'')), '$weight') ||\n";
						$update_vector .= " setweight(to_tsvector('$dico', coalesce($col,'')), '$weight') ||";
						$weight++;
					}
					$contruct_vector =~ s/\|\|$/;/s;
					$update_vector =~ s/\|\|$/;/s;
				}
				else
				{
					$contruct_vector = "\t\tto_tsvector('$dico', coalesce(new.$indexes{$idx}->[0],''))\n";
					$update_vector = " to_tsvector('$dico', coalesce($indexes{$idx}->[0],''))";
				}

				$fts_str .= qq{
-- When the data migration is done without trigger, create tsvector data for all the existing records
UPDATE $tb SET tsv_$newcolname = $update_vector

-- Trigger used to keep fts field up to date
CREATE FUNCTION $fctname() RETURNS trigger AS \$\$
BEGIN
	IF TG_OP = 'INSERT' OR new.$newcolname != old.$newcolname THEN
		new.tsv_$newcolname :=
$contruct_vector
	END IF;
	return new;
END
\$\$ LANGUAGE plpgsql;

CREATE TRIGGER $trig_name BEFORE INSERT OR UPDATE
  ON $tb
  FOR EACH ROW EXECUTE PROCEDURE $fctname();

} if (!$indexonly);
				if ($objtyp eq 'tables')
				{
					$str .= "CREATE$unique INDEX$concurrently " . $idxname
						 . " ON $table USING gin(tsv_$newcolname)";
				}
				else
				{
					$fts_str .= "CREATE$unique INDEX$concurrently " . $idxname
						. " ON $table USING gin(tsv_$newcolname)";
				}
			}
			elsif ($self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type} =~ /DOMAIN/i && $self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_name} !~ /SPATIAL_INDEX/)
			{
				$str .= "-- Was declared as DOMAIN index, please check for FTS adaptation if require\n";
				$str .= "-- CREATE$unique INDEX$concurrently " . $idxname
						. " ON $table ($columns)";
			}
			else
			{
				$str .= "CREATE$unique INDEX$concurrently " . $idxname
						. " ON $table ($columns)";
			}

			if ($#{$self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_include}} >= 0) {
				$str .= " INCLUDE (" . join(', ', @{$self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_include}}) . ')';
			}

			if ($self->{use_tablespace} && $self->{$objtyp}{$tbsaved}{idx_tbsp}{$idx} && !grep(/^$self->{$objtyp}{$tbsaved}{idx_tbsp}{$idx}$/i, @{$self->{default_tablespaces}}))
			{
				$str .= " TABLESPACE $self->{$objtyp}{$tbsaved}{idx_tbsp}{$idx}";
			}
			if ($str)
			{
				$str .= ";";
				push(@out, $str);
			}
			push(@fts_out, $fts_str) if ($fts_str);
		}
	}

	if ($has_to_char)
	{
		unshift(@out, qq{
-- Function used in indexes must be immutable, use immutable_to_char() instead of to_char()
CREATE OR REPLACE FUNCTION immutable_to_char(timestamp, fmt text) RETURNS text AS
\$\$ SELECT to_char(\$1, \$2); \$\$
LANGUAGE sql immutable;
});
	}

	return $indexonly ? (@out,@fts_out) : (join("\n", @out), join("\n", @fts_out));
}

=head2 _drop_indexes

This function return SQL code to drop indexes of a table

=cut
sub _drop_indexes
{
	my ($self, $table, %indexes) = @_;

	my $tbsaved = $table;
	$table = $self->get_replaced_tbname($table);

	my @out = ();
	# Set the index definition
	foreach my $idx (keys %indexes)
	{
		# Cluster, bitmap join, reversed and IOT indexes will not be exported at all
		next if ($self->{tables}{$tbsaved}{idx_type}{$idx}{type} =~ /JOIN|IOT|CLUSTER|REV/i);

		if (exists $self->{replaced_cols}{"\L$tbsaved\E"} && $self->{replaced_cols}{"\L$tbsaved\E"})
		{
			foreach my $c (keys %{$self->{replaced_cols}{"\L$tbsaved\E"}})
			{
				map { s/\b$c\b/$self->{replaced_cols}{"\L$tbsaved\E"}{"\L$c\E"}/i } @{$indexes{$idx}};
			}
		}
		map { if ($_ !~ /\(.*\)/) {
				$_ =~ s/(\s+.*)//; # DESC or ASC
				$_ = $self->quote_object_name($_);
				$_ .= $1;
			} } @{$indexes{$idx}};

                my $columns = '';
                foreach my $s (@{$indexes{$idx}})
                {
			if ($s =~ /\|\|/) {
				$columns .= '(' . $s . ')';
			} else {
				$columns .= ((exists $opclass_type{$s}) ? $opclass_type{$s} : $s) . ", ";
			}
                        # Add double quotes on column name if PRESERVE_CASE is enabled
                        foreach my $c (keys %{$self->{tables}{$tbsaved}{column_info}})
                        {
                                $columns =~ s/\b$c\b/"$c"/ if ($self->{preserve_case} && $columns !~ /"$c"/);
                        }
                }
                $columns =~ s/, $//s;
                $columns =~ s/\s+//gs;
                my $colscompare = $columns;
                $colscompare =~ s/"//gs;
                my $columnlist = '';
                my $skip_index_creation = 0;
                my %pk_hist = ();

                foreach my $consname (keys %{$self->{tables}{$tbsaved}{unique_key}})
                {
                        my $constype =  $self->{tables}{$tbsaved}{unique_key}->{$consname}{type};
                        next if (($constype ne 'P') && ($constype ne 'U'));
                        my @conscols = grep(!/^\d+$/, @{$self->{tables}{$tbsaved}{unique_key}->{$consname}{columns}});
                        for ($i = 0; $i <= $#conscols; $i++)
                        {
                                # Change column names
                                if (exists $self->{replaced_cols}{"\L$tbsaved\E"}{"\L$conscols[$i]\E"} && $self->{replaced_cols}{"\L$tbsaved\E"}{"\L$conscols[$i]\E"}) {
                                        $conscols[$i] = $self->{replaced_cols}{"\L$tbsaved\E"}{"\L$conscols[$i]\E"};
                                }
                        }
                        $columnlist = join(',', @conscols);
                        $columnlist =~ s/"//gs;
                        $columnlist =~ s/\s+//gs;
                        if ($constype eq 'P')
                        {
                                $pk_hist{$table} = $columnlist;
                        }
                        if (lc($columnlist) eq lc($colscompare)) {
                                $skip_index_creation = 1;
                                last;
                        }
                }

		# Do not create the index if there already a constraint on the same column list
		# the index will be automatically created by PostgreSQL at constraint import time.
		if (!$skip_index_creation)
		{
			# Cluster, bitmap join, reversed and IOT indexes will not be exported at all
			# Hash indexes will be exported as btree if PG < 10
			next if ($self->{tables}{$tbsaved}{idx_type}{$idx}{type} =~ /JOIN|IOT|CLUSTER|REV/i);

			if (exists $self->{replaced_cols}{"\L$tbsaved\E"} && $self->{replaced_cols}{"\L$tbsaved\E"})
			{
				foreach my $c (keys %{$self->{replaced_cols}{"\L$tbsaved\E"}}) {
					map { s/\b$c\b/$self->{replaced_cols}{"\L$tbsaved\E"}{"\L$c\E"}/i } @{$indexes{$idx}};
				}
			}

			for (my $j = 0; $j <= $#{$indexes{$idx}}; $j++)
			{
				$indexes{$idx}->[$j] =~ s/''/%%ESCAPED_STRING%%/g;
				my @strings = ();
				my $i = 0;
				while ($indexes{$idx}->[$j] =~ s/'([^']+)'/%%string$i%%/)
				{
					push(@strings, $1);
					$i++;
				}
				if ($self->{plsql_pgsql}) {
					$indexes{$idx}->[$j] = Ora2Pg::PLSQL::convert_plsql_code($self, $indexes{$idx}->[$j], @strings);
				}
				$indexes{$idx}->[$j] =~ s/%%ESCAPED_STRING%%/''/ig;

				for ($i = 0; $i <= $#strings; $i++) {
					$indexes{$idx}->[$j] =~ s/\%\%string$i\%\%/'$strings[$i]'/;
				}
			}

			my $idxname = $self->get_indexname($table, $idx, @{$indexes{$idx}});

			if ($self->{tables}{$table}{idx_type}{$idx}{type} =~ /DOMAIN/i && $self->{tables}{$table}{idx_type}{$idx}{type_name} !~ /SPATIAL_INDEX/)
			{
				push(@out, "-- Declared as DOMAIN index, uncomment line below if it must be removed");
				push(@out, "-- DROP INDEX $self->{pg_supports_ifexists} $idxname\L$self->{indexes_suffix}\E;");
			}
			else
			{
				push(@out, "DROP INDEX $self->{pg_supports_ifexists} $idxname\L$self->{indexes_suffix}\E;");
			}
		}
	}

	return wantarray ? @out : join("\n", @out);
}

=head2 _exportable_indexes

This function return the indexes that will be exported

=cut

sub _exportable_indexes
{
	my ($self, $table, %indexes) = @_;

	my @out = ();
	# Set the index definition
	foreach my $idx (keys %indexes)
	{

		map { if ($_ !~ /\(.*\)/) { s/^/"/; s/$/"/; } } @{$indexes{$idx}};
		map { s/"//gs } @{$indexes{$idx}};
		my $columns = join(',', @{$indexes{$idx}});
		my $colscompare = $columns;
		my $columnlist = '';
		my $skip_index_creation = 0;
		foreach my $consname (keys %{$self->{tables}{$table}{unique_key}})
		{
			my $constype =  $self->{tables}{$table}{unique_key}->{$consname}{type};
			next if (($constype ne 'P') && ($constype ne 'U'));
			my @conscols = @{$self->{tables}{$table}{unique_key}->{$consname}{columns}};
			$columnlist = join(',', @conscols);
			$columnlist =~ s/"//gs;
			if (lc($columnlist) eq lc($colscompare)) {
				$skip_index_creation = 1;
				last;
			}
		}

		# The index will not be created
		if (!$skip_index_creation) {
			push(@out, $idx);
		}
	}

	return @out;
}


=head2 is_primary_key_column

This function return 1 when the specified column is a primary key

=cut
sub is_primary_key_column
{
	my ($self, $table, $col) = @_;

	# Set the unique (and primary) key definition 
	foreach my $consname (keys %{ $self->{tables}{$table}{unique_key} }) {
		next if ($self->{tables}{$table}{unique_key}->{$consname}{type} ne 'P');
		my @conscols = @{$self->{tables}{$table}{unique_key}->{$consname}{columns}};
		for (my $i = 0; $i <= $#conscols; $i++) {
			if (lc($conscols[$i]) eq lc($col)) {
				return 1;
			}
		}
	}

	return 0;
}


=head2 _get_primary_keys

This function return SQL code to add primary keys of a create table definition

=cut
sub _get_primary_keys
{
	my ($self, $table, $unique_key) = @_;

	my $out = '';

	# Set the unique (and primary) key definition 
	foreach my $consname (keys %$unique_key)
	{
		next if ($self->{pkey_in_create} && ($unique_key->{$consname}{type} ne 'P'));
		my $constype =   $unique_key->{$consname}{type};
		my $constgen =   $unique_key->{$consname}{generated};
		my $index_name = $unique_key->{$consname}{index_name};
		my @conscols = @{$unique_key->{$consname}{columns}};
		my %constypenames = ('U' => 'UNIQUE', 'P' => 'PRIMARY KEY');
		my $constypename = $constypenames{$constype};
		for (my $i = 0; $i <= $#conscols; $i++)
		{
			# Change column names
			if (exists $self->{replaced_cols}{"\L$table\E"}{"\L$conscols[$i]\E"} && $self->{replaced_cols}{"\L$table\E"}{"\L$conscols[$i]\E"}) {
				$conscols[$i] = $self->{replaced_cols}{"\L$table\E"}{"\L$conscols[$i]\E"};
			}
		}
		map { $_ = $self->quote_object_name($_) } @conscols;

		my $columnlist = join(',', @conscols);
		if ($columnlist)
		{
			if ($self->{pkey_in_create})
			{
				if (!$self->{keep_pkey_names} || ($constgen eq 'GENERATED NAME')) {
					$out .= "\tPRIMARY KEY ($columnlist)";
				} else {
					$out .= "\tCONSTRAINT " .  $self->quote_object_name($consname) . " PRIMARY KEY ($columnlist)";
				}
				if ($self->{use_tablespace} && $self->{tables}{$table}{idx_tbsp}{$index_name} && !grep(/^$self->{tables}{$table}{idx_tbsp}{$index_name}$/i, @{$self->{default_tablespaces}})) {
					$out .= " USING INDEX TABLESPACE " .  $self->quote_object_name($self->{tables}{$table}{idx_tbsp}{$index_name});
				}
				$out .= ",\n";
			}
		}
	}
	$out =~ s/,$//s;

	return $out;
}


=head2 _create_unique_keys

This function return SQL code to create unique and primary keys of a table

=cut
sub _create_unique_keys
{
	my ($self, $table, $unique_key, $partition) = @_;

	my $out = '';

	my $tbsaved = $table;
	$table = $self->get_replaced_tbname($table);

	# Set the unique (and primary) key definition 
	foreach my $consname (keys %$unique_key)
	{
		next if ($self->{pkey_in_create} && ($unique_key->{$consname}{type} eq 'P'));
		my $constype =   $unique_key->{$consname}{type};
		my $constgen =   $unique_key->{$consname}{generated};
		my $index_name = $unique_key->{$consname}{index_name};
		my $deferrable = $unique_key->{$consname}{deferrable};
		my $deferred = $unique_key->{$consname}{deferred};
		my @conscols = @{$unique_key->{$consname}{columns}};

		# Exclude unique index used in PK when column list is the same
		next if (($constype eq 'U') && exists $pkcollist{$table} && ($pkcollist{$table} eq join(",", @conscols)));

		my %constypenames = ('U' => 'UNIQUE', 'P' => 'PRIMARY KEY');
		my $constypename = $constypenames{$constype};
		for (my $i = 0; $i <= $#conscols; $i++)
		{
			# Change column names
			if (exists $self->{replaced_cols}{"\L$tbsaved\E"}{"\L$conscols[$i]\E"} && $self->{replaced_cols}{"\L$tbsaved\L"}{"\L$conscols[$i]\E"}) {
				$conscols[$i] = $self->{replaced_cols}{"\L$tbsaved\E"}{"\L$conscols[$i]\E"};
			}
		}
		# Add the partition column if it is not is the PK
		if (!$self->{disable_partition} && ($constype eq 'P' || $constype eq 'U') && exists $self->{partitions_list}{"\L$tbsaved\E"})
		{
			for (my $j = 0; $j <= $#{$self->{partitions_list}{"\L$tbsaved\E"}{columns}}; $j++)
			{
				push(@conscols, $self->{partitions_list}{"\L$tbsaved\E"}{columns}[$j]) if (!grep(/^$self->{partitions_list}{"\L$tbsaved\E"}{columns}[$j]$/i, @conscols));
			}

			if ($partition)
			{
				for (my $j = 0; $j <= $#{$self->{subpartitions_list}{"\L$tbsaved\E"}{"\L$partition\E"}{columns}}; $j++)
				{
					push(@conscols, $self->{subpartitions_list}{"\L$tbsaved\E"}{"\L$partition\E"}{columns}[$j]) if (!grep(/^$self->{subpartitions_list}{"\L$tbsaved\E"}{"\L$partition\E"}{columns}[$j]$/i, @conscols));
				}
			}
			else
			{
				foreach my $part (keys %{$self->{subpartitions_list}{"\L$tbsaved\E"}})
				{
					for (my $j = 0; $j <= $#{$self->{subpartitions_list}{"\L$tbsaved\E"}{"\L$part\E"}{columns}}; $j++)
					{
						push(@conscols, $self->{subpartitions_list}{"\L$tbsaved\E"}{"\L$part\E"}{columns}[$j]) if (!grep(/^$self->{subpartitions_list}{"\L$tbsaved\E"}{"\L$part\E"}{columns}[$j]$/i, @conscols));
					}
				}
			}
		}
		map { $_ = $self->quote_object_name($_) } @conscols;

		my $reftable = $table;
		$reftable = $self->{partitions_list}{"\L$table\E"}{refrtable} if (exists $self->{partitions_list}{"\L$table\E"}{refrtable});
		foreach my $k (keys %{ $self->{tables}{"$reftable"}{column_info} })
		{
			next if (!grep(/^$k$/i, @{$self->{partitions_list}{"\L$reftable\E"}{columns}}));
			my $f = $self->{tables}{"$reftable"}{column_info}{$k};
			$f->[2] =~ s/[^0-9\-\.]//g;
			# Change column names
			my $fname = $f->[0];
			if (exists $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"} && $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"})
			{
				$self->logit("\tReplacing column \L$f->[0]\E as " . $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"} . "...\n", 1);
				$fname = $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"};
			}
			push(@conscols, $self->quote_object_name($fname));
		}

		my $columnlist = join(',', @conscols);
		if ($columnlist)
		{
			if (!$self->{keep_pkey_names} || ($constgen eq 'GENERATED NAME')) {
				$str .= "ALTER TABLE $table DROP $constypename;\n" if ($self->{drop_if_exists});
				$out .= "ALTER TABLE $table ADD $constypename ($columnlist)";
			} else {
				$str .= "ALTER TABLE $table DROP CONSTRAINT $self->{pg_supports_ifexists} " . $self->quote_object_name($consname) . ";\n" if ($self->{drop_if_exists});
				$out .= "ALTER TABLE $table ADD CONSTRAINT " . $self->quote_object_name($consname) . " $constypename ($columnlist)";
			}
			if ($self->{use_tablespace} && $self->{tables}{$tbsaved}{idx_tbsp}{$index_name} && !grep(/^$self->{tables}{$tbsaved}{idx_tbsp}{$index_name}$/i, @{$self->{default_tablespaces}})) {
				$out .= " USING INDEX TABLESPACE $self->{tables}{$tbsaved}{idx_tbsp}{$index_name}";
			}
			if ($deferrable eq "DEFERRABLE")
			{
				$out .= " DEFERRABLE";
				if ($deferred eq "DEFERRED") {
					$out .= " INITIALLY DEFERRED";
				}       
			}			
			$out .= ";\n";
		}
	}
	return $out;
}

=head2 _create_check_constraint

This function return SQL code to create the check constraints of a table

=cut
sub _create_check_constraint
{
	my ($self, $table, $check_constraint, $field_name, @skip_column_check) = @_;

	my $tbsaved = $table;
	$table = $self->get_replaced_tbname($table);

	my $out = '';
	# Set the check constraint definition 
	foreach my $k (sort keys %{$check_constraint->{constraint}})
	{
		my $chkconstraint = $check_constraint->{constraint}->{$k}{condition};
		my $validate = '';
		$validate = ' NOT VALID' if ($check_constraint->{constraint}->{$k}{validate} eq 'NOT VALIDATED');
		next if (!$chkconstraint);
		if ($chkconstraint =~ /^([^\s]+)\s+IS\s+NOT\s+NULL$/i)
		{
			my $col = $1;
			$col =~ s/"//g;
			if (exists $self->{replaced_cols}{"\L$tbsaved\E"} && $self->{replaced_cols}{"\L$tbsaved\E"})
			{
				foreach my $c (keys %{$self->{replaced_cols}{"\L$tbsaved\E"}})
				{
					next if (uc($col) ne uc($c));
					$col = $self->{replaced_cols}{"\L$tbsaved\E"}{$c};
				}
			}
			$out .= "ALTER TABLE $table ALTER COLUMN " . $self->quote_object_name($col) . " SET NOT NULL;\n";
		}
		else
		{
			if (exists $self->{replaced_cols}{"\L$tbsaved\E"} && $self->{replaced_cols}{"\L$tbsaved\E"})
			{
				foreach my $c (keys %{$self->{replaced_cols}{"\L$tbsaved\E"}})
				{
					$chkconstraint =~ s/"$c"/"$self->{replaced_cols}{"\L$tbsaved\E"}{"\L$c\E"}"/gsi;
					$chkconstraint =~ s/\b$c\b/$self->{replaced_cols}{"\L$tbsaved\E"}{"\L$c\E"}/gsi;
				}
			}
			if ($self->{plsql_pgsql}) {
				$chkconstraint = Ora2Pg::PLSQL::convert_plsql_code($self, $chkconstraint);
			}
			foreach my $c (@$field_name)
			{
				my $ret = $self->quote_object_name($c);
				$chkconstraint =~ s/\b$c\b/$ret/igs;
				$chkconstraint =~ s/""/"/igs;
			}
			$k = $self->quote_object_name($k);

			# If the column has been converted as a boolean do not export the constraint
			my $converted_as_boolean = 0;
			foreach my $c (@$field_name)
			{
				if (grep(/^$c$/i, @skip_column_check) && $chkconstraint =~ /\b$c\b/i) {
					$converted_as_boolean = 1;
				}
			}
			if (!$converted_as_boolean)
			{
				$chkconstraint = Ora2Pg::PLSQL::convert_plsql_code($self, $chkconstraint);
				$chkconstraint =~ s/,$//;
				$out .= "ALTER TABLE $table DROP CONSTRAINT $self->{pg_supports_ifexists} $k;\n" if ($self->{drop_if_exists});
				$out .= "ALTER TABLE $table ADD CONSTRAINT $k CHECK ($chkconstraint)$validate;\n";
			}
		}
	}

	return $out;
}

=head2 _create_foreign_keys

This function return SQL code to create the foreign keys of a table

=cut
sub _create_foreign_keys
{
	my ($self, $table) = @_;

	my @out = ();
	
	my $tbsaved = $table;
	$table = $self->get_replaced_tbname($table);

	# Add constraint definition
	my @done = ();
	foreach my $fkname (sort keys %{$self->{tables}{$tbsaved}{foreign_link}})
	{
		next if (grep(/^$fkname$/, @done));

		# Extract all attributes if the foreign key definition
		my $state;
		foreach my $h (@{$self->{tables}{$tbsaved}{foreign_key}})
		{
			if (lc($h->[0]) eq lc($fkname))
			{
				# @$h : CONSTRAINT_NAME,R_CONSTRAINT_NAME,SEARCH_CONDITION,DELETE_RULE,$deferrable,DEFERRED,R_OWNER,TABLE_NAME,OWNER,UPDATE_RULE,VALIDATED
				push(@$state, @$h);
				last;
			}
		}
		foreach my $desttable (sort keys %{$self->{tables}{$tbsaved}{foreign_link}{$fkname}{remote}})
		{
			push(@done, $fkname);

			# This is not possible to reference a partitionned table 
			next if ($self->{pg_supports_partition} && exists $self->{partitions_list}{lc($desttable)} && $self->{pg_version} <= 12);

			# Foreign key constraint on partitionned table do not support
			# NO VALID when the remote table is not partitionned
			my $allow_fk_notvalid = 1;
			$allow_fk_notvalid = 0 if ($self->{pg_supports_partition} && exists $self->{partitions_list}{lc($tbsaved)});
			my $str = '';
			# Add double quote to column name
			map { $_ = '"' . $_ . '"' } @{$self->{tables}{$tbsaved}{foreign_link}{$fkname}{local}};
			map { $_ = '"' . $_ . '"' } @{$self->{tables}{$tbsaved}{foreign_link}{$fkname}{remote}{$desttable}};

			# Get the name of the foreign table after replacement if any
			my $subsdesttable = $self->get_replaced_tbname($desttable);
			# Prefix the table name with the schema name if owner of
			# remote table is not the same as local one
			if ($self->{schema} && (lc($state->[6]) ne lc($state->[8]))) {
				$subsdesttable =  $self->quote_object_name($state->[6]) . '.' . $subsdesttable;
			}

			my @lfkeys = ();
			push(@lfkeys, @{$self->{tables}{$tbsaved}{foreign_link}{$fkname}{local}});
			if (exists $self->{replaced_cols}{"\L$tbsaved\E"} && $self->{replaced_cols}{"\L$tbsaved\E"}) {
				foreach my $c (keys %{$self->{replaced_cols}{"\L$tbsaved\E"}}) {
					map { s/"$c"/"$self->{replaced_cols}{"\L$tbsaved\E"}{"\L$c\E"}"/i } @lfkeys;
				}
			}
			my @rfkeys = ();
			push(@rfkeys, @{$self->{tables}{$tbsaved}{foreign_link}{$fkname}{remote}{$desttable}});
			if (exists $self->{replaced_cols}{"\L$desttable\E"} && $self->{replaced_cols}{"\L$desttable\E"})
			{
				foreach my $c (keys %{$self->{replaced_cols}{"\L$desttable\E"}}) {
					map { s/"$c"/"$self->{replaced_cols}{"\L$desttable\E"}{"\L$c\E"}"/i } @rfkeys;
				}
			}
			for (my $i = 0; $i <= $#lfkeys; $i++) {
				$lfkeys[$i] = $self->quote_object_name(split(/\s*,\s*/, $lfkeys[$i]));
			}
			for (my $i = 0; $i <= $#rfkeys; $i++) {
				$rfkeys[$i] = $self->quote_object_name(split(/\s*,\s*/, $rfkeys[$i]));
			}
			$fkname = $self->quote_object_name($fkname);
			$str .= "ALTER TABLE $table DROP CONSTRAINT $self->{pg_supports_ifexists} $fkname;\n" if ($self->{drop_if_exists});
			my $reftable = $table;
			$reftable = $self->{partitions_list}{"\L$table\E"}{refrtable} if (exists $self->{partitions_list}{"\L$table\E"}{refrtable});
			foreach my $k (keys %{ $self->{tables}{"$reftable"}{column_info} })
			{
				next if (!grep(/^$k$/i, @{$self->{partitions_list}{"\L$reftable\E"}{columns}}));
				my $f = $self->{tables}{"$reftable"}{column_info}{$k};
				# Change column names
				my $fname = $f->[0];
				if (exists $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"} && $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"})
				{
					$self->logit("\tReplacing column \L$f->[0]\E as " . $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"} . "...\n", 1);
					$fname = $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"};
				}
				push(@lfkeys, $self->quote_object_name($fname));
				push(@rfkeys, $self->quote_object_name($fname));
			}

			$str .= "ALTER TABLE $table ADD CONSTRAINT $fkname FOREIGN KEY (" . join(',', @lfkeys) . ") REFERENCES $subsdesttable(" . join(',', @rfkeys) . ")";
			$str .= " MATCH $state->[2]" if ($state->[2]);
			if ($state->[3]) {
				$str .= " ON DELETE $state->[3]";
			} else {
				$str .= " ON DELETE NO ACTION";
			}
			if ($self->{is_mysql}) {
				$str .= " ON UPDATE $state->[9]" if ($state->[9]);
			} else {
				if ( ($self->{fkey_add_update} eq 'ALWAYS') || ( ($self->{fkey_add_update} eq 'DELETE') && ($str =~ /ON DELETE CASCADE/) ) ) {
					$str .= " ON UPDATE CASCADE";
				}
			}
			# if DEFER_FKEY is enabled, force constraint to be
			# deferrable and defer it initially.
			if (!$self->{is_mysql})
			{
				$str .= (($self->{'defer_fkey'} ) ? ' DEFERRABLE' : " $state->[4]") if ($state->[4]);
				$state->[5] = 'DEFERRED' if ($state->[5] =~ /^Y/);
				$state->[5] ||= 'IMMEDIATE';
				$str .= " INITIALLY " . ( ($self->{'defer_fkey'} ) ? 'DEFERRED' : $state->[5] );
				if ($allow_fk_notvalid && $state->[9] eq 'NOT VALIDATED') {
					$str .= " NOT VALID";
				}
			}
			$str .= ";\n";
			push(@out, $str);
		}
	}

	return wantarray ? @out : join("\n", @out);
}

=head2 _drop_foreign_keys

This function return SQL code to the foreign keys of a table

=cut
sub _drop_foreign_keys
{
	my ($self, $table, @foreign_key) = @_;

	my @out = ();

	$table = $self->get_replaced_tbname($table);

	# Add constraint definition
	my @done = ();
	foreach my $h (@foreign_key) {
		next if (grep(/^$h->[0]$/, @done));
		push(@done, $h->[0]);
		my $str = '';
		$h->[0] =  $self->quote_object_name($h->[0]);
		$str .= "ALTER TABLE $table DROP CONSTRAINT $self->{pg_supports_ifexists} $h->[0];";
		push(@out, $str);
	}

	return wantarray ? @out : join("\n", @out);
}


=head2 _extract_sequence_info

This function retrieves the last value returned from the sequences in the
Oracle database. The result is a SQL script assigning the new start values
to the sequences found in the Oracle database.

=cut
sub _extract_sequence_info
{
	my $self = shift;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_extract_sequence_info($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_extract_sequence_info($self);
	} else {
		return Ora2Pg::Oracle::_extract_sequence_info($self);
	}
}


=head2 _howto_get_data TABLE

This function implements an Oracle-native data extraction.

Returns the SQL query to use to retrieve data

=cut

sub _howto_get_data
{
	my ($self, $table, $name, $type, $src_type, $part_name, $is_subpart) = @_;

	####
	# Overwrite the query if REPLACE_QUERY is defined for this table
	####
	if ($self->{replace_query}{"\L$table\E"})
	{
		$str = $self->{replace_query}{"\L$table\E"};
		$self->logit("DEGUG: Query sent to $self->{sgbd_name}: $str\n", 1);
		return $str;
	}

	# Fix a problem when the table need to be prefixed by the schema
	my $realtable = $table;
	$realtable =~ s/\"//g;
	# Do not use double quote with mysql, but backquote
	if ($self->{is_mysql})
	{
		$realtable =~ s/\`//g;
		$realtable = "\`$realtable\`";
	}
	elsif ($self->{is_mssql})
	{
		$realtable =~ s/[\[\]]+//g;
		$realtable = "\[$realtable\]";
		if (!$self->{schema} && $self->{export_schema}) {
			$realtable =~ s/\./\].\[/;
		}
	}
	else
	{
		if (!$self->{schema} && $self->{export_schema})
		{
			$realtable =~ s/\./"."/;
			$realtable = "\"$realtable\"";
			$reftable = "\"$reftable\"";
		}
		else
		{
			$realtable = "\"$realtable\"";
			$reftable = "\"$reftable\"";
			my $owner  = $self->{tables}{$table}{table_info}{owner} || $self->{tables}{$table}{owner} || '';
			if ($owner)
			{
				$owner =~ s/\"//g;
				$owner = "\"$owner\"";
				$realtable = "$owner.$realtable";
				$reftable = "$owner.$reftable";
			}
		}
	}

	delete $self->{nullable}{$table};

	my $alias = 'a';
	my $str = "SELECT ";
	if ($self->{tables}{$table}{table_info}{nested} eq 'YES') {
		$str = "SELECT /*+ nested_table_get_refs */ ";
	}

	if ($self->{is_mssql} && $self->{select_top}) {
		$str .= "TOP $self->{select_top} ";
	}

	my $reftable = $table;
	my $refcolumn_dst = '';
	my $refcolumn_src = '';
	my @lfkeys = ();
	my @rfkeys = ();
	if ($self->{partition_by_reference} eq 'duplicate' && exists $self->{partitions_list}{"\L$table\E"}{refrtable})
	{
		$reftable = $self->{partitions_list}{"\L$table\E"}{refrtable};
		my $fkname = $self->{partitions_list}{"\L$table\E"}{refconstraint};
		foreach my $desttable (sort keys %{$self->{tables}{$table}{foreign_link}{$fkname}{remote}})
		{
			next if ($desttable ne $reftable);

			# Foreign key constraint on partitionned table do not support
			# NO VALID when the remote table is not partitionned
			my $allow_fk_notvalid = 1;
			$allow_fk_notvalid = 0 if ($self->{pg_supports_partition} && exists $self->{partitions_list}{lc($table)});
			my $str = '';
			# Add double quote to column name
			map { $_ = '"' . $_ . '"' } @{$self->{tables}{$table}{foreign_link}{$fkname}{local}};
			map { $_ = '"' . $_ . '"' } @{$self->{tables}{$table}{foreign_link}{$fkname}{remote}{$desttable}};

			# Get the name of the foreign table after replacement if any
			my $subsdesttable = $self->get_replaced_tbname($desttable);
			# Prefix the table name with the schema name if owner of
			# remote table is not the same as local one
			if ($self->{schema} && (lc($state->[6]) ne lc($state->[8]))) {
				$subsdesttable =  $self->quote_object_name($state->[6]) . '.' . $subsdesttable;
			}

			push(@lfkeys, @{$self->{tables}{$table}{foreign_link}{$fkname}{local}});
			if (exists $self->{replaced_cols}{"\L$table\E"} && $self->{replaced_cols}{"\L$table\E"})
			{
				foreach my $c (keys %{$self->{replaced_cols}{"\L$table\E"}}) {
					map { s/"$c"/"$self->{replaced_cols}{"\L$table\E"}{"\L$c\E"}"/i } @lfkeys;
				}
			}
			push(@rfkeys, @{$self->{tables}{$table}{foreign_link}{$fkname}{remote}{$desttable}});
			if (exists $self->{replaced_cols}{"\L$desttable\E"} && $self->{replaced_cols}{"\L$desttable\E"})
			{
				foreach my $c (keys %{$self->{replaced_cols}{"\L$desttable\E"}}) {
					map { s/"$c"/"$self->{replaced_cols}{"\L$desttable\E"}{"\L$c\E"}"/i } @rfkeys;
				}
			}
		}

		$refcolumn_src = $self->{partitions_list}{"\L$table\E"}{refcolumn};
		foreach my $k (keys %{ $self->{tables}{"$reftable"}{column_info} })
		{
			next if (!grep(/^$k$/i, @{$self->{partitions_list}{"\L$reftable\E"}{columns}}));
			my $f = $self->{tables}{"$reftable"}{column_info}{$k};
			# Change column names
			my $fname = $f->[0];
			if (exists $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"} && $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"})
			{
				$self->logit("\tReplacing column \L$f->[0]\E as " . $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"} . "...\n", 1);
				$fname = $self->{replaced_cols}{"\L$reftable\E"}{"\L$fname\E"};
			}
			$refcolumn_dst = $fname;
		}
	}

	my $extraStr = "";
	# Lookup through columns information
	if ($#{$name} < 0)
	{
		# There a problem whe can't find any column in this table
		return '';
	}
	else
	{
		for my $k (0 .. $#{$name})
		{
			my $realcolname = $name->[$k];
			my $spatial_srid = '';
			$self->{nullable}{$table}{$k} = $self->{colinfo}->{$table}{$realcolname}{nullable};
			if ($self->{is_mysql})
			{
				if ($name->[$k] !~ /\`/) {
					$name->[$k] = '`' . $name->[$k] . '`';
				}
			}
			elsif ($self->{is_mssql})
			{
				#if ($name->[$k] !~ /\[/) {
					$name->[$k] = '[' . $name->[$k] . ']';
				#}
			}
			else
			{
				if ($name->[$k] !~ /"/) {
					$name->[$k] = '"' . $name->[$k] . '"';
				}
			}

			# If there is any transformation to apply replace the column name with the clause
			if (exists $self->{transform_value}{lc($table)} && exists $self->{transform_value}{lc($table)}{lc($realcolname)}) {
				$str .= $self->{transform_value}{lc($table)}{lc($realcolname)} . ",";
			}
			# Apply some default transformation following the data type
			elsif ( ( $src_type->[$k] =~ /^char/i) && ($type->[$k] =~ /(varchar|text)/i)) {
				my $colnm = $name->[$k];
				$colnm =~ s/^[^\.]+\.//;
				$str .= "trim($self->{trim_type} '$self->{trim_char}' FROM $alias.$name->[$k]) AS $colnm,";
			} elsif ($self->{is_mysql} && $src_type->[$k] =~ /bit/i) {
				$str .= "BIN($alias.$name->[$k]),";
			}
			# If dest type is bytea the content of the file is exported as bytea
			elsif ( ($src_type->[$k] =~ /bfile/i) && ($type->[$k] =~ /bytea/i) )
			{
				$self->{bfile_found} = 'bytea';
				$str .= "ora2pg_get_bfile($alias.$name->[$k]),";
			}
			# If dest type is efile the content of the file is exported to use the efile extension
			elsif ( ($src_type->[$k] =~ /bfile/i) && ($type->[$k] =~ /efile/i) )
			{
				$self->{bfile_found} = 'efile';
				$str .= "ora2pg_get_efile($alias.$name->[$k]),";
			}
			# Only extract path to the bfile if dest type is text.
			elsif ( ($src_type->[$k] =~ /bfile/i) && ($type->[$k] =~ /text/i) )
			{
				$self->{bfile_found} = 'text';
				$str .= "ora2pg_get_bfilename($alias.$name->[$k]),";
			}
			elsif ( $src_type->[$k] =~ /xmltype/i)
			{
				if ($self->{xml_pretty}) {
					$str .= "($alias.$name->[$k]).getStringVal(),";
				} else {
					$str .= "($alias.$name->[$k]).getClobVal(),";
				}
			}
			# ArcGis Geometries
			elsif ( !$self->{is_mysql} && $src_type->[$k] =~ /^(ST_|STGEOM_)/i)
			{
				if ($self->{geometry_extract_type} eq 'WKB') {
					$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN $self->{st_asbinary_function}($alias.$name->[$k]) ELSE NULL END,";
				} else {
					$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN $self->{st_astext_function}($alias.$name->[$k]) ELSE NULL END,";
				}
			}
			# Oracle geometries
			elsif ( !$self->{is_mysql} && $src_type->[$k] =~ /SDO_GEOMETRY/i)
			{

				# Set SQL query to get the SRID of the column
				if ($self->{convert_srid} > 1) {
					$spatial_srid = $self->{convert_srid};
				} else {
					$spatial_srid = $self->{colinfo}->{$table}{$realcolname}{spatial_srid};
				}

				# With INSERT statement we always use WKT
				if ($self->{type} eq 'INSERT')
				{
					if ($self->{geometry_extract_type} eq 'WKB') {
						$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN SDO_UTIL.TO_WKBGEOMETRY($alias.$name->[$k]) ELSE NULL END,";
					} elsif ($self->{geometry_extract_type} eq 'INTERNAL') {
						$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN $alias.$name->[$k] ELSE NULL END,";
					} else {
						$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN ST_GeomFromText(SDO_UTIL.TO_WKTGEOMETRY($alias.$name->[$k]), '$spatial_srid') ELSE NULL END,";
					}
				}
				else
				{
					if ($self->{geometry_extract_type} eq 'WKB') {
						$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN SDO_UTIL.TO_WKBGEOMETRY($alias.$name->[$k]) ELSE NULL END,";
					} elsif ($self->{geometry_extract_type} eq 'INTERNAL') {
						$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN $alias.$name->[$k] ELSE NULL END,";
					} else {
						$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN SDO_UTIL.TO_WKTGEOMETRY($alias.$name->[$k]) ELSE NULL END,";
					}
				}
			}
			# SQL Server geometry
 			elsif ( $self->{is_mssql} && $src_type->[$k] =~ /^GEOM(ETRY|GRAPHY)/i)
 			{
				if ($self->{geometry_extract_type} eq 'WKB') {
					$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN CONCAT('SRID=', $alias.$name->[$k].STSrid,';', $alias.$name->[$k].STAsText()) ELSE NULL END,";
				} else {
					$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN CONCAT('SRID=', $alias.$name->[$k].STSrid,';', $alias.$name->[$k].STAsText()) ELSE NULL END,";
				}
 			}
			# MySQL geometry
			elsif ( $self->{is_mysql} && $src_type->[$k] =~ /geometry/i && $self->{type} ne 'TEST_DATA')
			{
				if ($self->{db_version} < '5.7.6')
				{
					if ($self->{geometry_extract_type} eq 'WKB') {
						$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN CONCAT('SRID=',SRID($alias.$name->[$k]),';', AsBinary($alias.$name->[$k])) ELSE NULL END,";
					} else {
						$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN CONCAT('SRID=',SRID($alias.$name->[$k]),';', AsText($alias.$name->[$k])) ELSE NULL END,";
					}
				}
				else
				{
					if ($self->{geometry_extract_type} eq 'WKB') {
						$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN CONCAT('SRID=',$self->{st_srid_function}($alias.$name->[$k]),';', $self->{st_asbinary_function}($alias.$name->[$k])) ELSE NULL END,";
					} else {
						$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN CONCAT('SRID=',$self->{st_srid_function}($alias.$name->[$k]),';', $self->{st_astext_function}($alias.$name->[$k])) ELSE NULL END,";
					}
				}
			}
			# For data testing we retrieve geometry using ST_AsText/AsText
			elsif ( $self->{is_mysql} && $src_type->[$k] =~ /geometry/i && $self->{type} eq 'TEST_DATA')
			{
				if ($self->{db_version} < '5.7.6') {
					$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN AsText($alias.$name->[$k]) ELSE NULL END,";
				} else {
					$str .= "CASE WHEN $alias.$name->[$k] IS NOT NULL THEN $self->{st_astext_function}($alias.$name->[$k]) ELSE NULL END,";
				}
			}
			elsif ( !$self->{is_mysql} && (($src_type->[$k] =~ /clob/i) || ($src_type->[$k] =~ /blob/i)) )
			{
				if (!$self->{enable_blob_export} && $src_type->[$k] =~ /blob/i) {
					# user don't want to export blob
					next;
				}
				if (!$self->{enable_clob_export} && $src_type->[$k] =~ /clob/i) {
					# user don't want to export clob
					next;
				}
				if ($self->{empty_lob_null})
				{
					$str .= "CASE WHEN ";
					if (!$self->{has_dbms_log_execute_privilege})
					{
						if ($src_type->[$k] =~ /blob/i) {
							$str .= "LENGTHB";
						} else {
							$str .= "LENGTH";
						}
					} else {
						$str .= "dbms_lob.getlength";
					}
					$str .= "($alias.$name->[$k]) = 0 THEN NULL ELSE $alias.$name->[$k] END,";
				} else {
					$str .= "$alias.$name->[$k],";
				}
			}
			else
			{
				$str .= "$alias.$name->[$k],";
			}
			push(@{$self->{spatial_srid}{$table}}, $spatial_srid);
			
			if ( ($type->[$k] =~ /bytea/i && $self->{enable_blob_export}) ||
				($self->{clob_as_blob} && $src_type->[$k] =~ /CLOB/i) )
			{
				if ($self->{data_limit} >= 1000)
				{
					$self->{local_data_limit}{$table} = int($self->{data_limit}/10);
					while ($self->{local_data_limit}{$table} > 1000) {
						$self->{local_data_limit}{$table} = int($self->{local_data_limit}{$table}/10);
					}
				}
				else
				{
					$self->{local_data_limit}{$table} = $self->{data_limit};
				}
				$self->{local_data_limit}{$table} = $self->{blob_limit} if ($self->{blob_limit});
			}
		}
		$str =~ s/,$//;
	}

	# If we have a BFILE that might be exported as text we need to create a function
	my $bfile_function = '';
	if ($self->{bfile_found} eq 'text')
	{
		$self->logit("Creating function ora2pg_get_bfilename( p_bfile IN BFILE ) to retrieve path from BFILE.\n", 1);
		$bfile_function = qq{
CREATE OR REPLACE FUNCTION ora2pg_get_bfilename( p_bfile IN BFILE ) RETURN 
VARCHAR2
  AS
    l_dir   VARCHAR2(4000);
    l_fname VARCHAR2(4000);
    l_path  VARCHAR2(4000);
  BEGIN
    IF p_bfile IS NULL
    THEN RETURN NULL;
    ELSE
      dbms_lob.FILEGETNAME( p_bfile, l_dir, l_fname );
      SELECT DIRECTORY_PATH INTO l_path FROM $self->{prefix}_DIRECTORIES WHERE DIRECTORY_NAME = l_dir;
      l_dir := rtrim(l_path,'/');
      RETURN l_dir || '/' || replace(l_fname, '\\', '/');
  END IF;
  END;
};
	}
	# If we have a BFILE that might be exported as efile we need to create a function
	elsif ($self->{bfile_found} eq 'efile')
	{
		$self->logit("Creating function ora2pg_get_efile( p_bfile IN BFILE ) to retrieve EFILE from BFILE.\n", 1);
		my $quote = '';
		$quote = "''" if ($self->{type} eq 'INSERT');
		$bfile_function = qq{
CREATE OR REPLACE FUNCTION ora2pg_get_efile( p_bfile IN BFILE ) RETURN 
VARCHAR2
  AS
    l_dir   VARCHAR2(4000);
    l_fname VARCHAR2(4000);
  BEGIN
    IF p_bfile IS NULL THEN
      RETURN NULL;
    ELSE
      dbms_lob.FILEGETNAME( p_bfile, l_dir, l_fname );
      RETURN '($quote' || l_dir || '$quote,$quote' || replace(l_fname, '\\', '/') || '$quote)';
  END IF;
  END;
};
	}
	# If we have a BFILE that might be exported as bytea we need to create a
	# function that exports the bfile as a binary BLOB, a HEX encoded string
	elsif ($self->{bfile_found} eq 'bytea')
	{
		$self->logit("Creating function ora2pg_get_bfile( p_bfile IN BFILE ) to retrieve BFILE content as BLOB.\n", 1);
		$bfile_function = qq{
CREATE OR REPLACE FUNCTION ora2pg_get_bfile( p_bfile IN BFILE ) RETURN 
BLOB AS
        filecontent BLOB := NULL;
	src_file BFILE := NULL;
        l_step PLS_INTEGER := 12000;
	l_dir   VARCHAR2(4000);
	l_fname VARCHAR2(4000);
	offset NUMBER := 1;
BEGIN
    IF p_bfile IS NULL THEN
      RETURN NULL;
    END IF;

    DBMS_LOB.FILEGETNAME( p_bfile, l_dir, l_fname );
    src_file := BFILENAME( l_dir, l_fname );
    IF src_file IS NULL THEN
        RETURN NULL;
    END IF;

    DBMS_LOB.FILEOPEN(src_file, DBMS_LOB.FILE_READONLY);
    DBMS_LOB.CREATETEMPORARY(filecontent, true);
    DBMS_LOB.LOADBLOBFROMFILE (filecontent, src_file, DBMS_LOB.LOBMAXSIZE, offset, offset);
    DBMS_LOB.FILECLOSE(src_file);
    RETURN filecontent;
END;
};
	}

	if ($bfile_function)
	{
		my $local_dbh = $self->_db_connection();
		my $sth2 =  $local_dbh->do($bfile_function);
		$local_dbh->disconnect() if ($local_dbh);
	}

	# Fix empty column list with nested table
	if (!$self->{is_mysql}) {
		$str =~ s/ ""$/ \*/;
	} else {
		$str =~ s/ ``$/ \*/;
	}

	if ($part_name)
	{
		if ($is_subpart && !$self->{is_mysql}) {
			$realtable .= " SUBPARTITION(" . $self->quote_object_name($part_name) . ")";
		} else {
			$realtable .= " PARTITION(" . $self->quote_object_name($part_name) . ")";
		}
	}
	# Force parallelism on Oracle side
	if ($self->{default_parallelism_degree} > 1 && $self->{type} ne 'TEST_DATA')
	{
		# Only if the number of rows is upper than PARALLEL_MIN_ROWS
		$self->{tables}{$table}{table_info}{num_rows} ||= 0;
		if ($self->{tables}{$table}{table_info}{num_rows} > $self->{parallel_min_rows}) {
			$str =~ s#^SELECT #SELECT /*+ FULL(a) PARALLEL(a, $self->{default_parallelism_degree}) */ #;
		}
	}

	$str .= " FROM $realtable";
	if ($self->{start_scn} =~ /^\d+$/) {
		$str .= " AS OF SCN $self->{start_scn}";
	} elsif ($self->{start_scn}) {
		$str .= " AS OF TIMESTAMP $self->{start_scn}";
	} elsif (exists $self->{current_oracle_scn}{$table}) {
		$str .= " AS OF SCN $self->{current_oracle_scn}{$table}";
	}
	$str .= " $alias";
	if ($refcolumn_dst)
	{
		$str .= " JOIN $reftable";
		if ($self->{start_scn} =~ /^\d+$/) {
			$str .= " AS OF SCN $self->{start_scn}";
		} elsif ($self->{start_scn}) {
			$str .= " AS OF TIMESTAMP $self->{start_scn}";
		} elsif (exists $self->{current_oracle_scn}{$table}) {
			$str .= " AS OF SCN $self->{current_oracle_scn}{$table}";
		}
		# The partition by reference column, doesn't exist in the Oracle child table. Use the origin.
		$str =~ s/,$alias\."$refcolumn_dst"/,reftb."$refcolumn_dst"/i;
		$str .= " reftb ON (";
		map { s/^(.*)$/$alias\.$1/; } @lfkeys;
		map { s/^(.*)$/reftb\.$1/; } @rfkeys;
		for (my $k = 0; $k <= $#lfkeys; $k++) {
			$lfkeys[$k] =~ s/["]+/"/g;
			$rfkeys[$k] =~ s/["]+/"/g;
			$str .= "$lfkeys[$k] = $rfkeys[$k] AND";
		}
		$str =~ s/ AND$/)/;
	}

	if (exists $self->{where}{"\L$table\E"} && $self->{where}{"\L$table\E"})
	{
		($str =~ / WHERE /) ? $str .= ' AND ' : $str .= ' WHERE ';
		if (!$self->{is_mysql} || ($self->{where}{"\L$table\E"} !~ /\s+LIMIT\s+\d/)) {
			$str .= '(' . $self->{where}{"\L$table\E"} . ')';
		} else {
			$str .= $self->{where}{"\L$table\E"};
		}
		$self->logit("\tApplying WHERE clause on table: " . $self->{where}{"\L$table\E"} . "\n", 1);
	}
	elsif ($self->{global_where})
	{
		($str =~ / WHERE /) ? $str .= ' AND ' : $str .= ' WHERE ';
		if (!$self->{is_mysql} || ($self->{global_where} !~ /\s+LIMIT\s+\d/)) {
			$str .= '(' . $self->{global_where} . ')';
		} else {
			$str .= $self->{global_where};
		}
		$self->logit("\tApplying WHERE global clause: " . $self->{global_where} . "\n", 1);
	}

	# Automatically set the column on which query will be splitted
	# to the first column with a unique key and of type NUMBER.
	if ($self->{oracle_copies} > 1 && $self->{type} ne 'TEST_DATA')
	{
		if (!exists $self->{defined_pk}{"\L$table\E"})
		{
			foreach my $consname (keys %{$self->{tables}{$table}{unique_key}})
			{
				my $constype =   $self->{tables}{$table}{unique_key}->{$consname}{type};
				if (($constype eq 'P') || ($constype eq 'U'))
				{
					foreach my $c (@{$self->{tables}{$table}{unique_key}->{$consname}{columns}})
					{
					       for my $k (0 .. $#{$name})
					       {
							my $realcolname = $name->[$k]->[0];
							$realcolname =~ s/"//g;
							if ($c eq $realcolname)
							{
								if ($src_type->[$k] =~ /^number\(.*,.*\)/i)
								{
									$self->{defined_pk}{"\L$table\E"} = "ROUND($c)";
									last;
								}
								elsif ($src_type->[$k] =~ /^number/i)
								{
									$self->{defined_pk}{"\L$table\E"} = $c;
									last;
								}
							}
						}
						last if (exists $self->{defined_pk}{"\L$table\E"});
					}
				}
				last if (exists $self->{defined_pk}{"\L$table\E"});
			}
		}
		if ($self->{defined_pk}{"\L$table\E"})
		{
			my $colpk = $self->{defined_pk}{"\L$table\E"};
			if ($self->{preserve_case}) {
				$colpk = '"' . $colpk . '"';
			}
			if ($str =~ / WHERE /) {
				$str .= " AND";
			} else {
				$str .= " WHERE";
			}
			if ($self->{is_mssql}) {
				$str .= " ABS($colpk % $self->{oracle_copies}) = ?";
			} else {
				$str .= " ABS(MOD($colpk, $self->{oracle_copies})) = ?";
			}
		}
	}

	$self->logit("DEGUG: Query sent to $self->{sgbd_name}: $str\n", 1);

	return $str;
}

=head2 _howto_get_fdw_data TABLE

This function implements an Oracle data extraction through oracle_fdw.
Returns the SQL query to use to retrieve data

=cut

sub _howto_get_fdw_data
{
	my ($self, $table, $name, $type, $src_type, $part_name, $is_subpart) = @_;

	####
	# Overwrite the query if REPLACE_QUERY is defined for this table
	####
	if ($self->{replace_query}{"\L$table\E"})
	{
		$str = $self->{replace_query}{"\L$table\E"};
		$self->logit("DEGUG: Query sent to $self->{sgbd_name}: $str\n", 1);
		return $str;
	}

	# Fix a problem when the table need to be prefixed by the schema
	my $realtable = $table;
	$realtable =~ s/\"//g;
	# Do not use double quote with mysql, but backquote
	if (!$self->{is_mysql})
	{
		if (!$self->{schema} && $self->{export_schema})
		{
			$realtable =~ s/\./"."/;
			$realtable = "\"$realtable\"";
		}
		else
		{
			$realtable = "\"$realtable\"";
			my $owner  = $self->{tables}{$table}{table_info}{owner} || $self->{tables}{$table}{owner} || '';
			if ($owner)
			{
				$owner =~ s/\"//g;
				$owner = "\"$owner\"";
				$realtable = "$owner.$realtable";
			}
		}
	}
	else
	{
		$realtable = "\`$realtable\`";
	}

	delete $self->{nullable}{$table};

	my $alias = 'a';
	my $str = "SELECT ";

	my $extraStr = "";
	# Lookup through columns information
	if ($#{$name} < 0)
	{
		# There a problem whe can't find any column in this table
		return '';
	}
	else
	{
		for my $k (0 .. $#{$name})
		{
			my $realcolname = $name->[$k]->[0];
			my $spatial_srid = '';
			$self->{nullable}{$table}{$k} = $self->{colinfo}->{$table}{$realcolname}{nullable};
			if ($name->[$k]->[0] !~ /"/)
			{
				# Do not use double quote with mysql
				if (!$self->{is_mysql}) {
					$name->[$k]->[0] = '"' . $name->[$k]->[0] . '"';
				} else {
					$name->[$k]->[0] = '`' . $name->[$k]->[0] . '`';
				}
			}

			# If dest type is bytea the content of the file is exported as bytea
			if ( ($src_type->[$k] =~ /bfile/i) && ($type->[$k] =~ /bytea/i) )
			{
				$self->{bfile_found} = 'bytea';
			}
			# If dest type is efile the content of the file is exported to use the efile extension
			elsif ( ($src_type->[$k] =~ /bfile/i) && ($type->[$k] =~ /efile/i) )
			{
				$self->{bfile_found} = 'efile';
				$self->logit("FATAL: with oracle_fdw data export, BFILE can only be converted to bytea\n", 0, 1);
			}
			# Only extract path to the bfile if dest type is text.
			elsif ( ($src_type->[$k] =~ /bfile/i) && ($type->[$k] =~ /text/i) )
			{
				$self->{bfile_found} = 'text';
				$self->logit("FATAL: with oracle_fdw data export, BFILE can only be converted to bytea\n", 0, 1);
			}
			$str .= "$name->[$k]->[0],";

			push(@{$self->{spatial_srid}{$table}}, $spatial_srid);

			# Wit oracle_fdw export we migrate data in stream not in chunk
			$self->{data_limit} = 0;
			$self->{local_data_limit}{$table} = 0;
			$self->{blob_limit} = 0;
		}
		$str =~ s/,$//;
	}
	$str .= " FROM $realtable $alias";

	if (exists $self->{where}{"\L$table\E"} && $self->{where}{"\L$table\E"})
	{
		($str =~ / WHERE /) ? $str .= ' AND ' : $str .= ' WHERE ';
		if (!$self->{is_mysql} || ($self->{where}{"\L$table\E"} !~ /\s+LIMIT\s+\d/)) {
			$str .= '(' . $self->{where}{"\L$table\E"} . ')';
		} else {
			$str .= $self->{where}{"\L$table\E"};
		}
		$self->logit("\tApplying WHERE clause on table: " . $self->{where}{"\L$table\E"} . "\n", 1);
	}
	elsif ($self->{global_where})
	{
		($str =~ / WHERE /) ? $str .= ' AND ' : $str .= ' WHERE ';
		if (!$self->{is_mysql} || ($self->{global_where} !~ /\s+LIMIT\s+\d/)) {
			$str .= '(' . $self->{global_where} . ')';
		} else {
			$str .= $self->{global_where};
		}
		$self->logit("\tApplying WHERE global clause: " . $self->{global_where} . "\n", 1);
	}

	# Automatically set the column on which query will be splitted
	# to the first column with a unique key and of type NUMBER.
	if ($self->{oracle_copies} > 1)
	{
		if (!exists $self->{defined_pk}{"\L$table\E"})
		{
			foreach my $consname (keys %{$self->{tables}{$table}{unique_key}})
			{
				my $constype =   $self->{tables}{$table}{unique_key}->{$consname}{type};
				if (($constype eq 'P') || ($constype eq 'U'))
				{
					foreach my $c (@{$self->{tables}{$table}{unique_key}->{$consname}{columns}})
					{
					       for my $k (0 .. $#{$name})
					       {
							my $realcolname = $name->[$k]->[0];
							$realcolname =~ s/"//g;
							if ($c eq $realcolname)
							{
								if ($src_type->[$k] =~ /^number\(.*,.*\)/i)
								{
									$self->{defined_pk}{"\L$table\E"} = "ROUND($c)";
									last;
								}
								elsif ($src_type->[$k] =~ /^number/i)
								{
									$self->{defined_pk}{"\L$table\E"} = $c;
									last;
								}
							}
						}
						last if (exists $self->{defined_pk}{"\L$table\E"});
					}
				}
				last if (exists $self->{defined_pk}{"\L$table\E"});
			}
		}
		if ($self->{defined_pk}{"\L$table\E"})
		{
			my $colpk = $self->{defined_pk}{"\L$table\E"};
			if ($self->{preserve_case}) {
				$colpk = '"' . $colpk . '"';
			}
			if ($str =~ / WHERE /) {
				$str .= " AND";
			} else {
				$str .= " WHERE";
			}
			if ($self->{is_mssql}) {
				$str .= " ABS($colpk % $self->{oracle_copies}) = ?";
			} else {
				$str .= " ABS(MOD($colpk, $self->{oracle_copies})) = ?";
			}
		}
	}

	$self->logit("DEGUG: Query sent to $self->{sgbd_name}: $str\n", 1);

	return $str;
}

=head2 _sql_type INTERNAL_TYPE LENGTH PRECISION SCALE

This function returns the PostgreSQL data type corresponding to the
Oracle data type.

=cut

sub _sql_type
{
        my ($self, $type, $len, $precision, $scale, $default, $no_blob_to_oid) = @_;

	$type = uc($type); # Force uppercase

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_sql_type($self, $type, $len, $precision, $scale);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_sql_type($self, $type, $len, $precision, $scale);
	} else {
		return Ora2Pg::Oracle::_sql_type($self, $type, $len, $precision, $scale);
	}
}

=head2 _column_info TABLE OWNER

This function implements an Oracle-native column information.

Returns a list of array references containing the following information
elements for each column the specified table

[(
  column name,
  column type,
  column length,
  nullable column,
  default value
  ...
)]

=cut

sub _column_info
{
	my ($self, $table, $owner, $objtype, @expanded_views) = @_;

	$objtype ||= 'TABLE';

	$self->logit("Collecting column information for \L$objtype\E $table...\n", 1);

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_column_info($self,$table,$owner,$objtype,0,@expanded_views);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_column_info($self,$table,$owner,$objtype,0,@expanded_views);
	} else {
		return Ora2Pg::Oracle::_column_info($self,$table,$owner,$objtype,0,@expanded_views);
	}
}

sub _column_attributes
{
	my ($self, $table, $owner, $objtype) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_column_attributes($self,$table,$owner,'TABLE');
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_column_attributes($self,$table,$owner,'TABLE');
	} else {
		return Ora2Pg::Oracle::_column_attributes($self,$table,$owner,'TABLE');
	}
}

sub _encrypted_columns
{
	my ($self, $table, $owner) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_encrypted_columns($self,$table,$owner);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_encrypted_columns($self,$table,$owner);
	} else {
		return Ora2Pg::Oracle::_encrypted_columns($self,$table,$owner);
	}
}



=head2 _unique_key TABLE OWNER

This function implements an Oracle-native unique (including primary)
key column information.

Returns a hash of hashes in the following form:
    ( owner => table => constraintname => (type => 'PRIMARY',
                         columns => ('a', 'b', 'c')),
      owner => table => constraintname => (type => 'UNIQUE',
                         columns => ('b', 'c', 'd')),
      etc.
    )

=cut

sub _unique_key
{
	my ($self, $table, $owner, $type) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_unique_key($self,$table,$owner, $type);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_unique_key($self,$table,$owner, $type);
	} else {
		return Ora2Pg::Oracle::_unique_key($self,$table,$owner, $type);
	}
}

=head2 _check_constraint TABLE OWNER

This function implements an Oracle-native check constraint
information.

Returns a hash of lists of all column names defined as check constraints
for the specified table and constraint name.

=cut

sub _check_constraint
{
	my ($self, $table, $owner) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_check_constraint($self, $table, $owner);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_check_constraint($self, $table, $owner);
	} else {
		return Ora2Pg::Oracle::_check_constraint($self, $table, $owner);
	}
}

=head2 _foreign_key TABLE OWNER

This function implements an Oracle-native foreign key reference
information.

Returns a list of hash of hash of array references. Ouf! Nothing very difficult.
The first hash is composed of all foreign key names. The second hash has just
two keys known as 'local' and 'remote' corresponding to the local table where
the foreign key is defined and the remote table referenced by the key.

The foreign key name is composed as follows:

    'local_table_name->remote_table_name'

Foreign key data consists in two arrays representing at the same index for the
local field and the remote field where the first one refers to the second one.
Just like this:

    @{$link{$fkey_name}{local}} = @local_columns;
    @{$link{$fkey_name}{remote}} = @remote_columns;

=cut

sub _foreign_key
{
	my ($self, $table, $owner) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_foreign_key($self,$table,$owner);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_foreign_key($self,$table,$owner);
	} else {
		return Ora2Pg::Oracle::_foreign_key($self,$table,$owner);
	}
}


=head2 _get_privilege

This function implements an Oracle-native object priviledge information.

Returns a hash of all priviledge.

=cut

sub _get_privilege
{
	my($self) = @_;

	# If the user is given as not DBA, do not look at tablespace
	if ($self->{user_grants}) {
		$self->logit("WARNING: Exporting privilege as non DBA user is not allowed, see USER_GRANT\n", 0);
		return;
	}

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_privilege($self);
	} else {
		return Ora2Pg::Oracle::_get_privilege($self);
	}
}

=head2 _get_security_definer

This function implements an Oracle-native functions security definer / current_user information.

Returns a hash of all object_type/function/security.

=cut

sub _get_security_definer
{
	my ($self, $type) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_security_definer($self, $type);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_security_definer($self, $type);
	} else {
		return Ora2Pg::Oracle::_get_security_definer($self, $type);
	}
}


=head2 _get_indexes TABLE OWNER

This function implements an Oracle-native indexes information.

Returns a hash of an array containing all unique indexes and a hash of
array of all indexe names which are not primary keys for the specified table.

=cut

sub _get_indexes
{
	my ($self, $table, $owner, $generated_indexes) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_indexes($self,$table,$owner, $generated_indexes);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_indexes($self,$table,$owner, $generated_indexes);
	} else {
		return Ora2Pg::Oracle::_get_indexes($self,$table,$owner, $generated_indexes);
	}
}

=head2 _get_sequences

This function implements an Oracle-native sequences information.

Returns a hash of an array of sequence names with MIN_VALUE, MAX_VALUE,
INCREMENT and LAST_NUMBER for the specified table.

=cut

sub _get_sequences
{
	my ($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_sequences($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_sequences($self);
	} else {
		return Ora2Pg::Oracle::_get_sequences($self);
	}
}

=head2 _get_identities

This function retrieve information about IDENTITY columns that must be
exported as PostgreSQL serial.

=cut

sub _get_identities
{
	my ($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_identities($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_identities($self);
	} else {
		return Ora2Pg::Oracle::_get_identities($self);
	}
}

=head2 _get_external_tables

This function implements an Oracle-native external tables information.

Returns a hash of external tables names with the file they are based on.

=cut

sub _get_external_tables
{
	my ($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_external_tables($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_external_tables($self);
	} else {
		return Ora2Pg::Oracle::_get_external_tables($self);
	}
}

=head2 _get_directory

This function implements an Oracle-native directory information.

Returns a hash of directory names with the path they are based on.

=cut

sub _get_directory
{
	my ($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_directory($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_directory($self);
	} else {
		return Ora2Pg::Oracle::_get_directory($self);
	}
}

=head2 _get_dblink

This function implements an Oracle-native database link information.

Returns a hash of dblink names with the connection they are based on.

=cut


sub _get_dblink
{
	my($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_dblink($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_dblink($self);
	} else {
		return Ora2Pg::Oracle::_get_dblink($self);
	}
}

=head2 _get_job

This function implements an Oracle-native job information.

Reads together from view [ALL|DBA]_JOBS and from view [ALL|DBA]_SCHEDULER_JOBS.

Returns a hash of job number with the connection they are based on.

=cut


sub _get_job
{
	my ($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_job($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_job($self);
	} else {
		return Ora2Pg::Oracle::_get_job($self);
	}
}


=head2 _get_views

This function implements an Oracle-native views information.

Returns a hash of view names with the SQL queries they are based on.

=cut

sub _get_views
{
	my($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_views($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_views($self);
	} else {
		return Ora2Pg::Oracle::_get_views($self);
	}

}

=head2 _get_materialized_views

This function implements an Oracle-native materialized views information.

Returns a hash of view names with the SQL queries they are based on.

=cut

sub _get_materialized_views
{
	my($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_materialized_views($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_materialized_views($self);
	} else {
		return Ora2Pg::Oracle::_get_materialized_views($self);
	}
}

sub _get_materialized_view_names
{
	my($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_materialized_view_names($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_materialized_view_names($self);
	} else {
		return Ora2Pg::Oracle::_get_materialized_view_names($self);
	}
}

=head2 _get_triggers

This function implements an Oracle-native triggers information. 

Returns an array of refarray of all triggers information.

=cut

sub _get_triggers
{
	my($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_triggers($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_triggers($self);
	} else {
		return Ora2Pg::Oracle::_get_triggers($self);
	}
}

sub _list_triggers
{
	my($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_list_triggers($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_list_triggers($self);
	} else {
		return Ora2Pg::Oracle::_list_triggers($self);
	}
}

=head2 _get_plsql_metadata

This function retrieve all metadata on Oracle store procedure.

Returns a hash of all function names with their metadata
information (arguments, return type, etc.).

=cut

sub _get_plsql_metadata
{
	my $self = shift;
	my $owner = shift;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_plsql_metadata($self, $owner);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_plsql_metadata($self, $owner);
	} else {
		return Ora2Pg::Oracle::_get_plsql_metadata($self, $owner);
	}
}


=head2 _get_package_function_list

This function retrieve all function and procedure
defined on Oracle store procedure PACKAGE.

Returns a hash of all package function names

=cut

sub _get_package_function_list
{
	my $self = shift;
	my $owner = shift;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_package_function_list($self, $owner);
	} else {
		return Ora2Pg::Oracle::_get_package_function_list($self, $owner);
	}
}

=head2 _get_functions

This function implements an Oracle-native functions information.

Returns a hash of all function names with their PLSQL code.

=cut

sub _get_functions
{
	my $self = shift;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_functions($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_functions($self);
	} else {
		return Ora2Pg::Oracle::_get_functions($self);
	}
}

=head2 _get_procedures

This procedure implements an Oracle-native procedures information.

Returns a hash of all procedure names with their PLSQL code.

=cut

sub _get_procedures
{
	my $self = shift;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_procedures($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_procedures($self);
	} else {
		return Ora2Pg::Oracle::_get_procedures($self);
	}
}

=head2 _get_packages

This function implements an Oracle-native packages information.

Returns a hash of all package names with their PLSQL code.

=cut

sub _get_packages
{
	my ($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_packages($self);
	} else {
		return Ora2Pg::Oracle::_get_packages($self);
	}
}

=head2 _get_types

This function implements an Oracle custom types information.

Returns a hash of all type names with their code.

=cut

sub _get_types
{
	my ($self, $name) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_types($self, $name);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_types($self, $name);
	} else {
		return Ora2Pg::Oracle::_get_types($self, $name);
	}
}

=head2 _count_source_rows

This function retrieves real rows count from a table.

=cut

sub _count_source_rows
{
	my ($self, $dbhsrc, $t) = @_;

	$self->logit("DEBUG: pid $$ looking for real row count for source table $t...\n", 1);
	my $tbname = $t;
	if ($self->{is_mysql}) {
		$tbname = "`$t`";
	} elsif ($self->{is_mssql}) {
		$tbname = "[$t]";
		$tbname =~ s/\./\].\[/;
	} else {
		$tbname = qq{"$t"};
		$tbname = qq{"$self->{schema}"} . '.' . qq{"$t"} if ($self->{schema} && $t !~ /\./);
	}
	my $sql = "SELECT COUNT(*) FROM $tbname";
	my $sth = $dbhsrc->prepare( $sql ) or $self->logit("FATAL: " . $dbhsrc->errstr . "\n", 0, 1);
	$sth->execute or $self->logit("FATAL: " . $dbhsrc->errstr . "\n", 0, 1);
	my $size = $sth->fetch();
	$sth->finish();

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	my $fh = new IO::File;
	$fh->open(">>${dirprefix}ora2pg_count_rows") or $self->logit("FATAL: can't write to ${dirprefix}ora2pg_count_rows, $!\n", 0, 1);
	flock($fh, 2) || die "FATAL: can't lock file ${dirprefix}ora2pg_count_rows\n";
	$fh->print("$tbname:$size->[0]\n");
	$fh->close;
}


=head2 _table_info

This function retrieves all Oracle-native tables information.

Returns a handle to a DB query statement.

=cut

sub _table_info
{
	my $self = shift;
	my $do_real_row_count = shift;

	my %tables_infos = ();

	if ($self->{is_mysql}) {
		%tables_infos = Ora2Pg::MySQL::_table_info($self);
	} elsif ($self->{is_mssql}) {
		%tables_infos = Ora2Pg::MSSQL::_table_info($self);
	} else {
		%tables_infos = Ora2Pg::Oracle::_table_info($self);
	}

	# Collect real row count for each table
	if ($do_real_row_count)
	{
		my $t1 = Benchmark->new;

		my $parallel_tables_count = 0;
		my $dirprefix = '';
		$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
		unlink("${dirprefix}ora2pg_count_rows");
		foreach my $t (sort keys %tables_infos)
		{
			if ($self->{parallel_tables} > 1)
			{
				spawn sub {
					my $dbhsrc = $self->{dbh}->clone();
					$self->_count_source_rows($dbhsrc, $t);
					$dbhsrc->disconnect();
				};
				$parallel_tables_count++;

				# Wait for oracle connection terminaison
				while ($parallel_tables_count > $self->{parallel_tables})
				{
					my $kid = waitpid(-1, WNOHANG);
					if ($kid > 0)
					{
						$parallel_tables_count--;
						delete $RUNNING_PIDS{$kid};
					}
					usleep(50000);
				}
			}
			else
			{
				$self->_count_source_rows($self->{dbh}, $t);
			}
		}

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

		my $fh = new IO::File;
		$fh->open("${dirprefix}ora2pg_count_rows") or $self->logit("FATAL: can't read file ${dirprefix}ora2pg_count_rows, $!\n", 0, 1);
		my @ret = <$fh>;
		$fh->close;
		unlink("${dirprefix}ora2pg_count_rows");
		foreach my $s (@ret)
		{
			my ($tb, $cnt) = split(':', $s);
			chomp $cnt;
			$tb =~ s/"//g;
			my ($ora_owner, $ora_table) = split('\.', $tb);
			if ($tables_infos{$ora_table}{owner} eq $ora_owner) {
				$tables_infos{$ora_table}{num_rows} = $cnt || 0;
			}
		}

		my $t2 = Benchmark->new;
		$td = timediff($t2, $t1);
		$self->logit("Collecting tables real row count took: " . timestr($td) . "\n", 1);
	}

	return %tables_infos;
}

=head2 _global_temp_table_info

This function retrieves all Oracle-native global temporary tables information.

Returns a handle to a DB query statement.

=cut

sub _global_temp_table_info
{
	my $self = shift;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_global_temp_table_info($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_global_temp_table_info($self);
	} else {
		return Ora2Pg::Oracle::_global_temp_table_info($self);
	}
}


=head2 _queries

This function is used to retrieve all Oracle queries from DBA_AUDIT_TRAIL

Sets the main hash $self->{queries}.

=cut

sub _queries
{
	my ($self) = @_;

	$self->logit("Retrieving audit queries information...\n", 1);
	%{$self->{queries}} = $self->_get_audit_queries();

}


=head2 _get_audit_queries

This function extract SQL queries from dba_audit_trail 

Returns a hash of queries.

=cut

sub _get_audit_queries
{
	my($self) = @_;

	return if (!$self->{audit_user});

	# If the user is given as not DBA, do not look at tablespace
	if ($self->{user_grants}) {
		$self->logit("WARNING: Exporting audited queries as non DBA user is not allowed, see USER_GRANT\n", 0);
		return;
	}

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_audit_queries($self);
	} else {
		return Ora2Pg::Oracle::_get_audit_queries($self);
	}
}


=head2 _get_tablespaces

This function implements an Oracle-native tablespaces information.

Returns a hash of an array of tablespace names with their system file path.

=cut

sub _get_tablespaces
{
	my($self) = @_;

	# If the user is given as not DBA, do not look at tablespace
	if ($self->{user_grants}) {
		$self->logit("WARNING: Exporting tablespace as non DBA user is not allowed, see USER_GRANT\n", 0);
		return;
	}

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_tablespaces($self);
	} else {
		return Ora2Pg::Oracle::_get_tablespaces($self);
	}
}

sub _list_tablespaces
{
	my($self) = @_;

	# If the user is given as not DBA, do not look at tablespace
	if ($self->{user_grants}) {
		return;
	}

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_list_tablespaces($self);
	} else {
		return Ora2Pg::Oracle::_list_tablespaces($self);
	}
}


=head2 _get_partitions

This function implements an Oracle-native partitions information.
Return two hash ref with partition details and partition default.
=cut

sub _get_partitions
{
	my($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_partitions($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_partitions($self);
	} else {
		return Ora2Pg::Oracle::_get_partitions($self);
	}
}

=head2 _get_subpartitions

This function implements an Oracle-native subpartitions information.
Return two hash ref with partition details and partition default.

=cut

sub _get_subpartitions
{
	my($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_subpartitions($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_subpartitions($self);
	} else {
		return Ora2Pg::Oracle::_get_subpartitions($self);
	}
}


=head2 _synonyms

This function is used to retrieve all synonyms information.

Sets the main hash of the synonyms definition $self->{synonyms}.
Keys are the names of all synonyms retrieved from the current
database.

The synonyms hash is construct as follows:

	$hash{SYNONYM_NAME}{owner} = Owner of the synonym
	$hash{SYNONYM_NAME}{table_owner} = Owner of the object referenced by the synonym. 
	$hash{SYNONYM_NAME}{table_name} = Name of the object referenced by the synonym. 
	$hash{SYNONYM_NAME}{dblink} = Name of the database link referenced, if any

=cut

sub _synonyms
{
	my ($self) = @_;

	# Get all synonyms information
	$self->logit("Retrieving synonyms information...\n", 1);
	%{$self->{synonyms}} = $self->_get_synonyms();
}

=head2 _get_synonyms

This function implements an Oracle-native synonym information.

=cut

sub _get_synonyms
{
	my($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_synonyms($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_synonyms($self);
	} else {
		return Ora2Pg::Oracle::_get_synonyms($self);
	}
}

=head2 _get_partitions_list

This function implements an Oracle-native partitions information.
Return a hash of the partition table_name => type
=cut

sub _get_partitions_list
{
	my($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_partitions_list($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_partitions_list($self);
	} else {
		return Ora2Pg::Oracle::_get_partitions_list($self);
	}
}

=head2 _get_partitioned_table

Return a hash of the partitioned table list with the number of partition.

=cut

sub _get_partitioned_table
{
	my ($self, %subpart) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_partitioned_table($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_partitioned_table($self);
	} else {
		return Ora2Pg::Oracle::_get_partitioned_table($self);
	}
}

=head2 _get_subpartitioned_table

Return a hash of the partitioned table list with the number of partition.

=cut

sub _get_subpartitioned_table
{
	my($self) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_subpartitioned_table($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_subpartitioned_table($self);
	} else {
		return Ora2Pg::Oracle::_get_subpartitioned_table($self);
	}
}

sub _get_custom_types
{
        my ($self, $str, $parent) = @_;

	# Copy the type translation hash
	my %all_types = undef;
	if ($self->{is_mysql}) {
		%all_types = %Ora2Pg::MySQL::SQL_TYPE;
	} elsif ($self->{is_mssql}) {
		%all_types = %Ora2Pg::MSSQL::SQL_TYPE;
	} else {
		%all_types = %Ora2Pg::Oracle::SQL_TYPE;
	}
	# replace type double precision by single word double
	$all_types{'DOUBLE'} = $all_types{'DOUBLE PRECISION'};
	delete $all_types{'DOUBLE PRECISION'};
	# Remove any parenthesis after a type
	foreach my $t (keys %all_types) {
		$str =~ s/$t\s*\([^\)]+\)/$t/igs;
	}
	$str =~ s/^[^\(]+\(//s;
	$str =~ s/\s*\)\s*;$//s;
	$str =~ s/\/\*(.*?)\*\///gs;
	$str =~ s/\s*--[^\r\n]+//gs;
	my %types_found = ();
	my @type_def = split(/\s*,\s*/, $str);
	foreach my $s (@type_def)
	{
		my $cur_type = '';
		if ($s =~ /\s+OF\s+([^\s;]+)/) {
			$cur_type = $1;
		} elsif ($s =~ /\s+FROM\s+([^\s;]+)/) {
			$cur_type = uc($1);
		} elsif ($s =~ /^\s*([^\s]+)\s+([^\s]+)/) {
			$cur_type = $2;
		}
		push(@{$types_found{src_types}}, $cur_type);
		if (exists $all_types{$cur_type}) {
			push(@{$types_found{pg_types}}, $all_types{$cur_type});
		}
		else
		{
			my $custom_type = $self->_get_types($cur_type);
			foreach my $tpe (sort {length($a->{name}) <=> length($b->{name}) } @{$custom_type})
			{
				last if (uc($tpe->{name}) eq $cur_type); # prevent infinit loop
				$self->logit("\tLooking inside nested custom type $tpe->{name} to extract values...\n", 1);
				my %types_def = $self->_get_custom_types($tpe->{code}, $cur_type);
				if ($#{$types_def{pg_types}} >= 0)
				{
					$self->logit("\t\tfound subtype description: $tpe->{name}(" . join(',', @{$types_def{pg_types}}) . ")\n", 1);
					push(@{$types_found{pg_types}}, \@{$types_def{pg_types}});
					push(@{$types_found{src_types}}, \@{$types_def{src_types}});
				}
			}
		}
	}

        return %types_found;
}

sub format_data_row
{
	my ($self, $row, $data_types, $action, $src_data_types, $custom_types, $table, $colcond, $sprep) = @_;

	@{ $self->{tables}{$table}{pk_where_clause} } = ();
	@{ $self->{tables}{$table}{lo_import_id} } = ();

	my $has_geom = 0;
	$has_geom = 1 if (grep(/^(SDO_GEOMETRY|ST_|STGEOM_)/, @$src_data_types));
	for (my $idx = 0; $idx <= $#{$data_types}; $idx++)
	{
		my $data_type = $data_types->[$idx] || '';
		if ($has_geom && $row->[$idx] && $src_data_types->[$idx] =~ /(SDO_GEOMETRY|ST_|STGEOM_)/i)
		{
			if ($self->{type} ne 'INSERT')
			{
				if (!$self->{is_mysql} && ($self->{geometry_extract_type} eq 'INTERNAL'))
				{
					use Ora2Pg::GEOM;
					my $geom_obj = new Ora2Pg::GEOM('srid' => $self->{spatial_srid}{$table}->[$idx]);
					$geom_obj->{geometry}{srid} = '';
					$row->[$idx] = $geom_obj->parse_sdo_geometry($row->[$idx]) if ($row->[$idx] =~ /^ARRAY\(0x/);
					$row->[$idx] = 'SRID=' . $self->{spatial_srid}{$table}->[$idx] . ';' . $row->[$idx];
				}
				elsif ($self->{geometry_extract_type} eq 'WKB')
				{
					if ($self->{is_mysql}) {
						$row->[$idx] =~ s/^SRID=(\d+);//;
						$self->{spatial_srid}{$table}->[$idx] = $1;
					}
					$row->[$idx] = unpack('H*', $row->[$idx]);
					$row->[$idx]  = 'SRID=' . $self->{spatial_srid}{$table}->[$idx] . ';' . $row->[$idx];
				}
			}
			elsif ($self->{geometry_extract_type} eq 'WKB')
			{
				if ($self->{is_mysql})
				{
					$row->[$idx] =~ s/^SRID=(\d+);//;
					$self->{spatial_srid}{$table}->[$idx] = $1;
				}
				$row->[$idx] = unpack('H*', $row->[$idx]);
				$row->[$idx]  = "'SRID=" . $self->{spatial_srid}{$table}->[$idx] . ';' . $row->[$idx] . "'";
			}
			elsif (($self->{geometry_extract_type} eq 'INTERNAL') || ($self->{geometry_extract_type} eq 'WKT'))
			{
				if (!$self->{is_mysql})
				{
					if ($src_data_types->[$idx] =~ /SDO_GEOMETRY/i)
					{
						use Ora2Pg::GEOM;
						my $geom_obj = new Ora2Pg::GEOM('srid' => $self->{spatial_srid}{$table}->[$idx]);
						$geom_obj->{geometry}{srid} = '';
						$row->[$idx] = $geom_obj->parse_sdo_geometry($row->[$idx]) if ($row->[$idx] =~ /^ARRAY\(0x/);
						$row->[$idx] = "ST_GeomFromText('" . $row->[$idx] . "', " . $self->{spatial_srid}{$table}->[$idx] . ")";
					}
					else
					{
						$row->[$idx] = "ST_Geometry('" . $row->[$idx] . "', $self->{spatial_srid}{$table}->[$idx])";
					}
				}
				else
				{
					$row->[$idx] =~ s/^SRID=(\d+);//;
					$row->[$idx] = "ST_GeomFromText('" . $row->[$idx] . "', $1)";
				}
			}
			# Stores the filter to use in the WHERE clause
			if (exists $self->{tables}{$table}{pk_columns}{$idx}) {
				push(@{ $self->{tables}{$table}{pk_where_clause} }, "$self->{tables}{$table}{pk_columns}{$idx} = $row->[$idx]");
			}
		}
		elsif ($row->[$idx] =~ /^(?!(?!)\x{100})ARRAY\(0x/)
		{
			print STDERR "/!\\ WARNING /!\\: we should not be there !!!\n";
		}
		else
		{
			$row->[$idx] = $self->format_data_type($row->[$idx], $data_type, $action, $table, $src_data_types->[$idx], $idx, $colcond->[$idx], $sprep);

			# Construct a WHERE clause based onb PK columns values
			if ($self->{lo_import} && $colcond->[$idx]->{isoid} && $colcond->[$idx]->{blob})
			{
				# Store the uuid of the file containing the BLOB and set the oid to 0
				if ($colcond->[$idx]->{isoid} && $colcond->[$idx]->{blob})
				{
					push(@{ $self->{tables}{$table}{lo_import_id} }, $row->[$idx]);
					push(@{ $self->{tables}{$table}{lo_import_col} }, $self->{tables}{$table}{dest_column_name}[$idx]);
					$row->[$idx] = 0;
				}
			}
			# Stores the filter to use in the WHERE clause
			if (exists $self->{tables}{$table}{pk_columns}{$idx}) {
				push(@{ $self->{tables}{$table}{pk_where_clause} }, "$self->{tables}{$table}{pk_columns}{$idx} = $row->[$idx]");
			}
		}
	}

	# Now add the script to import later the BLOB(s) into the table as a large object
	if ($self->{lo_import} && $#{ $self->{tables}{$table}{pk_where_clause} } >= 0)
	{
		# Rename table and double-quote it if required
		my $tmptb = $self->get_replaced_tbname($table);

		# Replace Tablename by temporary table for DATADIFF (data will be inserted in real table at the end)
		if ($self->{datadiff}) {
			$tmptb = $self->get_tbname_with_suffix($tmptb, $self->{datadiff_ins_suffix});
		}

		my $where_clause = join(' AND ', @{ $self->{tables}{$table}{pk_where_clause} });
		# Generete the entry in the psql script
		foreach (my $i = 0; $i <= $#{ $self->{tables}{$table}{lo_import_id} }; $i++)
		{
			if ($self->{tables}{$table}{lo_import_id}[$i] ne '\N'
					&& $self->{tables}{$table}{lo_import_id}[$i] ne 'NULL')
			{
				$self->{post_lo_script} .= qq{
ret=`psql -c "\\lo_import '$self->{tables}{$table}{lo_import_id}[$i]'" | awk '{print \$2}'`
if [ "\${ret}a" != "a" ]; then
	psql -c "UPDATE $tmptb SET $self->{tables}{$table}{lo_import_col}[$i] = \${ret} WHERE $where_clause"
fi
};
			}
			else
			{
				$self->{post_lo_script} .= qq{
psql -c "UPDATE $tmptb SET $self->{tables}{$table}{lo_import_col}[$i] = NULL WHERE $where_clause"
};
			}
		}
	}
}

sub set_custom_type_value
{
	my ($self, $data_type, $user_type, $rows, $dest_type, $no_quote) = @_;

	my $has_array = 0;
	my @type_col = ();
	my $result = '';
	my $col_ref = [];
	push(@$col_ref, @$rows);
	my $num_arr = -1;
	my $isnested = 0;

	for (my $i = 0; $i <= $#{$col_ref}; $i++)
	{
		if ($col_ref->[$i] !~ /^ARRAY\(0x/)
		{
			if ($self->{type} eq 'COPY')
			{
				# Want to export the user defined type as a single array, not composite type
				if ($dest_type =~ /(text|char|varying)\[\d*\]$/i)
				{
					$has_array = 1;
					$col_ref->[$i] =~ s/"/\\\\"/gs;
					if ($col_ref->[$i] =~ /[,"]/) {
						$col_ref->[$i] = '"' . $col_ref->[$i] . '"';
					};
				# Data must be exported as an array of numeric types
				} elsif ($dest_type =~ /\[\d*\]$/) {
					$has_array = 1;
				}
				elsif ($dest_type =~ /(char|text)/)
				{
					$col_ref->[$i] =~ s/"/\\\\\\\\""/igs;
					if ($col_ref->[$i] =~ /[,"]/) {
						$col_ref->[$i] = '""' . $col_ref->[$i] . '""';
					};
				} else {
					$isnested = 1;
				}
			}
			else
			{
				# Want to export the user defined type as a single array, not composite type
				if ($dest_type =~ /(text|char|varying)\[\d*\]$/i)
				{
					$has_array = 1;
					$col_ref->[$i] =~ s/"/\\"/gs;
					$col_ref->[$i] =~ s/'/''/gs;
					if ($col_ref->[$i] =~ /[,"]/) {
						$col_ref->[$i] = '"' . $col_ref->[$i] . '"';
					};
				# Data must be exported as a simple array of numeric types
				} elsif ($dest_type =~ /\[\d*\]$/i) {
					$has_array = 1;
				} elsif ($dest_type =~ /(char|text)/) {
					$col_ref->[$i] = "'" . $col_ref->[$i] . "'" if ($col_ref->[0][$i] ne '');
				} else {
					$isnested = 1;
				}
			}
			push(@type_col, $col_ref->[$i]);
		}
		else
		{
			$num_arr++;

			my @arr_col = ();
			for (my $j = 0; $j <= $#{$col_ref->[$i]}; $j++)
			{
				# Look for data based on custom type to replace the reference by the value
				if ($col_ref->[$i][$j] =~ /^(?!(?!)\x{100})ARRAY\(0x/
				       	&& $user_type->{src_types}[$i][$j] !~ /SDO_GEOMETRY/i
				       	&& $user_type->{src_types}[$i][$j] !~ /^(ST_|STGEOM_)/i #ArGis geometry types
				)
				{
					my $dtype = uc($user_type->{src_types}[$i][$j]) || '';
					$dtype =~ s/\(.*//; # remove any precision
					if (!exists $self->{data_type}{$dtype} && !exists $self->{user_type}{$dtype}) {
						%{ $self->{user_type}{$dtype} } = $self->custom_type_definition($dtype);
					}
					$col_ref->[$i][$j] =  $self->set_custom_type_value($dtype, $self->{user_type}{$dtype}, $col_ref->[$i][$j], $user_type->{pg_types}[$i][$j], 1);
					if ($self->{type} ne 'COPY') {
						$col_ref->[$i][$j] =~ s/"/\\\\""/gs;
					} else {
						$col_ref->[$i][$j] =~ s/"/\\\\\\\\""/gs;
					}
				}

				if ($self->{type} eq 'COPY')
				{
					# Want to export the user defined type as charaters array
					if ($dest_type =~ /(text|char|varying)\[\d*\]$/i)
					{
						$has_array = 1;
						$col_ref->[$i][$j] =~ s/"/\\\\"/gs;
						if ($col_ref->[$i][$j] =~ /[,"]/) {
							$col_ref->[$i][$j] = '"' . $col_ref->[$i][$j] . '"';
						};
					}
					# Data must be exported as an array of numeric types
					elsif ($dest_type =~ /\[\d*\]$/) {
						$has_array = 1;
					}
				}
				else
				{
					# Want to export the user defined type as array
					if ($dest_type =~ /(text|char|varying)\[\d*\]$/i)
					{
						$has_array = 1;
						$col_ref->[$i][$j] =~ s/"/\\"/gs;
						$col_ref->[$i][$j] =~ s/'/''/gs;
						if ($col_ref->[$i][$j] =~ /[,"]/) {
							$col_ref->[$i][$j] = '"' . $col_ref->[$i][$j] . '"';
						};
					}
					# Data must be exported as an array of numeric types
					elsif ($dest_type =~ /\[\d*\]$/) {
						$has_array = 1;
					}
				}
				if ($col_ref->[$i][$j] =~ /[\(\)]/ && $col_ref->[$i][$j] !~ /^[\\]+""/)
				{
					if ($self->{type} ne 'COPY') {
						$col_ref->[$i][$j] = "\\\\\"\"" . $col_ref->[$i][$j] . "\\\\\"\"";
					} else {
						$col_ref->[$i][$j] = "\\\\\\\\\"\"" . $col_ref->[$i][$j] . "\\\\\\\\\"\"";
					}
				}
				push(@arr_col, $col_ref->[$i][$j]);
			}
			push(@type_col, '(' . join(',', @arr_col) . ')');
		}
	}

	if ($has_array) {
		 $result =  '{' . join(',', @type_col) . '}';
	}
	elsif ($isnested)
	{
		# ARRAY[ROW('B','C')]
		my $is_string = 0;
		foreach my $g (@{$self->{user_type}{$dest_type}->{pg_types}}) {
			$is_string = 1 if (grep(/(text|char|varying)/i, @$g));
		}
		if ($is_string) {
			$result =  '({"(' . join(',', @type_col) . ')"})';
		} else {
			$result =  '("{' . join(',', @type_col) . '}")';
		}
	}
	else
	{
		# This is the root call of the function, no global quoting is required
		if (!$no_quote)
		{
			#map { s/^$/NULL/; } @type_col;
			#$result = 'ROW(ARRAY[ROW(' . join(',', @type_col) . ')])';
			# With arrays of arrays the construction is different
			if ($num_arr > 1)
			{
				#### Expected
				# INSERT: '("{""(0,0,0,0,0,0,0,0,0,,,)"",""(0,0,0,0,0,0,0,0,0,,,)""}")'
				# COPY:    ("{""(0,0,0,0,0,0,0,0,0,,,)"",""(0,0,0,0,0,0,0,0,0,,,)""}")
				####
				$result =  "(\"{\"\"" . join('"",""', @type_col) . "\"\"}\")";
			}
			# When just one or none arrays are present
			else
			{
				#### Expected
				# INSERT: '("(1,1)",0,,)'
				# COPY:    ("(1,1)",0,,)
				####
				map { s/^\(([^\)]+)\)$/"($1)"/; } @type_col;
				$result =  "(" . join(',', @type_col) . ")";
			}
		# else we are in recusive call
		} else {
			$result =  "\"(" . join(',', @type_col) . ")\"";
		}
	}
	if (!$no_quote && $self->{type} ne 'COPY') {
		$result =  "'$result'";
	}
	while ($result =~ s/,"""",/,NULL,/gs) {};

	return $result;
}

sub format_data_type
{
	my ($self, $col, $data_type, $action, $table, $src_type, $idx, $cond, $sprep, $isnested) = @_;

	my $q = "'";
	$q = '"' if ($isnested);

	# Skip data type formatting when it has already been done in
	# set_custom_type_value(), aka when the data type is an array.
	next if ($data_type =~ /\[\d*\]/); 

	# Internal timestamp retrieves from custom type is as follow: 01-JAN-77 12.00.00.000000 AM (internal_date_max)
	if (($data_type eq 'char') && $col =~ /^(\d{2})-([A-Z]{3})-(\d{2}) (\d{2})\.(\d{2})\.(\d{2}\.\d+) (AM|PM)$/ )
	{
		my $d = $1;
		my $m = $ORACLE_MONTHS{$2};
		my $y = $3;
		my $h = $4;
		my $min = $5;
		my $s = $6;
		my $typeh = $7;
		if ($typeh eq 'PM') {
			$h += 12;
		}
		if ($d <= $self->{internal_date_max}) {
			$d += 2000;
		} else {
			$d += 1900;
		}
		$col = "$y-$m-$d $h:$min:$s";
		$data_type = 'timestamp';
		$src_type = 'internal timestamp';
	}

	# Workaround for a bug in DBD::Oracle with the ora_piece_lob option
	# (used when use_lob_locator is disabled) where null values fetch as
	# empty string for certain types.
	if (!$self->{use_lob_locator} and ($cond->{clob} or $cond->{blob} or $cond->{long})) {
		$col = undef if (!length($col));
	}

	# Preparing data for output
	if ($action ne 'COPY')
	{
		if (!defined $col)
		{
			if (!$cond->{isnotnull} || ($self->{empty_lob_null} && ($cond->{clob} || $cond->{isbytea}))) {
				$col = 'NULL' if (!$sprep);
			} else {
				$col = "$q$q";
			}
		}
		elsif ( ($src_type =~ /SDO_GEOMETRY/i) && ($self->{geometry_extract_type} eq 'WKB') )
		{
			$col = "St_GeomFromWKB($q\\x" . unpack('H*', $col) . "$q, $self->{spatial_srid}{$table}->[$idx])";
		}
		elsif ($cond->{isbytea} || ($self->{blob_to_lo} && $cond->{isoid} && $cond->{blob}))
		{
			$col = $self->_escape_lob($col, $cond->{raw} ? 'RAW' : 'BLOB', $cond, $isnested, $data_type);
		}
		elsif ($cond->{istext})
		{
			if ($cond->{clob}) {
				$col = $self->_escape_lob($col, 'CLOB', $cond, $isnested, $data_type);
			} elsif (!$sprep) {
				$col = $self->escape_insert($col, $isnested);
			}
		}
		elsif ($cond->{isbit})
		{
			$col = "B$q" . $col . "$q";
		}
		elsif ($cond->{isdate})
		{
			$q = '' if ( $col =~ /^['\`]/ );
			if ($col =~ /^0000-/) {
				$col = $self->{replace_zero_date} ?  "$q$self->{replace_zero_date}$q" : 'NULL';
			} elsif ($col =~ /^(\d+-\d+-\d+ \d+:\d+:\d+)\.$/) {
				$col = "$q$1$q";
			} else {
				$col = "$q$col$q";
			}
		}
		elsif ($cond->{isboolean})
		{
			if (exists $self->{ora_boolean_values}{lc($col)}) {
				$col = "$q" . $self->{ora_boolean_values}{lc($col)} . "$q";
			}
		}
		elsif ($cond->{isefile})
		{
			$col =~ s/\\/\\\\/g;
			$col =~ s/([\(\)])/\\$1/g;
			# escape comma except the first one
			$col =~ s/,/\,/g;
			$col =~ s/\,/,/;
		}
		elsif ($cond->{isinterval})
		{
			if ($col =~ /^-/) {
				$col =~ s/ (\d+)/ -$1/;
				$col = "'$col'";
			}
		}
		elsif ($cond->{isnum})
		{
			if (!$self->{pg_dsn}) {
				$col =~ s/^([\-]*)(\~|Inf)$/'$1Infinity'/i;
			} else {
				$col =~ s/^([\-]*)(\~|Inf)$/$1Infinity/i;
			}
		}
		else
		{
			if (!$sprep) {
				$col = 'NULL' if ($col eq '');
			} else {
				$col = undef if ($col eq '');
			}
		}
	}
	else
	{
		if (!defined $col)
		{
			if (!$cond->{isnotnull} || ($self->{empty_lob_null} && ($cond->{clob} || $cond->{isbytea}))) {
				$col = '\N';
			} else {
				$col = '';
			}
		}
		elsif ( $cond->{geometry} && ($self->{geometry_extract_type} eq 'WKB') )
		{
			$col = 'SRID=' . $self->{spatial_srid}{$table}->[$idx] . ';' . unpack('H*', $col);
		}
		elsif ($cond->{isboolean})
		{
			if (exists $self->{ora_boolean_values}{lc($col)}) {
				$col = $self->{ora_boolean_values}{lc($col)};
			}
		}
		elsif ($self->{lo_import} && $cond->{blob})
		{
			# In copy mode we write the BLOB data to an external file
			# for later lo_import and we insert the corresponding unique
			# reference of this BLOB into the Oid column. The value of
			# the Oid will be fixed with the right Oid when importing
			# the large object.
			$col = $self->_save_blob_to_lo($col);
		}
		elsif ($cond->{isbytea})
		{
			$col = $self->_escape_lob($col, $cond->{raw} ? 'RAW' : 'BLOB', $cond, $isnested, $data_type);
		}
		elsif ($cond->{isjson})
		{
			# preserve json escaping
			$col =~ s/\\/\\\\/g;
		}
		elsif ($cond->{isinterval})
		{
			if ($col =~ /^-/) {
				$col =~ s/ (\d+)/ -$1/;
			}
		}
		elsif ($cond->{isnum})
		{
			$col =~ s/([\-]*)(\~|Inf)/$1Infinity/i;
			$col = '\N' if ($col eq '');
		}
		elsif ($cond->{istext})
		{
			$cond->{clob} ? $col = $self->_escape_lob($col, 'CLOB', $cond, $isnested) : $col = $self->escape_copy($col, $isnested, $data_type);
		}
		elsif ($cond->{isdate})
		{
			if ($col =~ /^0000-/) {
				$col = $self->{replace_zero_date} || '\N';
			} elsif ($col =~ /^(\d+-\d+-\d+ \d+:\d+:\d+)\.$/) {
				$col = $1;
			}
		}
		elsif ($cond->{isefile})
		{
			$col =~ s/\\/\\\\/g;
			$col =~ s/([\(\)])/\\\\$1/g;
			# escape comma except the first one
			$col =~ s/,/\\,/g;
			$col =~ s/\\,/,/;
		}
		elsif ($cond->{isbit})
		{
			$col = $col;
		}
	}
	return $col;
}

sub _save_blob_to_lo
{
	my $self = shift;

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}" if ($self->{output_dir});

	my $filename = $dirprefix . &get_uuid() . '.lo';
	$self->logit("DEBUG: write blob to $filename\n", 1);
	my $fh = new IO::File;
	$fh->open(">$filename") or $self->logit("FATAL: can not write $filename\n", 0, 1);
	$fh->binmode(':raw');
	print $fh $_[0];
	$fh->close();

	return $filename;
}

sub get_uuid
{
	my $uuid = '';
	if (open(my $fh, "/proc/sys/kernel/random/uuid")) {
		$uuid = <$fh>;
		close($fh);
	} else {
		$uuid = `/usr/bin/uuidgen`;
	}
	chomp($uuid);

	return $uuid;
}

sub hs_cond
{
	my ($self, $data_types, $src_data_types, $table) = @_;

	my $col_cond = [];
	for (my $idx = 0; $idx < scalar(@$data_types); $idx++)
	{
		my $hs={};
		$hs->{geometry} = $src_data_types->[$idx] =~ /SDO_GEOMETRY/i ? 1 : 0;
		$hs->{isnum} = $data_types->[$idx] !~ /^(json|char|varchar|date|time|text|bytea|xml|uuid|citext|enum)/i ? 1 :0;
		$hs->{isdate} =  $data_types->[$idx] =~ /^(date|time)/i ? 1 : 0;
		$hs->{raw} = $src_data_types->[$idx] =~ /RAW/i ? 1 : 0;
		$hs->{clob} = $src_data_types->[$idx] =~ /CLOB/i ? 1 : 0;
		$hs->{blob} = $src_data_types->[$idx] =~ /BLOB/i ? 1 : 0;
		$hs->{long} = $src_data_types->[$idx] =~ /LONG/i ? 1 : 0;
		$hs->{istext} = $data_types->[$idx] =~ /(char|text|xml|uuid|citext)/i ? 1 : 0;
		$hs->{isbytea} = $data_types->[$idx] =~ /bytea/i ? 1 : 0;
		$hs->{isoid} = $data_types->[$idx] =~ /oid/i ? 1 : 0;
		$hs->{isbit} = $data_types->[$idx] =~ /bit/i ? 1 : 0;
		$hs->{isboolean} = $data_types->[$idx] =~ /boolean/i ? 1 : 0;
		$hs->{isefile} = $data_types->[$idx] =~ /efile/i ? 1 : 0;
		$hs->{isinterval} = $data_types->[$idx] =~ /interval/i ? 1 : 0;
		$hs->{isnotnull} = 0;
		$hs->{isjson} = $data_types->[$idx] =~ /json/i ? 1 : 0;
		if ($self->{nullable}{$table}{$idx} =~ /^N/) {
			$hs->{isnotnull} = 1;
		}
		push @$col_cond, $hs;
	}
	return $col_cond;
}

sub format_data
{
	my ($self, $rows, $data_types, $action, $src_data_types, $custom_types, $table) = @_;

	my $col_cond = $self->hs_cond($data_types,$src_data_types, $table);
	foreach my $row (@$rows) {
		$self->format_data_row($row,$data_types,$action,$src_data_types,$custom_types,$table,$col_cond);
	}
	if ($self->{post_lo_script}) {
		$self->append_lo_import_file($table);
	}
}

=head2 dump

This function dump data to the right export output (gzip file, file or stdout).

=cut

sub dump
{
	my ($self, $data, $fh) = @_;

	return if (!defined $data || $data eq '');

	if (!$self->{compress}) {
		if (defined $fh) {
			$fh->print($data);
		} elsif (defined $self->{fhout}) {
			$self->{fhout}->print($data);
		} else {
			print $data;
		}
	} elsif ($self->{compress} eq 'Zlib') {
		if (not defined $fh) {
			$self->{fhout}->gzwrite($data) or $self->logit("FATAL: error dumping compressed data\n", 0, 1);
		} else {
			$fh->gzwrite($data) or $self->logit("FATAL: error dumping compressed data\n", 0, 1);
		}
	} elsif (defined $self->{fhout}) {
		 $self->{fhout}->print($data);
	} else {
		$self->logit("FATAL: no filehandle to write output, this may not happen\n", 0, 1);
	}
}

=head2 data_dump

This function dump data to the right output (gzip file, file or stdout) in multiprocess safety.
File is open and locked before writing data, it is closed at end.

=cut

sub data_dump
{
	my ($self, $data, $tname, $pname) = @_;

	return if ($self->{oracle_speed});

	# get out of here if there is no data to dump
	return if (not defined $data or $data eq '');

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	my $filename = $self->{output};
	my $rname = $tname;
	$rname .= '_' . $pname if (!$self->{rename_partition} && $pname);
	$rname = $pname if ($self->{rename_partition} && $pname);
	if ($self->{file_per_table})
	{
		$filename = "${rname}_$self->{output}";
		$filename = "tmp_$filename";
	}
	# Set file temporary until the table export is done
	$self->logit("Dumping data from $rname to file: $filename\n", 1);

	if ( ($self->{jobs} > 1) || ($self->{oracle_copies} > 1) )
	{
		$self->close_export_file($self->{fhout}) if (defined $self->{fhout} && !$self->{file_per_table} && !$self->{pg_dsn});
		my $fh = $self->append_export_file($filename);
		$self->set_binmode($fh) if (!$self->{compress});
		flock($fh, 2) || die "FATAL: can't lock file $dirprefix$filename\n";
		$fh->print($data);
		$self->close_export_file($fh);
		$self->logit("Written " . length($data) . " bytes to $dirprefix$filename\n", 1);
		# Reopen default output file
		$self->create_export_file() if (defined $self->{fhout} && !$self->{file_per_table} && !$self->{pg_dsn});
	}
	elsif ($self->{file_per_table})
	{
		if ($pname)
		{
 			my $fh = $self->append_export_file($filename);
 			$self->set_binmode($fh) if (!$self->{compress});
 			$fh->print($data);
 			$self->close_export_file($fh);
			$self->logit("Written " . length($data) . " bytes to $dirprefix$filename\n", 1);
		}
		else
		{
			my $set_encoding = 0;
			if (!defined $self->{cfhout})
			{
				$self->{cfhout} = $self->open_export_file($filename);
				$set_encoding = 1;
			}

			if ($self->{compress} eq 'Zlib')
			{
				$self->{cfhout}->gzwrite($data) or $self->logit("FATAL: error writing compressed data into $filename :: $self->{cfhout}\n", 0, 1);
			}
			else
			{
				$self->set_binmode($self->{cfhout}) if (!$self->{compress} && $set_encoding);
				$self->{cfhout}->print($data);
			}
		}
	}
	else
	{
		$self->dump($data);
	}
}

=head2 read_config

This function read the specified configuration file.

=cut

sub read_config
{
	my ($self, $file) = @_;

	my $fh = new IO::File;
	binmode($fh, ":utf8");
	$fh->open($file) or $self->logit("FATAL: can't read configuration file $file, $!\n", 0, 1);
	while (my $l = <$fh>)
	{
		chomp($l);
		$l =~ s/\r//gs;
		$l =~ s/^\s*\#.*$//g;
		next if (!$l || ($l =~ /^\s+$/));
		$l =~ s/^\s*//; $l =~ s/\s*$//;
		&parse_config($l);
	}
	$self->close_export_file($fh);
}

sub  parse_config
{
	my $l = shift;

	my ($var, $val) = split(/\s+/, $l, 2);
	$var = uc($var);
	if ($var eq 'IMPORT')
	{
		if ($val)
		{
			$self->logit("Importing $val...\n", 1);
			$self->read_config($val);
			$self->logit("Done importing $val.\n",1);
		}
	}
	elsif ($var =~ /^SKIP/)
	{
		if ($val)
		{
			$self->logit("No extraction of \L$val\E\n",1);
			my @skip = split(/[\s;,]+/, $val);
			foreach my $s (@skip)
			{
				$s = 'indexes' if ($s =~ /^indices$/i);
				$AConfig{"skip_\L$s\E"} = 1;
			}
		}
	}
	elsif ($var eq 'DEFINED_PK_' . $AConfig{SCHEMA}) # add schema specific definition of partitioning columns
	{
		my @defined_pk = split(/[\s;]+/, $val);
		foreach my $r (@defined_pk)
		{
			my ($table, $col) = split(/:/, $r);
			$AConfig{DEFINED_PK}{lc($table)} = $col;
		}
	}
	# Should be a else statement but keep the list up to date to memorize the directives full list
	elsif (!grep(/^$var$/, 'TABLES','ALLOW','MODIFY_STRUCT','REPLACE_TABLES','REPLACE_COLS',
			'WHERE','EXCLUDE','VIEW_AS_TABLE','MVIEW_AS_TABLE','ORA_RESERVED_WORDS',
			'SYSUSERS','REPLACE_AS_BOOLEAN','BOOLEAN_VALUES','MODIFY_TYPE','DEFINED_PK',
			'ALLOW_PARTITION','REPLACE_QUERY','FKEY_ADD_UPDATE','DELETE',
			'LOOK_FORWARD_FUNCTION','ORA_INITIAL_COMMAND','PG_INITIAL_COMMAND',
			'TRANSFORM_VALUE','EXCLUDE_COLUMNS'
		))
	{
		$AConfig{$var} = $val;
		if ($var eq 'NO_LOB_LOCATOR') {
			print STDERR "WARNING: NO_LOB_LOCATOR is deprecated, use USE_LOB_LOCATOR instead see documentation about the logic change.\n";
			if ($val == 1) {
				$AConfig{USE_LOB_LOCATOR} = 0;
			} else {
				$AConfig{USE_LOB_LOCATOR} = 1;
			}
		}
		if ($var eq 'NO_BLOB_EXPORT') {
			print STDERR "WARNING: NO_BLOB_EXPORT is deprecated, use ENABLE_BLOB_EXPORT instead see documentation about the logic change.\n";
			if ($val == 1) {
				$AConfig{ENABLE_BLOB_EXPORT} = 0;
			} else {
				$AConfig{ENABLE_BLOB_EXPORT} = 1;
			}
		}
	} elsif ($var =~ /VIEW_AS_TABLE/) {
		push(@{$AConfig{$var}}, split(/[\s;,]+/, $val) );
	} elsif ($var eq 'LOOK_FORWARD_FUNCTION') {
		push(@{$AConfig{$var}}, split(/[\s;,]+/, $val) );
	}
	elsif ( ($var eq 'TABLES') || ($var eq 'ALLOW') || ($var eq 'EXCLUDE')
		|| ($var eq 'ALLOW_PARTITION') )
	{
		$var = 'ALLOW' if ($var eq 'TABLES');
		if ($var eq 'ALLOW_PARTITION')
		{
			$var = 'ALLOW';
			push(@{$AConfig{$var}{PARTITION}}, split(/[,\s]+/, $val) );
		}
		else
		{
			# Syntax: TABLE[regex1 regex2 ...];VIEW[regex1 regex2 ...];glob_regex1 glob_regex2 ...
			# Global regex will be applied to the export type only
			my @vlist = split(/\s*;\s*/, $val);
			foreach my $a (@vlist)
			{
				if ($a =~ /^([^\[]+)\[(.*)\]$/) {
					push(@{$AConfig{$var}{"\U$1\E"}}, split(/[,\s]+/, $2) );
				} else {
					push(@{$AConfig{$var}{ALL}}, split(/[,\s]+/, $a) );
				}
			}
		}
	}
	elsif ( $var =~ /_INITIAL_COMMAND/ ) {
		push(@{$AConfig{$var}}, $val);
	} elsif ( $var eq 'SYSUSERS' ) {
		push(@{$AConfig{$var}}, split(/[\s;,]+/, $val) );
	} elsif ( $var eq 'ORA_RESERVED_WORDS' ) {
		push(@{$AConfig{$var}}, split(/[\s;,]+/, $val) );
	}
	elsif ( $var eq 'FKEY_ADD_UPDATE' )
	{
		if (grep(/^$val$/i, @FKEY_OPTIONS)) {
			$AConfig{$var} = uc($val);
		} else {
			$self->logit("FATAL: invalid option, see FKEY_ADD_UPDATE in configuration file\n", 0, 1);
		}
	}
	elsif ($var eq 'MODIFY_STRUCT' or $var eq 'EXCLUDE_COLUMNS')
	{
		while ($val =~ s/([^\(\s]+)\s*\(([^\)]+)\)\s*//)
		{
			my $table = $1;
			my $fields = $2;
			$fields =~ s/^\s+//;
			$fields =~ s/\s+$//;
			push(@{$AConfig{$var}{$table}}, split(/[\s,]+/, $fields) );
		}
	}
	elsif ($var eq 'MODIFY_TYPE')
	{
		$val =~ s/\\,/#NOSEP#/gs;
		my @modif_type = split(/[,;]+/, $val);
		foreach my $r (@modif_type)
		{ 
			$r =~ s/#NOSEP#/,/gs;
			my ($table, $col, $type) = split(/:/, lc($r));
			$AConfig{$var}{$table}{$col} = $type;
		}
	}
	elsif ($var eq 'REPLACE_COLS')
	{
		while ($val =~ s/([^\(\s]+)\s*\(([^\)]+)\)[,;\s]*//)
		{
			my $table = $1;
			my $fields = $2;
			$fields =~ s/^\s+//;
			$fields =~ s/\s+$//;
			my @rel = split(/[,]+/, $fields);
			foreach my $r (@rel)
			{
				my ($old, $new) = split(/:/, $r);
				$AConfig{$var}{$table}{$old} = $new;
			}
		}
	}
	elsif ($var eq 'REPLACE_TABLES')
	{
		my @replace_tables = split(/[\s,;]+/, $val);
		foreach my $r (@replace_tables)
		{ 
			my ($old, $new) = split(/:/, $r);
			$AConfig{$var}{$old} = $new;
		}
	}
	elsif ($var eq 'REPLACE_AS_BOOLEAN')
	{
		my @replace_boolean = split(/[\s;]+/, $val);
		foreach my $r (@replace_boolean)
		{ 
			my ($table, $col) = split(/:/, $r);
			push(@{$AConfig{$var}{uc($table)}}, uc($col));
		}
	}
	elsif ($var eq 'BOOLEAN_VALUES')
	{
		my @replace_boolean = split(/[\s,;]+/, $val);
		foreach my $r (@replace_boolean)
		{ 
			my ($yes, $no) = split(/:/, $r);
			$AConfig{$var}{lc($yes)} = 't';
			$AConfig{$var}{lc($no)} = 'f';
		}
	}
	elsif ($var eq 'DEFINED_PK')
	{
		my @defined_pk = split(/[\s;]+/, $val);
		foreach my $r (@defined_pk)
		{ 
			my ($table, $col) = split(/:/, $r);
			$AConfig{$var}{lc($table)} = $col;
		}
	}
	elsif ($var eq 'WHERE')
	{
		while ($val =~ s/([^\[\s]+)\s*\[([^\]]+)\]\s*//)
		{
			my $table = $1;
			my $where = $2;
			$where =~ s/^\s+//;
			$where =~ s/\s+$//;
			$AConfig{$var}{$table} = $where;
		}
		if ($val) {
			$AConfig{"GLOBAL_WHERE"} = $val;
		}
	}
	elsif ($var eq 'TRANSFORM_VALUE')
	{
		my @vals = split(/\s*;\s*/, $val);
		foreach my $v (@vals)
		{
			while ($v =~ s/([^\[\s]+)\s*\[\s*([^:,]+)\s*[,:]\s*([^\]]+)\s*\]\s*//)
			{
				my $table = $1;
				my $column = $2;
				my $clause = $3;
				$column =~ s/"//g;
				$AConfig{$var}{lc($table)}{lc($column)} = $clause;
			}
		}
	}
	elsif ($var eq 'DELETE')
	{
		while ($val =~ s/([^\[\s]+)\s*\[([^\]]+)\]\s*//)
		{
			my $table = $1;
			my $delete = $2;
			$delete =~ s/^\s+//;
			$delete =~ s/\s+$//;
			$AConfig{$var}{$table} = $delete;
		}
		if ($val) {
			$AConfig{"GLOBAL_DELETE"} = $val;
		}
	}
	elsif ($var eq 'REPLACE_QUERY')
	{
		while ($val =~ s/([^\[\s]+)\s*\[([^\]]+)\]\s*//)
		{
			my $table = lc($1);
			my $query = $2;
			$query =~ s/^\s+//;
			$query =~ s/\s+$//;
			$AConfig{$var}{$table} = $query;
		}
	}
}

sub _extract_functions
{
	my ($self, $content) = @_;

	my @lines = split(/\n/s, $content);
	my @functions = ('');
	my $before = '';
	my $fcname =  '';
	my $type = '';
	for (my $i = 0; $i <= $#lines; $i++)
	{ 
		if ($lines[$i] =~ /^(?:CREATE|CREATE OR REPLACE)?\s*(?:NONEDITIONABLE|EDITIONABLE)?\s*(FUNCTION|PROCEDURE)\s+([a-z0-9_\$\-\."]+)(.*)/i)
		{
			$type = uc($1);
			$fcname = $2;
			my $after = $3;
			$fcname =~ s/^.*\.//;
			$fcname =~ s/"//g;
			$type = 'FUNCTION' if (!$self->{pg_supports_procedure});
			if ($before) {
				push(@functions, "$before\n");
				$functions[-1] .= "$type $fcname $after\n";
			} else {
				push(@functions, "$type $fcname $after\n");
			}
			$before = '';
		} elsif ($fcname) {
			$functions[-1] .= "$lines[$i]\n";
		} else {
			$before .= "$lines[$i]\n";
		}
		$fcname = '' if ($lines[$i] =~ /^\s*END\s+["]*\Q$fcname\E["]*\b/i);
	}

	map { s/\bEND\s+(?!IF|LOOP|CASE|INTO|FROM|END|,)[a-z0-9_"\$]+\s*;/END;/igs; } @functions;

	return @functions;
}

=head2 _convert_package

This function is used to rewrite Oracle PACKAGE code to
PostgreSQL SCHEMA. Called only if PLSQL_PGSQL configuration directive
is set to 1.

=cut

sub _convert_package
{
	my ($self, $pkg) = @_;

	return if (!$pkg || !exists $self->{packages}{$pkg});

	my $owner = $self->{packages}{$pkg}{owner} || '';

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	my $content = '';

	if ($self->{package_as_schema})
	{
		my $pname =  $self->quote_object_name($pkg);
		$pname =~ s/^[^\.]+\.//;
		$content .= "\nDROP SCHEMA $self->{pg_supports_ifexists} $pname CASCADE;\n";
		$content .= "CREATE SCHEMA IF NOT EXISTS $pname;\n";
		if ($self->{force_owner})
		{
			$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
			if ($owner) {
				$content .= "ALTER SCHEMA \L$pname\E OWNER TO " .  $self->quote_object_name($owner) . ";\n";
			}
		}
	}

	# Grab global declaration from the package header
	if ($self->{packages}{$pkg}{desc} =~ /CREATE OR REPLACE PACKAGE\s+([^\s]+)(?:\s*\%ORA2PG_COMMENT\d+\%)*\s*((?:AS|IS)(?:\s*\%ORA2PG_COMMENT\d+\%)*)\s*(.*)/is)
	{
		my $pname = $1;
		my $type = $2;
		my $glob_declare = $3;
		$pname =~ s/"//g;
		$pname =~ s/^.*\.//g;
		$self->logit("Looking global declaration in package $pname...\n", 1);

		# Process package spec to extract global variables
		$self->_remove_comments(\$glob_declare);

		# Remove multiline comment from declaration part
		while ($glob_declare =~ s/\%OPEN_COMMENT\%((?:.*)?\*\/)//s) {};

		if ($glob_declare)
		{
			my @cursors = ();
			($glob_declare, @cursors) = $self->clear_global_declaration($pname, $glob_declare, 0);
			# Then dump custom type
			foreach my $tpe (sort {$a->{pos} <=> $b->{pos}} @{$self->{types}})
			{
				$self->logit("Dumping type $tpe->{name} from package description $pname...\n", 1);
				if ($self->{plsql_pgsql}) {
					$tpe->{code} = $self->_convert_type($tpe->{code}, $tpe->{owner}, %{$self->{pkg_type}{$pname}});
				} else {
					if ($tpe->{code} !~ /^SUBTYPE\s+/i) {
						$tpe->{code} = "CREATE$self->{create_or_replace} $tpe->{code}\n";
					}
				}
				$tpe->{code} =~ s/REPLACE type/REPLACE TYPE/;
				$content .= $tpe->{code} . "\n";
				$i++;
			}
			$content .= join("\n", @cursors) . "\n";
			$glob_declare = $self->register_global_variable($pname, $glob_declare);
		}
		@{$self->{types}} = ();
	}

	# Convert the package body part
	if ($self->{packages}{$pkg}{text} =~ /CREATE OR REPLACE PACKAGE\s+BODY\s*([^\s\%]+)(?:\s*\%ORA2PG_COMMENT\d+\%)*\s*(AS|IS)\s*(.*)/is)
	{

		my $pname = $1;
		my $type = $2;
		my $ctt = $3;
		my $glob_declare = $3;

		$pname =~ s/"//g;
		$pname =~ s/^.*\.//g;
		$self->logit("Dumping package $pname...\n", 1);

		# Process package spec to extract global variables
		$self->_remove_comments(\$glob_declare);
		if ($glob_declare && $glob_declare !~ /^(?:\s*\%ORA2PG_COMMENT\d+\%)*(FUNCTION|PROCEDURE)/is)
		{
			my @cursors = ();
			($glob_declare, @cursors) = $self->clear_global_declaration($pname, $glob_declare, 1);
			# Then dump custom type
			foreach my $tpe (sort {$a->{pos} <=> $b->{pos}} @{$self->{types}})
			{
				next if (!exists $self->{pkg_type}{$pname}{$tpe->{name}});
				$self->logit("Dumping type $tpe->{name} from package body $pname...\n", 1);
				if ($self->{plsql_pgsql}) {
					$tpe->{code} = $self->_convert_type($tpe->{code}, $tpe->{owner}, %{$self->{pkg_type}{$pname}});
				} else {
					if ($tpe->{code} !~ /^SUBTYPE\s+/i) {
						$tpe->{code} = "CREATE$self->{create_or_replace} $tpe->{code}\n";
					}
				}
				$tpe->{code} =~ s/REPLACE type/REPLACE TYPE/;
				$content .= $tpe->{code} . "\n";
				$i++;
			}
			$content .= join("\n", @cursors) . "\n";
			$glob_declare = $self->register_global_variable($pname, $glob_declare);
		}
		if ($self->{file_per_function})
		{
			my $dir = "$dirprefix".lc("$pname");
			if (!-d "$dir") {
				if (not mkdir($dir)) {
					$self->logit("Fail creating directory package : $dir - $!\n", 1);
					next;
				} else {
					$self->logit("Creating directory package: $dir\n", 1);
				}
			}
		}
		$ctt =~ s/\bEND[^;]*;$//is;

		my @functions = $self->_extract_functions($ctt);

		# Try to detect local function
		for (my $i = 0; $i <= $#functions; $i++)
		{
			my %fct_detail = $self->_lookup_function($functions[$i], $pname);
			if (!exists $fct_detail{name}) {
				$functions[$i] = '';
				next;
			}
			$fct_detail{name} =~ s/^.*\.//;
			$fct_detail{name} =~ s/"//g;
			next if (!$fct_detail{name});
			$fct_detail{name} =  lc($fct_detail{name});
			if (!exists $self->{package_functions}{"\L$pname\E"}{$fct_detail{name}})
			{
				my $res_name = $fct_detail{name};
				$res_name =~ s/^[^\.]+\.//;
				$fct_detail{name} =~ s/^([^\.]+)\.//;
				if ($self->{package_as_schema}) {
					$res_name = $pname . '.' . $res_name;
				} else {
					$res_name = $pname . '_' . $res_name;
				}
				$res_name =~ s/"_"/_/g;
				$self->{package_functions}{"\L$pname\E"}{"\L$fct_detail{name}\E"}{name}    = $self->quote_object_name($res_name);
				$self->{package_functions}{"\L$pname\E"}{"\L$fct_detail{name}\E"}{package} = $pname;
			}
		}

		$self->{pkgcost} = 0;
		foreach my $f (@functions)
		{
			next if (!$f);
			$content .= $self->_convert_function($owner, $f, $pkg || $pname);
		}
		if ($self->{estimate_cost}) {
			$self->{total_pkgcost} += $self->{pkgcost} || 0;
		}

	}

	@{$self->{types}} = ();

	return $content;
}

=head2 _restore_comments

This function is used to restore comments into SQL code previously
remove for easy parsing

=cut

sub _restore_comments
{
	my ($self, $content) = @_;

	# Replace text values that was replaced in code
	$self->_restore_text_constant_part($content);

	# Restore comments
	while ($$content =~ /(\%ORA2PG_COMMENT\d+\%)[\n]*/is) {
		my $id = $1;
		my $sep = "\n";
		# Do not append newline if this is a hint
		$sep = '' if ($self->{comment_values}{$id} =~ /^\/\*\+/);
		$$content =~ s/$id[\n]*/$self->{comment_values}{$id}$sep/is;
		delete $self->{comment_values}{$id};
	};

	# Restore start comment in a constant string
	$$content =~ s/\%OPEN_COMMENT\%/\/\*/gs;

	if ($self->{string_constant_regexp}) {
		# Replace potential text values that was replaced in comments
		$self->_restore_text_constant_part($content);
	}
}

=head2 _remove_comments

This function is used to remove comments from SQL code
to allow easy parsing

=cut

sub _remove_comments
{
	my ($self, $content, $no_constant) = @_;

	# Fix comment in a string constant
	$$content = encode('UTF-8', $$content) if (!$self->{input_file} && $self->{force_plsql_encoding});
	while ($$content =~ s/('[^';\n]*)\/\*([^';\n]*')/$1\%OPEN_COMMENT\%$2/s) {};

	my %default_values = ();
	my $j = 0;
	while ($$content =~ s/(DEFAULT\s+)('[^']*')/$1\%DEFAULT$j\%/s) {
		$default_values{$j} = $2;
		$j++;
	};

	# Fix unterminated comment at end of the code
	$$content =~ s/(\/\*(?:(?!\*\/).)*)$/$1 \*\//s;

	# multiline comment flags
	my $m_comment_flag = 'False';

	# Replace some other cases that are breaking the parser (presence of -- in constant string, etc.)
	my @lines = split(/([\n\r]+)/, $$content);
	for (my $i = 0; $i <= $#lines; $i++)
	{
		next if ($lines[$i] !~ /\S/);

		# Fix mysql # comments
		if ($self->{is_mysql}) {
			$lines[$i] =~ s/^([\t ]*)#/$1--/;
		}

		if ($lines[$i] !~ /^[\t ]*\--.*\/\*.*\*\/.*$/ and $lines[$i] !~ /\/\*.*\*\/$/)
		{
			# Single line comment --...-- */ is replaced by  */ only
			$lines[$i] =~ s/^([\t ]*)\-[\-]+\s*\*\//$1\*\//;

			# to check if we have starting multiline comment /*
			if (!($lines[$i] =~ /.*--.*\/\*/ and $lines[$i] !~ /.*\/\*.*--/))
			{
				if ($lines[$i] =~ /\/\*.*$/ and $m_comment_flag eq 'False')
				{
					$m_comment_flag = 'True'; # setting flag to true
				}
			}

			# Check for -- and */ in the same line
			if ($lines[$i] =~ /(.*?--.*?)(\*\/.*)$/ and $m_comment_flag eq 'True')
			{
				$lines[$i] = $1;
				splice(@lines, $i + 1, 0, $2);
				$m_comment_flag = 'False';

			}
			elsif ($lines[$i] =~ /(.*\*\/)/ and $m_comment_flag eq 'True')
			{
				$m_comment_flag = 'False';
			}

		}

		# Single line comment --
		if ($lines[$i] =~ s/^([\t ]*\-\-.*)$/$1\%ORA2PG_COMMENT$self->{idxcomment}\%/)
		{
			$self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $2;
			$self->{idxcomment}++;
		}
 
		# Single line comment /* ... */
		if ($lines[$i] =~ s/^([\t ]*\/\*.*\*\/)$/$1\%ORA2PG_COMMENT$self->{idxcomment}\%/)
		{
			$self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $2;
			$self->{idxcomment}++;
		}

		# ex:		v := 'literal'    -- commentaire avec un ' guillemet
		if ($lines[$i] =~ s/^([^']+'[^']*'\s*)(\-\-.*)$/$1\%ORA2PG_COMMENT$self->{idxcomment}\%/)
		{
			$self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $2;
			$self->{idxcomment}++;
		}

		# ex:       ---/* REN 16.12.2010 ZKOUSKA TEST NA KOLURC
		if ($lines[$i] =~ s/^(\s*)(\-\-(?:(?!\*\/\s*$).)*)$/$1\%ORA2PG_COMMENT$self->{idxcomment}\%/)
		{
			$self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $2;
			$self->{idxcomment}++;
		}

		# ex: var1 := SUBSTR(var2,1,28) || ' -- ' || var3 || ' --  ' || SUBSTR(var4,1,26) ;
		while ($lines[$i] =~ s/('[^;']*\-\-[^']*')/\?TEXTVALUE$self->{text_values_pos}\?/)
		{
			$self->{text_values}{$self->{text_values_pos}} = $1;
			$self->{text_values_pos}++;
		}
	}
	$$content =join('', @lines);

	while ($$content =~ s/\%DEFAULT(\d+)\%/$default_values{$1}/s) {};
	%default_values = ();

	# First remove hints they are not supported in PostgreSQL and it break the parser
	while ($$content =~ s/(\/\*\+(?:.*?)\*\/)/\%ORA2PG_COMMENT$self->{idxcomment}\%/s)
	{
		$self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $1;
		$self->{idxcomment}++;
	}

	# Replace /* */ comments by a placeholder and save the comment
	while ($$content =~ s/(\/\*(.*?)\*\/)/\%ORA2PG_COMMENT$self->{idxcomment}\%/s)
	{
		$self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $1;
		$self->{idxcomment}++;
	}

	while ($$content =~ s/(\'[^\'\n\r]+\b(PROCEDURE|FUNCTION)\s+[^\'\n\r]+\')/\%ORA2PG_COMMENT$self->{idxcomment}\%/is)
	{
		$self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $1;
		$self->{idxcomment}++;
	}
	@lines = split(/\n/, $$content);
	for (my $j = 0; $j <= $#lines; $j++)
	{
		if (!$self->{is_mysql})
		{
			# Extract multiline comments as a single placeholder
			my $old_j = $j;
			my $cmt = '';
			while ($lines[$j] =~ /^(\s*\-\-.*)$/)
			{
				$cmt .= "$1\n";
				$j++;
			}
			if ( $j > $old_j )
			{
				chomp($cmt);
				$lines[$old_j] =~ s/^(\s*\-\-.*)$/\%ORA2PG_COMMENT$self->{idxcomment}\%/;
				$self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $cmt;
				$self->{idxcomment}++;
				$j--;
				while ($j > $old_j)
				{
					delete $lines[$j];
					$j--;
				}
			}
			my $nocomment = '';
			if ($lines[$j] =~ s/^([^']*)('[^\-\']*\-\-[^\-\']*')/$1\%NO_COMMENT\%/) {
				$nocomment = $2;
			}
			if ($lines[$j] =~ s/(\s*\-\-.*)$/\%ORA2PG_COMMENT$self->{idxcomment}\%/)
			{
				$self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $1;
				chomp($self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"});
				$self->{idxcomment}++;
			}
			$lines[$j] =~ s/\%NO_COMMENT\%/$nocomment/;
		}
		else
		{
			# Mysql supports differents kinds of comment's starter
			if ( ($lines[$j] =~ s/(\s*\-\- .*)$/\%ORA2PG_COMMENT$self->{idxcomment}\%/) ||
				(!grep(/^$self->{type}$/, 'FUNCTION', 'PROCEDURE') && $lines[$j] =~ s/(\s*COMMENT\s+'.*)$/\%ORA2PG_COMMENT$self->{idxcomment}\%/) ||
				($lines[$j] =~ s/(\s*\# .*)$/\%ORA2PG_COMMENT$self->{idxcomment}\%/) )
			{
				$self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $1;
				chomp($self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"});
				# Normalize start of comment
				$self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} =~ s/^(\s*)COMMENT/$1\-\- /;
				$self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} =~ s/^(\s*)\#/$1\-\- /;
				$self->{idxcomment}++;
			}
		}
	}
	$$content = join("\n", @lines);

	# Replace subsequent comment by a single one
	while ($$content =~ s/(\%ORA2PG_COMMENT\d+\%\s*\%ORA2PG_COMMENT\d+\%)/\%ORA2PG_COMMENT$self->{idxcomment}\%/s)
	{
		$self->{comment_values}{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $1;
		$self->{idxcomment}++;
	}

	# Restore possible false positive constant replacement inside comment
	foreach my $k (keys %{ $self->{comment_values} } ) { 
		$self->{comment_values}{$k} =~ s/\?TEXTVALUE(\d+)\?/$self->{text_values}{$1}/gs;
	}

	# Then replace text constant part to prevent a split on a ; or -- inside a text
	if (!$no_constant) {
		$self->_remove_text_constant_part($content);
	}
}

=head2 _convert_function

This function is used to rewrite Oracle FUNCTION code to
PostgreSQL. Called only if PLSQL_PGSQL configuration directive               
is set to 1.

=cut

sub _convert_function
{
	my ($self, $owner, $plsql, $pname) = @_;

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});

	my %fct_detail = $self->_lookup_function($plsql, $pname);
	if ($self->{is_mysql}) {
		$pname = '';
	}
	return if (!exists $fct_detail{name});

	$fct_detail{name} =~ s/^.*\.// if ( (!$self->{is_mssql} || $self->{schema}) && (!$self->{input_file} && $self->{type} ne 'PACKAGE') );
	$fct_detail{name} =~ s/"//gs;

	my $sep = '.';
	$sep = '_' if (!$self->{package_as_schema});
	my $fname =  $self->quote_object_name($fct_detail{name});
	$fname =  $self->quote_object_name("$pname$sep$fct_detail{name}") if ($pname && !$self->{is_mysql} && !$self->{is_mssql});
	$fname =~ s/"_"/_/gs;

	# rewrite argument syntax
	# Replace alternate syntax for default value
	$fct_detail{args} =~ s/:=/DEFAULT/igs;
	# NOCOPY not supported
	$fct_detail{args} =~ s/\s*NOCOPY//igs;
	# IN OUT should be INOUT
	$fct_detail{args} =~ s/\bIN\s+OUT/INOUT/igs;
	# Remove default IN keyword
	$fct_detail{args} =~ s/\s+IN\s+/ /igs;
	# Remove %ROWTYPE from arguments, we can use the table name as type
	$fct_detail{args} =~ s/\%ROWTYPE//igs;

	# Replace DEFAULT EMPTY_BLOB() from function/procedure arguments by DEFAULT NULL
	$fct_detail{args} =~ s/\s+DEFAULT\s+EMPTY_[CB]LOB\(\)/DEFAULT NULL/igs;

	# Input parameters after one with a default value must also have defaults
	# we add DEFAULT NULL to all remaining parameter without a default value.
	my @args_sorted = ();
	$fct_detail{args} =~ s/^\((.*)\)(\s*\%ORA2PG_COMMENT\d+\%)*\s*$/$1/gs;
	my $param_comments = $2 || '';

	# Preserve parameters with precision and scale
	my $h = 0;
	my %param_param = ();
	while ($fct_detail{args} =~ s/\(([^\)]+)\)/%%tmp$h%%/s)
	{
		$param_param{$h} = $1;
		$h++;
	}
	if ($self->{use_default_null})
	{
		my $has_default = 0;
		@args_sorted = split(',', $fct_detail{args});
		for (my $i = 0; $i <= $#args_sorted; $i++)
		{
			$has_default = 1 if ($args_sorted[$i] =~ /\s+DEFAULT\s/i);
			if ($has_default && $args_sorted[$i] !~ /\s+DEFAULT\s/i)
			{
				# Add default null if this is not an OUT parameter
				if ( $args_sorted[$i] !~ /[,\(\s]OUT[\s,\)]/i && $args_sorted[$i] !~ /^OUT\s/i) {
					$args_sorted[$i] .= ' DEFAULT NULL';
				}
			}
		}
	}
	else
	{
		# or we need to sort the arguments so the ones with default values will be on the bottom
		push(@args_sorted, grep {!/\sdefault\s/i} split ',', $fct_detail{args});
		push(@args_sorted, grep {/\sdefault\s/i} split ',', $fct_detail{args});
		my @orig_args = split(',', $fct_detail{args});

		# Show a warning when there is parameters reordering
		my $fct_warning = '';
		for (my $i = 0; $i <= $#args_sorted; $i++)
		{
			if ($args_sorted[$i] ne $orig_args[$i])
			{
				my $str = $fct_detail{args};
				$str =~ s/\%ORA2PG_COMMENT\d+\%//sg;
				$str =~ s/[\n\r]+//gs;
				$str =~ s/\s+/ /g;
				$self->_restore_text_constant_part(\$str);
				$fct_warning = "\n-- WARNING: parameters order has been changed by Ora2Pg to move parameters with default values at end\n";
				$fct_warning .= "-- Original order was: $fname($str)\n";
				$fct_warning .= "-- You will need to manually reorder parameters in the function calls\n";
				print STDERR $fct_warning;
				last;
			}
		}
	}

	# Apply parameter list with translation for default values and reordering if needed
	for (my $i = 0; $i <= $#args_sorted; $i++)
	{
		if ($args_sorted[$i] =~ / DEFAULT ([^'].*)/i)
		{
			my $cod = Ora2Pg::PLSQL::convert_plsql_code($self, $1);
			$args_sorted[$i] =~ s/( DEFAULT )([^'].*)/$1$cod/i;
		}
	}
	$fct_detail{args} = '(' . join(',', @args_sorted) . ')';
	$fct_detail{args} =~ s/\%\%tmp(\d+)\%\%/($param_param{$1})/gs;

	# Set the return part
	my $func_return = '';
	$fct_detail{setof} = ' SETOF' if ($fct_detail{setof});

	my $search_path = '';
	if ($self->{export_schema} && !$self->{schema}) {
		$search_path = $self->set_search_path($owner);
	}

	# PostgreSQL procedure do not support OUT parameter, translate them into INOUT params
	if (!$fct_detail{hasreturn} && $self->{pg_supports_procedure}
		&&!$self->{pg_supports_outparam} && ($fct_detail{args} =~ /\bOUT\s+[^,\)]+/i)) {
		$fct_detail{args} =~ s/\bOUT(\s+[^,\)]+)/INOUT$1/igs;
	}

	my @nout = $fct_detail{args} =~ /\bOUT\s+([^,\)]+)/igs;
	my @ninout = $fct_detail{args} =~ /\bINOUT\s+([^,\)]+)/igs;
	my $out_return = 0;
	if ($fct_detail{hasreturn})
	{
		my $nbout = $#nout+1 + $#ninout+1;

		# When there is one or more out parameter, let PostgreSQL
		# choose the right type with not using a RETURNS clause.
		if ($nbout > 0) {
			$func_return = " AS \$body\$\n";
			if ($fct_detail{type} ne 'PROCEDURE' || !$self->{PG_SUPPORTS_PROCEDURE})
			{
				push(@nout, "extra_param $fct_detail{func_ret_type}");
				$func_return = " RETURNS record AS \$body\$\n";
				$out_return = 1;
			}
			$fct_detail{args} =~ s/\)$/, OUT extra_param $fct_detail{func_ret_type}\)/;
		} else {
			# Returns the right type
			$func_return = " RETURNS$fct_detail{setof} $fct_detail{func_ret_type} AS \$body\$\n";
		}
	}
	elsif (!$self->{pg_supports_procedure})
	{
		# Return void when there's no out parameters
		if (($#nout < 0) && ($#ninout < 0)) {
			$func_return = " RETURNS VOID AS \$body\$\n";
		} else {
			# When there is one or more out parameter, let PostgreSQL
			# choose the right type with not using a RETURNS clause.
			$func_return = " AS \$body\$\n";
		}
	}
	else
	{
		$func_return = " AS \$body\$\n";
	}
	$func_return .= $param_comments;
	$func_return =~ s/\s+AS(\s+AS\s+)/$1/is;

	# extract custom type declared in a stored procedure
	my $create_type = '';
	while ($fct_detail{declare} =~ s/\s+TYPE\s+([^\s]+)\s+IS\s+RECORD\s*\(([^;]+)\)\s*;//is)
	{
		my $tname = $1;
		my $tcode = $2;
		if ($pname && $self->{package_as_schema} && !$self->{is_mysql} && !$self->{is_mssql}) {
			$tname = $self->quote_object_name("$pname$sep$tname");
		} else {
			$tname = $self->quote_object_name($tname);
		}
		$create_type .= "DROP TYPE $self->{pg_supports_ifexists} $tname;\n";
		$create_type .= "CREATE TYPE $tname AS ($tcode);\n";
	}
	while ($fct_detail{declare} =~ s/\s+TYPE\s+([^\s]+)\s+(AS|IS)\s*(VARRAY|VARYING ARRAY)\s*\((\d+)\)\s*OF\s*([^;]+);//is) {
		my $type_name = $1;
		my $size = $4;
		my $tbname = $5;
		$tbname =~ s/\s+NOT\s+NULL//g;
		chomp($tbname);
		$type_name =~ s/"//g;
		my $internal_name = $type_name;
		if ($pname && $self->{package_as_schema} && !$self->{is_mysql} && !$self->{is_mssql}) {
			$type_name = $self->quote_object_name("$pname$sep$type_name");
		} elsif ($self->{export_schema} && !$self->{schema} && $owner) {
			$type_name = $self->quote_object_name("$owner.$type_name");
		}
		$internal_name  =~ s/^[^\.]+\.//;
		my $declar = $self->_replace_sql_type($tbname);
		$declar =~ s/[\n\r]+//s;
		$create_type .= "DROP TYPE $self->{pg_supports_ifexists} $type_name;\n";
		$create_type .= "CREATE TYPE $type_name AS ($internal_name $declar\[$size\]);\n";
	}
	
	my @at_ret_param = ();
	my @at_ret_type = ();
	my $at_suffix = '';
	my $at_inout = 0;

	if ($fct_detail{declare} =~ s/\s*(PRAGMA\s+AUTONOMOUS_TRANSACTION[\s;]*)/-- $1/is && $self->{autonomous_transaction})
	{
		$at_suffix = '_atx';
		# COMMIT is not allowed in PLPGSQL function
		$fct_detail{code} =~ s/\bCOMMIT\s*;//;
		# Remove the pragma when a conversion is done
		$fct_detail{declare} =~ s/--\s+PRAGMA\s+AUTONOMOUS_TRANSACTION[\s;]*//is;
		my @tmp = split(',', $fct_detail{args});
		$tmp[0] =~ s/^\(//;
		$tmp[-1] =~ s/\)$//;
		foreach my $p (@tmp)
		{
			if ($p =~ s/\bOUT\s+//)
			{
				$at_inout++;
				push(@at_ret_param, $p);
				push(@at_ret_type, $p);
			}
			elsif ($p =~ s/\bINOUT\s+//)
			{
				$at_inout++;
				push(@at_ret_param, $p);
				push(@at_ret_type, $p);
			}
		}
		map { s/^(.*?) //; } @at_ret_type;
		if ($fct_detail{hasreturn} && $#at_ret_param < 0)
		{
			push(@at_ret_param, 'ret ' . $fct_detail{func_ret_type});
			push(@at_ret_type, $fct_detail{func_ret_type});
		}
		map { s/^\s+//; } @at_ret_param;
		map { s/\s+$//; } @at_ret_param;
		map { s/^\s+//; } @at_ret_type;
		map { s/\s+$//; } @at_ret_type;
	}

	my $name = $fname;
	my $type = $fct_detail{type};
	$type = 'FUNCTION' if (!$self->{pg_supports_procedure});

	my $function = "\n$create_type\n\n${fct_warning}CREATE$self->{create_or_replace} $type $fname$at_suffix $fct_detail{args}";
	if (!$pname || !$self->{package_as_schema})
	{
		if ($self->{export_schema} && !$self->{schema})
		{
			if ($owner && !$self->{pg_schema}) {
				$function = "\n$create_type\n\n${fct_warning}CREATE$self->{create_or_replace} $type " . $self->quote_object_name("$owner.$fname$at_suffix") . " $fct_detail{args}";
				$name =  $self->quote_object_name("$owner.$fname");
				$self->logit("Parsing function " . $self->quote_object_name("$owner.$fname") . "...\n", 1);
			} else {
				$function = "\n$create_type\n\n${fct_warning}CREATE$self->{create_or_replace} $type " . $self->quote_object_name("$fname$at_suffix") . " $fct_detail{args}";
				$name =  $self->quote_object_name("$fname");
				$self->logit("Parsing function " . $self->quote_object_name("$fname") . "...\n", 1);
			}
		}
		elsif ($self->{export_schema} && $self->{schema})
		{
			if (!$self->{pg_schema})
			{
				$function = "\n$create_type\n\n${fct_warning}CREATE$self->{create_or_replace} $type " . $self->quote_object_name("$self->{schema}.$fname$at_suffix") . " $fct_detail{args}";
				$name =  $self->quote_object_name("$self->{schema}.$fname");
				$self->logit("Parsing function " . $self->quote_object_name("$self->{schema}.$fname") . "...\n", 1);
			}
			else
			{
				$function = "\n$create_type\n\n${fct_warning}CREATE$self->{create_or_replace} $type " . $self->quote_object_name("$fname$at_suffix") . " $fct_detail{args}";
				$name =  $self->quote_object_name("$fname");
				$self->logit("Parsing function " . $self->quote_object_name("$fname") . "...\n", 1);
			}
		}
	}
	else
	{
		$self->logit("Parsing function $fname...\n", 1);
	}

	# Create a wrapper for the function if we found an autonomous transaction
	my $at_wrapper = '';
	if ($at_suffix && !$self->{pg_background})
	{
		$at_wrapper = qq{
$search_path
--
-- dblink wrapper to call function $name as an autonomous transaction
--
CREATE EXTENSION IF NOT EXISTS dblink;

};
		$at_wrapper .= "CREATE$self->{create_or_replace} $type $name $fct_detail{args}$func_return";
		my $params = '';
		if ($#{$fct_detail{at_args}} >= 0)
		{
			map { s/(.+)/quote_nullable($1)/; }  @{$fct_detail{at_args}};
			$params = " ' || " . join(" || ',' || ", @{$fct_detail{at_args}}) . " || ' ";
		}
		my $dblink_conn = $self->{dblink_conn} || "'port=5432 dbname=testdb host=localhost user=pguser password=pgpass'";
		$at_wrapper .= qq{DECLARE
	-- Change this to reflect the dblink connection string
	v_conn_str  text := $dblink_conn;
	v_query     text;
};
		my $call_str = 'SELECT * FROM';
		$call_str = 'CALL' if (uc($type) eq 'PROCEDURE');

		if ($#at_ret_param == 0)
		{
			my $varname = $at_ret_param[0];
			$varname =~ s/\s+.*//;
			my $vartype = $at_ret_type[0];
			$vartype =~ s/.*\s+//;
			if (!$fct_detail{hasreturn})
			{
				$at_wrapper .= qq{
BEGIN
	v_query := 'CALL $fname$at_suffix ($params)';
	SELECT v_ret INTO $varname FROM dblink(v_conn_str, v_query) AS p (v_ret $vartype);
};
			}
			else
			{
				$at_ret_type[0] = $fct_detail{func_ret_type};
				$at_ret_param[0] = 'ret ' . $fct_detail{func_ret_type};
				$at_wrapper .= qq{
	v_ret	$at_ret_type[0];
BEGIN
	v_query := 'SELECT * FROM $fname$at_suffix ($params)';
	SELECT * INTO v_ret FROM dblink(v_conn_str, v_query) AS p ($at_ret_param[0]);
	RETURN v_ret;
};
			}
		}
		elsif ($#at_ret_param > 0)
		{
			my $varnames = '';
			my $vartypes = '';
			for (my $i = 0; $i <= $#at_ret_param; $i++)
			{
				my $v = $at_ret_param[$i];
				$v =~ s/\s+.*//;
				$varnames .= "$v, ";
				$vartypes .= "v_ret$i ";
				my $t = $at_ret_type[$i];
				$t =~ s/.*\s+//;
				$vartypes .= "$t, ";
			}
			$varnames =~ s/, $//;
			$vartypes =~ s/, $//;
			if (!$fct_detail{hasreturn})
			{
				$at_wrapper .= qq{
BEGIN
	v_query := 'CALL $fname$at_suffix ($params)';
	SELECT * FROM dblink(v_conn_str, v_query) AS p ($vartypes) INTO $varnames;
};
			}
			else
			{
				$at_ret_type[0] = $fct_detail{func_ret_type};
				$at_ret_param[0] = 'ret ' . $fct_detail{func_ret_type};
				$at_wrapper .= qq{
	v_ret	$at_ret_type[0];
BEGIN
	v_query := 'SELECT * FROM $fname$at_suffix ($params)';
	SELECT * INTO v_ret FROM dblink(v_conn_str, v_query) AS p ($at_ret_param[0]);
	RETURN v_ret;
};
			}
		}
		elsif (!$fct_detail{hasreturn})
		{
			$at_wrapper .= qq{
BEGIN
	v_query := 'CALL $fname$at_suffix ($params)';
	PERFORM * FROM dblink(v_conn_str, v_query) AS p (ret boolean);
};
		}
		else
		{
			print STDERR "WARNING: we should not be there, please send the Oracle code of the $self->{type} to the author for debuging.\n";
		}
		$at_wrapper .= qq{
END;
\$body\$ LANGUAGE plpgsql SECURITY DEFINER;
};

	}
	elsif ($at_suffix && $self->{pg_background})
	{
		$at_wrapper = qq{
$search_path
--
-- pg_background wrapper to call function $name as an autonomous transaction
--
CREATE EXTENSION IF NOT EXISTS pg_background;

};
		$at_wrapper .= "CREATE$self->{create_or_replace} $type $name $fct_detail{args}$func_return";
		my $params = '';
		if ($#{$fct_detail{at_args}} >= 0)
		{
			map { s/(.+)/quote_nullable($1)/; }  @{$fct_detail{at_args}};
			$params = " ' || " . join(" || ',' || ", @{$fct_detail{at_args}}) . " || ' ";
		}

		$at_wrapper .= qq{
DECLARE
	v_query     text;
};
		if (!$fct_detail{hasreturn})
		{
			$at_wrapper .= qq{
BEGIN
	v_query := 'SELECT true FROM $fname$at_suffix ($params)';
	PERFORM * FROM pg_background_result(pg_background_launch(v_query)) AS p (ret boolean);
};
		}
		elsif ($#at_ret_param == 0)
		{
			my $prm = join(',', @at_ret_param);
			$at_wrapper .= qq{
	v_ret	$at_ret_type[0];
BEGIN
	v_query := 'SELECT * FROM $fname$at_suffix ($params)';
	SELECT * INTO v_ret FROM pg_background_result(pg_background_launch(v_query)) AS p ($at_ret_param[0]);
	RETURN v_ret;
};
		}
		$at_wrapper .= qq{
END;
\$body\$ LANGUAGE plpgsql SECURITY DEFINER;
};


	}

	# Add the return part of the function declaration
	$function .= $func_return;
	if ($fct_detail{immutable}) {
		$fct_detail{immutable} = ' IMMUTABLE';
	} elsif ($plsql =~  /^FUNCTION/i)
	{
		# Oracle function can't modify data so always mark them as stable
		if ($self->{function_stable}) {
			$fct_detail{immutable} = ' STABLE';
		}
	}
	if ($language && ($language !~ /SQL/i)) {
		$function .= "AS '$fct_detail{library}', '$fct_detail{library_fct}'\nLANGUAGE $language$fct_detail{immutable};\n";
		$function =~ s/AS \$body\$//;
	}

	my $revoke = '';
	if ($fct_detail{code})
	{
		$fct_detail{declare} = '' if ($fct_detail{declare} !~ /[a-z]/is);
		$fct_detail{declare} =~ s/^\s*DECLARE//i;
		$fct_detail{declare} .= ';' if ($fct_detail{declare} && $fct_detail{declare} !~ /;\s*$/s && $fct_detail{declare} !~ /\%ORA2PG_COMMENT\d+\%\s*$/s);
		my $code_part = '';
		$code_part .= "DECLARE\n$fct_detail{declare}\n" if ($fct_detail{declare});
		$fct_detail{code} =~ s/^BEGIN\b//is;
		$code_part .= "BEGIN" . $fct_detail{code};
		# Replace PL/SQL code into PL/PGSQL similar code
		$function .= Ora2Pg::PLSQL::convert_plsql_code($self, $code_part);
		$function .= ';' if ($function !~ /END\s*;\s*$/is && $fct_detail{code} !~ /\%ORA2PG_COMMENT\d+\%\s*$/);
		$function .= "\n\$body\$\nLANGUAGE PLPGSQL\n";

		# Fix RETURN call when the function has OUT parameters
		if ($out_return)
		{
			$self->_remove_text_constant_part(\$function);
			$function =~ s/(\s+)RETURN\s*(\([^;]+\))\s*;/$1extra_param := $2;$1RETURN;/igs;
			$function =~ s/(\s+)RETURN\s+([^;]+);/$1extra_param := $2;$1RETURN;/igs;
			$self->_restore_text_constant_part(\$function);
		}
		$revoke = "-- REVOKE ALL ON $type $name $fct_detail{args} FROM PUBLIC;";
		if ($at_suffix) {
			$revoke .= "\n-- REVOKE ALL ON $type $name$at_suffix $fct_detail{args} FROM PUBLIC;";
		}
		$revoke =~ s/[\n\r]+\s*/ /gs;
		$revoke .= "\n";
		if ($self->{force_security_invoker}) {
			$function .= "SECURITY INVOKER\n";
		}
		else
		{
			if ($self->{type} ne 'PACKAGE')
			{
				if (!$self->{is_mysql}) {
					# A SECURITY DEFINER procedure cannot execute transaction control statements
					$function .= "SECURITY DEFINER\n" if ($self->{security}{"\U$fct_detail{name}\E"}{security} eq 'DEFINER' && $fct_detail{code} !~ /\b(COMMIT|ROLLBACK)\s*;/i);
				} else  {
					$function .= "SECURITY DEFINER\n" if ($fct_detail{security} eq 'DEFINER');
				}
			}
			else
			{
				# A SECURITY DEFINER procedure cannot execute transaction control statements
				$function .= "SECURITY DEFINER\n" if ($self->{security}{"\U$pname\E"}{security} eq 'DEFINER' && $fct_detail{code} !~ /\b(COMMIT|ROLLBACK)\s*;/i);
			}
		}
		$fct_detail{immutable} = '' if ($fct_detail{code} =~ /\b(UPDATE|INSERT|DELETE|CALL)\b/is);
		$function .= "$fct_detail{immutable};\n";
		$function = "\n$fct_detail{before}$function";
	}

	if ($self->{force_owner})
	{
		$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
		if ($owner)
		{
			$function .= "ALTER $type $name $fct_detail{args} OWNER TO";
			$function .= " " . $self->quote_object_name($owner) . ";\n";
			if ($at_suffix)
			{
				$function .= "ALTER $type $name$at_suffix $fct_detail{args} OWNER TO";
				$function .= " " . $self->quote_object_name($owner) . ";\n";
			}
		}
	}
	my $act_type = $type;
	$act_type = 'FUNCTION' if (!$self->{pg_supports_procedure});
	$function .= "\nCOMMENT ON $act_type $name$at_suffix $fct_detail{args} IS $fct_detail{comment};\n" if ($fct_detail{comment});
	$function .= $revoke;
	$function = $at_wrapper . $function;

	$fname =~ s/"//g; # Remove case sensitivity quoting
	$fname =~ s/^$pname\.//i; # remove package name
	if ($pname && $self->{file_per_function})
	{
		$self->logit("\tDumping to one file per function: $dirprefix\L$pname/$fname\E_$self->{output}\n", 1);
		my $sql_header = "-- Generated by Ora2Pg, the Oracle database Schema converter, version $VERSION\n";
		$sql_header .= "-- Copyright 2000-2025 Gilles DAROLD. All rights reserved.\n";
		$sql_header .= "-- DATASOURCE: $self->{oracle_dsn}\n\n";
		if ($self->{client_encoding}) {
			$sql_header .= "SET client_encoding TO '\U$self->{client_encoding}\E';\n";
		}
		$sql_header .= $self->set_search_path(undef, $pname);
		$sql_header .= "SET check_function_bodies = false;\n\n" if (!$self->{function_check});
		$sql_header = '' if ($self->{no_header});

		if ($self->{print_dependencies} && $self->{plsql_pgsql} && !$self->{no_function_metadata})
		{
			# look for other routines call in the stored procedure
			foreach my $sch (sort keys %{ $self->{function_metadata} })
			{
				foreach my $pkg_name (sort keys %{ $self->{function_metadata}{$sch} })
				{
					foreach my $fct_name (sort keys %{ $self->{function_metadata}{$sch}{$pkg_name} })
					{
						next if ($fct_name =~ /^$fname$/i || $fct_name =~ /^.*\.$fname$/i);
						if ($fct_detail{code} =~ /\b$fct_name\b/is) {
							push(@{ $self->{object_dependencies}{uc("$pname.$fname")}{routines} }, uc("$sch.$fct_name"));
						}
					}
				}
			}
			# Look for merge/insert/update/delete
			@{ $self->{object_dependencies}{uc("$pname.$fname")}{merge} } = $function =~ /\bMERGE\s+INTO\s+([^\(\s;,]+)/igs;
			@{ $self->{object_dependencies}{uc("$pname.$fname")}{insert} } = $function =~ /\bINSERT\s+INTO\s+([^\(\s;,]+)/igs;
			@{ $self->{object_dependencies}{uc("$pname.$fname")}{update} } = $function =~ /(?:(?!FOR).)*?\s*\bUPDATE\s+([^\s;,]+)\s+/igs;
			@{ $self->{object_dependencies}{uc("$pname.$fname")}{delete} } = $function =~ /\b(?:DELETE\s+FROM|TRUNCATE\s+TABLE)\s+([^\s;,]+)\s+/igs;
		}

		my $fhdl = $self->open_export_file("$dirprefix\L$pname/$fname\E_$self->{output}", 1);
		$self->set_binmode($fhdl) if (!$self->{compress});
		$self->_restore_comments(\$function);
		$self->normalize_function_call(\$function);
		$function =~ s/(-- REVOKE ALL ON (?:FUNCTION|PROCEDURE) [^;]+ FROM PUBLIC;)/&remove_newline($1)/sge;
		$self->dump($sql_header . $function, $fhdl);
		$self->close_export_file($fhdl);
		my $f = "$dirprefix\L$pname/$fname\E_$self->{output}";
		$f = "\L$pname/$fname\E_$self->{output}" if ($self->{psql_relative_path});
		$f =~ s/\.(?:gz|bz2)$//i;
		$function = "\\i$self->{psql_relative_path} $f\n";
		$self->save_filetoupdate_list(lc($pname), lc($fname), "$dirprefix\L$pname/$fname\E_$self->{output}");
		return $function;
	} elsif ($pname) {
		$self->save_filetoupdate_list(lc($pname), lc($fname), "$dirprefix$self->{output}");
	}

	$function =~ s/\r//gs;
	$function =~ s/\bEND[\s;]+;/END;/is;
	my @lines = split(/\n/, $function);
	map { s/^\/$//; } @lines;

	return join("\n", @lines);
}

=head2 _convert_declare

This function is used to rewrite Oracle FUNCTION declaration code
to PostgreSQL. Called only if PLSQL_PGSQL configuration directive
is set to 1.

=cut

sub _convert_declare
{
	my ($self, $declare) = @_;

	$declare =~ s/\s+$//s;

	return if (!$declare);

	my @allwithcomments = split(/(\%ORA2PG_COMMENT\d+\%\n*)/s, $declare);
	for (my $i = 0; $i <= $#allwithcomments; $i++) {
		next if ($allwithcomments[$i] =~ /ORA2PG_COMMENT/);
		my @pg_declare = ();
		foreach my $tmp_var (split(/;/,$allwithcomments[$i])) {
			# Not cursor declaration
			if ($tmp_var !~ /\bcursor\b/is) {
				# Extract default assignment
				my $tmp_assign = '';
				if ($tmp_var =~ s/\s*(:=|DEFAULT)(.*)$//is) {
					$tmp_assign = " $1$2";
				}
				# Extract variable name and type
				my $tmp_pref = '';
				my $tmp_name = '';
				my $tmp_type = '';
				if ($tmp_var =~ /(\s*)([^\s]+)\s+(.*?)$/s) {
					$tmp_pref = $1;
					$tmp_name = $2;
					$tmp_type = $3;
					$tmp_type =~ s/\s+//gs;
					if ($tmp_type =~ /([^\(]+)\(([^\)]+)\)/) {
						my $type_name = $1;
						my ($prec, $scale) = split(/,/, $2);
						$scale ||= 0;
						my $len = $prec;
						$prec = 0 if (!$scale);
						$len =~ s/\D//g;
						$tmp_type = $self->_sql_type($type_name,$len,$prec,$scale,$tmp_assign);
					} else {
						$tmp_type = $self->_sql_type($tmp_type);
					}
					push(@pg_declare, "$tmp_pref$tmp_name $tmp_type$tmp_assign;");
				}
			} else {
				push(@pg_declare, "$tmp_var;");
			}
		}
		$allwithcomments[$i] = join("", @pg_declare);
	}

	return join("", @allwithcomments);
}


=head2 _format_view

This function is used to rewrite Oracle VIEW declaration code
to PostgreSQL.

=cut

sub _format_view
{
	my ($self, $view, $sqlstr) = @_;

	$self->_remove_comments(\$sqlstr);

	# Retrieve the column part of the view to remove double quotes
	if (!$self->{preserve_case} && $sqlstr =~ s/^(.*?)\bFROM\b/FROM/is) {
		my $tmp = $1;
		$tmp =~ s/"//gs;
		$sqlstr = $tmp . $sqlstr;
	}
	
	my @tbs = ();
	# Retrieve all tbs names used in view if possible
	if ($sqlstr =~ /\bFROM\b(.*)/is) {
		my $tmp = $1;
		$tmp =~  s/\%ORA2PG_COMMENT\d+\%//gs;
		$tmp =~ s/\s+/ /gs;
		$tmp =~ s/\bWHERE.*//is;
		# Remove all SQL reserved words of FROM STATEMENT
		$tmp =~ s/(LEFT|RIGHT|INNER|OUTER|NATURAL|CROSS|JOIN|\(|\))//igs;
		# Remove all ON join, if any
		$tmp =~ s/\bON\b[A-Z_\.\s]*=[A-Z_\.\s]*//igs;
		# Sub , with whitespace
		$tmp =~ s/,/ /g;
		my @tmp_tbs = split(/\s+/, $tmp);
		foreach my $p (@tmp_tbs) {
			push(@tbs, $p) if ($p =~ /^[A-Z_0-9\$]+$/i);
		}
	}
	foreach my $tb (@tbs) {
		next if (!$tb);
		my $regextb = $tb;
		$regextb =~ s/\$/\\\$/g;
		if (!$self->{preserve_case}) {
			# Escape column name
			$sqlstr =~ s/["']*\b$regextb\b["']*\.["']*([A-Z_0-9\$]+)["']*(,?)/$tb.$1$2/igs;
			# Escape table name
			$sqlstr =~ s/(^=\s?)["']*\b$regextb\b["']*/$tb/igs;
		} else {
			# Escape column name
			$sqlstr =~ s/["']*\b${regextb}["']*\.["']*([A-Z_0-9\$]+)["']*(,?)/"$tb"."$1"$2/igs;
			# Escape table name
			$sqlstr =~ s/(^=\s?)["']*\b$regextb\b["']*/"$tb"/igs;
			if ($tb =~ /(.*)\.(.*)/) {
				my $prefx = $1;
				my $sufx = $2;
				$sqlstr =~ s/"$regextb"/"$prefx"\."$sufx/g;
			}
		}
	}

	# replace column name in view query definition if needed
	foreach my $c (sort { $b cmp $a } keys %{ $self->{replaced_cols}{"\L$view\E"} })
	{
		my $nm = $self->{replaced_cols}{"\L$view\E"}{$c};
		$sqlstr =~ s/([\(,\s\."])$c([,\s\.:"\)])/$1$nm$2/ig;
	}

	if ($self->{plsql_pgsql}) {
		$sqlstr = Ora2Pg::PLSQL::convert_plsql_code($self, $sqlstr);
	}

	$self->_restore_comments(\$sqlstr);

	return $sqlstr;
}

=head2 randpattern

This function is used to replace the use of perl module String::Random
and is simply a cut & paste from this module.

=cut

sub randpattern
{
	my $patt = shift;

	my $string = '';

	my @upper=("A".."Z");
	my @lower=("a".."z");
	my @digit=("0".."9");
	my %patterns = (
	    'C' => [ @upper ],
	    'c' => [ @lower ],
	    'n' => [ @digit ],
	);
	for my $ch (split(//, $patt)) {
		if (exists $patterns{$ch}) {
			$string .= $patterns{$ch}->[int(rand(scalar(@{$patterns{$ch}})))];
		} else {
			$string .= $ch;
		}
	}

	return $string;
}

=head2 logit

This function log information to STDOUT or to a logfile
following a debug level. If critical is set, it dies after
writing to log.

=cut

sub logit
{
	my ($self, $message, $level, $critical) = @_;

	# Assessment report are dumped to stdin so avoid printing debug info
	return if (!$critical && $self->{type} eq 'SHOW_REPORT');

	$level ||= 0;

	$message = '[' . strftime("%Y-%m-%d %H:%M:%S", localtime(time)) . '] ' . $message if ($self->{debug});
	if ($self->{debug} >= $level) {
		if (defined $self->{fhlog}) {
			$self->{fhlog}->print($message);
		} else {
			print $message;
		}
	}
	if ($critical)
	{
		if ($self->{debug} < $level)
		{
			if (defined $self->{fhlog}) {
				$self->{fhlog}->print($message);
			} else {
				print "$message\n";
			}
		}
		$self->{fhlog}->close() if (defined $self->{fhlog});
		$self->{dbh}->disconnect() if ($self->{dbh});
		$self->{dbhdest}->disconnect() if ($self->{dbhdest});
		die "Aborting export...\n";
	}
}

=head2 logrep

This function log report's information to STDOUT or to a logfile.

=cut

sub logrep
{
	my ($self, $message) = @_;

	if (defined $self->{fhlog}) {
		$self->{fhlog}->print($message);
	} else {
		print $message;
	}
}


=head2 _convert_type

This function is used to rewrite Oracle TYPE DDL

=cut

sub _convert_type
{
	my ($self, $plsql, $owner, %pkg_type) = @_;

        my ($package, $filename, $line) = caller;

	my $unsupported = "-- Unsupported, please edit to match PostgreSQL syntax\n";
	my $content = '';
	my $type_name = '';

	$plsql =~ s/AUTHID DEFINER//is;

	# Replace SUBTYPE declaration into DOMAIN declaration
        if ($plsql =~ s/SUBTYPE\s+/CREATE DOMAIN /i)
	{
		$plsql =~ s/\s+IS\s+/ AS /;
		$plsql =~ s/^CREATE TYPE/TYPE/i;
		$plsql = $self->_replace_sql_type($plsql);
		return $plsql;
	}

	$plsql =~ s/\s*INDEX\s+BY\s+([^\s;]+)//is;
	$plsql =~ s/TYPE BODY \w+ AS.+?\nEND;//is; # Remove BODY
	$plsql =~ s/ +, CONSTRUCTOR FUNCTION [^\n]+ RETURN self AS RESULT\n//i; # Remove Constructor

	if ($plsql =~ /TYPE\s+([^\s]+)\s+(IS|AS)\s+TABLE\s+OF\s+(.*)/is)
	{
		$type_name = $1;
		my $type_of = $3;
		$type_name =~ s/"//g;
		if ($self->{export_schema} && !$self->{schema} && $owner && $type_name !~ /\./) {
			$type_name = "$owner.$type_name";
		}
		my $internal_name = $type_name;
		$internal_name  =~ s/^[^\.]+\.//;
		$type_of =~ s/\s*\(\s*/\(/;
		$type_of =~ s/\s*\)\s*/\)/;
		$type_of =~ s/\s*NOT[\t\s]+NULL//is;
		$type_of =~ s/\s*;\s*$//s;
		$type_of =~ s/^\s+//s;
		if ($type_of !~ /\s/s
				|| $type_of =~ /VARCHAR2\(\d+ (CHAR|BYTE)\)/ # workaround for VARCHAR2 with type
		)
		{ 
			$type_of = $self->_replace_sql_type($type_of);
			$self->{type_of_type}{'Nested Tables'}++;
			$content .= "DROP TYPE $self->{pg_supports_ifexists} " . $self->quote_object_name($type_name) . ";\n" if ($self->{drop_if_exists});
			$content .= "CREATE TYPE " . $self->quote_object_name($type_name) . " AS (" . $self->quote_object_name($internal_name) . " $type_of\[\]);\n";
		}
		else
		{
			$self->{type_of_type}{'Associative Arrays'}++;
			$self->logit("WARNING: this kind of Nested Tables are not supported, skipping type $1\n", 1);
			return "${unsupported}CREATE$self->{create_or_replace} $plsql";
		}
	}
	elsif ($plsql =~ /TYPE\s+([^\s]+)\s+(AS|IS)\s*REF\s+CURSOR/is)
	{
		$self->logit("WARNING: TYPE REF CURSOR are not supported, skipping type $1\n", 1);
		$plsql =~ s/\bREF\s+CURSOR/REFCURSOR/is;
		$self->{type_of_type}{'Type Ref Cursor'}++;
		return "${unsupported}CREATE$self->{create_or_replace} $plsql";
	}
	elsif ($plsql =~ /TYPE\s+([^\s]+)\s+(AS|IS)\s*OBJECT\s*\((.*?)(TYPE BODY.*)/is)
	{
		$self->{type_of_type}{'Type Boby'}++;
		$self->logit("WARNING: TYPE BODY are not supported, skipping type $1\n", 1);
		return "${unsupported}CREATE$self->{create_or_replace} $plsql";
	}
	elsif ($plsql =~ /TYPE\s+([^\s]+)\s+(AS|IS)\s*(?:OBJECT|RECORD)\s*\((.*)\)([^\)]*)/is)
	{
		$type_name = $1;
		my $description = $3;
		my $notfinal = $4;
		$notfinal =~ s/\s+/ /gs;
		if ($description =~ /\s*(MAP MEMBER|MEMBER|CONSTRUCTOR)\s+(FUNCTION|PROCEDURE).*/is)
		{
			$self->{type_of_type}{'Type with member method'}++;
			$self->logit("WARNING: TYPE with CONSTRUCTOR and MEMBER FUNCTION are not supported, skipping type $type_name\n", 1);
			return "${unsupported}CREATE$self->{create_or_replace} $plsql";
		}
		$description =~ s/^\s+//s;
		my $declar = $self->_replace_sql_type($description);
		$type_name =~ s/"//g;
		if ($self->{export_schema} && !$self->{schema} && $owner && $type_name !~ /\./) {
			$type_name = "$owner.$type_name";
		}
		if ($notfinal =~ /FINAL/is)
		{
			$content = "-- Inherited types are not supported in PostgreSQL, replacing with inherited table\n";
			$content .= "CREATE TABLE " . $self->quote_object_name($type_name) . qq{ (
$declar
);
};
			$self->{type_of_type}{'Type inherited'}++;
		}
		else
		{
			$content = "DROP TYPE $self->{pg_supports_ifexists} " . $self->quote_object_name($type_name) . ";\n" if ($self->{drop_if_exists}); # add optional DROP
			$content .= "CREATE TYPE " . $self->quote_object_name($type_name) . " AS (";
			$content .= qq{
$declar
);
};
			$self->{type_of_type}{'Object type'}++;
		}
	}
	elsif ($plsql =~ /TYPE\s+([^\s]+)\s+UNDER\s*([^\s]+)\s+\((.*)\)([^\)]*)/is)
	{
		$type_name = $1;
		my $type_inherit = $2;
		my $description = $3;
		if ($description =~ /\s*(MAP MEMBER|MEMBER|CONSTRUCTOR)\s+(FUNCTION|PROCEDURE).*/is) {
			$self->logit("WARNING: TYPE with CONSTRUCTOR and MEMBER FUNCTION are not supported, skipping type $type_name\n", 1);
			$self->{type_of_type}{'Type with member method'}++;
			return "${unsupported}CREATE$self->{create_or_replace} $plsql";
		}
		$description =~ s/^\s+//s;
		my $declar = $self->_replace_sql_type($description);
		$type_name =~ s/"//g;
		if ($self->{export_schema} && !$self->{schema} && $owner && $type_name !~ /\./) {
			$type_name = "$owner.$type_name";
		}
		$content = "CREATE TABLE " . $self->quote_object_name($type_name) . " (";
		$content .= qq{
$declar
) INHERITS (\L$type_inherit\E);
};
		$self->{type_of_type}{'Subtype'}++;
	}
	elsif ($plsql =~ /TYPE\s+([^\s]+)\s+(AS|IS)\s*(VARRAY|VARYING ARRAY)\s*\((\d+)\)\s*OF\s*(.*)/is)
	{
		$type_name = $1;
		my $size = $4;
		my $tbname = $5;
		$type_name =~ s/"//g;
		if ($self->{export_schema} && !$self->{schema} && $owner && $type_name !~ /\./) {
			$type_name = "$owner.$type_name";
		}
		$tbname =~ s/;//g;
		$tbname =~ s/\s+NOT\s+NULL//g;
		my $internal_name = $type_name;
		chomp($tbname);
		$internal_name  =~ s/^[^\.]+\.//;
		my $declar = $self->_replace_sql_type($tbname);
		$declar =~ s/[\n\r]+//s;
		$content = "CREATE TYPE " . $self->quote_object_name($type_name) . " AS (" . $self->quote_object_name($internal_name) . " $declar\[$size\]);\n";
		$self->{type_of_type}{Varrays}++;
	}
	elsif ($plsql =~ /TYPE\s+([^\s]+)\s+FROM\s+(.*)( NOT NULL)?;/is)
	{
		my $typname = $1;
		my $notnull = $3;
		my $dtype = $self->_replace_sql_type($2);
		$content .= "CREATE DOMAIN $typname AS $dtype$notnull;\n";
	}
	else
	{
		$self->{type_of_type}{Unknown}++;
		$plsql =~ s/;$//s;
		$content = "${unsupported}CREATE$self->{create_or_replace} $plsql;"
	}

	if ($self->{force_owner})
	{
		$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
		if ($owner)
		{
			$content .= "ALTER TYPE " . $self->quote_object_name($type_name)
					. " OWNER TO " . $self->quote_object_name($owner) . ";\n";
		}
	}

	# Prefix type with their own package name
	foreach my $t (keys %pkg_type) {
		$content =~ s/(\s+)($t)\b/$1$pkg_type{$2}/igs;
	}

	return $content;
}

sub ask_for_data
{
	my ($self, $table, $cmd_head, $cmd_foot, $s_out, $nn, $tt, $sprep, $stt, $part_name, $is_subpart) = @_;

	# Build SQL query to retrieve data from this table
	if (!$part_name) {
		$self->logit("Looking how to retrieve data from $table...\n", 1);
	} elsif ($is_subpart) {
		$self->logit("Looking how to retrieve data from $table subpartition $part_name...\n", 1);
	} else {
		$self->logit("Looking how to retrieve data from $table partition $part_name...\n", 1);
	}
	my $query = $self->_howto_get_data($table, $nn, $tt, $stt, $part_name, $is_subpart);

	# Query with no column 
	if (!$query) {
		$self->logit("WARNING: can not extract data from $table, no column found...\n", 0);
		return 0;
	}

	# Check for boolean rewritting
	for (my $i = 0; $i <= $#{$nn}; $i++)
	{
		my $colname = $nn->[$i]->[0];
		$colname =~ s/["`]//g;
		my $typlen = $nn->[$i]->[5];
		$typlen ||= $nn->[$i]->[2];
		# Check if this column should be replaced by a boolean following table/column name
		if (grep(/^$colname$/i, @{$self->{'replace_as_boolean'}{uc($table)}})) {
			$tt->[$i] = 'boolean';
		}
		# Check if this column should be replaced by a boolean following type/precision
		elsif (exists $self->{'replace_as_boolean'}{uc($nn->[$i]->[1])} && ($self->{'replace_as_boolean'}{uc($nn->[$i]->[1])}[0] == $typlen)) {
			$tt->[$i] = 'boolean';
		}
	}

	# check if destination column type must be changed
	for (my $i = 0; $i <= $#{$nn}; $i++)
	{
		my $colname = $nn->[$i]->[0];
		$colname =~ s/["`]//g;
		$tt->[$i] = $self->{'modify_type'}{"\L$table\E"}{"\L$colname\E"} if (exists $self->{'modify_type'}{"\L$table\E"}{"\L$colname\E"});
	}

	# Look for user defined type
	if (!$self->{is_mysql})
	{
		for (my $idx = 0; $idx < scalar(@$stt); $idx++)
		{
			my $data_type = uc($stt->[$idx]) || '';
			$data_type =~ s/\(.*//; # remove any precision
			# in case of user defined type try to gather the underlying base types
			if (!exists $self->{data_type}{$data_type} && !exists $self->{user_type}{$data_type}
				       	&& $data_type !~ /SDO_GEOMETRY/i
				       	&& $data_type !~ /^(ST_|STGEOM_)/i #ArGis geometry types
			) {
				%{ $self->{user_type}{$data_type} } = $self->custom_type_definition($data_type);
			}
		}
	}

	if ( ($self->{oracle_copies} > 1) && $self->{defined_pk}{"\L$table\E"} )
	{
		$self->{ora_conn_count} = 0;
		while ($self->{ora_conn_count} < $self->{oracle_copies})
		{
			spawn sub {
				$self->logit("Creating new connection to database to extract data...\n", 1);
				$self->_extract_data($query, $table, $cmd_head, $cmd_foot, $s_out, $nn, $tt, $sprep, $stt, $part_name, $self->{ora_conn_count});
			};
			$self->{ora_conn_count}++;
		}
		# Wait for oracle connection terminaison
		while ($self->{ora_conn_count} > 0)
		{
			my $kid = waitpid(-1, WNOHANG);
			if ($kid > 0)
			{
				$self->{ora_conn_count}--;
				delete $RUNNING_PIDS{$kid};
			}
			usleep(50000);
		}
		if (defined $pipe)
		{
			my $t_name = $part_name || $table;
			my $t_time = time();
			$pipe->print("TABLE EXPORT ENDED: $t_name, end: $t_time, report all parts\n");
		}
	}
	else
	{
		my $total_record = $self->_extract_data($query, $table, $cmd_head, $cmd_foot, $s_out, $nn, $tt, $sprep, $stt, $part_name);
		# Only useful for single process
		return $total_record;
	}

	return;
}

sub custom_type_definition
{
	my ($self, $custom_type, $parent, $is_nested) = @_;

	my %user_type = ();
	my $orig = $custom_type;

	my $data_type = uc($custom_type) || '';
	$data_type =~ s/\(.*//; # remove any precision
	if (!exists $self->{data_type}{$data_type})
	{
		if (!$is_nested) {
			$self->logit("Data type $custom_type is not native, searching on custom types.\n", 1);
		} else {
			$self->logit("\tData type $custom_type nested from type $parent is not native, searching on custom types.\n", 1);
		}
		$custom_type = $self->_get_types($custom_type);
		foreach my $tpe (sort {length($a->{name}) <=> length($b->{name}) } @{$custom_type})
		{
			$self->logit("\tLooking inside custom type $tpe->{name} to extract values...\n", 1);
			my %types_def = $self->_get_custom_types($tpe->{code});
			if ($#{$types_def{pg_types}} >= 0)
			{
				$self->logit("\tfound type description: $tpe->{name}(" . join(',', @{$types_def{pg_types}}) . ")\n", 1);
				push(@{$user_type{pg_types}} , \@{$types_def{pg_types}});
				push(@{$user_type{src_types}}, \@{$types_def{src_types}});
			}
			else
			{
				if ($tpe->{code} =~ /AS\s+VARRAY\s*(.*?)\s+OF\s+([^\s;]+);/is) {
					return $self->custom_type_definition(uc($2), $orig, 1);
				} elsif ($tpe->{code} =~ /(.*FROM\s+[^;\s\(]+)/is) {
					%types_def = $self->_get_custom_types($1);
					push(@{$user_type{pg_types}} , \@{$types_def{pg_types}});
					push(@{$user_type{src_types}}, \@{$types_def{src_types}});
				}
				elsif ($tpe->{code} =~ /\s+([^\s]+)\s+AS\s+TABLE\s+OF\s+([^;]+);/is)
				{
					%types_def = $self->_get_custom_types("varname $2");
					push(@{$user_type{pg_types}} , \@{$types_def{pg_types}});
					push(@{$user_type{src_types}}, \@{$types_def{src_types}});
				}
				else {
					$self->logit("\tCan not found subtype for $tpe->{name} into code: $tpe->{code}\n", 1);
				}
			}
		}
	}

	return %user_type;
}

sub _extract_data
{
	my ($self, $query, $table, $cmd_head, $cmd_foot, $s_out, $nn, $tt, $sprep, $stt, $part_name, $proc) = @_;

	$0 = "ora2pg - querying table $table";

	# Overwrite the query if REPLACE_QUERY is defined for this table
	if ($self->{replace_query}{"\L$table\E"})
	{
		$query = $self->{replace_query}{"\L$table\E"};
		if (($self->{oracle_copies} > 1) && $self->{defined_pk}{"\L$table\E"})
		{
			my $colpk = $self->{defined_pk}{"\L$table\E"};
			if ($self->{preserve_case}) {
				$colpk = '"' . $colpk . '"';
			}
			my $cond = " ABS(MOD($colpk, $self->{oracle_copies})) = ?";
			$cond = " ABS($colpk % $self->{oracle_copies}) = ?" if ($self->{is_mssql});
			if ($query !~ s/\bWHERE\s+/WHERE $cond AND /)
			{
				if ($query !~ s/\b(ORDER\s+BY\s+.*)/WHERE $cond $1/) {
					$query .= " WHERE $cond";
				}
			}
		}
	}

	my %user_type = ();
	my $rname = $part_name || $table;
	my $dbh = 0;
	my $sth = 0;
	my @has_custom_type = ();
	@{ $self->{data_cols}{$table} } = ();

	# Look for user defined type
	if (!$self->{is_mysql})
	{
		for (my $idx = 0; $idx < scalar(@$stt); $idx++)
		{
			my $data_type = uc($stt->[$idx]) || '';
			$data_type =~ s/\(.*//; # remove any precision
			# in case of user defined type try to gather the underlying base types
			if (!exists $self->{data_type}{$data_type} && exists $self->{user_type}{$stt->[$idx]})
			{
				push(@has_custom_type, $idx);
				%{ $user_type{$idx} } = %{ $self->{user_type}{$stt->[$idx]} };
			}
		}
	}

	if ( ($self->{parallel_tables} > 1) || (($self->{oracle_copies} > 1) && $self->{defined_pk}{"\L$table\E"}) )
	{
		$self->logit("DEBUG: cloning Oracle database connection.\n", 1);
		$dbh = $self->{dbh}->clone();

		# Force execution of initial command
		$self->_ora_initial_command($dbh);
		if (!$self->{is_mysql} && !$self->{is_mssql})
		{
			# Force numeric format into the cloned session
			$self->_numeric_format($dbh);
			# Force datetime format into the cloned session
			$self->_datetime_format($dbh);
			# Set the action name on Oracle side to see which table is exported
			$dbh->do("CALL DBMS_APPLICATION_INFO.set_action('$table')") or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
		}

		# Set row cache size
		$dbh->{RowCacheSize} = int($self->{data_limit}/10);
		if (exists $self->{local_data_limit}{$table}) {
			$dbh->{RowCacheSize} = $self->{local_data_limit}{$table};
		}

		# prepare the query before execution
		if ($self->{is_mysql})
		{
			$query =~ s/^SELECT\s+/SELECT \/\*\!40001 SQL_NO_CACHE \*\/ /s;
			$sth = $dbh->prepare($query, { mysql_use_result => 1, mysql_use_row => 1 }) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
		}
		elsif ($self->{is_mssql})
		{
			$sth = $dbh->prepare($query) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
			$sth->{'LongReadLen'} = $self->{longreadlen};
		}
		else
		{
			if (!$self->{use_lob_locator}) {
				$sth = $dbh->prepare($query,{ora_piece_lob => 1, ora_piece_size => $self->{longreadlen}, ora_exe_mode=>OCI_STMT_SCROLLABLE_READONLY, ora_check_sql => 1}) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
			} else {
				$sth = $dbh->prepare($query,{'ora_auto_lob' => 0, ora_exe_mode=>OCI_STMT_SCROLLABLE_READONLY, ora_check_sql => 1}) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
			}
		}
	}
	else
	{
		# Set row cache size
		$self->{dbh}->{RowCacheSize} = int($self->{data_limit}/10);
		if (exists $self->{local_data_limit}{$table}) {
			$self->{dbh}->{RowCacheSize} = $self->{local_data_limit}{$table};
		} 

		# prepare the query before execution
		if ($self->{is_mysql})
		{
			$query =~ s/^SELECT\s+/SELECT \/\*\!40001 SQL_NO_CACHE \*\/ /s;
			$sth = $self->{dbh}->prepare($query, { mysql_use_result => 1, mysql_use_row => 1 }) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		}
		elsif ($self->{is_mssql})
		{
			$sth = $self->{dbh}->prepare($query) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
			$sth->{'LongReadLen'} = $self->{longreadlen};
		}
		else
		{
			# Set the action name on Oracle side to see which table is exported
			$self->{dbh}->do("CALL DBMS_APPLICATION_INFO.set_action('$table')") or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

			if (!$self->{use_lob_locator}) {
				$sth = $self->{dbh}->prepare($query,{ora_piece_lob => 1, ora_piece_size => $self->{longreadlen}, ora_exe_mode=>OCI_STMT_SCROLLABLE_READONLY, ora_check_sql => 1});
			} else {
				$sth = $self->{dbh}->prepare($query,{'ora_auto_lob' => 0, ora_exe_mode=>OCI_STMT_SCROLLABLE_READONLY, ora_check_sql => 1});
			}

			if ($self->{dbh}->errstr =~ /ORA-00942/)
			{
				 $self->logit("WARNING: table $table is not yet physically created and has no data.\n", 0, 0);

				# Only useful for single process
				return 0;
			} elsif ($self->{dbh}->errstr) {
				 $self->logit("FATAL: _extract_data() " . $self->{dbh}->errstr . "\n", 1, 1);
			}
		}
	}

	# Extract data now by chunk of DATA_LIMIT and send them to a dedicated job
	$self->logit("Fetching all data from $rname tuples...\n", 1);

	my $start_time   = time();
	my $total_record = 0;
	my $total_row = $self->{tables}{$table}{table_info}{num_rows};

	# Send current table in progress
	if (defined $pipe) {
		my $t_name = $part_name || $table;
		if ($proc ne '') {
			$pipe->print("TABLE EXPORT IN PROGESS: $t_name-part-$proc, start: $start_time, rows $total_row\n");
		} else {
			$pipe->print("TABLE EXPORT IN PROGESS: $t_name, start: $start_time, rows $total_row\n");
		}
	}

	my @params = ();
	if (defined $proc) {
		unshift(@params, $proc);
		$self->logit("Parallelizing on core #$proc with query: $query\n", 1);
	}
	if ( ($self->{parallel_tables} > 1) || (($self->{oracle_copies} > 1) && $self->{defined_pk}{"\L$table\E"}) ) {
		$sth->execute(@params) or $self->logit("FATAL: " . $dbh->errstr . ", SQL: $query\n", 0, 1);
	} else {
		$sth->execute(@params) or $self->logit("FATAL: " . $self->{dbh}->errstr . ", SQL: $query\n", 0, 1);
	}

	my $col_cond = $self->hs_cond($tt,$stt, $table);

	# Oracle allow direct retreiving of bchunk of data 
	if (!$self->{is_mysql})
	{
		my $data_limit = $self->{data_limit};
		if (exists $self->{local_data_limit}{$table}) {
			$data_limit = $self->{local_data_limit}{$table};
		}
		my $has_blob = 0;
		$has_blob = 1 if (grep(/LOB|XMLTYPE/, @$stt));

		# With rows that not have custom type nor blob unless the user doesn't want to use lob locator
		if (($#has_custom_type == -1) && (!$has_blob || !$self->{use_lob_locator}))
		{
			while ( my $rows = $sth->fetchall_arrayref(undef,$data_limit))
			{
				if ( ($self->{parallel_tables} > 1) || (($self->{oracle_copies} > 1) && $self->{defined_pk}{"\L$table\E"}) )
				{
					if ($dbh->errstr)
					{
						$self->logit("ERROR: " . $dbh->errstr . "\n", 0, 0);
						last;
					}
				}
				elsif ( $self->{dbh}->errstr )
				{
					$self->logit("ERROR: " . $self->{dbh}->errstr . "\n", 0, 0);
					last;
				}

				$total_record += @$rows;
				$self->{current_total_row} += @$rows;
				$self->logit("DEBUG: number of rows $total_record extracted from table $table\n", 1);

				# Do we just want to test Oracle output speed
				if ($self->{oracle_speed} && !$self->{ora2pg_speed})
				{
					my $tt_record = @$rows;
					$self->print_to_progressbar($table, $part_name, $procnum, $start_time, $total_record, $tt_record, $self->{tables}{$table}{table_info}{num_rows});
					next;
				}

				if ( ($self->{jobs} > 1) || ($self->{oracle_copies} > 1) )
				{
					while ($self->{child_count} >= $self->{jobs})
					{
						my $kid = waitpid(-1, WNOHANG);
						if ($kid > 0)
						{
							$self->{child_count}--;
							delete $RUNNING_PIDS{$kid};
						}
						usleep(50000);
					}
					spawn sub {
						$self->_dump_to_pg($proc, $rows, $table, $cmd_head, $cmd_foot, $s_out, $tt, $sprep, $stt, $start_time, $part_name, $total_record, %user_type);
					};
					$self->{child_count}++;
				}
				else
				{
					$self->_dump_to_pg($proc, $rows, $table, $cmd_head, $cmd_foot, $s_out, $tt, $sprep, $stt, $start_time, $part_name, $total_record, %user_type);
				}
			}
		}
		else
		{
			my @rows = ();
			while ( my @row = $sth->fetchrow_array())
			{
				if ( ($self->{parallel_tables} > 1) || (($self->{oracle_copies} > 1) && $self->{defined_pk}{"\L$table\E"}) )
				{
					if ($dbh->errstr)
					{
						$self->logit("ERROR: " . $dbh->errstr . "\n", 0, 0);
						last;
					}
				}
				elsif ( $self->{dbh}->errstr )
				{
					$self->logit("ERROR: " . $self->{dbh}->errstr . "\n", 0, 0);
					last;
				}

				# Then foreach row use the returned lob locator to retrieve data
				# and all column with a LOB data type, extract data by chunk
				for (my $j = 0; $j <= $#$stt; $j++)
				{
					# Look for data based on custom type to replace the reference by the value
					if ($row[$j] =~ /^(?!(?!)\x{100})ARRAY\(0x/ && $stt->[$j] !~ /SDO_GEOMETRY/i)
					{
						my $data_type = uc($stt->[$j]) || '';
						$data_type =~ s/\(.*//; # remove any precision
						$row[$j] =  $self->set_custom_type_value($data_type, $user_type{$j}, $row[$j], $tt->[$j], 0);
					}
					# Retrieve LOB data from locator
					elsif (($stt->[$j] =~ /LOB|XMLTYPE/) && $row[$j])
					{
						my $lob_content = '';
						my $offset = 1;   # Offsets start at 1, not 0
						if ( ($self->{parallel_tables} > 1) || (($self->{oracle_copies} > 1) && $self->{defined_pk}{"\L$table\E"}) )
						{
							# Get chunk size
							my $chunk_size = $self->{lob_chunk_size} || $dbh->ora_lob_chunk_size($row[$j]) || 8192;
							while (1)
							{
								my $lobdata = $dbh->ora_lob_read($row[$j], $offset, $chunk_size );
								if ($dbh->errstr) {
									$self->logit("ERROR: " . $dbh->errstr . "\n", 0, 0) if ($dbh->errstr !~ /ORA-22831/);
									last;
								}
								last unless (defined $lobdata && length $lobdata);
								$offset += $chunk_size;
								$lob_content .= $lobdata;
							}
						}
						else
						{
							# Get chunk size
							my $chunk_size = $self->{lob_chunk_size} || $self->{dbh}->ora_lob_chunk_size($row[$j]) || 8192;
							while (1)
							{
								my $lobdata = $self->{dbh}->ora_lob_read($row[$j], $offset, $chunk_size );
								if ($self->{dbh}->errstr)
								{
									$self->logit("ERROR: " . $self->{dbh}->errstr . "\n", 0, 0) if ($self->{dbh}->errstr !~ /ORA-22831/);
									last;
								}
								last unless (defined $lobdata && length $lobdata);
								$offset += $chunk_size;
								$lob_content .= $lobdata;
							}
						}
						if ($lob_content ne '') {
							$row[$j] = $lob_content;
						} else {
							$row[$j] = undef;
						}

					}
					elsif (($stt->[$j] =~ /LOB/) && !$row[$j])
					{
						# This might handle case where the LOB is NULL and might prevent error:
						# DBD::Oracle::db::ora_lob_read: locator is not of type OCILobLocatorPtr
						$row[$j] = undef;
					}
				}
				$total_record++;
				$self->{current_total_row}++;

				push(@rows, [ @row ] );

				if ($#rows == $data_limit)
				{
					# Do we just want to test Oracle output speed
					if ($self->{oracle_speed} && !$self->{ora2pg_speed})
					{
						my $tt_record = @$rows;
						$self->print_to_progressbar($table, $part_name, $procnum, $start_time, $total_record, $tt_record, $self->{tables}{$table}{table_info}{num_rows});
						next;
					}

					if ( ($self->{jobs} > 1) || ($self->{oracle_copies} > 1) )
					{
						while ($self->{child_count} >= $self->{jobs})
						{
							my $kid = waitpid(-1, WNOHANG);
							if ($kid > 0)
							{
								$self->{child_count}--;
								delete $RUNNING_PIDS{$kid};
							}
							usleep(50000);
						}
						spawn sub {
							$self->_dump_to_pg($proc, \@rows, $table, $cmd_head, $cmd_foot, $s_out, $tt, $sprep, $stt, $start_time, $part_name, $total_record, %user_type);
						};
						$self->{child_count}++;
					}
					else
					{
						$self->_dump_to_pg($proc, \@rows, $table, $cmd_head, $cmd_foot, $s_out, $tt, $sprep, $stt, $start_time, $part_name, $total_record, %user_type);
					}
					@rows = ();
				}
			}

			# Do we just want to test Oracle output speed
			# Do we just want to test Oracle output speed
			if ($self->{oracle_speed} && !$self->{ora2pg_speed})
			{
				my $tt_record = @$rows;
				$self->print_to_progressbar($table, $part_name, $procnum, $start_time, $total_record, $tt_record, $self->{tables}{$table}{table_info}{num_rows});
				next;
			}

			# Flush last extracted data
			if ( ($self->{jobs} > 1) || ($self->{oracle_copies} > 1) )
			{
				while ($self->{child_count} >= $self->{jobs})
				{
					my $kid = waitpid(-1, WNOHANG);
					if ($kid > 0)
					{
						$self->{child_count}--;
						delete $RUNNING_PIDS{$kid};
					}
					usleep(50000);
				}
				spawn sub {
					$self->_dump_to_pg($proc, \@rows, $table, $cmd_head, $cmd_foot, $s_out, $tt, $sprep, $stt, $start_time, $part_name, $total_record, %user_type);
				};
				$self->{child_count}++;
			}
			else
			{
				$self->_dump_to_pg($proc, \@rows, $table, $cmd_head, $cmd_foot, $s_out, $tt, $sprep, $stt, $start_time, $part_name, $total_record, %user_type);
			}
			@rows = ();
		}
	}
	else
	{
		my @rows = ();
		my $num_row = 0;
		while (my @row = $sth->fetchrow())
		{
			push(@rows, \@row);
			$num_row++;
			if ($num_row == $self->{data_limit})
			{
				$num_row  = 0;
				$total_record += @rows;
				$self->{current_total_row} += @rows;
				# Do we just want to test Oracle output speed
				next if ($self->{oracle_speed} && !$self->{ora2pg_speed});
				$self->_dump_to_pg($proc, \@rows, $table, $cmd_head, $cmd_foot, $s_out, $tt, $sprep, $stt, $start_time, $part_name, $total_record, %user_type);
				@rows = ();
			}
		}

		if (!$self->{oracle_speed} || $self->{ora2pg_speed})
		{
			$total_record += @rows;
			$self->{current_total_row} += @rows;
			$self->_dump_to_pg($proc, \@rows, $table, $cmd_head, $cmd_foot, $s_out, $tt, $sprep, $stt, $start_time, $part_name, $total_record, %user_type);
		}
	}

	$sth->finish();

	# When copy freeze is required, close the transaction
	if ($self->{copy_freeze} && !$self->{pg_dsn})
	{
		if ($self->{file_per_table}) {
			$self->data_dump("COMMIT;\n",  $table);
		} else {
			$self->dump("\nCOMMIT;\n");
		}
	}

	# Close global data file in use when parallel table is used without output mutliprocess
	$self->close_export_file($self->{cfhout}) if (defined $self->{cfhout});
	$self->{cfhout} = undef;

	if (!$self->{quiet} && !$self->{debug})
	{
		if ( ($self->{jobs} <= 1) && ($self->{oracle_copies} <= 1) && ($self->{parallel_tables} <= 1))
		{
			my $end_time = time();
			my $dt = $end_time - $self->{global_start_time};
			my $rps = int($self->{current_total_row} / ($dt||1));
			print STDERR "\n";
			print STDERR $self->progress_bar($self->{current_total_row}, $self->{global_rows}, 25, '=', 'total rows', "- ($dt sec., avg: $rps recs/sec).") . "\n";
		}
	}

	# Wait for all child end
	while ($self->{child_count} > 0)
	{
		my $kid = waitpid(-1, WNOHANG);
		if ($kid > 0) {
			$self->{child_count}--;
			delete $RUNNING_PIDS{$kid};
		}
		usleep(50000);
	}

	if (defined $pipe)
	{
		my $t_name = $part_name || $table;
		my $t_time = time();
		if ($proc ne '') {
			$pipe->print("TABLE EXPORT ENDED: $t_name-part-$proc, end: $t_time, rows $total_record\n");
		} else {
			$pipe->print("TABLE EXPORT ENDED: $t_name, end: $t_time, rows $total_record\n");
		}
	}

	$dbh->disconnect() if ($dbh);

	# Only useful for single process
	return $total_record;
}

sub log_error_copy
{
	my ($self, $table, $s_out, $rows) = @_;

	my $outfile = '';
	if ($self->{output_dir} && !$noprefix) {
		$outfile = $self->{output_dir} . '/';
	}
	$outfile .= $table . '_error.log';

	my $filehdl = new IO::File;
	$filehdl->open(">>$outfile") or $self->logit("FATAL: Can't write to $outfile: $!\n", 0, 1);
	$filehdl->print($s_out);
	foreach my $row (@$rows) {
		$filehdl->print(join("\t", @$row) . "\n");
	}
	$filehdl->print("\\.\n");
	$self->close_export_file($filehdl);
}

sub log_error_insert
{
	my ($self, $table, $sql_out) = @_;

	my $outfile = '';
	if ($self->{output_dir} && !$noprefix) {
		$outfile = $self->{output_dir} . '/';
	}
	$outfile .= $table . '_error.log';

	my $filehdl = new IO::File;
	$filehdl->open(">>$outfile") or $self->logit("FATAL: Can't write to $outfile: $!\n", 0, 1);
	$filehdl->print("$sql_out\n");
	$self->close_export_file($filehdl);
}

sub print_to_progressbar
{
        my ($self, $table, $part_name, $procnum, $ora_start_time, $total_row, $tt_record, $glob_total_record) = @_;

	my $end_time = time();
	$ora_start_time = $end_time if (!$ora_start_time);
	my $dt = $end_time - $ora_start_time;
	my $rps = int($glob_total_record / ($dt||1));
	my $t_name = $part_name || $table;
	if (!$self->{quiet} && !$self->{debug})
	{
		# Send current table in progress
		if (defined $pipe)
		{
			if ($procnum ne '')
			{
				$pipe->print("CHUNK $$ DUMPED: $t_name-part-$procnum, time: $end_time, rows $tt_record\n");
			}
			else
			{
				$pipe->print("CHUNK $$ DUMPED: $t_name, time: $end_time, rows $tt_record\n");
			}
		}
		else
		{
			print STDERR $self->progress_bar($glob_total_record, $total_row, 25, '=', 'rows', "Table $t_name ($rps recs/sec)"), "\r";
		}
	}
	elsif ($self->{debug})
	{
		$self->logit("Extracted records from table $t_name: total_records = $glob_total_record (avg: $rps recs/sec)\n", 1);
	}
}

sub _dump_to_pg
{
	my ($self, $procnum, $rows, $table, $cmd_head, $cmd_foot, $s_out, $tt, $sprep, $stt, $ora_start_time, $part_name, $glob_total_record, %user_type) = @_;

	my @tempfiles = ();

	if ($^O !~ /MSWin32|dos/i) {
		push(@tempfiles, [ tempfile('tmp_ora2pgXXXXXX', SUFFIX => '', DIR => $TMP_DIR, UNLINK => 1 ) ]);
	}

	# Oracle source table or partition
	my $rname = $part_name || $table;
	# Destination PostgreSQL table (direct import to partition is not allowed with native partitioning)
	my $dname = $table;
	$dname = $part_name if (!$self->{pg_supports_partition});

	if ($self->{pg_dsn})
	{
		$0 = "ora2pg - sending data from table $rname to table $dname";
	} else {
		$0 = "ora2pg - writing to file data from table $rname to table $dname";
	}

	# Connect to PostgreSQL if direct import is enabled
	my $dbhdest = undef;
	if ($self->{pg_dsn} && !$self->{oracle_speed})
	{
		$dbhdest = $self->_send_to_pgdb();
		$self->logit("Dumping data from table $rname into PostgreSQL table $dname...\n", 1);
		$self->logit("Setting client_encoding to $self->{client_encoding}...\n", 1);
		my $s = $dbhdest->do( "SET client_encoding TO '\U$self->{client_encoding}\E';") or $self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
		if (!$self->{synchronous_commit})
		{
			$self->logit("Disabling synchronous commit when writing to PostgreSQL...\n", 1);
			$s = $dbhdest->do("SET synchronous_commit TO off") or $self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
		}
	}

	# Build header of the file
	my $h_towrite = '';
	foreach my $cmd (@$cmd_head)
	{
		if ($self->{pg_dsn} && !$self->{oracle_speed})
		{
			$self->logit("Executing pre command to PostgreSQL: $cmd\n", 1);
			my $s = $dbhdest->do("$cmd") or $self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
		} else {
			$h_towrite .= "$cmd\n";
		}
	}

	# Build footer of the file
	my $e_towrite = '';
	foreach my $cmd (@$cmd_foot)
	{
		if ($self->{pg_dsn} && !$self->{oracle_speed})
		{
			my $s = $dbhdest->do("$cmd") or $self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
		} else {
			$e_towrite .= "$cmd\n";
		}
	}

	# Preparing data for output
	if ( !$sprep && ($#{$rows} >= 0) ) {
		my $data_limit = $self->{data_limit};
		if (exists $self->{local_data_limit}{$table}) {
			$data_limit = $self->{local_data_limit}{$table};
		}
		my $len = @$rows;
		$self->logit("DEBUG: Formatting bulk of $data_limit data (real: $len rows) for PostgreSQL.\n", 1);
		$self->format_data($rows, $tt, $self->{type}, $stt, \%user_type, $table);
	}

	# Add COPY header to the output
	my $sql_out = $s_out;

	# Creating output
	my $data_limit = $self->{data_limit};
	if (exists $self->{local_data_limit}{$table})
	{
		$data_limit = $self->{local_data_limit}{$table};
	}
	$self->logit("DEBUG: Creating output for $data_limit tuples\n", 1);
	if ($self->{type} eq 'COPY')
	{
		if ($self->{pg_dsn})
		{
			$sql_out =~ s/;$//;
			if (!$self->{oracle_speed})
			{
				$self->logit("DEBUG: Sending COPY bulk output directly to PostgreSQL backend\n", 1);
				$dbhdest->do($sql_out) or $self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
				$sql_out = '';
				my $skip_end = 0;
				foreach my $row (@$rows)
				{
					unless($dbhdest->pg_putcopydata(join("\t", @$row) . "\n"))
					{
						if ($self->{log_on_error})
						{
							$self->logit("ERROR (log error enabled): " . $dbhdest->errstr . "\n", 0, 0);
							$self->log_error_copy($table, $s_out, $rows);
							$skip_end = 1;
							last;
						} else {
							$self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
						}
					}
				}
				unless ($dbhdest->pg_putcopyend())
				{
					if ($self->{log_on_error})
					{
						$self->logit("ERROR (log error enabled): " . $dbhdest->errstr . "\n", 0, 0);
						$self->log_error_copy($table, $s_out, $rows) if (!$skip_end);
					} else {
						$self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
					}
				}
			}
			else
			{
				foreach my $row (@$rows) {
					# do nothing, just add loop time nothing must be sent to PG
				}
			}
		}
		else
		{
			# then add data to the output
			map { $sql_out .= join("\t", @$_) . "\n"; } @$rows;
			$sql_out .= "\\.\n";
		}
	}
	elsif (!$sprep)
	{
		$sql_out = '';
		foreach my $row (@$rows)
		{
			$sql_out .= $s_out;
			$sql_out .= join(',', @$row) . ")";
			if ($self->{insert_on_conflict}) {
				$sql_out .= " ON CONFLICT DO NOTHING";
			}
			$sql_out .= ";\n";
		}
	}

	# Insert data if we are in online processing mode
	if ($self->{pg_dsn})
	{
		if ($self->{type} ne 'COPY')
		{
			if (!$sprep && !$self->{oracle_speed})
			{
				$self->logit("DEBUG: Sending INSERT output directly to PostgreSQL backend\n", 1);
				unless($dbhdest->do("BEGIN;\n" . $sql_out . "COMMIT;\n"))
				{
					if ($self->{log_on_error})
					{
						$self->logit("WARNING (log error enabled): " . $dbhdest->errstr . "\n", 0, 0);
						$self->log_error_insert($table, "BEGIN;\n" . $sql_out . "COMMIT;\n");
					} else {
						$self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
					}
				}
			}
			else
			{
				my $ps = undef;
				if (!$self->{oracle_speed}) {
					$ps = $dbhdest->prepare($sprep) or $self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
				}
				my @date_cols = ();
				my @bool_cols = ();
				for (my $i = 0; $i <= $#{$tt}; $i++)
				{
					if ($tt->[$i] eq 'bytea') {
						if (!$self->{oracle_speed}) {
							$ps->bind_param($i+1, undef, { pg_type => DBD::Pg::PG_BYTEA });
						}
					} elsif ($tt->[$i] eq 'boolean') {
						push(@bool_cols, $i);
					} elsif ($tt->[$i] =~ /(date|time)/i) {
						push(@date_cols, $i);
					}
				}
				$self->logit("DEBUG: Sending INSERT bulk output directly to PostgreSQL backend\n", 1);
				my $col_cond = $self->hs_cond($tt, $stt, $table);
				foreach my $row (@$rows)
				{
					# Even with prepared statement we need to replace zero date
					foreach my $j (@date_cols)
					{
						if ($row->[$j] =~ /^0000-/)
						{
							if (!$self->{replace_zero_date}) {
								$row->[$j] = undef;
							} else {
								$row->[$j] = $self->{replace_zero_date};
							}
						}
					}
					# Format user defined type and geometry data
					$self->format_data_row($row,$tt,'INSERT', $stt, \%user_type, $table, $col_cond, 1);
					# Replace boolean 't' and 'f' by 0 and 1 for bind parameters.
					foreach my $j (@bool_cols) {
						($row->[$j] eq "'f'") ? $row->[$j] = 0 : $row->[$j] = 1;
					}
					# Apply bind parmeters
					if (!$self->{oracle_speed})
					{
						unless ( $ps->execute(@$row) )
						{
							if ($self->{log_on_error})
							{
								$self->logit("ERROR (log error enabled): " . $ps->errstr . "\n", 0, 0);
								$s_out =~ s/\([,\?]+\)/\(/;
								$self->format_data_row($row,$tt,'INSERT', $stt, \%user_type, $table, $col_cond);
								$self->log_error_insert($table, $s_out . join(',', @$row) . ");\n");
							}
							else
							{
								$self->logit("FATAL: " . $ps->errstr . "\n", 0, 1);
							}
						}
					}
				}
				if (!$self->{oracle_speed}) {
					$ps->finish();
				}
			}
		}
	}
	else
	{
		if ($part_name && $self->{rename_partition})  {
			$part_name = $table . '_' . $part_name;
		}
		$sql_out = $h_towrite . $sql_out . $e_towrite;
		if (!$self->{oracle_speed}) {
			$self->data_dump($sql_out, $table, $part_name);
		}
	}

	my $total_row = $self->{tables}{$table}{table_info}{num_rows};
	my $tt_record = @$rows;
	$dbhdest->disconnect() if ($dbhdest);

	$self->print_to_progressbar($table, $part_name, $procnum, $ora_start_time, $total_row, $tt_record, $glob_total_record);

	if ($^O !~ /MSWin32|dos/i)
	{
		if (defined $tempfiles[0]->[0])
		{
			close($tempfiles[0]->[0]);
		}
		unlink($tempfiles[0]->[1]) if (-e $tempfiles[0]->[1]);
	}
}

sub _pload_to_pg
{
	my ($self, $idx, $query, @settings) = @_;

	if (!$self->{pg_dsn})
	{
		$self->logit("FATAL: No connection to PostgreSQL database set, aborting...\n", 0, 1);
	}

	my @tempfiles = ();

	if ($^O !~ /MSWin32|dos/i)
	{
		push(@tempfiles, [ tempfile('tmp_ora2pgXXXXXX', SUFFIX => '', DIR => $TMP_DIR, UNLINK => 1 ) ]);
	}

	# Open a connection to the postgreSQL database
	$0 = "ora2pg - sending query to PostgreSQL database";

	# Connect to PostgreSQL if direct import is enabled
	my $dbhdest = $self->_send_to_pgdb();
	$self->logit("Loading query #$idx: $query\n", 1);
	if ($#settings == -1)
	{
		$self->logit("Applying settings from configuration\n", 1);
		# Apply setting from configuration
		$dbhdest->do( "SET client_encoding TO '\U$self->{client_encoding}\E';") or $self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
		my $search_path = $self->set_search_path();
		if ($search_path)
		{
			$self->logit("Setting search_path using: $search_path...\n", 1);
			$dbhdest->do($search_path) or $self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
		}
	}
	else
	{
		$self->logit("Applying settings from input file\n", 1);
		# Apply setting from source file
		foreach my $set (@settings) {
			$dbhdest->do($set) or $self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
		}
	}
	# Execute query
	$dbhdest->do("$query") or $self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
	$dbhdest->disconnect() if ($dbhdest);

	if ($^O !~ /MSWin32|dos/i)
	{
		if (defined $tempfiles[0]->[0])
		{
			close($tempfiles[0]->[0]);
		}
		unlink($tempfiles[0]->[1]) if (-e $tempfiles[0]->[1]);
	}
}


# Global array, to store the converted values
my @bytea_array;
sub build_escape_bytea
{
	foreach my $tmp (0..255)
	{
		my $out;
		if ($tmp >= 32 and $tmp <= 126) {
			if ($tmp == 92) {
				$out = '\\\\134';
			} elsif ($tmp == 39) {
				$out = '\\\\047';
			} else {
				$out = chr($tmp);
			}
		} else { 
			$out = sprintf('\\\\%03o',$tmp);
		}
		$bytea_array[$tmp] = $out;
	}
}

=head2 escape_bytea

This function return an escaped bytea entry for Pg.

=cut


sub escape_bytea
{
	my $data = shift;

	# In this function, we use the array built by build_escape_bytea
	my @array= unpack("C*", $data);
	foreach my $elt (@array) {
		$elt = $bytea_array[$elt];
	}
	return join('', @array);
}

=head2 _show_infos

This function display a list of schema, table or column only to stdout.

=cut

sub _show_infos
{
	my ($self, $type) = @_;

	if ($type eq 'SHOW_ENCODING')
	{
		my ($db_encoding, $collation, $client_encoding, $timestamp_format, $date_format) = $self->_get_encoding($self->{dbh});
		if ($self->{is_mysql})
		{
			$self->logit("Current encoding settings that will be used by Ora2Pg:\n", 0);
			$self->logit("\tMySQL database and client encoding: $self->{nls_lang}\n", 0);
			$self->logit("\tMySQL collation encoding: $self->{nls_nchar}\n", 0);
			$self->logit("\tPostgreSQL CLIENT_ENCODING $self->{client_encoding}\n", 0);
			$self->logit("\tPerl output encoding '$self->{binmode}'\n", 0);
			$self->logit("Showing current MySQL encoding and possible PostgreSQL client encoding:\n", 0);
			$self->logit("\tMySQL database and client encoding: $db_encoding\n", 0);
			$self->logit("\tMySQL collation encoding: $collation\n", 0);
			$self->logit("\tPostgreSQL CLIENT_ENCODING: $client_encoding\n", 0);
			$self->logit("MySQL SQL mode: $self->{mysql_mode}\n", 0);
		} elsif ($self->{is_mssql})
		{
			$self->logit("Current encoding settings that will be used by Ora2Pg:\n", 0);
			$self->logit("\tMSSQL database and client encoding: utf8\n", 0);
			$self->logit("\tMSSQL collation encoding: $self->{nls_nchar}\n", 0);
			$self->logit("\tPostgreSQL CLIENT_ENCODING: $self->{client_encoding}\n", 0);
			$self->logit("\tPerl output encoding '$self->{binmode}'\n", 0);
			$self->logit("\tOra2Pg use UTF8 export to export from MSSQL, change to NSL_LANG and\n", 0);
			$self->logit("\tNLS_NCHAR have no effect. CLIENT_ENCODING must be set to UFT8\n", 0);
			$self->logit("Showing current MSSQL encoding and possible PostgreSQL client encoding:\n", 0);
			$self->logit("\tMSSQL database encoding: $self->{nls_lang}\n", 0);
			$self->logit("\tMSSQL collation encoding: $self->{nls_nchar}\n", 0);
			$self->logit("\tPostgreSQL CLIENT_ENCODING: $client_encoding\n", 0);
		} else {
			$self->logit("Current encoding settings that will be used by Ora2Pg:\n", 0);
			$self->logit("\tOracle NLS_LANG $self->{nls_lang}\n", 0);
			$self->logit("\tOracle NLS_NCHAR $self->{nls_nchar}\n", 0);
			if ($self->{enable_microsecond}) {
				$self->logit("\tOracle NLS_TIMESTAMP_FORMAT YYYY-MM-DD HH24:MI:SS.FF\n", 0);
			} else {
				$self->logit("\tOracle NLS_TIMESTAMP_FORMAT YYYY-MM-DD HH24:MI:SS\n", 0);
			}
			$self->logit("\tOracle NLS_DATE_FORMAT YYYY-MM-DD HH24:MI:SS\n", 0);
			$self->logit("\tPostgreSQL CLIENT_ENCODING $self->{client_encoding}\n", 0);
			$self->logit("\tPerl output encoding '$self->{binmode}'\n", 0);
			$self->logit("Showing current Oracle encoding and possible PostgreSQL client encoding:\n", 0);
			$self->logit("\tOracle NLS_LANG $db_encoding\n", 0);
			$self->logit("\tOracle NLS_NCHAR $collation\n", 0);
			$self->logit("\tOracle NLS_TIMESTAMP_FORMAT $timestamp_format\n", 0);
			$self->logit("\tOracle NLS_DATE_FORMAT $date_format\n", 0);
			$self->logit("\tPostgreSQL CLIENT_ENCODING $client_encoding\n", 0);
		}
	}
	elsif ($type eq 'SHOW_VERSION')
	{
		$self->logit("Showing Database Version...\n", 1);
		$self->logit("$self->{db_version}\n", 0);
	}
	elsif ($type eq 'SHOW_REPORT')
	{
		# Get all tables information specified by the DBI method table_info
		if ($self->{is_mssql})
		{
			my $sth = $self->_schema_list() or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
			while ( my @row = $sth->fetchrow()) {
				push(@{$self->{local_schemas}}, $row[0]);
			}
			$sth->finish();
		}
		if ($#{$self->{local_schemas}} >= 0) {
			$self->{local_schemas_regex} = '(' . join('|', @{$self->{local_schemas}}) . ')';
		}
		print STDERR "Reporting Oracle Content...\n" if ($self->{debug});
		my $uncovered_score = 'Ora2Pg::PLSQL::UNCOVERED_SCORE';
		if ($self->{is_mysql}) {
			$uncovered_score = 'Ora2Pg::PLSQL::UNCOVERED_MYSQL_SCORE';
		} elsif ($self->{is_mssql}) {
			$uncovered_score = 'Ora2Pg::PLSQL::UNCOVERED_MSSQL_SCORE';
		}
		# Get Oracle database version and size
		print STDERR "Looking at Oracle server version...\n" if ($self->{debug});
		my $ver = $self->_get_version();
		print STDERR "Looking at Oracle database size...\n" if ($self->{debug});
		my $size = $self->_get_database_size();
		# Get the list of all database objects
		print STDERR "Looking at Oracle defined objects...\n" if ($self->{debug});
		my %objects = $self->_get_objects();

		# Extract all tables informations
		my %all_indexes = ();
		$self->{skip_fkeys} = $self->{skip_indices} = $self->{skip_indexes} = $self->{skip_checks} = 0;
		$self->{view_as_table} = ();
		$self->{mview_as_table} = ();
		print STDERR "Looking at table definition...\n" if ($self->{debug});
		$self->_tables(1);
		my $total_index = 0;
		my $total_table_objects = 0;
		my $total_index_objects = 0;
		foreach my $table (sort keys %{$self->{tables}})
		{
			$total_table_objects++;
			push(@exported_indexes, $self->_exportable_indexes($table, %{$self->{tables}{$table}{indexes}}));
			$total_index_objects += scalar keys %{$self->{tables}{$table}{indexes}};
			foreach my $idx (sort keys %{$self->{tables}{$table}{idx_type}})
			{
				next if (!grep(/^$idx$/i, @exported_indexes));
				my $typ = $self->{tables}{$table}{idx_type}{$idx}{type};
				push(@{$all_indexes{$typ}}, $idx);
				$total_index++;
			}
		}
		# Convert Oracle user defined type to PostgreSQL
		if (!$self->{is_mysql})
		{
			$self->_types();
			foreach my $tpe (sort { $a->{pos} <=> $b->{pos} } @{$self->{types}})
			{
				# We dont want the result but only the array @{$self->{types}}
				# define in the _convert_type() function
				$self->_convert_type($tpe->{code}, $tpe->{owner});
			}
		}
		print STDERR "Looking at views definition...\n" if ($self->{debug});
		my %view_infos = ();
		%view_infos = $self->_get_views() if ($self->{estimate_cost});

		# Get definition of Database Link
		print STDERR "Looking at database links...\n" if ($self->{debug});
		my %dblink = $self->_get_dblink();
		$objects{'DATABASE LINK'} = scalar keys %dblink;	
		print STDERR "\tFound $objects{'DATABASE LINK'} DATABASE LINK.\n" if ($self->{debug});
		# Get Jobs
		print STDERR "Looking at jobs...\n" if ($self->{debug});
		my %jobs = $self->_get_job();
		$objects{'JOB'} = scalar keys %jobs;
		print STDERR "\tFound $objects{'JOB'} JOB.\n" if ($self->{debug});
		# Get synonym information
		print STDERR "Looking at synonyms...\n" if ($self->{debug});
		my %synonyms = $self->_synonyms();
		$objects{'SYNONYM'} = scalar keys %synonyms;	
		print STDERR "\tFound $objects{'SYNONYM'} SYNONYM.\n" if ($self->{debug});
		# Get all global temporary tables
		print STDERR "Looking at global temporary table...\n" if ($self->{debug});
		my %global_tables = $self->_global_temp_table_info();
		$objects{'GLOBAL TEMPORARY TABLE'} = scalar keys %global_tables;
		print STDERR "\tFound $objects{'GLOBAL TEMPORARY TABLE'} GLOBAL TEMPORARY TABLE.\n" if ($self->{debug});
		# Look for encrypted columns and identity columns
		my %encrypted_column = ();
		if ($self->{db_version} !~ /Release [89]/)
		{
			print STDERR "Looking at encrypted columns...\n" if ($self->{debug});
			%encrypted_column = $self->_encrypted_columns('',$self->{schema});
			print STDERR "\tFound ", scalar keys %encrypted_column, " encrypted column.\n" if ($self->{debug});
			print STDERR "Looking at identity columns...\n" if ($self->{debug});
			# Identity column are collected in call to sub _tables() above
			print STDERR "\tFound ", scalar keys %{$self->{identity_info}}, " identity column.\n" if ($self->{debug});
		}

		# Look at all database objects to compute report
		my %report_info = ();
		$report_info{'Version'} = $ver || 'Unknown';
		$report_info{'Schema'} = $self->{schema} || '';
		$report_info{'Size'} = $size || 'Unknown';
		my $idx = 0;
		my $num_total_obj = scalar keys %objects;
		foreach my $typ (sort keys %objects)
		{
			$idx++;
			next if ($typ eq 'EVALUATION CONTEXT'); # Do not care about rule evaluation context
			next if ($self->{is_mysql} && $typ eq 'SYNONYM');
			next if ($typ eq 'PACKAGE'); # Package are scanned with PACKAGE BODY not PACKAGE objects
			print STDERR "Building report for object $typ...\n" if ($self->{debug});
			if (!$self->{quiet} && !$self->{debug}) {
				print STDERR $self->progress_bar($idx, $num_total_obj, 25, '=', 'objects types', "inspecting object $typ" ), "\r";
			}
			$report_info{'Objects'}{$typ}{'number'} = 0;
			$report_info{'Objects'}{$typ}{'invalid'} = 0;
			if (!grep(/^$typ$/, 'DATABASE LINK', 'JOB', 'TABLE', 'INDEX',
					'SYNONYM','GLOBAL TEMPORARY TABLE'))
			{
				for (my $i = 0; $i <= $#{$objects{$typ}}; $i++)
				{
					$report_info{'Objects'}{$typ}{'number'}++;
					$report_info{'Objects'}{$typ}{'invalid'}++ if ($objects{$typ}[$i]->{invalid});
				}
			}
			elsif ($typ eq 'TABLE')
			{
				$report_info{'Objects'}{$typ}{'number'} = $total_table_objects;
			}
			elsif ($typ eq 'INDEX')
			{
				$report_info{'Objects'}{$typ}{'number'} = $total_index_objects;
			}
			else
			{
				$report_info{'Objects'}{$typ}{'number'} = $objects{$typ};
			}

			$report_info{'total_object_invalid'} += $report_info{'Objects'}{$typ}{'invalid'};
			$report_info{'total_object_number'} += $report_info{'Objects'}{$typ}{'number'};

			if ($report_info{'Objects'}{$typ}{'number'} > 0)
			{
				$report_info{'Objects'}{$typ}{'real_number'} = ($report_info{'Objects'}{$typ}{'number'} - $report_info{'Objects'}{$typ}{'invalid'});
				$report_info{'Objects'}{$typ}{'real_number'} = $report_info{'Objects'}{$typ}{'number'} if ($self->{export_invalid});
			}

			if ($self->{estimate_cost})
			{
				$report_info{'Objects'}{$typ}{'cost_value'} = ($report_info{'Objects'}{$typ}{'real_number'}*$Ora2Pg::PLSQL::OBJECT_SCORE{$typ});
				# Minimal unit is 1
				$report_info{'Objects'}{$typ}{'cost_value'} = 1 if ($report_info{'Objects'}{$typ}{'cost_value'} =~ /^0\./);
				# For some object's type do not set migration unit upper than 2 days.
				if (grep(/^$typ$/, 'TABLE PARTITION', 'GLOBAL TEMPORARY TABLE', 'TRIGGER', 'VIEW'))
				{
					$report_info{'Objects'}{$typ}{'cost_value'} = 168 if ($report_info{'Objects'}{$typ}{'cost_value'} > 168);
					if (grep(/^$typ$/, 'TRIGGER', 'VIEW') && $report_info{'Objects'}{$typ}{'real_number'} > 500) {
						$report_info{'Objects'}{$typ}{'cost_value'} += 84 * int(($report_info{'Objects'}{$typ}{'real_number'} - 500) / 500);
					}
				}
				elsif (grep(/^$typ$/, 'TABLE', 'INDEX', 'SYNONYM'))
				{
					$report_info{'Objects'}{$typ}{'cost_value'} = 84 if ($report_info{'Objects'}{$typ}{'cost_value'} > 84);
				}
			}

			if ($typ eq 'INDEX')
			{
				my $bitmap = 0;
				foreach my $t (sort keys %INDEX_TYPE)
				{
					my $len = ($#{$all_indexes{$t}}+1);
					$report_info{'Objects'}{$typ}{'detail'} .= "\L$len $INDEX_TYPE{$t} index(es)\E\n" if ($len);
					if ($self->{estimate_cost} && $len &&
						( ($t =~ /FUNCTION.*NORMAL/) || ($t eq 'FUNCTION-BASED BITMAP') ) )
					{
						$report_info{'Objects'}{$typ}{'cost_value'} += ($len * $Ora2Pg::PLSQL::OBJECT_SCORE{'FUNCTION-BASED-INDEX'});
					}
					if ($self->{estimate_cost} && $len && ($t =~ /REV/)) {
						$report_info{'Objects'}{$typ}{'cost_value'} += ($len * $Ora2Pg::PLSQL::OBJECT_SCORE{'REV-INDEX'});
					}
				}
				$report_info{'Objects'}{$typ}{'cost_value'} += ($Ora2Pg::PLSQL::OBJECT_SCORE{$typ}*$total_index) if ($self->{estimate_cost});
				$report_info{'Objects'}{$typ}{'comment'} = "$total_index index(es) are concerned by the export, others are automatically generated and will do so on PostgreSQL.";
				my $hash_index = '';
				if ($self->{pg_version} < 10) {
					$hash_index = ' and hash index(es) will be exported as b-tree index(es) if any';
				}
				if (!$self->{is_mysql})
				{
					my $bitmap = 'Bitmap';
					if ($self->{bitmap_as_gin}) {
						$bitmap = 'Bitmap will be exported as btree_gin index(es)';
					}
					$report_info{'Objects'}{$typ}{'comment'} .= " $bitmap$hash_index. Domain index are exported as b-tree but commented to be edited to mainly use FTS. Cluster, bitmap join and IOT indexes will not be exported at all. Reverse indexes are not exported too, you may use a trigram-based index (see pg_trgm) or a reverse() function based index and search. Use 'varchar_pattern_ops', 'text_pattern_ops' or 'bpchar_pattern_ops' operators in your indexes to improve search with the LIKE operator respectively into varchar, text or char columns.";
				}
				else
				{
					$report_info{'Objects'}{$typ}{'comment'} .= "$hash_index. Use 'varchar_pattern_ops', 'text_pattern_ops' or 'bpchar_pattern_ops' operators in your indexes to improve search with the LIKE operator respectively into varchar, text or char columns. Fulltext search indexes will be replaced by using a dedicated tsvector column, Ora2Pg will set the DDL to create the column, function and trigger together with the index.";
				}
			}
			elsif ($typ eq 'MATERIALIZED VIEW')
			{
				$report_info{'Objects'}{$typ}{'comment'}= "All materialized view will be exported as snapshot materialized views, they are only updated when fully refreshed.";
				my %mview_infos = $self->_get_materialized_views();
				my $oncommit = 0;
				foreach my $mview (sort keys %mview_infos)
				{
					if ($mview_infos{$mview}{refresh_mode} eq 'COMMIT')
					{
						$oncommit++;
						$report_info{'Objects'}{$typ}{'detail'} .= "$mview, ";
					}
				}
				if ($oncommit)
				{
					$report_info{'Objects'}{$typ}{'detail'} =~ s/, $//;
					$report_info{'Objects'}{$typ}{'detail'} = "$oncommit materialized views are refreshed on commit ($report_info{'Objects'}{$typ}{'detail'}), this is not supported by PostgreSQL, you will need to use triggers to have the same behavior or use a simple view.";
				}
			}
			elsif ($typ eq 'TABLE')
			{
				my $exttb = scalar keys %{$self->{external_table}};
				if ($exttb)
				{
					if (!$self->{external_to_fdw})
					{
						$report_info{'Objects'}{$typ}{'comment'} = "$exttb external table(s) will be exported as standard table. See EXTERNAL_TO_FDW configuration directive to export as file_fdw foreign tables or use COPY in your code if you just want to load data from external files.";
					}
					else
					{
						$report_info{'Objects'}{$typ}{'comment'} = "$exttb external table(s) will be exported as file_fdw foreign table. See EXTERNAL_TO_FDW configuration directive to export as standard table or use COPY in your code if you just want to load data from external files.";
					}
				}

				my %table_detail = ();
				my $virt_column = 0;
				my @done = ();
				my $id = 0;
				my $total_check = 0;
				my $total_row_num = 0;
				my $rdbms_error_table = 0;
				my $quartz_scheduler = 0;
				my @maxtablen = ();
				my @maxcollen = ();
				# Set the table information for each class found
				foreach my $t (sort keys %{$self->{tables}})
				{
					my $tbname = $t;
					$tbname =~ s/.*\.//;

					# Set the total number of rows
					$total_row_num += $self->{tables}{$t}{table_info}{num_rows};

					# Look if this is a RDBMS table
					$rdbms_error_table++ if ($tbname =~ /^ERR\$_/i);
					# Object name too long
					push(@maxtablen, $t) if (length($tbname) > 63);
					# Look if this is a Quartz Scheduler table
					$quartz_scheduler++ if ($tbname =~ /^QRTZ_/i);
					# Look at reserved words if tablename is found
					my $r = $self->is_reserved_words($t);
					if (($r > 0) && ($r != 3))
					{
						$table_detail{'reserved words in table name'}++;
						$report_info{'Objects'}{$typ}{'cost_value'} += 12; # one hour to solve reserved keyword might be enough
					}
					# Get fields informations
					foreach my $k (sort {$self->{tables}{$t}{column_info}{$a}[11] <=> $self->{tables}{$t}{column_info}{$a}[11]} keys %{$self->{tables}{$t}{column_info}})
					{
						# Column name too long
						push(@maxcollen, "$t.$self->{tables}{$t}{column_info}{$k}[0]") if (length($self->{tables}{$t}{column_info}{$k}[0]) > 63);
						$r = $self->is_reserved_words($self->{tables}{$t}{column_info}{$k}[0]);
						if (($r > 0) && ($r != 3))
						{
							$table_detail{'reserved words in column name'}++;
							$report_info{'Objects'}{$typ}{'cost_value'} += 12; # one hour to solve reserved keyword might be enough
						}
						elsif ($r == 3)
						{
							$table_detail{'system columns in column name'}++;
							$report_info{'Objects'}{$typ}{'cost_value'} += 12; # one hour to solve reserved keyword might be enough
						}
						$self->{tables}{$t}{column_info}{$k}[1] =~ s/TIMESTAMP\(\d+\)/TIMESTAMP/i;
						if (!exists $self->{data_type}{uc($self->{tables}{$t}{column_info}{$k}[1])}) {
							$table_detail{'unknown types'}++;
						}
						if ( (uc($self->{tables}{$t}{column_info}{$k}[1]) eq 'NUMBER') && ($self->{tables}{$t}{column_info}{$k}[2] eq '') ) {
							$table_detail{'numbers with no precision'}++;
						}
						if ( $self->{data_type}{uc($self->{tables}{$t}{column_info}{$k}[1])} eq 'bytea' ) {
							$table_detail{'binary columns'}++;
						}
					}
					# Get check constraints information related to this table
					my $constraints = $self->_count_check_constraint($self->{tables}{$t}{check_constraint});
					$total_check += $constraints;
					if ($self->{estimate_cost} && $constraints >= 0) {
						$report_info{'Objects'}{$typ}{'cost_value'} += $constraints * $Ora2Pg::PLSQL::OBJECT_SCORE{'CHECK'};
					}
				}
				$report_info{'Objects'}{$typ}{'comment'} .= " $total_check check constraint(s)." if ($total_check);
				foreach my $d (sort keys %table_detail) {
					$report_info{'Objects'}{$typ}{'comment'} .= "\L$table_detail{$d} $d\E.\n";
				}
				$report_info{'Objects'}{$typ}{'detail'} .= "Total number of rows: $total_row_num\n";
				$report_info{'Objects'}{$typ}{'detail'} .= "Top $self->{top_max} of tables sorted by number of rows:\n";
				my $j = 1;
				foreach my $t (sort {$self->{tables}{$b}{table_info}{num_rows} <=> $self->{tables}{$a}{table_info}{num_rows}} keys %{$self->{tables}})
				{
					next if ($self->{tables}{$t}{table_info}{num_rows} == 0);
					$report_info{'Objects'}{$typ}{'detail'} .= "\L$t\E has $self->{tables}{$t}{table_info}{num_rows} rows\n";
					$j++;
					last if ($j > $self->{top_max});
				}
				$report_info{'Objects'}{$typ}{'detail'} .= "Top $self->{top_max} of largest tables:\n";
				$j = 1;
				if ($self->{is_mssql} || $self->{is_mysql})
				{
					foreach my $t (sort {$self->{tables}{$b}{table_info}{size} <=> $self->{tables}{$a}{table_info}{size}} keys %{$self->{tables}})
					{
						next if ($self->{tables}{$t}{table_info}{size} == 0);
						$report_info{'Objects'}{$typ}{'detail'} .= "\L$t\E: $self->{tables}{$t}{table_info}{size} MB (" . $self->{tables}{$t}{table_info}{num_rows} . " rows)\n";
						$j++;
						last if ($j > $self->{top_max});
					}
				}
				else
				{
					# Because we avoid using JOIN when querying the Oracle catalog, look for all table size
					my %largest_table = ();
					%largest_table = $self->_get_largest_tables() if ($self->{is_mysql});
					foreach my $t (sort { $largest_table{$b} <=> $largest_table{$a} } keys %largest_table)
					{
						$report_info{'Objects'}{$typ}{'detail'} .= "\L$t\E: $largest_table{$t} MB (" . $self->{tables}{$t}{table_info}{num_rows} . " rows)\n";
						$j++;
						last if ($j > $self->{top_max});
					}
				}

				$comment = "Nothing particular." if (!$comment);
				$report_info{'Objects'}{$typ}{'cost_value'} =~ s/(\.\d).*$/$1/;
				if (scalar keys %encrypted_column > 0)
				{
					$report_info{'Objects'}{$typ}{'comment'} .= "\n" . (scalar keys %encrypted_column) . " encrypted column(s).\n";
					foreach my $k (sort keys %encrypted_column) {
						$report_info{'Objects'}{$typ}{'comment'} .= "\L$k\E ($encrypted_column{$k})\n";
					}
					$report_info{'Objects'}{$typ}{'comment'} .= ". You must use the pg_crypto extension to use encryption.\n";
					if ($self->{estimate_cost}) {
						$report_info{'Objects'}{$typ}{'cost_value'} += (scalar keys %encrypted_column) * $Ora2Pg::PLSQL::OBJECT_SCORE{'ENCRYPTED COLUMN'};
					}
					$report_info{'Objects'}{$typ}{'comment'} .= "Table(s) name too long: " . ($#maxtablen+1) . "\n" if ($#maxtablen >= 0);
					$report_info{'Objects'}{$typ}{'detail'} .= "List of table(s) name too long:\n" . join(",\n", @maxtablen) . "\n" if ($#maxtablen >= 0);
					$report_info{'Objects'}{$typ}{'comment'} .= "Column(s) name too long: " . ($#maxcollen+1) . "\n" if ($#maxcollen >= 0);
					$report_info{'Objects'}{$typ}{'detail'} .= "List of column(s) name too long:\n" . join(",\n", @maxcollen) . "\n" if ($#maxcollen >= 0);
					$report_info{'Objects'}{$typ}{'comment'} .= "RDBMS_ERROR is used on $rdbms_error_table tables.\n" if ($rdbms_error_table);
					$report_info{'Objects'}{$typ}{'comment'} .= "Quartz Scheduler looks to be used.\n" if ($quartz_scheduler);
				}
				if (scalar keys %{$self->{identity_info}} > 0)
				{
					$report_info{'Objects'}{$typ}{'comment'} .= "\n" . (scalar keys %{$self->{identity_info}}) . " identity column(s).\n";
					$report_info{'Objects'}{$typ}{'comment'} .= " Identity columns are fully supported since PG10.\n";
				}
			}
			elsif ($typ eq 'TYPE')
			{
				my $total_type = $report_info{'Objects'}{'TYPE'}{'number'};
				foreach my $t (sort keys %{$self->{type_of_type}})
				{
					$total_type-- if (grep(/^$t$/, 'Associative Arrays','Type Boby','Type with member method', 'Type Ref Cursor'));
					$report_info{'Objects'}{$typ}{'detail'} .= "\L$self->{type_of_type}{$t} $t\E\n" if ($self->{type_of_type}{$t});
				}
				$report_info{'Objects'}{$typ}{'cost_value'} = ($Ora2Pg::PLSQL::OBJECT_SCORE{$typ}*$total_type) if ($self->{estimate_cost});
				$report_info{'Objects'}{$typ}{'comment'} = "$total_type type(s) are concerned by the export, others are not supported. Note that Type inherited and Subtype are converted as table, type inheritance is not supported.";
			}
			elsif ($typ eq 'TYPE BODY')
			{
				$report_info{'Objects'}{$typ}{'comment'} = "Export of type with member method are not supported, they will not be exported.";
			}
			elsif ($typ eq 'TRIGGER')
			{
				my $triggers = $self->_get_triggers();
				my $total_size = 0;
				foreach my $trig (@{$triggers})
				{
                                        # Remove comment and text constant, they are not useful in assessment
                                        $self->_remove_comments(\$trig->[4]);
                                        $self->{comment_values} = ();
                                        $self->{text_values} = ();
                                        $self->{text_values_pos} = 0;
                                        if ($self->{is_mysql}) {
                                                $trig->[4] = $self->_convert_function($trig->[8], $trig->[4], $trig->[0]);
                                        } else {
                                                $trig->[4] = $self->_convert_function($trig->[8], $trig->[4]);
                                        }
					$total_size += length($trig->[4]);
					if ($self->{estimate_cost})
					{
						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $trig->[4], 'TRIGGER');
						$report_info{'Objects'}{$typ}{'cost_value'} += $cost;
						$report_info{'Objects'}{$typ}{'detail'} .= "\L$trig->[0]: $cost\E\n";
						$report_info{full_trigger_details}{"\L$trig->[0]\E"}{count} = $cost;
						foreach my $d (sort { $cost_detail{$b} <=> $cost_detail{$a} } keys %cost_detail)
						{
							next if (!$cost_detail{$d});
							$report_info{full_trigger_details}{"\L$trig->[0]\E"}{info} .= "\t$d => $cost_detail{$d}";
							$report_info{full_trigger_details}{"\L$trig->[0]\E"}{info} .= " (cost: ${$uncovered_score}{$d})" if (${$uncovered_score}{$d});
							$report_info{full_trigger_details}{"\L$trig->[0]\E"}{info} .= "\n";
							push(@{$report_info{full_trigger_details}{"\L$trig->[0]\E"}{keywords}}, $d) if (($d ne 'SIZE') && ($d ne 'TEST')); 
						}
					}
				}
				$report_info{'Objects'}{$typ}{'comment'} = "Total size of trigger code: $total_size bytes.";
			}
			elsif ($typ eq 'SEQUENCE')
			{
				$report_info{'Objects'}{$typ}{'comment'} = "Sequences are fully supported, but all call to sequence_name.NEXTVAL or sequence_name.CURRVAL will be transformed into NEXTVAL('sequence_name') or CURRVAL('sequence_name').";
			}
			elsif ($typ eq 'FUNCTION')
			{
				my $functions = $self->_get_functions();
				my $total_size = 0;
				foreach my $fct (keys %{$functions})
				{
                                        # Remove comment and text constant, they are not useful in assessment
                                        $self->_remove_comments(\$functions->{$fct}{text});
                                        $self->{comment_values} = ();
                                        $self->{text_values} = ();
                                        $self->{text_values_pos} = 0;
                                        if ($self->{is_mysql}) {
                                                $functions->{$fct}{text} = $self->_convert_function($functions->{$fct}{owner}, $functions->{$fct}{text}, $fct);
                                        } else {
                                                $functions->{$fct}{text} = $self->_convert_function($functions->{$fct}{owner}, $functions->{$fct}{text});
                                        }
					$total_size += length($functions->{$fct}{text});
					if ($self->{estimate_cost})
					{
						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $functions->{$fct}{text}, 'FUNCTION');
						$report_info{'Objects'}{$typ}{'cost_value'} += $cost;
						$report_info{'Objects'}{$typ}{'detail'} .= "\L$fct: $cost\E\n";
						$report_info{full_function_details}{"\L$fct\E"}{count} = $cost;
						foreach my $d (sort { $cost_detail{$b} <=> $cost_detail{$a} } keys %cost_detail)
						{
							next if (!$cost_detail{$d});
							$report_info{full_function_details}{"\L$fct\E"}{info} .= "\t$d => $cost_detail{$d}";
							$report_info{full_function_details}{"\L$fct\E"}{info} .= " (cost: ${$uncovered_score}{$d})" if (${$uncovered_score}{$d});
							$report_info{full_function_details}{"\L$fct\E"}{info} .= "\n";
							push(@{$report_info{full_function_details}{"\L$fct\E"}{keywords}}, $d) if (($d ne 'SIZE') && ($d ne 'TEST')); 
						}
					}
				}
				$report_info{'Objects'}{$typ}{'comment'} = "Total size of function code: $total_size bytes.";
			}
			elsif ($typ eq 'PROCEDURE')
			{
				my $procedures = $self->_get_procedures();
				my $total_size = 0;
				foreach my $proc (keys %{$procedures})
				{
					# Remove comment and text constant, they are not useful in assessment
					$self->_remove_comments(\$procedures->{$proc}{text});
					$self->{comment_values} = ();
					$self->{text_values} = ();
					$self->{text_values_pos} = 0;
					if ($self->{is_mysql}) {
						$procedures->{$proc}{text} = $self->_convert_function($procedures->{$proc}{owner}, $procedures->{$proc}{text}, $proc);
					} else {
						$procedures->{$proc}{text} = $self->_convert_function($procedures->{$proc}{owner}, $procedures->{$proc}{text});
					}
					$total_size += length($procedures->{$proc}{text});
					if ($self->{estimate_cost})
					{
						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $procedures->{$proc}{text}, 'PROCEDURE');
						$report_info{'Objects'}{$typ}{'cost_value'} += $cost;
						$report_info{'Objects'}{$typ}{'detail'} .= "\L$proc: $cost\E\n";
						$report_info{full_procedure_details}{"\L$proc\E"}{count} = $cost;
						foreach my $d (sort { $cost_detail{$b} <=> $cost_detail{$a} } keys %cost_detail)
						{
							next if (!$cost_detail{$d});
							$report_info{full_procedure_details}{"\L$proc\E"}{info} .= "\t$d => $cost_detail{$d}";
							$report_info{full_procedure_details}{"\L$proc\E"}{info} .= " (cost: ${$uncovered_score}{$d})" if (${$uncovered_score}{$d});
							$report_info{full_procedure_details}{"\L$proc\E"}{info} .= "\n";
							push(@{$report_info{full_procedure_details}{"\L$proc\E"}{keywords}}, $d) if (($d ne 'SIZE') && ($d ne 'TEST')); 
						}
					}
				}
				$report_info{'Objects'}{$typ}{'comment'} = "Total size of procedure code: $total_size bytes.";
			}
			elsif ($typ eq 'PACKAGE BODY')
			{
				$self->{packages} = $self->_get_packages();
				my $total_size = 0;
				my $number_fct = 0;
				my $number_pkg = 0;
				foreach my $pkg (sort keys %{$self->{packages}})
				{
					next if (!$self->{packages}{$pkg}{text});
					$number_pkg++;
					$total_size += length($self->{packages}{$pkg}{text});
					# Remove comment and text constant, they are not useful in assessment
					$self->_remove_comments(\$self->{packages}{$pkg}{text});
					$self->{comment_values} = ();
					$self->{text_values} = ();
					$self->{text_values_pos} = 0;
					my @codes = split(/CREATE(?: OR REPLACE)?(?: EDITIONABLE| NONEDITIONABLE)? PACKAGE\s+/i, $self->{packages}{$pkg}{text});
					foreach my $txt (@codes)
					{
						next if ($txt !~ /^BODY\s+/is);
						my %infos = $self->_lookup_package("CREATE OR REPLACE PACKAGE $txt");
						foreach my $f (sort keys %infos)
						{
							next if (!$f);
							if ($self->{estimate_cost})
							{
								my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $infos{$f}{code}, $infos{$f}{type});
								$report_info{'Objects'}{$typ}{'cost_value'} += $cost;
								$report_info{'Objects'}{$typ}{'detail'} .= "\L$f: $cost\E\n";
								$report_info{full_package_details}{"\L$f\E"}{count} = $cost;
								foreach my $d (sort { $cost_detail{$b} <=> $cost_detail{$a} } keys %cost_detail)
								{
									next if (!$cost_detail{$d});
									$report_info{full_package_details}{"\L$f\E"}{info} .= "\t$d => $cost_detail{$d}";
									$report_info{full_package_details}{"\L$f\E"}{info} .= " (cost: ${$uncovered_score}{$d})" if (${$uncovered_score}{$d});
									$report_info{full_package_details}{"\L$f\E"}{info} .= "\n";
									push(@{$report_info{full_package_details}{"\L$f\E"}{keywords}}, $d) if (($d ne 'SIZE') && ($d ne 'TEST')); 
								}
							}
							$number_fct++;
						}
					}
				}
				$self->{packages} = ();
				if ($self->{estimate_cost}) {
					$report_info{'Objects'}{$typ}{'cost_value'} += ($number_pkg*$Ora2Pg::PLSQL::OBJECT_SCORE{'PACKAGE BODY'});
				}
				$report_info{'Objects'}{$typ}{'comment'} = "Total size of package code: $total_size bytes. Number of procedures and functions found inside those packages: $number_fct.";
			}
			elsif ( ($typ eq 'SYNONYM') && !$self->{is_mysql} )
			{
				foreach my $t (sort {$a cmp $b} keys %synonyms)
				{
					if ($synonyms{$t}{dblink}) {
						$report_info{'Objects'}{$typ}{'detail'} .= "\L$t\E is a link to \L$synonyms{$t}{table_owner}.$synonyms{$t}{table_name}\@$synonyms{$t}{dblink}\E\n";
					} else {
						$report_info{'Objects'}{$typ}{'detail'} .= "\L$t\E is an alias to $synonyms{$t}{table_owner}.$synonyms{$t}{table_name}\n";
					}
				}
				$report_info{'Objects'}{$typ}{'comment'} = "SYNONYMs will be exported as views. SYNONYMs do not exists with PostgreSQL but a common workaround is to use views or set the PostgreSQL search_path in your session to access object outside the current schema.";
			}
			elsif ($typ eq 'INDEX PARTITION')
			{
				$report_info{'Objects'}{$typ}{'comment'} = "Only local indexes partition are exported, they are build on the column used for the partitioning.";
			}
			elsif ($typ eq 'TABLE PARTITION')
			{
				my %partitions = $self->_get_partitions_list();
				foreach my $t (sort keys %partitions) {
					$report_info{'Objects'}{$typ}{'detail'} .= "$t\n";
				}
				$report_info{'Objects'}{$typ}{'comment'} = "Partitions are well supported by PostgreSQL except key partition which will not be exported.";
			}
			elsif ($typ eq 'GLOBAL TEMPORARY TABLE')
			{
				$report_info{'Objects'}{$typ}{'comment'} = "Global temporary table are not supported by PostgreSQL and will not be exported. You will have to rewrite some application code to match the PostgreSQL temporary table behavior.";
				foreach my $t (sort keys %global_tables) {
					$report_info{'Objects'}{$typ}{'detail'} .= "\L$t\E\n";
				}
			}
			elsif ($typ eq 'CLUSTER')
			{
				$report_info{'Objects'}{$typ}{'comment'} = "Clusters are not supported by PostgreSQL and will not be exported.";
			}
			elsif ($typ eq 'VIEW')
			{
				if ($self->{estimate_cost})
				{
					foreach my $view (sort keys %view_infos)
					{
						# Remove unsupported definitions from the ddl statement
						$view_infos{$view}{text} =~ s/\s*WITH\s+READ\s+ONLY//is;
						$view_infos{$view}{text} =~ s/\s*OF\s+([^\s]+)\s+(WITH|UNDER)\s+[^\)]+\)//is;
						$view_infos{$view}{text} =~ s/\s*OF\s+XMLTYPE\s+[^\)]+\)//is;
						$view_infos{$view}{text} = $self->_format_view($view, $view_infos{$view}{text});

						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $view_infos{$view}{text}, 'VIEW');
						$report_info{'Objects'}{$typ}{'cost_value'} += $cost;
						# Do not show view that just have to be tested
						next if (!$cost);
						$cost += $Ora2Pg::PLSQL::OBJECT_SCORE{'VIEW'};
						# Show detail about views that might need manual rewritting
						$report_info{'Objects'}{$typ}{'detail'} .= "\L$view: $cost\E\n";
						$report_info{full_view_details}{"\L$view\E"}{count} = $cost;
						foreach my $d (sort { $cost_detail{$b} <=> $cost_detail{$a} } keys %cost_detail)
						{
							next if (!$cost_detail{$d});
							$report_info{full_view_details}{"\L$view\E"}{info} .= "\t$d => $cost_detail{$d}";
							$report_info{full_view_details}{"\L$view\E"}{info} .= " (cost: ${$uncovered_score}{$d})" if (${$uncovered_score}{$d});
							$report_info{full_view_details}{"\L$view\E"}{info} .= "\n";
							push(@{$report_info{full_view_details}{"\L$view\E"}{keywords}}, $d); 
						}
					}
				}
				$report_info{'Objects'}{$typ}{'comment'} = "Views are fully supported but can use specific functions.";
			}
			elsif ($typ eq 'DATABASE LINK')
			{
				my $def_fdw = 'oracle_fdw';
				$def_fdw = 'mysql_fdw' if ($self->{is_mysql});
				$report_info{'Objects'}{$typ}{'comment'} = "Database links will be exported as SQL/MED PostgreSQL's Foreign Data Wrapper (FDW) extensions using $def_fdw.";
				if ($self->{estimate_cost}) {
					$report_info{'Objects'}{$typ}{'cost_value'} = ($Ora2Pg::PLSQL::OBJECT_SCORE{'DATABASE LINK'}*$objects{$typ});
				}
			}
			elsif ($typ eq 'JOB')
			{
				$report_info{'Objects'}{$typ}{'comment'} = "Job are not exported. You may set external cron job with them.";
				if ($self->{estimate_cost}) {
					$report_info{'Objects'}{$typ}{'cost_value'} = ($Ora2Pg::PLSQL::OBJECT_SCORE{'JOB'}*$objects{$typ});
				}
			}
			# Apply maximum cost per object type
			if (exists $Ora2Pg::PLSQL::MAX_SCORE{$typ} && $report_info{'Objects'}{$typ}{'cost_value'} > $Ora2Pg::PLSQL::MAX_SCORE{$typ}) {
				$report_info{'Objects'}{$typ}{'cost_value'} = $Ora2Pg::PLSQL::MAX_SCORE{$typ};
			}
			$report_info{'total_cost_value'} += $report_info{'Objects'}{$typ}{'cost_value'};
			$report_info{'Objects'}{$typ}{'cost_value'} = sprintf("%2.2f", $report_info{'Objects'}{$typ}{'cost_value'});
		}

		if (!$self->{quiet} && !$self->{debug})
		{
			print STDERR $self->progress_bar($idx, $num_total_obj, 25, '=', 'objects types', 'end of objects auditing.'), "\n";
		}

		# DBA_AUDIT_TRAIL queries will not be count if no audit user is give
		if ($self->{audit_user})
		{
			my $tbname = 'DBA_AUDIT_TRAIL';
			$tbname = 'general_log' if ($self->{is_mysql});
			$report_info{'Objects'}{'QUERY'}{'number'} = 0;
			$report_info{'Objects'}{'QUERY'}{'invalid'} = 0;
			$report_info{'Objects'}{'QUERY'}{'comment'} = "Normalized queries found in $tbname for user(s): $self->{audit_user}";
			my %queries = $self->_get_audit_queries();
			foreach my $q (sort {$a <=> $b} keys %queries)
			{
				$report_info{'Objects'}{'QUERY'}{'number'}++;
				my $sql_q = Ora2Pg::PLSQL::convert_plsql_code($self, $queries{$q});
				if ($self->{estimate_cost})
				{
					my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $sql_q, 'QUERY');
					$cost += $Ora2Pg::PLSQL::OBJECT_SCORE{'QUERY'};
					$report_info{'Objects'}{'QUERY'}{'cost_value'} += $cost;
					$report_info{'total_cost_value'} += $cost;
				}
			}
			$report_info{'Objects'}{'QUERY'}{'cost_value'} = sprintf("%2.2f", $report_info{'Objects'}{'QUERY'}{'cost_value'});
		}
		$report_info{'total_cost_value'} = sprintf("%2.2f", $report_info{'total_cost_value'});

		# Display report in the requested format
		$self->_show_report(%report_info);

	}
	elsif ($type eq 'SHOW_SCHEMA')
	{
		# Get all tables information specified by the DBI method table_info
		$self->logit("Showing all schema...\n", 1);
		my $sth = $self->_schema_list() or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		while ( my @row = $sth->fetchrow()) 
		{
			my $warning = '';
			my $ret = $self->is_reserved_words($row[0]);
			if ($ret == 1) {
				$warning = " (Warning: '$row[0]' is a reserved word in PostgreSQL)";
			} elsif ($ret == 2) {
				$warning = " (Warning: '$row[0]' object name with numbers only must be double quoted in PostgreSQL)";
			}
			if (!$self->{is_mysql}) {
				$self->logit("SCHEMA $row[0]$warning\n", 0);
			} else {
				$self->logit("DATABASE $row[0]$warning\n", 0);
			}
		}
		$sth->finish();
	}
	elsif ( ($type eq 'SHOW_TABLE') || ($type eq 'SHOW_COLUMN') )
	{

		# Get all tables information specified by the DBI method table_info
		$self->logit("Showing table information...\n", 1);

		# Retrieve tables informations
		my %tables_infos = $self->_table_info(($type eq 'SHOW_TABLE') ? $self->{count_rows}: 0);

		# Retrieve column identity information
		$self->logit("Retrieving column identity information...\n", 1);
		%{ $self->{identity_info} } = $self->_get_identities();

		# Retrieve all columns information
		my %columns_infos = ();
		if ($type eq 'SHOW_COLUMN')
		{
			%columns_infos = $self->_column_info('',$self->{schema}, 'TABLE');
			foreach my $tb (keys %columns_infos)
			{
				foreach my $c (keys %{$columns_infos{$tb}}) {
					push(@{$self->{tables}{$tb}{column_info}{$c}}, @{$columns_infos{$tb}{$c}});
				}
			}
			%columns_infos = ();

			# Look for encrypted columns
			%{$self->{encrypted_column}} = $self->_encrypted_columns('',$self->{schema});

			# Retrieve index informations
			my ($uniqueness, $indexes, $idx_type, $idx_tbsp) = $self->_get_indexes('',$self->{schema});
			foreach my $tb (keys %{$indexes})
			{
				next if (!exists $tables_infos{$tb});
				%{$self->{tables}{$tb}{indexes}} = %{$indexes->{$tb}};
			}
			foreach my $tb (keys %{$idx_type})
			{
				next if (!exists $tables_infos{$tb});
				%{$self->{tables}{$tb}{idx_type}} = %{$idx_type->{$tb}};
			}
			foreach my $idx (keys %{ $self->{tables}{$tb}{idx_type} })
			{
				if ($self->{tables}{$tb}{idx_type}{$idx}{type} =~ /COLUMNSTORE/) {
					$self->{tables}{$tb}{columnstore} = 1;
				}
			}
		}

		# Get partition list to mark tables with partition.
		$self->logit("Looking to subpartition information...\n", 1);
		my %subpartitions_list = $self->_get_subpartitioned_table();
		$self->logit("Looking to partitioned tables information...\n", 1);
		my %partitions = $self->_get_partitioned_table(%subpartitions_list);

                # Look for external tables
                my %externals = ();
		if (!$self->{is_mysql} && ($self->{db_version} !~ /Release 8/))
		{
			$self->logit("Looking to external tables information...\n", 1);
			%externals = $self->_get_external_tables();
		}

		# Ordering tables by name by default
		my @ordered_tables = sort { $a cmp $b } keys %tables_infos;
		if (lc($self->{data_export_order}) eq 'size')
		{
			@ordered_tables = sort {
				($tables_infos{$b}{num_rows} || $tables_infos{$a}{num_rows}) ?
					$tables_infos{$b}{num_rows} <=> $tables_infos{$a}{num_rows} :
						$a cmp $b
		       	} keys %tables_infos;
		}
		# User provide the ordered list of table from a file
		elsif (-e $self->{data_export_order})
		{
			if (open(my $tfh, '<', $self->{data_export_order}))
			{
				@ordered_tables = ();
				while (my $l = <$tfh>)
				{
					chomp($l);
					next if (!exists $self->{tables}{$!});
					push(@ordered_tables, $l);
				}
				close($tfh);
			}
			else
			{
				$self->logit("FATAL: can't read file $self->{data_export_order} for ordering table export. $!\n", 0, 1);
			}
		}

		my @done = ();
		my $id = 0;
		# Set the table information for each class found
		my $i = 1;
		my $total_row_num = 0;
		foreach my $t (@ordered_tables)
		{
			# Jump to desired extraction
			if (grep(/^\Q$t\E$/, @done))
			{
				$self->logit("Duplicate entry found: $t\n", 1);
				next;
			} else {
				push(@done, $t);
			}
			my $warning = '';

			# Add a warning when the table name is > 63 character
			if (length($t) > 63) {
				$warning .= " (>63)";
			}

			# Signal that the table use columnstore
			if ($self->{is_mssql} && $self->{tables}{$tb}{columnstore} == 1) {
				$warning .= " - storage: columnar";
			}
			# Show compression type
			if ($self->{is_mssql} && exists $tables_infos{$t}{compressed} && $tables_infos{$t}{compressed} ne 'NONE') {
				$warning .= " - compression: $tables_infos{$t}{compressed}";
			}

			# Set the number of partition if any
			if (exists $partitions{"\L$t\E"})
			{
				my $upto = '';
				$upto = 'up to ' if ($partitions{"\L$t\E"}{count} == 1048575);
				$warning .= " - $upto" . $partitions{"\L$t\E"}{count} . " " . $partitions{"\L$t\E"}{type} . " partitions";
				$warning .= " with subpartitions" if ($partitions{"\L$t\E"}{composite});
			}

			# Search for reserved keywords
			my $ret = $self->is_reserved_words($t);
			if ($ret == 1) {
				$warning .= " (Warning: '$t' is a reserved word in PostgreSQL)";
			} elsif ($ret == 2) {
				$warning .= " (Warning: '$t' object name with numbers only must be double quoted in PostgreSQL)";
			}

			$total_row_num += $tables_infos{$t}{num_rows};

			# Show table information
			my $kind = '';
			$kind = ' FOREIGN'  if ($tables_infos{$t}{connection});
			if ($tables_infos{$t}{partitioned}) {
				$kind = ' PARTITIONED';
			}
			if (exists $externals{$t}) {
				$kind = ' EXTERNAL';
			}
			if ($tables_infos{$t}{nologging}) {
				$kind .= ' UNLOGGED';
			}
			if ($tables_infos{$t}{index_type}) {
				$warning .= " - Indexed type: $tables_infos{$t}{index_type}";
			}
			my $tname = $t;
			if (!$self->{is_mysql}) {
				$tname = "$tables_infos{$t}{owner}.$t" if ($self->{debug});
				$self->logit("[$i]$kind TABLE $tname (owner: $tables_infos{$t}{owner}, $tables_infos{$t}{num_rows} rows)$warning\n", 0);
			} else {
				$self->logit("[$i]$kind TABLE $tname ($tables_infos{$t}{num_rows} rows)$warning\n", 0);
			}

			# Set the fields information
			if ($type eq 'SHOW_COLUMN')
			{
				# Collect column's details for the current table with attempt to preserve column declaration order
				foreach my $k (sort { 
						if (!$self->{reordering_columns}) {
							$self->{tables}{$t}{column_info}{$a}[11] <=> $self->{tables}{$t}{column_info}{$b}[11];
						}
						else
						{
							my $tmpa = $self->{tables}{$t}{column_info}{$a};
							$tmpa->[2] =~ s/\D//g;
							my $typa = $self->_sql_type($tmpa->[1], $tmpa->[2], $tmpa->[5], $tmpa->[6], $tmpa->[4]);
							$typa =~ s/\(.*//;
							my $tmpb = $self->{tables}{$t}{column_info}{$b};
							$tmpb->[2] =~ s/\D//g;
							my $typb = $self->_sql_type($tmpb->[1], $tmpb->[2], $tmpb->[5], $tmpb->[6], $tmpb->[4]);
							$typb =~ s/\(.*//;
							$TYPALIGN{$typb} <=> $TYPALIGN{$typa};
						}
					} keys %{$self->{tables}{$t}{column_info}})
				{
					$warning = '';
					# COLUMN_NAME,DATA_TYPE,DATA_LENGTH,NULLABLE,DATA_DEFAULT,DATA_PRECISION,DATA_SCALE,CHAR_LENGTH,TABLE_NAME,OWNER,VIRTUAL_COLUMN,POSITION,AUTO_INCREMENT,SRID,SDO_DIM,SDO_GTYPE
					my $d = $self->{tables}{$t}{column_info}{$k};
					$d->[2] =~ s/[^0-9\-\.]//g;
					my $type1 = $self->_sql_type($d->[1], $d->[2], $d->[5], $d->[6], $d->[4]);
					$type1 = "$d->[1], $d->[2]" if (!$type1);
					$warning .= " (numeric?)" if ($type1 =~ s/#$//);

					# Check if we need auto increment
					if ($d->[12] eq 'auto_increment' || $d->[12] eq '1')
					{
						if ($type1 !~ s/bigint/bigserial/)
						{
							if ($type1 !~ s/smallint/smallserial/) {
								$type1 =~ s/integer/serial/;
								$type1 =~ s/numeric.*/bigserial/;
							}
						}
						if ($type1 =~ /serial/) {
							$warning = " - Seq last value: $tables_infos{$t}{auto_increment}";
						}
					}
					$type1 = $self->{'modify_type'}{"\L$t\E"}{"\L$k\E"} if (exists $self->{'modify_type'}{"\L$t\E"}{"\L$k\E"});
					my $align = '';
					my $len = $d->[2];
					if (($d->[1] =~ /char/i) && ($d->[7] > $d->[2])) {
						$d->[2] = $d->[7];
					}
					if (length($d->[0]) > 63) {
						$warning .= " (>63)";
					}
					if ($d->[1] eq 'DATE') {
						$warning .= " (date?)";
					}
					if ($self->{is_mssql} && $d->[16]) {
						$warning .= " [Masked with: $d->[17]]";
					}
					$self->logit("\t$d->[0] : $d->[1]");
					if ($d->[1] !~ /SDO_GEOMETRY/)
					{
						if ($d->[2] && !$d->[5] && $d->[1] !~ /\(\d+\)/) {
							$self->logit("($d->[2])");
						}
						elsif ($d->[5] && ($d->[1] =~ /NUMBER/i) )
						{
							$self->logit("($d->[5]");
							$self->logit(",$d->[6]") if ($d->[6]);
							$self->logit(")");
						}
						if ($self->{reordering_columns})
						{
							my $typ = $type1;
							$typ =~ s/\(.*//;
							$align = " - typalign: $TYPALIGN{$typ}";
						}
					}
					else
					{
						# 12:SRID,13:SDO_DIM,14:SDO_GTYPE
						# Set the dimension, array is (srid, dims, gtype)
						my $suffix = '';
						if ($d->[13] == 3) {
							$suffix = 'Z';
						} elsif ($d->[13] == 4) {
							$suffix = 'ZM';
						}
						my $gtypes = '';
						if (!$d->[14] || ($d->[14] =~  /,/) ) {
							$gtypes = $Ora2Pg::Oracle::ORA2PG_SDO_GTYPE{0};
						} else {
							$gtypes = $d->[14];
						}
						$type1 = "geometry($gtypes$suffix";
						if ($d->[12]) {
							$type1 .= ",$d->[12]";
						}
						$type1 .= ")";
						$type1 .= " - $d->[14]" if ($d->[14] =~  /,/);
						
					}
					my $ret = $self->is_reserved_words($d->[0]);
					if ($ret == 1) {
						$warning .= " (Warning: '$d->[0]' is a reserved word in PostgreSQL)";
					} elsif ($ret == 2) {
						$warning .= " (Warning: '$d->[0]' object name with numbers only must be double quoted in PostgreSQL)";
					} elsif ($ret == 3) {
						$warning = " (Warning: '$d->[0]' is a system column in PostgreSQL)";
					}
					# Check if this column should be replaced by a boolean following table/column name
					my $typlen = $d->[5];
					$typlen ||= $d->[2];
					if (grep(/^$d->[0]$/i, @{$self->{'replace_as_boolean'}{uc($t)}})) {
						$type1 = 'boolean';
					}
					# Check if this column should be replaced by a boolean following type/precision
					elsif (exists $self->{'replace_as_boolean'}{uc($d->[1])} && ($self->{'replace_as_boolean'}{uc($d->[1])}[0] == $typlen)) {
						$type1 = 'boolean';
					}

					# Autoincremented columns
					if (!$self->{schema} && $self->{export_schema}) {
						$d->[8] = "$d->[9].$d->[8]";
					}
					if (exists $self->{identity_info}{$d->[8]}{$d->[0]})
					{
						if ($self->{pg_supports_identity})
						{
							$type1 = 'bigint' if ($self->{force_identity_bigint}); # Force bigint
							$type1 .= " GENERATED $self->{identity_info}{$d->[8]}{$d->[0]}{generation} AS IDENTITY";
							if (exists $self->{identity_info}{$d->[8]}{$d->[0]}{options}
								&& $self->{identity_info}{$d->[8]}{$d->[0]}{options} ne '')
							{
								# Adapt automatically the max value following the data type
								if ($type1 =~ /^(integer|int4|int)$/) {
									$self->{identity_info}{$d->[8]}{$d->[0]}{options} =~ s/ 9223372036854775807/ 2147483647/s;
								}
								$type1 .= " (" . $self->{identity_info}{$d->[8]}{$d->[0]}{options} . ')';
							}
						}
						else
						{
							$type1 =~ s/bigint$/bigserial/;
							$type1 =~ s/smallint/smallserial/;
							$type1 =~ s/(integer|int)$/serial/;
						}
					}

					my $encrypted = '';
					$encrypted = " [encrypted]" if (exists $self->{encrypted_column}{"$t.$k"});
					my $virtual = '';
					$virtual = " [virtual column]" if ($d->[10] eq 'YES' && $d->[4]);
					$self->logit(" => $type1$warning$align$virtual$encrypted\n");
				}
			}
			$i++;
		}
		$self->logit("----------------------------------------------------------\n", 0);
		$self->logit("Total number of rows: $total_row_num\n\n", 0);

		# Looking for Global temporary tables
		my %global_tables = $self->_global_temp_table_info();
		$self->logit("Global Temporary Tables:\n", 0);
		foreach my $k (sort keys %global_tables) {
			$self->logit("\t$k\n", 0);
		}
		$self->logit("\n\n", 0);

		$self->logit("Top $self->{top_max} of tables sorted by number of rows:\n", 0);
		$i = 1;
		foreach my $t (sort {$tables_infos{$b}{num_rows} <=> $tables_infos{$a}{num_rows}} keys %tables_infos)
		{
			next if ($tables_infos{$t}{num_rows} == 0);
			my $tname = $t;
			if (!$self->{is_mysql}) {
				$tname = "$tables_infos{$t}{owner}.$t" if ($self->{debug});
			}
			$self->logit("\t[$i] TABLE $tname has $tables_infos{$t}{num_rows} rows\n", 0);
			$i++;
			last if ($i > $self->{top_max});
		}
		$self->logit("Top $self->{top_max} of largest tables:\n", 0);
		$i = 1;
		if ($self->{is_mssql} || $self->{is_mysql})
		{
			foreach my $t (sort {$tables_infos{$b}{size} <=> $tables_infos{$a}{size}} keys %tables_infos)
			{
				next if ($tables_infos{$t}{size} == 0);
				my $tname = $t;
				if (!$self->{is_mysql} && !$self->{is_mssql}) {
					$tname = "$tables_infos{$t}{owner}.$t" if ($self->{debug});
				}
				$self->logit("\t[$i] TABLE $tname: $tables_infos{$t}{size} MB (" . $tables_infos{$t}{num_rows} . " rows)\n", 0);
				$i++;
				last if ($i > $self->{top_max});
			}
		}
		else
		{
			# Because we avoid using JOIN when querying the Oracle catalog, look for all table size
			my %largest_table = $self->_get_largest_tables();
			foreach my $t (sort { $largest_table{$b} <=> $largest_table{$a} } keys %largest_table) {
				last if ($i > $self->{top_max});
				my $tname = $t;
				$tname = "$tables_infos{$t}{owner}.$t" if ($self->{debug});
				$self->logit("\t[$i] TABLE $tname: $largest_table{$t} MB (" . $tables_infos{$t}{num_rows} . " rows)\n", 0);
				$i++;
			}
		}
	}
}

sub show_test_errors
{
	my ($self, $lbl_type, @errors) = @_;

	print "[ERRORS \U$lbl_type\E COUNT]\n";
	if ($#errors >= 0)
	{
		foreach my $msg (@errors) {
			print "DIFF: $msg\n";
		}
	}
	else
	{
		if ($self->{pg_dsn}) {
			print "OK, Oracle and PostgreSQL have the same number of $lbl_type.\n";
		} else {
			print "No PostgreSQL connection, can not check number of $lbl_type.\n";
		}
	}
}

sub set_pg_relation_name
{
	my ($self, $table) = @_;

	my $tbmod = $self->get_replaced_tbname($table);
	my $cmptb = $tbmod;
	$cmptb =~ s/"//g;
	my $orig = '';
	$orig = " (origin: $table)" if (lc($cmptb) ne lc($table));
	my $tbname = $tbmod;
	$tbname =~ s/[^"\.]+\.//;
	my $schm = $self->{schema};
	$schm = $self->{pg_schema} if ($self->{pg_schema});
	$schm =~ s/"//g;
	$tbname =~ s/"//g;
	$schm = $self->quote_object_name($schm);
	$tbname = $self->quote_object_name($tbname);
	if ($self->{pg_schema})
	{
		if ($self->{preserve_case}) {
			return ($tbmod, $orig, $self->{pg_schema}, "\"$schm\".\"$tbname\"");
		} else {
			return ($tbmod, $orig, $self->{pg_schema}, "$schm.$tbname");
		}
	}
	elsif ($self->{schema} && $self->{export_schema})
	{
		if ($self->{preserve_case}) {
			return ($tbmod, $orig, $self->{schema}, "\"$schm\".\"$tbname\"");
		} else {
			return ($tbmod, $orig, $self->{schema}, "$schm.$tbname");
		}
	}

	return ($tbmod, $orig, '', $tbname);
}

sub get_schema_condition
{
	my ($self, $attrname, $local_schema) = @_;

	$attrname ||= 'n.nspname';

	if ($local_schema && $self->{export_schema}) {
		return " AND lower($attrname) = quote_ident('\L$local_schema\E')";
	} elsif ($self->{pg_schema} && $self->{export_schema}) {
		my $sql = " AND lower($attrname) IN (";
		foreach my $s (split(/\s*,\s*/, $self->{pg_schema})) {
			$sql .= "quote_ident('\L$s\E'),";
		}
		$sql =~ s/,$//;
		return $sql . ")";
	} elsif ($self->{schema} && $self->{export_schema}) {
		return "AND lower($attrname) = quote_ident('\L$self->{schema}\E')";
	} elsif ($self->{pg_schema}) {
		return "AND lower($attrname) = quote_ident('\L$self->{pg_schema}\E')";
	}

	my $cond = " AND $attrname <> 'pg_catalog' AND $attrname <> 'information_schema' AND $attrname !~ '^pg_toast'";

	return $cond;
}

sub _count_pg_rows
{
	my ($self, $dbhdest, $lbl, $t, $num_rows) = @_;

	chomp($num_rows);

	my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
	if ($self->{pg_dsn})
	{
		my $err = '';
		my $dirprefix = '';
		$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});

		$self->logit("DEBUG: pid $$ looking for real row count for destination table $t...\n", 1);

		my $sql = "SELECT count(*) FROM $both;";
#		if ($self->{preserve_case}) {
#			$sql = "SELECT count(*) FROM \"$schema\".\"$t\";";
#		}
		my $s = $dbhdest->prepare($sql);
		if (not defined $s)
		{
			$err = "Table $both$orig does not exists in PostgreSQL database." if ($s->state eq '42P01');
		}
		else
		{
			if (not $s->execute)
			{
				$err = "Table $both$orig does not exists in PostgreSQL database." if ($s->state eq '42P01');
			}
			else
			{
				my $fh = new IO::File;
				$fh->open(">>${dirprefix}ora2pg_stdout_locker") or $self->logit("FATAL: can't write to ${dirprefix}ora2pg_stdout_locker, $!\n", 0, 1);
				flock($fh, 2) || die "FATAL: can't lock file ${dirprefix}ora2pg_stdout_locker\n";
				print "$lbl:$t:$num_rows\n";
				while ( my @row = $s->fetchrow())
				{
					print "POSTGRES:$both$orig:$row[0]\n";
					if ($row[0] != $num_rows) {
						$fh->print("Table $both$orig doesn't have the same number of line in source database ($num_rows) and in PostgreSQL ($row[0]).\n");
					}
					last;
				}
				$s->finish();
				$fh->close;
			}
		}
		if ($err)
		{
			my $fh = new IO::File;
			$fh->open(">>${dirprefix}ora2pg_stdout_locker") or $self->logit("FATAL: can't write to ${dirprefix}ora2pg_stdout_locker, $!\n", 0, 1);
			flock($fh, 2) || die "FATAL: can't lock file ${dirprefix}ora2pg_stdout_locker\n";
			print "$lbl:$t:$num_rows\n";
			$fh->print("$err\n");
			$fh->close;
		}
	}
}

sub _table_row_count
{
	my $self = shift;

	my $lbl = 'ORACLEDB';
	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});

	# Get all tables information specified by the DBI method table_info
	$self->logit("Looking for real row count in source database and PostgreSQL tables...\n", 1);

	# Retrieve tables informations
	my %tables_infos = $self->_table_info($self->{count_rows});

	####
	# Test number of row in tables
	####
	my @errors = ();
	print "\n";
	print "[TEST ROWS COUNT]\n";
	foreach my $t (sort keys %tables_infos)
	{
		if ($self->{parallel_tables} > 1)
		{
			spawn sub {
				my $dbhpg = $self->{dbhdest}->clone();
				$self->_count_pg_rows($dbhpg, $lbl, $t, $tables_infos{$t}{num_rows});
				$dbhpg->disconnect();
			};
			$parallel_tables_count++;

			# Wait for connection terminaison
			while ($parallel_tables_count > $self->{parallel_tables})
			{
				my $kid = waitpid(-1, WNOHANG);
				if ($kid > 0)
				{
					$parallel_tables_count--;
					delete $RUNNING_PIDS{$kid};
				}
				usleep(50000);
			}
		}
		else
		{
			$self->_count_pg_rows($self->{dbhdest}, $lbl, $t, $tables_infos{$t}{num_rows});
		}
	}

	# Wait for all child die
	if ($self->{parallel_tables} > 1)
	{
		while (scalar keys %RUNNING_PIDS > 0)
		{
			my $kid = waitpid(-1, WNOHANG);
			if ($kid > 0) {
				delete $RUNNING_PIDS{$kid};
			}
			usleep(50000);
		}
	}
	
	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	if (-e "${dirprefix}ora2pg_stdout_locker")
	{
		my $fh = new IO::File;
		$fh->open("${dirprefix}ora2pg_stdout_locker") or $self->logit("FATAL: can't read file ${dirprefix}ora2pg_stdout_locker, $!\n", 0, 1);
		@errors = <$fh>;
		$fh->close;
		unlink("${dirprefix}ora2pg_stdout_locker");
		chomp @errors;
	}

	$self->show_test_errors('rows', @errors);
}

sub is_in_struct
{
	my ($self, $t, $cn) = @_;

	if (!$self->{preserve_case})
	{
		if (exists $self->{modify}{"\L$t\E"}) {
			return 0 if (!grep(/^\Q$cn\E$/i, @{$self->{modify}{"\L$t\E"}}));
		} elsif (exists $self->{exclude_columns}{"\L$t\E"}) {
			return 0 if (grep(/^\Q$cn\E$/i, @{$self->{exclude_columns}{"\L$t\E"}}));
		}
	}
	else
	{
		if (exists $self->{modify}{"$t"}) {
			return 0 if (!grep(/^\Q$cn\E$/i, @{$self->{modify}{"$t"}}));
		} elsif (exists $self->{exclude_columns}{"$t"}) {
			return 0 if (grep(/^\Q$cn\E$/i, @{$self->{exclude_columns}{"$t"}}));
		}
	}

	return 1;
}

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

	my %col_count = ();
	if ($self->{is_mysql}) {
		%col_count = Ora2Pg::MySQL::_col_count($self, $table, $schema);
	} elsif ($self->{is_mssql}) {
		%col_count = Ora2Pg::MSSQL::_col_count($self, $table, $schema);
	} else {
		%col_count = Ora2Pg::Oracle::_col_count($self, $table, $schema);
	}

	return %col_count;
}

sub _test_table
{
	my $self = shift;

	my @errors = ();

	# Get all tables information specified by the DBI method table_info
	$self->logit("Looking for objects count related to source database and PostgreSQL tables...\n", 1);

	# Retrieve tables informations
	my %tables_infos = $self->_table_info();

	my $lbl = 'ORACLEDB';
	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});

	####
	# Test number of column in tables
	####
	print "[TEST COLUMNS COUNT]\n";
	my %col_count = $self->_col_count('', $self->{schema});
	$schema_cond = $self->get_schema_condition('pg_class.relnamespace::regnamespace::text');
	my $sql = qq{
SELECT upper(pg_namespace.nspname||'.'||pg_class.relname), pg_attribute.attname
FROM pg_attribute
JOIN pg_class ON (pg_class.oid=pg_attribute.attrelid)
JOIN pg_namespace ON (pg_class.relnamespace=pg_namespace.oid)
WHERE pg_class.relkind IN ('r', 'p') AND pg_attribute.attnum > 0 AND NOT pg_attribute.attisdropped $schema_cond
ORDER BY pg_attribute.attnum
};
	my %pgret = ();
	if ($self->{pg_dsn})
	{
		my $s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about indexes.");
			return;
		}
		while ( my @row = $s->fetchrow())
		{
			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
			push(@{$pgret{$row[0]}}, $row[1]);
		}
		$s->finish;
	}
	my %pgcount = ();
	foreach my $t (keys %pgret) {
		$pgcount{$t} = $#{$pgret{$t}} + 1;
	}

	my @tables_names = keys %tables_infos;
	foreach my $t (sort keys %col_count)
	{
		next if (!grep(/^\Q$t\E$/i, @tables_names));
		print "$lbl:$t:$col_count{$t}\n";
		if ($self->{pg_dsn})
		{
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
			my $tbname = uc($both);
			$tbname =~ s/"//g;
			$pgcount{$tbname} ||= 0;
			print "POSTGRES:$both$orig:", $pgcount{$tbname}, "\n";
			if ($pgcount{$tbname} != $col_count{$t}) {
				push(@errors, "Table $both$orig doesn't have the same number of columns in source database ($col_count{$t}) and in PostgreSQL (" . $pgcount{$tbname} . ").");
				push(@errors, "\tPostgreSQL modified struct: $both$orig(" . join(',', @{$pgret{$tbname}}) . ")");
			}
		}
	}
	%pgcount = ();
	$self->show_test_errors('columns', @errors);
	@errors = ();

	####
	# Test number of index in tables
	####
	print "[TEST INDEXES COUNT]\n";
	my ($uniqueness, $indexes, $idx_type, $idx_tbsp) = undef;
	# special case for MySQL, FIXME: we should use $self->_get_indexes() for both
	if ($self->{is_mysql}) {
		$indexes = Ora2Pg::MySQL::_count_indexes($self, '', $self->{schema});
	} else {
		($uniqueness, $indexes, $idx_type, $idx_tbsp) = $self->_get_indexes('', $self->{schema}, 1);
	}
	$schema_cond = $self->get_schema_condition('tn.nspname');
	my $exclude_unique = '';
	$exclude_unique = 'AND NOT i.indisunique' if ($self->{is_mssql});
	$sql = qq{SELECT tn.nspname||'.'||t.relname, count(*)
FROM pg_index i
JOIN pg_class c on c.oid = i.indexrelid
JOIN pg_namespace n on n.oid = c.relnamespace
JOIN pg_class t on t.oid = i.indrelid
JOIN pg_namespace tn on tn.oid = t.relnamespace
WHERE 1=1 $exclude_unique $schema_cond
GROUP BY tn.nspname, t.relname
};
	%pgret = ();
	if ($self->{pg_dsn})
	{
		my $s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about indexes.");
			return;
		}
		while ( my @row = $s->fetchrow())
		{
			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
			$pgret{"\U$row[0]\E"} = $row[1];
		}
		$s->finish;
	}
	# Initialize when there is no indexes in a table
	foreach my $t (keys %tables_infos) {
		$indexes->{$t} = {} if (not exists $indexes->{$t});
	}

	foreach my $t (sort keys %{$indexes})
	{
		next if (!exists $tables_infos{$t});
		my $numixd = scalar keys %{$indexes->{$t}};
		print "$lbl:$t:$numixd\n";
		if ($self->{pg_dsn})
		{
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
			$pgret{"\U$both\E"} ||= 0;
			print "POSTGRES:$both$orig:", $pgret{"\U$both\E"}, "\n";
			if ($pgret{"\U$both\E"} != $numixd) {
				push(@errors, "Table $both$orig doesn't have the same number of indexes in source database ($numixd) and in PostgreSQL (" . $pgret{"\U$both\E"} . ").");
			}
		}
	}
	$self->show_test_errors('indexes', @errors);
	@errors = ();

	####
	# Test unique constraints (excluding primary keys)
	####
	print "\n";
	print "[TEST UNIQUE CONSTRAINTS COUNT]\n";
	my %unique_keys = $self->_unique_key('',$self->{schema},'U');
	$schema_cond = $self->get_schema_condition('tn.nspname');
	$exclude_unique = '';
	$exclude_unique = 'AND i.indisunique' if ($self->{is_mssql});
	$sql = qq{SELECT tn.nspname||'.'||t.relname, count(*)
FROM pg_constraint c
JOIN pg_class t on t.oid = c.conrelid
JOIN pg_namespace tn on tn.oid = c.connamespace
WHERE 1=1 $exclude_unique $schema_cond
AND c.contype = 'u'
AND c.conindid > 1
GROUP BY tn.nspname, t.relname
};
	%pgret = ();
	if ($self->{pg_dsn})
	{
		my $s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about unique constraints.");
			return;
		}
		while ( my @row = $s->fetchrow())
		{
			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
			$pgret{"\U$row[0]\E"} = $row[1];
		}
		$s->finish;
	}
	# Initialize when there is not unique key in a table
	foreach my $t (keys %tables_infos) {
		$unique_keys{$t} = {} if (not exists $unique_keys{$t});
	}

	foreach my $t (sort keys %unique_keys)
	{
		next if (!exists $tables_infos{$t});
		my $numixd = scalar keys %{$unique_keys{$t}};
		if ($self->{is_mysql})
		{
			foreach my $k (keys %{$unique_keys{$t}}) {
				$numixd-- if ($unique_keys{$t}{$k}{type} eq 'P');
			}
		}
		print "$lbl:$t:$numixd\n";
		if ($self->{pg_dsn})
		{
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
			$pgret{"\U$both\E"} ||= 0;
			print "POSTGRES:$both$orig:", $pgret{"\U$both\E"}, "\n";
			if ($pgret{"\U$both\E"} != $numixd) {
				push(@errors, "Table $both$orig doesn't have the same number of unique constraints in source database ($numixd) and in PostgreSQL (" . $pgret{"\U$both\E"} . ").");
			}
		}
	}
	$self->show_test_errors('unique constraints', @errors);
	@errors = ();

	####
	# Test primary keys only
	####
	print "\n";
	print "[TEST PRIMARY KEYS COUNT]\n";
	%unique_keys = $self->_unique_key('',$self->{schema},'P');
	$schema_cond = $self->get_schema_condition('pg_class.relnamespace::regnamespace::text');
	$sql = qq{
SELECT schemaname||'.'||tablename, count(*)
FROM pg_indexes
JOIN pg_class ON (pg_class.relname=pg_indexes.indexname AND pg_class.relnamespace=pg_indexes.schemaname::regnamespace::oid)
JOIN pg_constraint ON (pg_constraint.conname=pg_class.relname AND pg_constraint.connamespace=pg_class.relnamespace)
WHERE pg_constraint.contype = 'p' $schema_cond
GROUP BY schemaname,tablename
};
	%pgret = ();
	if ($self->{pg_dsn})
	{
		my $s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about primary keys.");
			return;
		}
		while ( my @row = $s->fetchrow())
		{
			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
			$pgret{"\U$row[0]\E"} = $row[1];
		}
		$s->finish;
	}
	# Initialize when there is not unique key in a table
	foreach my $t (keys %tables_infos) {
		$unique_keys{$t} = {} if (not exists $unique_keys{$t});
	}

	foreach my $t (sort keys %unique_keys)
	{
		next if (!exists $tables_infos{$t});
		my $nbpk = 0;
		foreach my $c (keys %{$unique_keys{$t}}) {
			$nbpk++ if ($unique_keys{$t}{$c}{type} eq 'P');
		}
		print "$lbl:$t:$nbpk\n";
		if ($self->{pg_dsn})
		{
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
			$pgret{"\U$both\E"} ||= 0;
			print "POSTGRES:$both$orig:", $pgret{"\U$both\E"}, "\n";
			if ($pgret{"\U$both\E"} != $nbpk) {
				push(@errors, "Table $both$orig doesn't have the same number of primary keys in source database ($nbpk) and in PostgreSQL (" . $pgret{"\U$both\E"} . ").");
			}
		}
	}
	%unique_keys = ();
	$self->show_test_errors('primary keys', @errors);
	@errors = ();

	####
	# Test check constraints
	####
	my %nbnotnull = {}; # will be used in the NOT NULL constraint count as based on a CHECK constraints
	print "\n";
	print "[TEST CHECK CONSTRAINTS COUNT]\n";
	my %check_constraints = $self->_check_constraint('',$self->{schema});
	$schema_cond = $self->get_schema_condition('n.nspname');
	$sql = qq{
SELECT CASE WHEN regexp_count(r.conrelid::regclass::text, n.nspname||'.') > 0 THEN r.conrelid::regclass::text ELSE n.nspname::regnamespace||'.'||r.conrelid::regclass END, count(*)
FROM pg_catalog.pg_constraint r JOIN pg_class c ON (r.conrelid=c.oid) JOIN pg_namespace n ON (c.relnamespace=n.oid)
WHERE r.contype = 'c' $schema_cond
GROUP BY n.nspname,r.conrelid
};
	%pgret = ();
	if ($self->{pg_dsn})
	{
		my $s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about check constraints.");
			return;
		}
		while ( my @row = $s->fetchrow())
		{
			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
			$pgret{"\U$row[0]\E"} = $row[1];
		}
		$s->finish;
	}
	# Initialize when there is not unique key in a table
	foreach my $t (keys %tables_infos) {
		$check_constraints{$t}{constraint} = {} if (not exists $check_constraints{$t});
	}

	foreach my $t (sort keys %check_constraints)
	{
		next if (!exists $tables_infos{$t});
		my $nbcheck = 0;
		foreach my $cn (keys %{$check_constraints{$t}{constraint}})
		{
			if ($check_constraints{$t}{constraint}{$cn}{condition} =~ /^[^\s]+\s+IS\s+NOT\s+NULL$/i) {
				$nbnotnull{$t}{$check_constraints{$t}{constraint}{$cn}{condition}} = 1;
			} else {
				$nbcheck++;
			}
		}
		print "$lbl:$t:$nbcheck\n";
		if ($self->{pg_dsn})
		{
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
			$pgret{"\U$both\E"} ||= 0;
			print "POSTGRES:$both$orig:", $pgret{"\U$both\E"}, "\n";
			if ($pgret{"\U$both\E"} != $nbcheck) {
				push(@errors, "Table $both$orig doesn't have the same number of check constraints in source database ($nbcheck) and in PostgreSQL (" . $pgret{"\U$both\E"} . ").");
			}
		}
	}
	%check_constraints = ();
	$self->show_test_errors('check constraints', @errors);
	@errors = ();

	####
	# Test NOT NULL constraints
	####
	print "\n";
	print "[TEST NOT NULL CONSTRAINTS COUNT]\n";
	my %column_infos = $self->_column_attributes('', $self->{schema}, 'TABLE');
	$schema_cond = $self->get_schema_condition('n.nspname');
	$sql = qq{
SELECT n.nspname||'.'||e.oid::regclass, count(*)
FROM pg_catalog.pg_attribute a
JOIN pg_class e ON (e.oid=a.attrelid)
JOIN pg_namespace n ON (e.relnamespace=n.oid)
WHERE a.attnum > 0
  AND e.relkind IN ('r')
  AND NOT a.attisdropped AND a.attnotnull 
 $schema_cond
GROUP BY n.nspname,e.oid
};
	%pgret = ();
	if ($self->{pg_dsn})
	{
		my $s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about not null constraints using query: $sql");
			$self->show_test_errors('not null constraints', @errors);
			return;
		}
		while ( my @row = $s->fetchrow())
		{
			$row[0] =~ s/^[^\.]+\.([^\.]+\.)/$1/; # remove possible duplicate schema prefix
			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
			$pgret{"\U$row[0]\E"} = $row[1];
		}
		$s->finish;
	}
	foreach my $t (sort keys %column_infos)
	{
		next if (!exists $tables_infos{$t});
		my $nbnull = 0;
		foreach my $cn (keys %{$column_infos{$t}})
		{
			next if (!$self->is_in_struct($t, $cn));
			if ($column_infos{$t}{$cn}{nullable} =~ /^N/) {
				$nbnull++;
			}
		}
		# Append the CHECK not null constraints if any
		if (exists $nbnotnull{$t})
		{
			$nbnull = scalar keys %{ $nbnotnull{$t} } if (scalar keys %{ $nbnotnull{$t} } > $nbnull);
		}

		print "$lbl:$t:$nbnull\n";
		if ($self->{pg_dsn})
		{
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
			$pgret{"\U$both\E"} ||= 0;
			print "POSTGRES:$both$orig:", $pgret{"\U$both\E"}, "\n";
			if ($pgret{"\U$both\E"} != $nbnull) {
				push(@errors, "Table $both$orig doesn't have the same number of not null constraints in source database ($nbnull) and in PostgreSQL (" . $pgret{"\U$both\E"} . ").");
			}
		}
	}
	$self->show_test_errors('not null constraints', @errors);
	@errors = ();

	####
	# Test column default values
	####
	print "\n";
	print "[TEST COLUMN DEFAULT VALUE COUNT]\n";
	$schema_cond = $self->get_schema_condition('n.nspname');
	# SELECT n.nspname||'.'||e.oid::regclass,
	$sql = qq{
SELECT n.nspname||'.'||e.oid::regclass,
  count((SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)
   FROM pg_catalog.pg_attrdef d
   WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef)) "default value"
FROM pg_catalog.pg_attribute a JOIN pg_class e ON (e.oid=a.attrelid) JOIN pg_namespace n ON (e.relnamespace=n.oid)
WHERE a.attnum > 0 AND NOT a.attisdropped
$schema_cond
GROUP BY n.nspname,e.oid
};
	%pgret = ();
	if ($self->{pg_dsn})
	{
		my $s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about column default values.");
			return;
		}
		while ( my @row = $s->fetchrow())
		{
			$row[0] =~ s/^[^\.]+\.([^\.]+\.)/$1/; # remove possible duplicate schema prefix
			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
			$pgret{"\U$row[0]\E"} = $row[1];
		}
		$s->finish;
	}
	my @seqs = ();
	# MySQL do not have sequences but we cound autoincrement column as sequences
	if ($self->{is_mysql}) {
		@seqs = Ora2Pg::MySQL::_count_sequences($self);
	}
	foreach my $t (sort keys %column_infos)
	{
		next if (!exists $tables_infos{$t});
		my $nbdefault = 0;
		foreach my $cn (keys %{$column_infos{$t}})
		{
			next if (!$self->is_in_struct($t, $cn));
			if ($column_infos{$t}{$cn}{default} ne ''
				&& uc($column_infos{$t}{$cn}{default}) ne 'NULL'
				# identity column
				&& ( $column_infos{$t}{$cn}{default} !~ /ISEQ\$\$_.*nextval/i
					|| $self->{is_mysql} || !$self->{pg_supports_identity})

			)
			{
				$nbdefault++;
			}
		}
		if (grep(/^$t$/i, @seqs)) {
			$nbdefault++;
		}
		print "$lbl:$t:$nbdefault\n";
		if ($self->{pg_dsn})
		{
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
			$pgret{"\U$both\E"} ||= 0;
			print "POSTGRES:$both$orig:", $pgret{"\U$both\E"}, "\n";
			if ($pgret{"\U$both\E"} != $nbdefault) {
				push(@errors, "Table $both$orig doesn't have the same number of column default value in source database ($nbdefault) and in PostgreSQL (" . $pgret{"\U$both\E"} . ").");
			}
		}
	}
	%column_infos = ();
	$self->show_test_errors('column default value', @errors);
	@errors = ();

	####
	# Test identity columns
	####
	if ($self->{is_mysql} || !$self->{pg_supports_identity})
	{
		print "\n";
		print "[TEST IDENTITY COLUMN COUNT]\n";
		$schema_cond = $self->get_schema_condition('n.nspname');
		$sql = qq{
SELECT e.oid::regclass,
  count((SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)
   FROM pg_catalog.pg_attrdef d
   WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef)) "default value"
FROM pg_catalog.pg_attribute a JOIN pg_class e ON (e.oid=a.attrelid) JOIN pg_namespace n ON (e.relnamespace=n.oid)
WHERE a.attnum > 0 AND NOT a.attisdropped AND a.attidentity IN ('a', 'd')
$schema_cond
GROUP BY e.oid
};
		%pgret = ();
		if ($self->{pg_dsn})
		{
			my $s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
			if (not $s->execute())
			{
				push(@errors, "Can not extract information from catalog about identity columns.");
				return;
			}
			while ( my @row = $s->fetchrow())
			{
				$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
				$pgret{"\U$row[0]\E"} = $row[1];
			}
			$s->finish;
		}
		@seqs = ();
		# MySQL do not have sequences but we cound autoincrement column as sequences
		if ($self->{is_mysql}) {
			@seqs = Ora2Pg::MySQL::_count_sequences($self);
		}
		foreach my $t (sort keys %column_infos)
		{
			next if (!exists $tables_infos{$t});
			my $nbidty = 0;
			foreach my $cn (keys %{$column_infos{$t}})
			{
				next if (!$self->is_in_struct($t, $cn));
				if ($column_infos{$t}{$cn}{default} =~ /ISEQ\$\$_.*nextval/i) {
					$nbidty++;
				}
			}
			if (grep(/^$t$/i, @seqs)) {
				$nbidty++;
			}
			print "$lbl:$t:$nbidty\n";
			if ($self->{pg_dsn})
			{
				my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
				$pgret{"\U$both\E"} ||= 0;
				print "POSTGRES:$both$orig:", $pgret{"\U$both\E"}, "\n";
				if ($pgret{"\U$both\E"} != $nbidty) {
					push(@errors, "Table $both$orig doesn't have the same number of identity column in source database ($nbidty) and in PostgreSQL (" . $pgret{"\U$both\E"} . ").");
				}
			}
		}
		%column_infos = ();
		$self->show_test_errors('column default value', @errors);
		@errors = ();
	}

	%column_infos = ();

	####
	# Test foreign keys
	####
	print "\n";
	print "[TEST FOREIGN KEYS COUNT]\n";
	my ($foreign_link, $foreign_key) = $self->_foreign_key('',$self->{schema});
	$schema_cond = $self->get_schema_condition('n.nspname');
	$sql = qq{
SELECT CASE WHEN regexp_count(r.conrelid::regclass::text, n.nspname||'.') > 0 THEN r.conrelid::regclass::text ELSE n.nspname::regnamespace||'.'||r.conrelid::regclass END, count(*)
FROM pg_catalog.pg_constraint r JOIN pg_class c ON (r.conrelid=c.oid) JOIN pg_namespace n ON (c.relnamespace=n.oid)
WHERE r.contype = 'f' $schema_cond
GROUP BY n.nspname,r.conrelid
};
	%pgret = ();
	if ($self->{pg_dsn})
	{
		my $s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about foreign keys constraints.");
			return;
		}
		while ( my @row = $s->fetchrow())
		{
			$row[0] =~ s/^[^\.]+\.([^\.]+\.)/$1/; # remove possible duplicate schema prefix
			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
			$pgret{"\U$row[0]\E"} = $row[1];
		}
		$s->finish;
	}
	# Initialize when there is not unique key in a table
	foreach my $t (keys %tables_infos) {
		$foreign_link->{$t} = {} if (not exists $foreign_link->{$t});
	}

	foreach my $t (sort keys %{$foreign_link})
	{
		next if (!exists $tables_infos{$t});
		my $nbfk = scalar keys %{$foreign_link->{$t}};
		print "$lbl:$t:$nbfk\n";
		if ($self->{pg_dsn})
		{
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
			$pgret{"\U$both\E"} ||= 0;
			print "POSTGRES:$both$orig:", $pgret{"\U$both\E"}, "\n";
			if ($pgret{"\U$both\E"} != $nbfk) {
				push(@errors, "Table $both$orig doesn't have the same number of foreign key constraints in source database ($nbfk) and in PostgreSQL (" . $pgret{"\U$both\E"} . ").");
			}
		}
	}
	$self->show_test_errors('foreign keys', @errors);
	@errors = ();
	$foreign_link = undef;
	$foreign_key = undef;

	####
	# Test partitions
	####
	print "\n";
	print "[TEST PARTITION COUNT]\n";
	my %partitions = $self->_get_partitioned_table();
	$schema_cond = $self->get_schema_condition('nmsp_parent.nspname');
	$sql = qq{
SELECT
    nmsp_parent.nspname || '.' || parent.relname,
    COUNT(*)                     
FROM pg_inherits
    JOIN pg_class parent        ON pg_inherits.inhparent = parent.oid
    JOIN pg_class child     ON pg_inherits.inhrelid   = child.oid
    JOIN pg_namespace nmsp_parent   ON nmsp_parent.oid  = parent.relnamespace
    JOIN pg_namespace nmsp_child    ON nmsp_child.oid   = child.relnamespace
WHERE child.relkind = 'r' $schema_cond
GROUP BY nmsp_parent.nspname || '.' || parent.relname                                                                   
};
	my %pg_part = ();
	if ($self->{pg_dsn})
	{
		$s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about PARTITION.");
			next;
		}
		while ( my @row = $s->fetchrow())
		{
			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
			$pg_part{$row[0]} = $row[1];
		}
		$s->finish();
	}

	foreach my $t (sort keys %tables_infos)
	{
		my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
		next if (!exists $partitions{$t} && !exists $pg_part{$tbmod});
		$partitions{$t}{count} ||= 0;
		print "$lbl:$t:$partitions{$t}{count}\n";
		$pg_part{$tbmod} ||= 0;
		print "POSTGRES:$both$orig:$pg_part{$tbmod}\n";
		if ($pg_part{$tbmod} != $partitions{$t}{count}) {
			push(@errors, "Table $both$orig doesn't have the same number of partitions in source database ($partitions{$t}{count}) and in PostgreSQL ($pg_part{$tbmod}).");
		}
	}
	$self->show_test_errors('PARTITION', @errors);
	@errors = ();
	%partitions = ();

	print "\n";
	print "[TEST TABLE COUNT]\n";
	my $nbobj = scalar keys %tables_infos;
	$schema_cond = $self->get_schema_condition();
	$sql = qq{
SELECT count(*)
FROM pg_catalog.pg_class c
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'p') AND NOT c.relispartition
    AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = c.oid AND d.deptype = 'e')
 $schema_cond
};

	print "$lbl:TABLE:$nbobj\n";
	if ($self->{pg_dsn})
	{
		$s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about $obj_type.");
			next;
		}
		while ( my @row = $s->fetchrow())
		{
			print "POSTGRES:TABLE:$row[0]\n";
			if ($row[0] != $nbobj) {
				push(@errors, "TABLE does not have the same count in source database ($nbobj) and in PostgreSQL ($row[0]).");
			}
			last;
		}
		$s->finish();
	}
	$self->show_test_errors('TABLE', @errors);
	@errors = ();

	####
	# Test triggers
	####
	print "\n";
	print "[TEST TABLE TRIGGERS COUNT]\n";
	my %triggers = $self->_list_triggers();
	$schema_cond = $self->get_schema_condition();
	$sql = qq{
SELECT n.nspname||'.'||c.relname, count(*)
FROM pg_catalog.pg_trigger t JOIN pg_class c ON (t.tgrelid=c.oid) JOIN pg_namespace n ON (c.relnamespace=n.oid)
WHERE (NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D'))
 $schema_cond
GROUP BY n.nspname,c.relname
};
	%pgret = ();
	if ($self->{pg_dsn})
	{
		my $s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about table triggrers.");
			return;
		}
		while ( my @row = $s->fetchrow())
		{
			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
			$pgret{"\U$row[0]\E"} = $row[1];
		}
		$s->finish;
	}
	# Initialize when there is not unique key in a table
	foreach my $t (keys %tables_infos) {
		$triggers{$t} = () if (not exists $triggers{$t});
	}

	foreach my $t (sort keys %triggers)
	{
		next if (!exists $tables_infos{$t});
		my $nbtrg = @{$triggers{$t}};
		print "$lbl:$t:$nbtrg\n";
		if ($self->{pg_dsn})
		{
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
			$pgret{"\U$both\E"} ||= 0;
			print "POSTGRES:$both$orig:", $pgret{"\U$both\E"}, "\n";
			if ($pgret{"\U$both\E"} != $nbtrg) {
				push(@errors, "Table $both$orig doesn't have the same number of triggers in source database ($nbtrg) and in PostgreSQL (" . $pgret{"\U$both\E"} . ").");
			}
		}
	}
	$s->finish() if ($self->{pg_dsn});
	$self->show_test_errors('table triggers', @errors);
	@errors = ();

	print "\n";
	print "[TEST TRIGGER COUNT]\n";
	$nbobj = 0;
	foreach my $t (keys %triggers) {
		next if (!exists $tables_infos{$t});
		$nbobj += $#{$triggers{$t}}+1;
	}
	$schema_cond = $self->get_schema_condition();
	$sql = qq{
SELECT count(*)
FROM pg_catalog.pg_trigger t JOIN pg_class c ON (c.oid = t.tgrelid) JOIN pg_namespace n ON (c.relnamespace=n.oid)
WHERE (NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D'))
 $schema_cond
};

	print "$lbl:TRIGGER:$nbobj\n";
	if ($self->{pg_dsn})
	{
		$s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about $obj_type.");
			next;
		}
		while ( my @row = $s->fetchrow())
		{
			print "POSTGRES:TRIGGER:$row[0]\n";
			if ($row[0] != $nbobj) {
				push(@errors, "TRIGGER does not have the same count in source database ($nbobj) and in PostgreSQL ($row[0]).");
			}
			last;
		}
		$s->finish();
	}
	$self->show_test_errors('TRIGGER', @errors);
	@errors = ();
}

sub _unitary_test_views
{
	my $self = shift;

	# Get all tables information specified by the DBI method table_info
	$self->logit("Unitary test of views between source database and PostgreSQL...\n", 1);

	# First of all extract all views from PostgreSQL database
	my $schema_clause = $self->get_schema_condition();
	my $sql = qq{
SELECT c.relname,n.nspname
FROM pg_catalog.pg_class c
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'v' AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND objid = c.oid AND deptype = 'e')
      $schema_clause
};
	my %list_views  = ();
	if ($self->{pg_dsn})
	{
		my $s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about views.");
			next;
		}
		while ( my @row = $s->fetchrow())
		{
			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
			$list_views{$row[0]} = $self->{schema} || $row[1];
		}
		$s->finish();
	}

	my $lbl = 'ORACLEDB';
	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});

	print "[UNITARY TEST OF VIEWS]\n";
	foreach my $v (sort keys %list_views)
	{
		# Execute init settings if any
		# Count rows returned by all view on the source database
		my $vname = $v;	
		$sql = "SELECT count(*) FROM $list_views{$v}.$v";
		my $sth = $self->{dbh}->prepare($sql)  or $self->logit("ERROR: " . $self->{dbh}->errstr . "\n", 0, 0);
		$sth->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 0);
		my @row = $sth->fetchrow();
		my $ora_ct = $row[0];
		print "$lbl:$vname:", join('|', @row), "\n";
		$sth->finish;
		# Execute view in the PostgreSQL database
		$vname = "$list_views{$v}.$v" if ($self->{export_schema});
		$vname = "$self->{pg_schema}.$v" if ($self->{pg_schema});
		$sql = "SELECT count(*) FROM " .$self->quote_object_name($vname);
		$sth = $self->{dbhdest}->prepare($sql);
		if (not defined $sth)
		{
			$self->logit("ERROR: " . $self->{dbhdest}->errstr . "\n", 0, 0);
			next;
		}
		$sth->execute or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 0);
		@row = $sth->fetchrow();
		$sth->finish;
		my $pg_ct = $row[0];
		print "POSTGRES:$vname:", join('|', @row), "\n";
		if ($pg_ct != $ora_ct) {
			print "ERROR: view $v returns different row count [oracle: $ora_ct, postgresql: $pg_ct]\n";
		}
	}
}

sub _count_object
{
	my $self = shift;
	my $obj_type = shift;

	# Get all tables information specified by the DBI method table_info
	$self->logit("Looking for source database and PostgreSQL objects count...\n", 1);

	my $lbl = 'ORACLEDB';
	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});

	my $schema_clause = $self->get_schema_condition();
	my $nbobj = 0;
	my $sql = '';
	if ($obj_type eq 'VIEW')
	{
		my %obj_infos = $self->_get_views();
		$nbobj = scalar keys %obj_infos;
		$sql = qq{
SELECT count(*)
FROM pg_catalog.pg_class c
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('v','') AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = c.oid AND d.deptype = 'e')
      $schema_clause
};
	}
	elsif ($obj_type eq 'MVIEW')
	{
		my %obj_infos = $self->_get_materialized_views();
		$nbobj = scalar keys %obj_infos;
		$sql = qq{
SELECT count(*)
FROM pg_catalog.pg_class c
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('m','') AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = c.oid AND d.deptype = 'e')
      $schema_clause
};
	}
	elsif ($obj_type eq 'SEQUENCE')
	{
		my $obj_infos = {};
		if (!$self->{is_mysql}) {
			$obj_infos = $self->_get_sequences();
		} else {
			# MySQL do not have sequences but we cound autoincrement column as sequences
			$obj_infos = Ora2Pg::MySQL::_count_sequences($self);
		}
		$nbobj = scalar keys %$obj_infos;
		$sql = qq{
SELECT count(*)
FROM pg_catalog.pg_class c
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('S','') AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = c.oid AND d.deptype = 'e')
     AND NOT EXISTS (SELECT 1 FROM pg_attribute a WHERE NOT a.attisdropped AND a.attidentity IN ('a', 'd') AND c.oid = pg_get_serial_sequence(a.attrelid::regclass::text, a.attname)::regclass::oid)
     $schema_clause
};
	}
	elsif ($obj_type eq 'TYPE')
	{
		my $obj_infos = $self->_get_types();
		$nbobj = scalar @{$obj_infos};
		$schema_clause .= " AND pg_catalog.pg_type_is_visible(t.oid)" if ($schema_clause =~ /information_schema/);
		$sql = qq{
SELECT count(*)
FROM pg_catalog.pg_type t
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
WHERE (t.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid))
  AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid)
  AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = t.oid AND d.deptype = 'e')
  $schema_clause
};
	}
	elsif ($obj_type eq 'FDW')
	{
		my %obj_infos = $self->_get_external_tables();
		$nbobj = scalar keys %obj_infos;
		$sql = qq{
SELECT count(*)
FROM pg_catalog.pg_class c
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('f','') AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = c.oid AND d.deptype = 'e')
      $schema_clause
};
	}
	else
	{
		return;
	}

	print "\n";
	print "[TEST $obj_type COUNT]\n";

	if ($self->{is_mysql} && ($obj_type eq 'SEQUENCE')) {
		print "$lbl:AUTOINCR:$nbobj\n";
	} else {
		print "$lbl:$obj_type:$nbobj\n";
	}
	if ($self->{pg_dsn})
	{
		my $s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about $obj_type.");
			next;
		}
		while ( my @row = $s->fetchrow())
		{
			print "POSTGRES:$obj_type:$row[0]\n";
			if ($row[0] != $nbobj) {
				push(@errors, "\U$obj_type\E does not have the same count in source database ($nbobj) and in PostgreSQL ($row[0]).");
			}
			last;
		}
		$s->finish();
	}
	$self->show_test_errors($obj_type, @errors);
	@errors = ();
}

sub _test_function
{
	my $self = shift;

	my @errors = ();

	$self->logit("Looking for functions count related to source database and PostgreSQL functions...\n", 1);

	my $lbl = 'ORACLEDB';
	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});

	####
	# Test number of function
	####
	print "\n";
	print "[TEST FUNCTION COUNT]\n";
	my @fct_infos = $self->_list_all_functions();
	my $schema_clause = $self->get_schema_condition();
	if ($self->{package_as_schema})
	{
		my %processed_pkgs;
		foreach my $f (@fct_infos)
		{
			my ($pkg, $fct) = split(/\./, $f);
			next if $processed_pkgs{$pkg}++; # Skip if package already processed
			$schema_clause .= ") OR (lower(n.nspname) = quote_ident('\L$pkg\E')";
		}
		if (scalar keys %processed_pkgs > 0)
		{
			$schema_clause =~ s/^(\s*AND\s+)/$1\(/;
			$schema_clause =~ s/$/\)/;
		}
	}
	$sql = qq{
SELECT n.nspname,proname,prorettype
FROM pg_catalog.pg_proc p
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
     LEFT JOIN pg_catalog.pg_type t ON t.oid=p.prorettype
WHERE t.typname <> 'trigger'
     AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = p.oid AND d.deptype = 'e')
$schema_clause
};

	my $nbobj = $#fct_infos + 1;
	print "$lbl:FUNCTION:$nbobj\n";
	if ($self->{pg_dsn})
	{
		$s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about $obj_type.");
			next;
		}
		my $pgfct = 0;
		my %pg_function = ();
		while ( my @row = $s->fetchrow())
		{
			$pgfct++;
			my $fname = $row[1];
			if ($row[0] ne 'public') {
				$fname = $row[0] . '.' . $row[1];
			}
			$pg_function{lc($fname)} = 1;
		}
		print "POSTGRES:FUNCTION:$pgfct\n";
		if ($pgfct != $nbobj) {
			push(@errors, "FUNCTION does not have the same count in source database ($nbobj) and in PostgreSQL ($pgfct).");
		}
		$s->finish();
		# search for missing funtions
		foreach my $f (@fct_infos)
		{
			my $found = 0;
			foreach my $pgf (keys %pg_function)
			{
				$found = 1, last if (lc($f) eq lc($pgf));
				if ($f !~ /\./) {
					$found = 1, last if ($pgf =~ /^[^\.]+\.\Q$f\E$/i);
				} else {
					$found = 1, last if ($pgf =~ /^\Q$f\E$/i);
				}
			}
			push(@errors, "Function $f is missing in PostgreSQL database.") if (!$found);
		}
		# search for additional functions
		foreach my $pgf (keys %pg_function)
		{
			my $found = 0;
			foreach my $f (@fct_infos)
			{
				$found = 1, last if (lc($f) eq lc($pgf));
				if ($f !~ /\./) {
					$found = 1, last if ($pgf =~ /^[^\.]+\.\Q$f\E$/i);
				} else {
					$found = 1, last if ($pgf =~ /^\Q$f\E$/i);
				}
			}
			push(@errors, "Function $pgf is in addition in PostgreSQL database.") if (!$found);
		}
	}
	$self->show_test_errors('FUNCTION', @errors);
	@errors = ();
	print "\n";
}

sub _test_seq_values
{
	my $self = shift;

	my @errors = ();

	$self->logit("Looking for last values related to source database and PostgreSQL sequences...\n", 1);

	my $lbl = 'ORACLEDB';
	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});

	####
	# Test number of function
	####
	print "\n";
	print "[TEST SEQUENCE VALUES]\n";
	my $obj_infos = {};
	if (!$self->{is_mysql}) {
		$obj_infos = $self->_get_sequences();
	} else {
		# MySQL do not have sequences but we cound autoincrement column as sequences
		$obj_infos = Ora2Pg::MySQL::_count_sequences($self);
	}

	my %pgret = ();
	if ($self->{pg_dsn})
	{
		# create a function to extract the last value of all sequences
		my $fqdn = '';
		$fqdn = "$self->{pg_schema}\." if ($self->{pg_schema});
		my $sql = qq{
CREATE OR REPLACE FUNCTION ${fqdn}get_sequence_last_values() RETURNS TABLE(seqname text,val bigint) AS
\$\$
DECLARE
    seq_name varchar(128);
BEGIN
    FOR seq_name in SELECT relnamespace::regnamespace::text || '.' || quote_ident(relname::text) FROM pg_class c WHERE (relkind = 'S') AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = c.oid AND d.deptype = 'e')
    LOOP
        RETURN QUERY EXECUTE  'SELECT ' || quote_literal(seq_name) || ',last_value FROM ' || seq_name;
    END LOOP;
    RETURN;
END
\$\$
LANGUAGE 'plpgsql';
};
		$self->{dbhdest}->do($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		my $s = $self->{dbhdest}->prepare("SELECT * FROM ${fqdn}get_sequence_last_values()") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from catalog about last values of sequences.");
			return;
		}
		while ( my @row = $s->fetchrow())
		{
			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
			$pgret{"\U$row[0]\E"} = $row[1];
		}
		$s->finish;
		$self->{dbhdest}->do("DROP FUNCTION ${fqdn}get_sequence_last_values") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	}

	foreach my $r (sort keys %$obj_infos)
	{
		$r =~ s/^[^\.]+\.// if (!$self->{export_schema});
		print "$lbl:$r:$obj_infos->{$r}->[4]\n";
		if ($self->{pg_dsn})
		{
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($r);
			$pgret{"\U$both$orig\E"} ||= 0;
			print "POSTGRES:$both$orig:", $pgret{"\U$both$orig\E"}, "\n";
			if ($pgret{"\U$both$orig\E"} != $obj_infos->{$r}->[4]) {
				push(@errors, "Sequence $both$orig doesn't have the same value in source database ($obj_infos->{$r}->[4]) and in PostgreSQL (" . $pgret{"\U$both$orig\E"} . "). Verify +/- cache size: $obj_infos->{$r}->[5].");
			}
		}
	}
	$self->show_test_errors('sequence values', @errors);
	@errors = ();
	print "\n";
}

=head2 _get_version

This function retrieves the Oracle version information

=cut

sub _get_version
{
	my $self = shift;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_version($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_version($self);
	} else {
		return Ora2Pg::Oracle::_get_version($self);
	}
}

=head2 _get_database_size

This function retrieves the size of the Oracle database in MB

=cut

sub _get_database_size
{
	my $self = shift;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_database_size($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_database_size($self);
	} else {
		return Ora2Pg::Oracle::_get_database_size($self);
	}
}

=head2 _get_objects

This function retrieves all object the Oracle information
except SYNONYM and temporary objects

=cut

sub _get_objects
{
	my $self = shift;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_objects($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_objects($self);
	} else {
		return Ora2Pg::Oracle::_get_objects($self);
	}
}

sub _list_all_functions
{
	my $self = shift;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_list_all_functions($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_list_all_functions($self);
	} else {
		return Ora2Pg::Oracle::_list_all_functions($self);
	}
}

=head2 _schema_list

This function retrieves all Oracle-native user schema.

Returns a handle to a DB query statement.

=cut

sub _schema_list
{
	my $self = shift;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_schema_list($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_schema_list($self);
	} else {
		return Ora2Pg::Oracle::_schema_list($self);
	}
}

=head2 _table_exists

This function return the table name if the given table exists
else returns a empty string.

=cut

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

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_table_exists($self, $schema, $table);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_table_exists($self, $schema, $table);
	} else {
		return Ora2Pg::Oracle::_table_exists($self, $schema, $table);
	}
}

=head2 _get_largest_tables

This function retrieves the list of largest table of the Oracle database in MB

=cut

sub _get_largest_tables
{
	my $self = shift;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_largest_tables($self);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_largest_tables($self);
	} else {
		return Ora2Pg::Oracle::_get_largest_tables($self);
	}
}

=head2 _get_encoding

This function retrieves the Oracle database encoding

Returns a handle to a DB query statement.

=cut

sub _get_encoding
{
	my ($self, $dbh) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_get_encoding($self, $self->{dbh});
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_get_encoding($self, $self->{dbh});
	} else {
		return Ora2Pg::Oracle::_get_encoding($self, $self->{dbh});
	}
}


=head2 _compile_schema

This function force Oracle database to compile a schema and validate or
invalidate PL/SQL code.

When parameter $schema is the name of a schema, only this schema is recompiled
When parameter $schema is equal to 1 and SCHEMA directive is set, only this schema is recompiled
When parameter $schema is equal to 1 and SCHEMA directive is unset, all schema will be recompiled

=cut


sub _compile_schema
{
	my ($self, $schema) = @_;

	my @to_compile = ();

	if ($schema and ($schema =~ /[a-z]/i)) {
		push(@to_compile, $schema);
	} elsif ($schema and $self->{schema}) {
		push(@to_compile, $self->{schema});
	}
	elsif ($schema)
	{
		my $sth = $self->_schema_list() or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		while ( my @row = $sth->fetchrow()) {
			push(@to_compile, $row[0]);
		}
		$sth->finish();
	}

	if ($#to_compile >= 0 && $self->{type} !~ /^SHOW_/i)
	{
		foreach my $schm (@to_compile)
		{
			$self->logit("Force Oracle to compile schema $schm before code extraction\n", 1);
			my $sql = "BEGIN\nDBMS_UTILITY.compile_schema(schema => '$schm', compile_all => FALSE);\nEND;";
			my $sth = $self->{dbh}->do($sql)
						or $self->logit("FATAL: " . $self->{dbh}->errstr . ", SQL: $sql\n", 0, 1);
		}
	}
}

=head2 _datetime_format

This function force Oracle database to format the time correctly

=cut

sub _datetime_format
{
	my ($self, $dbh) = @_;

	$dbh = $self->{dbh} if (!$dbh);

	if ($self->{enable_microsecond}) {
		my $dim = 6;
		$dim = '' if ($self->{db_version} =~ /Release [89]/);
		my $sth = $dbh->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS.FF$dim'") or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	} else {
		my $sth = $dbh->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'") or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	}
	my $sth = $dbh->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'") or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	if ($self->{enable_microsecond}) {
		my $dim = 6;
		$dim = '' if ($self->{db_version} =~ /Release [89]/);
		$sth = $dbh->do("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT='YYYY-MM-DD HH24:MI:SS.FF$dim TZH:TZM'") or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	} else {
		$sth = $dbh->do("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT='YYYY-MM-DD HH24:MI:SS TZH:TZM'") or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	}
}

sub _numeric_format
{
	my ($self, $dbh) = @_;

	$dbh = $self->{dbh} if (!$dbh);

	my $sth = $dbh->do("ALTER SESSION SET NLS_NUMERIC_CHARACTERS = '.,'") or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
}

sub _ora_initial_command
{
	my ($self, $dbh) = @_;

	return if ($#{ $self->{ora_initial_command} } < 0);

	$dbh = $self->{dbh} if (!$dbh);


	# Lookup if the user have provided some sessions settings
	foreach my $q (@{$self->{ora_initial_command}}) {
		next if (!$q);
		$self->logit("DEBUG: executing initial command to Oracle: $q\n", 1);
		my $sth = $dbh->do($q) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	}

}

sub _pg_initial_command
{
	my ($self, $dbh) = @_;

	return if ($#{ $self->{pg_initial_command} } < 0);

	$dbh = $self->{dbhdest} if (!$dbh);

	# Lookup if the user have provided some sessions settings
	foreach my $q (@{$self->{pg_initial_command}}) {
		$self->logit("DEBUG: executing initial command to PostgreSQL: $q\n", 1);
		my $sth = $dbh->do($q) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	}

}



=head2 multiprocess_progressbar

This function is used to display a progress bar during object scanning.

=cut

sub multiprocess_progressbar
{
	my ($self) = @_;

	$self->logit("Starting progressbar writer process\n", 1);

	$0 = 'ora2pg logger';

	$| = 1;

	my $DEBUG_PBAR = 0;
	my $width = 25;
	my $char  = '=';
	my $kind  = 'rows';
	my $table_count = 0;
	my $table = '';
	my $global_start_time = 0;
	my $total_rows = 0;
	my %table_progress = ();
	my $global_line_counter = 0;

	my $refresh_time = 3; #Update progress bar each 3 seconds
	my $last_refresh = time();
	my $refresh_rows = 0;

	# Terminate the process when we doesn't read the complete file but must exit
	local $SIG{USR1} = sub
	{
		if ($global_line_counter)
		{
			my $end_time = time();
			my $dt = $end_time - $global_start_time;
			$dt ||= 1;
			my $rps = int($global_line_counter / $dt);
			print STDERR $self->progress_bar($global_line_counter, $total_rows, 25, '=', 'rows', "on total estimated data ($dt sec., avg: $rps tuples/sec)"), "\n";
		}
		exit 0;
	};

	$pipe->reader();
	while ( my $r = <$pipe> )
	{
		chomp($r);
		# When quit is received, then exit immediatly
		last if ($r eq 'quit');

		# Store data export start time
		if ($r =~ /^GLOBAL EXPORT START TIME: (\d+)/)
		{
print STDERR "GLOBAL EXPORT START TIME: $1\n" if ($DEBUG_PBAR);
			$global_start_time = $1;
		}
		# Store total number of tuples exported
		elsif ($r =~ /^GLOBAL EXPORT ROW NUMBER: (\d+)/)
		{
print STDERR "GLOBAL EXPORT ROW NUMBER: $1\n" if ($DEBUG_PBAR);
			$total_rows = $1;
		}
		# A table export is starting (can be called multiple time with -J option)
		elsif ($r =~ /TABLE EXPORT IN PROGESS: (.*?), start: (\d+), rows (\d+)/)
		{
print STDERR "TABLE EXPORT IN PROGESS: $1, start: $2, rows $3\n" if ($DEBUG_PBAR);
			$table_progress{$1}{start} = $2 if (!exists $table_progress{$1}{start});
			$table_progress{$1}{rows} = $3  if (!exists $table_progress{$1}{rows});
		}
		# A table export is ending
		elsif ($r =~ /TABLE EXPORT ENDED: (.*?), end: (\d+), rows (\d+)/)
		{
print STDERR "TABLE EXPORT ENDED: $1, end: $2, rows $3\n" if ($DEBUG_PBAR);
			# Store timestamp at end of table export
			$table_progress{$1}{end} = $2;

			# Stores total number of rows exported when we do not used chunk of data
			if (!exists $table_progress{$1}{progress})
			{
				$table_progress{$1}{progress} = $3;
				$global_line_counter += $3;
			}

			# Display table progression
			my $dt = $table_progress{$1}{end} - $table_progress{$1}{start};
			my $rps = int($table_progress{$1}{progress}/ ($dt||1));
			print STDERR $self->progress_bar($table_progress{$1}{progress}, $table_progress{$1}{rows}, 25, '=', 'rows', "Table $1 ($dt sec., $rps recs/sec)"), "\n";
			# Display global export progression
			my $cur_time = time();
			$dt = $cur_time - $global_start_time;
			$rps = int($global_line_counter/ ($dt || 1));
			print STDERR $self->progress_bar($global_line_counter, $total_rows, 25, '=', 'total rows', "- ($dt sec., avg: $rps recs/sec), $1 in progress."), "\r";
			$last_refresh = $cur_time;
		}
		# A chunk of DATA_LIMIT row is exported
		elsif ($r =~ /CHUNK \d+ DUMPED: (.*?), time: (\d+), rows (\d+)/)
		{
print STDERR "CHUNK X DUMPED: $1, time: $2, rows $3\n" if ($DEBUG_PBAR);
			$table_progress{$1}{progress} += $3;
			$global_line_counter += $3;
			my $cur_time = time();
			if ($cur_time >= ($last_refresh + $refresh_time))
			{
				my $dt = $cur_time - $global_start_time;
				my $rps = int($global_line_counter/ ($dt || 1));
				print STDERR $self->progress_bar($global_line_counter, $total_rows, 25, '=', 'total rows', "- ($dt sec., avg: $rps recs/sec), $1 in progress."), "\r";
				$last_refresh = $cur_time;
			}
		}
		# A table export is ending
		elsif ($r =~ /TABLE EXPORT ENDED: (.*?), end: (\d+), report all parts/)
		{
print STDERR "TABLE EXPORT ENDED: $1, end: $2, report all parts\n" if ($DEBUG_PBAR);
			# Store timestamp at end of table export
			$table_progress{$1}{end} = $2;

			# Get all statistics from multiple Oracle query
			for (my $i = 0; $i < $self->{oracle_copies}; $i++) {
				$table_progress{$1}{start} = $table_progress{"$1-part-$i"}{start} if (!exists $table_progress{$1}{start});
				$table_progress{$1}{rows} = $table_progress{"$1-part-$i"}{rows};
				delete $table_progress{"$1-part-$i"};
			}

			# Stores total number of rows exported when we do not used chunk of data
			if (!exists $table_progress{$1}{progress}) {
				$table_progress{$1}{progress} = $3;
				$global_line_counter += $3;
			}

			# Display table progression
			my $dt = $table_progress{$1}{end} - $table_progress{$1}{start};
			my $rps = int($table_progress{$1}{rows}/ ($dt||1));
			print STDERR $self->progress_bar($table_progress{$1}{rows}, $table_progress{$1}{rows}, 25, '=', 'rows', "Table $1 ($dt sec., $rps recs/sec)"), "\n";
		}
		else
		{
			print "PROGRESS BAR ERROR (unrecognized line sent to pipe): $r\n";
		}

	}

	if ($global_line_counter)
	{
		my $end_time = time();
		my $dt = $end_time - $global_start_time;
		$dt ||= 1;
		my $rps = int($global_line_counter / $dt);
		print STDERR $self->progress_bar($global_line_counter, $total_rows, 25, '=', 'rows', "on total estimated data ($dt sec., avg: $rps tuples/sec)"), "\n";
	}

	exit 0;
}


=head2 progress_bar

This function is used to display a progress bar during object scanning.

=cut

sub progress_bar
{
	my ($self, $got, $total, $width, $char, $kind, $msg) = @_;

	$width ||= 25;
	$char  ||= '=';
	$kind  ||= 'rows';
	my $num_width = length $total;
	my $ratio = 1;
	if ($total > 0) {
		$ratio = $got / +$total;
	}
	my $len = (($width - 1) * $ratio);
	$len = $width - 1 if ($len >= $width);
	my $str = sprintf(
		"[%-${width}s] %${num_width}s/%s $kind (%.1f%%) $msg",
		$char x $len . '>',
		$got, $total, 100 * $ratio
	);
	$len = length($str);
	$self->{prgb_len} ||= $len;
	if ($len < $self->{prgb_len}) {
		$str .= ' ' x ($self->{prgb_len} - $len);
	}
	$self->{prgb_len} = $len;

	# prepend time
	$str = '[' . strftime("%Y-%m-%d %H:%M:%S", localtime(time)) . '] ' . $str;

	return $str;
}

# Construct a query to exclude or only include some object wanted by the user
# following the ALLOW and EXCLUDE configuration directive. The filter returned
# must be used with the bind parameters stored in the @{$self->{query_bind_params}}
# when calling the execute() function after the call of prepare().
sub limit_to_objects
{
	my ($self, $obj_type, $column) = @_;

	my $str = '';
	$obj_type ||= $self->{type};
	$column ||= 'TABLE_NAME';

	my @cols = split(/\|/, $column);
	my @arr_type = split(/\|/, $obj_type);
	my @done = ();
	my $has_limitation = 0;
	$self->{query_bind_params} = ();

	for (my $i = 0; $i <= $#arr_type; $i++)
	{
		my $colname = $cols[0];
		$colname = $cols[$i] if (($#cols >= $i) && $cols[$i]);

		# Do not double exclusion/inclusion when column name is the same
		next if (grep(/^$colname$/, @done) && ! exists $self->{limited}{$arr_type[$i]});
		push(@done, $colname);

		my $have_lookahead = 0;
		if ($#{$self->{limited}{$arr_type[$i]}} >= 0)
		{
			$str .= ' AND (';
			if ($self->{db_version} =~ /Release [89]/)
			{
				for (my $j = 0; $j <= $#{$self->{limited}{$arr_type[$i]}}; $j++)
				{
					if ($self->{limited}{$arr_type[$i]}->[$j] =~ /^\!/)
					{
						$have_lookahead = 1;
						next;
					}
					$str .= "upper($colname) LIKE ?";
					push(@{$self->{query_bind_params}}, uc($self->{limited}{$arr_type[$i]}->[$j]));
					if ($j < $#{$self->{limited}{$arr_type[$i]}}) {
						$str .= " OR ";
					}
				}
				$str =~ s/ OR $//;
			}
			else
			{
				for (my $j = 0; $j <= $#{$self->{limited}{$arr_type[$i]}}; $j++)
				{
					if ($self->{limited}{$arr_type[$i]}->[$j] =~ /^\!/)
					{
						$have_lookahead = 1;
						next;
					}
					if ($self->{is_mysql}) {
						$str .= "upper($colname) RLIKE ?" ;
					} elsif ($self->{is_mssql}) {
						#$str .= "PATINDEX(?, upper($colname)) != 0" ;
						$str .= "upper($colname) LIKE ?" ;
					} else {
						$str .= "REGEXP_LIKE(upper($colname), ?)" ;
					}

					my $objname = $self->{limited}{$arr_type[$i]}->[$j];
					$objname =~ s/\$/\\\$/g; # support dollar sign
					if (!$self->{is_mssql}) {
						push(@{$self->{query_bind_params}}, uc("\^$objname\$"));
					} else {
						push(@{$self->{query_bind_params}}, uc("$objname"));
					}
					if ($j < $#{$self->{limited}{$arr_type[$i]}}) {
						$str .= " OR ";
					}
				}
				$str =~ s/ OR $//;
			}
			$str .= ')';
			$str =~ s/ AND \(\)//;

			if ($have_lookahead)
			{
				if ($self->{db_version} =~ /Release [89]/)
				{
					for (my $j = 0; $j <= $#{$self->{limited}{$arr_type[$i]}}; $j++)
					{
						next if ($self->{limited}{$arr_type[$i]}->[$j] !~ /^\!(.+)/);
						$str .= " AND upper($colname) NOT LIKE ?";
						push(@{$self->{query_bind_params}}, uc($1));
					}
				}
				else
				{
					for (my $j = 0; $j <= $#{$self->{limited}{$arr_type[$i]}}; $j++)
					{
						next if ($self->{limited}{$arr_type[$i]}->[$j] !~ /^\!(.+)/);
						if ($self->{is_mysql}) {
							$str .= " AND upper($colname) NOT RLIKE ?" ;
						} elsif ($self->{is_mssql}) {
							#$str .= "PATINDEX(?, upper($colname)) != 0" ;
							$str .= "upper($colname) LIKE ?" ;
						} else {
							$str .= " AND NOT REGEXP_LIKE(upper($colname), ?)" ;
						}
						my $objname = $1;
						$objname =~ s/\$/\\\$/g; # support dollar sign
						if (!$self->{is_mssql}) {
							push(@{$self->{query_bind_params}}, uc("\^$objname\$"));
						} else {
							push(@{$self->{query_bind_params}}, uc("$objname"));
						}
					}
				}
			}
			$has_limitation = 1;
		}
		elsif ($#{$self->{excluded}{$arr_type[$i]}} >= 0)
		{
			if ($self->{db_version} =~ /Release [89]/)
			{
				$str .= ' AND (';
				for (my $j = 0; $j <= $#{$self->{excluded}{$arr_type[$i]}}; $j++)
				{
					$str .= "upper($colname) NOT LIKE ?" ;
					push(@{$self->{query_bind_params}}, uc($self->{excluded}{$arr_type[$i]}->[$j]));
					if ($j < $#{$self->{excluded}{$arr_type[$i]}}) {
						$str .= " AND ";
					}
				}
				$str .= ')';
			}
			else
			{
				$str .= ' AND (';
				for (my $j = 0; $j <= $#{$self->{excluded}{$arr_type[$i]}}; $j++)
				{
					if ($self->{is_mysql}) {
						$str .= "upper($colname) NOT RLIKE ?" ;
					} elsif ($self->{is_mssql}) {
						#$str .= "PATINDEX(?, upper($colname)) = 0" ;
						$str .= "upper($colname) NOT LIKE ?" ;
					} else {
						$str .= "NOT REGEXP_LIKE(upper($colname), ?)" ;
					}
					if (!$self->{is_mssql}) {
						push(@{$self->{query_bind_params}}, uc("\^$self->{excluded}{$arr_type[$i]}->[$j]\$"));
					} else {
						push(@{$self->{query_bind_params}}, uc("$self->{excluded}{$arr_type[$i]}->[$j]"));
					}
					if ($j < $#{$self->{excluded}{$arr_type[$i]}}) {
						$str .= " AND ";
					}
				}
				$str .= ')';
			}
		}

		# Always exclude unwanted tables
		if (!$self->{is_mysql} && !$self->{is_mssql} && !$self->{no_excluded_table} && !$has_limitation
			&& ($arr_type[$i] =~ /TABLE|SEQUENCE|VIEW|TRIGGER|TYPE|SYNONYM/))
		{
			if ($self->{db_version} =~ /Release [89]/)
			{
				$str .= ' AND (';
				foreach my $t (@EXCLUDED_TABLES_8I)
				{
					$str .= " AND upper($colname) NOT LIKE ?";
					push(@{$self->{query_bind_params}}, uc($t));
				}
				$str .= ')';
			}
			else
			{
				$str .= ' AND ( ';
				for (my $j = 0; $j <= $#EXCLUDED_TABLES; $j++)
				{
					if ($self->{is_mssql}) {
						#$str .= "PATINDEX(?, upper($colname)) = 0" ;
						$str .= "upper($colname) NOT LIKE ?" ;
						push(@{$self->{query_bind_params}}, uc("$EXCLUDED_TABLES[$j]"));
					} else {
						$str .= " NOT REGEXP_LIKE(upper($colname), ?)" ;
						push(@{$self->{query_bind_params}}, uc("\^$EXCLUDED_TABLES[$j]\$"));
					}
					if ($j < $#EXCLUDED_TABLES) {
						$str .= " AND ";
					}
				}
				$str .= ')';
			}
		}
	}

	$str =~ s/ AND \( AND/ AND \(/g;
	$str =~ s/ AND \(\)//g;
	$str =~ s/ OR \(\)//g;

	return uc($str);
}


# Preload the bytea array at lib init
BEGIN
{
	build_escape_bytea();
}


=head2 _count_check_constraint

This function return the number of check constraints on a given table
excluding CHECK IS NOT NULL constraint.

=cut
sub _count_check_constraint
{
	my ($self, $check_constraint) = @_;

	my  $num_chk_constr = 0;

	# Set the check constraint definition 
	foreach my $k (keys %{$check_constraint->{constraint}})
	{
		my $chkconstraint = $check_constraint->{constraint}->{$k}{condition};
		next if (!$chkconstraint);
		# Skip NOT NULL constraint only
		next if ($chkconstraint =~ /^[^\s]+\s+IS\s+NOT\s+NULL$/i);
		$num_chk_constr++;
	}

	return $num_chk_constr;
}

=head2 _lookup_package

This function is used to look at Oracle PACKAGE code to estimate the cost
of a migration. It return an hash: function name => function code

=cut

sub _lookup_package
{
	my ($self, $plsql) = @_;

	my $content = '';
	my %infos = ();
	if ($plsql =~ /(?:CREATE|CREATE OR REPLACE)?\s*(?:EDITIONABLE|NONEDITIONABLE)?\s*PACKAGE\s+BODY\s*([^\s\%\(]+)((?:\s*\%ORA2PG_COMMENT\d+\%)*\s*(?:AS|IS))\s*(.*)/is)
	{
		my $pname = $1;
		my $type = $2;
		$content = $3;
		$pname =~ s/"//g;
		$self->logit("Looking at package $pname...\n", 1);
		$content =~ s/\bEND[^;]*;$//is;
		my @functions = $self->_extract_functions($content);
		foreach my $f (@functions)
		{
			next if (!$f);
			my %fct_detail = $self->_lookup_function($f, $pname);
			next if (!exists $fct_detail{name} || $self->excluded_functions($fct_detail{name}));
			$fct_detail{name} =~ s/^.*\.//;
			$fct_detail{name} =~ s/"//g;
			%{$infos{"$pname.$fct_detail{name}"}} = %fct_detail;
		}
	}

	return %infos;
}

# Returns 1 if the function match a EXCLUDED clause, 0 otherwise
sub excluded_functions
{
	my ($self, $fct_name) = @_;

	my @done = ();

	# Case where there is nothing to do here
	return 0 if (!$fct_name || (!exists $self->{excluded}{FUNCTION} && !exists $self->{excluded}{PROCEDURE}));
	push(@done, $fct_name);

	foreach my $type ('FUNCTION', 'PROCEDURE')
	{
		for (my $j = 0; $j <= $#{$self->{excluded}{$type}}; $j++)
		{
			if ($self->{excluded}{$type}->[$j] =~ /^!$fct_name$/i) {
				return 0;
			} elsif ($self->{excluded}{$type}->[$j] =~ /^$fct_name$/i) {
				return 1;
			}
		}
	}

	return 0;
}

=head2 _lookup_function

This function is used to look at Oracle FUNCTION code to extract
all parts of a fonction

Return a hast with the details of the function

=cut

sub _lookup_function
{
	my ($self, $plsql, $pname, $meta) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_lookup_function($self, $plsql, $pname, $meta);
	} elsif ($self->{is_mssql}) {
		return Ora2Pg::MSSQL::_lookup_function($self, $plsql, $pname, $meta);
	} else {
		return Ora2Pg::Oracle::_lookup_function($self, $plsql, $pname, $meta);
	}
}

####
# Return a string to set the current search path
####
sub set_search_path
{
	my ($self, $owner, $pkg_path) = @_;

	my $local_path = '';
	if ($self->{postgis_schema}) {
		$local_path = ',' . $self->quote_object_name($self->{postgis_schema});
	}
	if ($self->{data_type}{BFILE} eq 'efile') {
			$local_path .= ',external_file';
	}
	my $orafce_path = '';
	$orafce_path = ',oracle' if ($self->{'use_orafce'});
	$local_path .= "$orafce_path,public";
	
	my $search_path = '';
	if (!$self->{schema} && $self->{export_schema} && $owner) {
		$pkg_path = ',' . $pkg_path if ($pkg_path);
		$search_path = "SET search_path = " . $self->quote_object_name($owner) . "$pkg_path$local_path;";
	} elsif (!$owner) {
		my @pathes = ();
		# When PG_SCHEMA is set, always take the value as search path
		if ($self->{pg_schema}) {
			@pathes = split(/\s*,\s*/, $self->{pg_schema});
		} elsif ($self->{export_schema} && $self->{schema}) {
			# When EXPORT_SCHEMA is enable and we are working on a specific schema
			# set it as default search_path. Useful when object are not prefixed
			# with their destination schema.
			push(@pathes, $self->{schema});
		}
		push(@pathes, $pkg_path) if ($pkg_path);
		if ($#pathes >= 0) {
			map { $_ =  $self->quote_object_name($_); } @pathes;
			$search_path = "SET search_path = " . join(',', @pathes) . "$local_path;";
		}
	}

	return "$search_path\n" if ($search_path);
}

sub _get_human_cost
{
	my ($self, $total_cost_value) = @_;

	return 0 if (!$total_cost_value);

	my $human_cost = $total_cost_value * $self->{cost_unit_value};
	if ($human_cost >= 420) {
		my $tmp = $human_cost/420;
		$tmp++ if ($tmp =~ s/\.\d+//);
		$human_cost = "$tmp person-day(s)";
	} else {
		#my $tmp = $human_cost/60;
		#$tmp++ if ($tmp =~ s/\.\d+//);
		#$human_cost = "$tmp man-hour(s)";
		# mimimum to 1 day, hours are not really relevant
		$human_cost = "1 person-day(s)";
	} 

	return $human_cost;
}

sub difficulty_assessment
{
	my ($self, %report_info) = @_;

	# Migration that might be run automatically
	# 1 = trivial: no stored functions and no triggers
	# 2 = easy: no stored functions but with triggers
	# 3 = simple: stored functions and/or triggers
	# Migration that need code rewrite
	# 4 = manual: no stored functions but with triggers or view
	# 5 = difficult: with stored functions and/or triggers
	my $difficulty = 1;

	my @stored_function = (
		'FUNCTION',
		'PACKAGE BODY',
		'PROCEDURE'
	);

	foreach my $n (@stored_function) {
		if (exists $report_info{'Objects'}{$n} && $report_info{'Objects'}{$n}{'number'}) {
			$difficulty = 3;
			last;
		}
	}
	if ($difficulty < 3) {
		$difficulty += 1 if ( exists $report_info{'Objects'}{'TRIGGER'} && $report_info{'Objects'}{'TRIGGER'}{'number'});
	}


	if ($difficulty < 3) {
		foreach my $fct (keys %{ $report_info{'full_trigger_details'} } ) {
			next if (!exists $report_info{'full_trigger_details'}{$fct}{keywords});
			$difficulty = 4;
			last;
		}
	}
	if ($difficulty <= 3) {
		foreach my $fct (keys %{ $report_info{'full_view_details'} } ) {
			next if (!exists $report_info{'full_view_details'}{$fct}{keywords});
			$difficulty = 4;
			last;
		}
	}
	if ($difficulty >= 3) {
		foreach my $fct (keys %{ $report_info{'full_function_details'} } ) {
			next if (!exists $report_info{'full_function_details'}{$fct}{keywords});
			$difficulty = 5;
			last;
		}
		foreach my $fct (keys %{ $report_info{'full_procedure_details'} } ) {
			next if (!exists $report_info{'full_procedure_details'}{$fct}{keywords});
			$difficulty = 5;
			last;
		}
		foreach my $fct (keys %{ $report_info{'full_package_details'} } ) {
			next if (!exists $report_info{'full_package_details'}{$fct}{keywords});
			$difficulty = 5;
			last;
		}
	}

	my $tmp = $report_info{'total_cost_value'}/84;
	$tmp++ if ($tmp =~ s/\.\d+//);

	my $level = 'A';
	$level = 'B' if ($difficulty > 3);
	$level = 'C' if ( ($difficulty > 3) && ($tmp > $self->{human_days_limit}) );

	return "$level-$difficulty";
}

sub _show_report
{
	my ($self, %report_info) = @_;

	my @ora_object_type = (
		'DATABASE LINK',
		'DIRECTORY',
		'FUNCTION',
		'INDEX',
		'JOB',
		'MATERIALIZED VIEW',
		'PACKAGE BODY',
		'PROCEDURE',
		'QUERY',
		'SEQUENCE',
		'SYNONYM',
		'TABLE',
		'TABLE PARTITION',
		'TABLE SUBPARTITION',
		'TRIGGER',
		'TYPE',
		'VIEW',

# Other object type
#CLUSTER
#CONSUMER GROUP
#DESTINATION
#DIMENSION
#EDITION
#EVALUATION CONTEXT
#INDEX PARTITION
#INDEXTYPE
#JAVA CLASS
#JAVA DATA
#JAVA RESOURCE
#JAVA SOURCE
#JOB CLASS
#LIBRARY
#LOB
#LOB PARTITION
#OPERATOR
#PACKAGE
#PROGRAM
#QUEUE
#RESOURCE PLAN
#RULE
#RULE SET
#SCHEDULE
#SCHEDULER GROUP
#TYPE BODY
#UNDEFINED
#UNIFIED AUDIT POLICY
#WINDOW
#XML SCHEMA
	);
        my $report_exported = 0;
	my $difficulty = $self->difficulty_assessment(%report_info);
	my $lbl_mig_type = qq{
Migration levels:
    A - Migration that might be run automatically
    B - Migration with code rewrite and a human-days cost up to $self->{human_days_limit} days
    C - Migration with code rewrite and a human-days cost above $self->{human_days_limit} days
Technical levels:
    1 = trivial: no stored functions and no triggers
    2 = easy: no stored functions but with triggers, no manual rewriting
    3 = simple: stored functions and/or triggers, no manual rewriting
    4 = manual: no stored functions but with triggers or views with code rewriting
    5 = difficult: stored functions and/or triggers with code rewriting
};
	# Generate report text report
	if (!$self->{dump_as_html} && !$self->{dump_as_csv} && !$self->{dump_as_sheet} && !$self->{dump_as_json})
	{
		my $cost_header = '';
		$report_exported = 1;
		$self->_select_output_file_suffix("txt");
		$cost_header = "\tEstimated cost" if ($self->{estimate_cost});
		$self->logrep("-------------------------------------------------------------------------------\n");
		$self->logrep("Ora2Pg v$VERSION - Database Migration Report\n");
		$self->logrep("-------------------------------------------------------------------------------\n");
		$self->logrep("Version\t$report_info{'Version'}\n");
		$self->logrep("Schema\t$report_info{'Schema'}\n");
		$self->logrep("Size\t$report_info{'Size'}\n\n");
		$self->logrep("-------------------------------------------------------------------------------\n");
		$self->logrep("Object\tNumber\tInvalid$cost_header\tComments\tDetails\n");
		$self->logrep("-------------------------------------------------------------------------------\n");
		foreach my $typ (sort keys %{ $report_info{'Objects'} } ) {
			$report_info{'Objects'}{$typ}{'detail'} =~ s/\n/\. /gs;
			if ($self->{estimate_cost}) {
				$self->logrep("$typ\t$report_info{'Objects'}{$typ}{'number'}\t$report_info{'Objects'}{$typ}{'invalid'}\t$report_info{'Objects'}{$typ}{'cost_value'}\t$report_info{'Objects'}{$typ}{'comment'}\t$report_info{'Objects'}{$typ}{'detail'}\n");
			} else {
				$self->logrep("$typ\t$report_info{'Objects'}{$typ}{'number'}\t$report_info{'Objects'}{$typ}{'invalid'}\t$report_info{'Objects'}{$typ}{'comment'}\t$report_info{'Objects'}{$typ}{'detail'}\n");
			}
		}
		$self->logrep("-------------------------------------------------------------------------------\n");
		if ($self->{estimate_cost}) {
			my $human_cost = $self->_get_human_cost($report_info{'total_cost_value'});
			my $comment = "$report_info{'total_cost_value'} cost migration units means approximatively $human_cost. The migration unit was set to $self->{cost_unit_value} minute(s)\n";
			$self->logrep("Total\t$report_info{'total_object_number'}\t$report_info{'total_object_invalid'}\t$report_info{'total_cost_value'}\t$comment\n");
		} else {
			$self->logrep("Total\t$report_info{'total_object_number'}\t$report_info{'total_object_invalid'}\n");
		}
		$self->logrep("-------------------------------------------------------------------------------\n");
		if ($self->{estimate_cost})
		{
			$self->logrep("Migration level : $difficulty\n");
			$self->logrep("-------------------------------------------------------------------------------\n");
			$self->logrep($lbl_mig_type);
			$self->logrep("-------------------------------------------------------------------------------\n");
			if (scalar keys %{ $report_info{'full_function_details'} })
			{
				$self->logrep("\nDetails of cost assessment per function\n");
				foreach my $fct (sort { $report_info{'full_function_details'}{$b}{count} <=> $report_info{'full_function_details'}{$a}{count} } keys %{ $report_info{'full_function_details'} } ) {
					$self->logrep("Function $fct total estimated cost: $report_info{'full_function_details'}{$fct}{count}\n");
					$self->logrep($report_info{'full_function_details'}{$fct}{info});
				}
				$self->logrep("-------------------------------------------------------------------------------\n");
			}
			if (scalar keys %{ $report_info{'full_procedure_details'} })
			{
				$self->logrep("\nDetails of cost assessment per procedure\n");
				foreach my $fct (sort { $report_info{'full_procedure_details'}{$b}{count} <=> $report_info{'full_procedure_details'}{$a}{count} } keys %{ $report_info{'full_procedure_details'} } ) {
					$self->logrep("Function $fct total estimated cost: $report_info{'full_procedure_details'}{$fct}{count}\n");
					$self->logrep($report_info{'full_procedure_details'}{$fct}{info});
				}
				$self->logrep("-------------------------------------------------------------------------------\n");
			}
			if (scalar keys %{ $report_info{'full_package_details'} })
			{
				$self->logrep("\nDetails of cost assessment per package function\n");
				foreach my $fct (sort { $report_info{'full_package_details'}{$b}{count} <=> $report_info{'full_package_details'}{$a}{count} } keys %{ $report_info{'full_package_details'} } ) {
					$self->logrep("Function $fct total estimated cost: $report_info{'full_package_details'}{$fct}{count}\n");
					$self->logrep($report_info{'full_package_details'}{$fct}{info});
				}
				$self->logrep("-------------------------------------------------------------------------------\n");
			}
			if (scalar keys %{ $report_info{'full_trigger_details'} })
			{
				$self->logrep("\nDetails of cost assessment per trigger\n");
				foreach my $fct (sort { $report_info{'full_trigger_details'}{$b}{count} <=> $report_info{'full_trigger_details'}{$a}{count} } keys %{ $report_info{'full_trigger_details'} } ) {
					$self->logrep("Trigger $fct total estimated cost: $report_info{'full_trigger_details'}{$fct}{count}\n");
					$self->logrep($report_info{'full_trigger_details'}{$fct}{info});
				}
				$self->logrep("-------------------------------------------------------------------------------\n");
			}
			if (scalar keys %{ $report_info{'full_view_details'} })
			{
				$self->logrep("\nDetails of cost assessment per view\n");
				foreach my $fct (sort { $report_info{'full_view_details'}{$b}{count} <=> $report_info{'full_view_details'}{$a}{count} } keys %{ $report_info{'full_view_details'} } ) {
					$self->logrep("View $fct total estimated cost: $report_info{'full_view_details'}{$fct}{count}\n");
					$self->logrep($report_info{'full_view_details'}{$fct}{info});
				}
				$self->logrep("-------------------------------------------------------------------------------\n");
			}
		}
	}
	if ($self->{dump_as_csv} && ($self->{dump_as_file_prefix} || !$report_exported))
	{
		$report_exported = 1;
		$self->_select_output_file_suffix("csv");
		$self->logrep("-------------------------------------------------------------------------------\n");
		$self->logrep("Ora2Pg v$VERSION - Database Migration Report\n");
		$self->logrep("-------------------------------------------------------------------------------\n");
		$self->logrep("Version\t$report_info{'Version'}\n");
		$self->logrep("Schema\t$report_info{'Schema'}\n");
		$self->logrep("Size\t$report_info{'Size'}\n\n");
		$self->logrep("-------------------------------------------------------------------------------\n\n");
		$self->logrep("Object;Number;Invalid;Estimated cost;Comments\n");
		foreach my $typ (sort keys %{ $report_info{'Objects'} } ) {
			$report_info{'Objects'}{$typ}{'detail'} =~ s/\n/\. /gs;
			$self->logrep("$typ;$report_info{'Objects'}{$typ}{'number'};$report_info{'Objects'}{$typ}{'invalid'};$report_info{'Objects'}{$typ}{'cost_value'};$report_info{'Objects'}{$typ}{'comment'}\n");
		}
		my $human_cost = $self->_get_human_cost($report_info{'total_cost_value'});
		$difficulty = '' if (!$self->{estimate_cost});
		$self->logrep("\n");
		$self->logrep("Total Number;Total Invalid;Total Estimated cost;Human days cost;Migration level\n");
		$self->logrep("$report_info{'total_object_number'};$report_info{'total_object_invalid'};$report_info{'total_cost_value'};$human_cost;$difficulty\n");
	}
	if ($self->{dump_as_json} && ($self->{dump_as_file_prefix} || !$report_exported))
	{
		$report_exported = 1;
		$self->_select_output_file_suffix("json");
		$self->logrep("{\n");
		$self->logrep("\"ora2pg version\": $VERSION,\n");
		$self->logrep("\"Version\": \"$report_info{'Version'}\",\n");
		$self->logrep("\"Schema\": \"$report_info{'Schema'}\",\n");
		$self->logrep("\"Size\": \"$report_info{'Size'}\",\n");
		my $cnt=0;
		$self->logrep("\"objects\": [");
		foreach my $typ (sort keys %{ $report_info{'Objects'} } )
		{
			$report_info{'Objects'}{$typ}{'detail'} =~ s/\n/\. /gs;
			$cnt++;
			if ($cnt ne 1) {
			    $self->logrep(",");
			}
			$self->logrep("{");
			$self->logrep("\"object\":\"$typ\",\"number\":$report_info{'Objects'}{$typ}{'number'},");
			$self->logrep("\"invalid\":$report_info{'Objects'}{$typ}{'invalid'},");
			$self->logrep("\"cost value\":$report_info{'Objects'}{$typ}{'cost_value'},");
			my $json_comment = ($report_info{'Objects'}{$typ}{'comment'} =~ s/\n/\\n/gr);
                        $self->logrep("\"comment\":\"$json_comment\",\n");
			$self->logrep("\"details\":\"$report_info{'Objects'}{$typ}{'detail'}\"}\n");
		}
		$self->logrep("]\n");
		my $human_cost = $self->_get_human_cost($report_info{'total_cost_value'});
		$difficulty = '' if (!$self->{estimate_cost});
		$self->logrep(",\"total number\":$report_info{'total_object_number'}");
		$self->logrep(",\"total invalid\":$report_info{'total_object_invalid'}");
		$self->logrep(",\"total cost\":$report_info{'total_cost_value'}");
		$self->logrep(",\"human days cost\":\"$human_cost\"");
		$self->logrep(",\"migration level\":\"$difficulty\"");
		$self->logrep("}\n");
	}
	if ($self->{dump_as_sheet} && ($self->{dump_as_file_prefix} || !$report_exported))
	{
		$report_exported = 1;
		$self->_select_output_file_suffix("sheet.csv");
		$difficulty = '' if (!$self->{estimate_cost});
		my @header = ('Instance', 'Version', 'Schema', 'Size', 'Cost assessment', 'Migration type');
		my $human_cost = $self->_get_human_cost($report_info{'total_cost_value'});
		my @infos  = ($self->{oracle_dsn}, $report_info{'Version'}, $report_info{'Schema'}, $report_info{'Size'}, $human_cost, $difficulty);
		foreach my $typ (sort @ora_object_type) {
			push(@header, $typ);
			$report_info{'Objects'}{$typ}{'number'} ||= 0;
			$report_info{'Objects'}{$typ}{'invalid'} ||= 0;
			$report_info{'Objects'}{$typ}{'cost_value'} ||= 0;
			push(@infos, "$report_info{'Objects'}{$typ}{'number'}/$report_info{'Objects'}{$typ}{'invalid'}/$report_info{'Objects'}{$typ}{'cost_value'}");
		}
		push(@header, "Total assessment");
		push(@infos, "$report_info{total_object_number}/$report_info{total_object_invalid}/$report_info{total_cost_value}");
		if ($self->{print_header}) {
			$self->logrep('"' . join('";"', @header) . '"' . "\n");
		}
		$self->logrep('"' . join('";"', @infos) . '"' . "\n");
	}
	if ($self->{dump_as_html} && ($self->{dump_as_file_prefix} || !$report_exported))
	{
		$report_exported = 1;
		$self->_select_output_file_suffix("html");
		my $cost_header = '';
		$cost_header = "<th>Estimated cost</th>" if ($self->{estimate_cost});
		my $date = localtime(time);
		my $html_header = qq{<!DOCTYPE html>
<html>
  <head>
  <title>Ora2Pg - Database Migration Report</title>
  <meta HTTP-EQUIV="Generator" CONTENT="Ora2Pg v$VERSION">
  <meta HTTP-EQUIV="Date" CONTENT="$date">
  <style>
body {
	margin: 30px 0;
	padding: 0;
	background: #EFEFEF;
	font-size: 12px;
	color: #1e1e1e;
}

h1 {
	margin-bottom: 20px;
	border-bottom: 1px solid #DFDFDF;
	font-size: 22px;
	padding: 0px;
	padding-bottom: 5px;
	font-weight: bold;
	color: #0094C7;
}

h2 {
	margin-bottom: 10px;
	font-size: 18px;
	padding: 0px;
	padding-bottom: 5px;
	font-weight: bold;
	color: #0094C7;
}

#header table {
	padding: 0 5px 0 5px;
	border: 1px solid #DBDBDB;
	margin-bottom: 20px;
	margin-left: 30px;
}

#header th {
	padding: 0 5px 0 5px;
	text-decoration: none;
	font-size: 16px;
	color: #EC5800;
}

#content table {
	padding: 0 5px 0 5px;
	border: 1px solid #DBDBDB;
	margin-bottom: 20px;
	margin-left: 10px;
	margin-right: 10px;
}
#content td {
	padding: 0 5px 0 5px;
	border-bottom: 1px solid #888888;
	margin-bottom: 20px;
	text-align: left;
	vertical-align: top;
}

#content th {
	border-bottom: 1px solid #BBBBBB;
	padding: 0 5px 0 5px;
	text-decoration: none;
	font-size: 16px;
	color: #EC5800;
}

.object_name {
        font-weight: bold;
        color: #0094C7;
	text-align: left;
	white-space: pre;
}

.detail {
	white-space: pre;
}

#footer {
	margin-right: 10px;
	text-align: right;
}

#footer a {
	color: #EC5800;
}

#footer a:hover {
	text-decoration: none;
}
  </style>
</head>
<body>
<div id="header">
<h1>Ora2Pg - Database Migration Report</h1>
<table>
<tr><th>Version</th><td>$report_info{'Version'}</td></tr>
<tr><th>Schema</th><td>$report_info{'Schema'}</td></tr>
<tr><th>Size</th><td>$report_info{'Size'}</td></tr>
</table>
</div>
<div id="content">
<table>
<tr><th>Object</th><th>Number</th><th>Invalid</th>$cost_header<th>Comments</th><th>Details</th></tr>
};

		$self->logrep($html_header);
		foreach my $typ (sort keys %{ $report_info{'Objects'} } ) {
			$report_info{'Objects'}{$typ}{'detail'} =~ s/\n/<br>/gs;
			$report_info{'Objects'}{$typ}{'detail'} = "<details><summary>See details</summary>$report_info{'Objects'}{$typ}{'detail'}</details>" if ($report_info{'Objects'}{$typ}{'detail'} ne '');
			if ($self->{estimate_cost}) {
				$self->logrep("<tr><td class=\"object_name\">$typ</td><td style=\"text-align: center;\">$report_info{'Objects'}{$typ}{'number'}</td><td style=\"text-align: center;\">$report_info{'Objects'}{$typ}{'invalid'}</td><td style=\"text-align: center;\">$report_info{'Objects'}{$typ}{'cost_value'}</td><td>$report_info{'Objects'}{$typ}{'comment'}</td><td class=\"detail\">$report_info{'Objects'}{$typ}{'detail'}</td></tr>\n");
			} else {
				$self->logrep("<tr><td class=\"object_name\">$typ</td><td style=\"text-align: center;\">$report_info{'Objects'}{$typ}{'number'}</td><td style=\"text-align: center;\">$report_info{'Objects'}{$typ}{'invalid'}</td><td>$report_info{'Objects'}{$typ}{'comment'}</td><td class=\"detail\">$report_info{'Objects'}{$typ}{'detail'}</td></tr>\n");
			}
		}
		if ($self->{estimate_cost}) {
			my $human_cost = $self->_get_human_cost($report_info{'total_cost_value'});
			my $comment = "$report_info{'total_cost_value'} cost migration units means approximatively $human_cost. The migration unit was set to $self->{cost_unit_value} minute(s)\n";
			$self->logrep("<tr><th style=\"text-align: center; border-bottom: 0px; vertical-align: bottom;\">Total</th><td style=\"text-align: center; border-bottom: 0px; vertical-align: bottom;\">$report_info{'total_object_number'}</td><td style=\"text-align: center; border-bottom: 0px; vertical-align: bottom;\">$report_info{'total_object_invalid'}</td><td style=\"text-align: center; border-bottom: 0px; vertical-align: bottom;\">$report_info{'total_cost_value'}</td><td colspan=\"2\" style=\"border-bottom: 0px; vertical-align: bottom;\">$comment</td></tr>\n");
		} else {
			$self->logrep("<tr><th style=\"text-align: center; border-bottom: 0px; vertical-align: bottom;\">Total</th><td style=\"text-align: center; border-bottom: 0px; vertical-align: bottom; border-bottom: 0px; vertical-align: bottom;\">$report_info{'total_object_number'}</td><td style=\"text-align: center; border-bottom: 0px; vertical-align: bottom;\">$report_info{'total_object_invalid'}</td><td colspan=\"3\" style=\"border-bottom: 0px; vertical-align: bottom;\"></td></tr>\n");
		}
		$self->logrep("</table>\n</div>\n");
		if ($self->{estimate_cost}) {
			$self->logrep("<h2>Migration level: $difficulty</h2>\n");
			$lbl_mig_type = qq{
<ul>
<li>Migration levels:</li>
  <ul>
    <li>A - Migration that might be run automatically</li>
    <li>B - Migration with code rewrite and a human-days cost up to $self->{human_days_limit} days</li>
    <li>C - Migration with code rewrite and a human-days cost above $self->{human_days_limit} days</li>
  </ul>
<li>Technical levels:</li>
  <ul>
    <li>1 = trivial: no stored functions and no triggers</li>
    <li>2 = easy: no stored functions but with triggers, no manual rewriting</li>
    <li>3 = simple: stored functions and/or triggers, no manual rewriting</li>
    <li>4 = manual: no stored functions but with triggers or views with code rewriting</li>
    <li>5 = difficult: stored functions and/or triggers with code rewriting</li>
  </ul>
</ul>
};
			$self->logrep($lbl_mig_type);
			if (scalar keys %{ $report_info{'full_function_details'} })
			{
				$self->logrep("<h2>Details of cost assessment per function</h2>\n");
				$self->logrep("<details><summary>Show</summary><ul>\n");
				foreach my $fct (sort { $report_info{'full_function_details'}{$b}{count} <=> $report_info{'full_function_details'}{$a}{count} } keys %{ $report_info{'full_function_details'} } ) {
					
					$self->logrep("<li>Function $fct total estimated cost: $report_info{'full_function_details'}{$fct}{count}</li>\n");
					$self->logrep("<ul>\n");
					$report_info{'full_function_details'}{$fct}{info} =~ s/\t/<li>/gs;
					$report_info{'full_function_details'}{$fct}{info} =~ s/\n/<\/li>\n/gs;
					$self->logrep($report_info{'full_function_details'}{$fct}{info});
					$self->logrep("</ul>\n");
				}
				$self->logrep("</ul></details>\n");
			}
			if (scalar keys %{ $report_info{'full_procedure_details'} })
			{
				$self->logrep("<h2>Details of cost assessment per procedure</h2>\n");
				$self->logrep("<details><summary>Show</summary><ul>\n");
				foreach my $fct (sort { $report_info{'full_procedure_details'}{$b}{count} <=> $report_info{'full_procedure_details'}{$a}{count} } keys %{ $report_info{'full_procedure_details'} } ) {
					
					$self->logrep("<li>Procedure $fct total estimated cost: $report_info{'full_procedure_details'}{$fct}{count}</li>\n");
					$self->logrep("<ul>\n");
					$report_info{'full_procedure_details'}{$fct}{info} =~ s/\t/<li>/gs;
					$report_info{'full_procedure_details'}{$fct}{info} =~ s/\n/<\/li>\n/gs;
					$self->logrep($report_info{'full_procedure_details'}{$fct}{info});
					$self->logrep("</ul>\n");
				}
				$self->logrep("</ul></details>\n");
			}
			if (scalar keys %{ $report_info{'full_package_details'} })
			{
				$self->logrep("<h2>Details of cost assessment per package function</h2>\n");
				$self->logrep("<details><summary>Show</summary><ul>\n");
				foreach my $fct (sort { $report_info{'full_package_details'}{$b}{count} <=> $report_info{'full_package_details'}{$a}{count} } keys %{ $report_info{'full_package_details'} } ) {
					
					$self->logrep("<li>Function $fct total estimated cost: $report_info{'full_package_details'}{$fct}{count}</li>\n");
					$self->logrep("<ul>\n");
					$report_info{'full_package_details'}{$fct}{info} =~ s/\t/<li>/gs;
					$report_info{'full_package_details'}{$fct}{info} =~ s/\n/<\/li>\n/gs;
					$self->logrep($report_info{'full_package_details'}{$fct}{info});
					$self->logrep("</ul>\n");
				}
				$self->logrep("</ul></details>\n");
			}
			if (scalar keys %{ $report_info{'full_trigger_details'} })
			{
				$self->logrep("<h2>Details of cost assessment per trigger</h2>\n");
				$self->logrep("<details><summary>Show</summary><ul>\n");
				foreach my $fct (sort { $report_info{'full_trigger_details'}{$b}{count} <=> $report_info{'full_trigger_details'}{$a}{count} } keys %{ $report_info{'full_trigger_details'} } ) {
					
					$self->logrep("<li>Trigger $fct total estimated cost: $report_info{'full_trigger_details'}{$fct}{count}</li>\n");
					$self->logrep("<ul>\n");
					$report_info{'full_trigger_details'}{$fct}{info} =~ s/\t/<li>/gs;
					$report_info{'full_trigger_details'}{$fct}{info} =~ s/\n/<\/li>\n/gs;
					$self->logrep($report_info{'full_trigger_details'}{$fct}{info});
					$self->logrep("</ul>\n");
				}
				$self->logrep("</ul></details>\n");
			}
			if (scalar keys %{ $report_info{'full_view_details'} }) {
				$self->logrep("<h2>Details of cost assessment per view</h2>\n");
				$self->logrep("<details><summary>Show</summary><ul>\n");
				foreach my $fct (sort { $report_info{'full_view_details'}{$b}{count} <=> $report_info{'full_view_details'}{$a}{count} } keys %{ $report_info{'full_view_details'} } ) {
					
					$self->logrep("<li>View $fct total estimated cost: $report_info{'full_view_details'}{$fct}{count}</li>\n");
					$self->logrep("<ul>\n");
					$report_info{'full_view_details'}{$fct}{info} =~ s/\t/<li>/gs;
					$report_info{'full_view_details'}{$fct}{info} =~ s/\n/<\/li>\n/gs;
					$self->logrep($report_info{'full_view_details'}{$fct}{info});
					$self->logrep("</ul>\n");
				}
				$self->logrep("</ul></details>\n");
			}
		}
		my $html_footer = qq{
<div id="footer">
Generated by <a href="http://ora2pg.darold.net/">Ora2Pg v$VERSION</a>
</div>
</body>
</html>
};
		$self->logrep($html_footer);
	}
}

sub get_kettle_xml
{

	return <<EOF
<transformation>
  <info>
    <name>template</name>
    <description/>
    <extended_description/>
    <trans_version/>
    <trans_type>Normal</trans_type>
    <trans_status>0</trans_status>
    <directory>&#47;</directory>
    <parameters>
    </parameters>
    <log>
<trans-log-table><connection/>
<schema/>
<table/>
<size_limit_lines/>
<interval/>
<timeout_days/>
<field><id>ID_BATCH</id><enabled>Y</enabled><name>ID_BATCH</name></field><field><id>CHANNEL_ID</id><enabled>Y</enabled><name>CHANNEL_ID</name></field><field><id>TRANSNAME</id><enabled>Y</enabled><name>TRANSNAME</name></field><field><id>STATUS</id><enabled>Y</enabled><name>STATUS</name></field><field><id>LINES_READ</id><enabled>Y</enabled><name>LINES_READ</name><subject/></field><field><id>LINES_WRITTEN</id><enabled>Y</enabled><name>LINES_WRITTEN</name><subject/></field><field><id>LINES_UPDATED</id><enabled>Y</enabled><name>LINES_UPDATED</name><subject/></field><field><id>LINES_INPUT</id><enabled>Y</enabled><name>LINES_INPUT</name><subject/></field><field><id>LINES_OUTPUT</id><enabled>Y</enabled><name>LINES_OUTPUT</name><subject/></field><field><id>LINES_REJECTED</id><enabled>Y</enabled><name>LINES_REJECTED</name><subject/></field><field><id>ERRORS</id><enabled>Y</enabled><name>ERRORS</name></field><field><id>STARTDATE</id><enabled>Y</enabled><name>STARTDATE</name></field><field><id>ENDDATE</id><enabled>Y</enabled><name>ENDDATE</name></field><field><id>LOGDATE</id><enabled>Y</enabled><name>LOGDATE</name></field><field><id>DEPDATE</id><enabled>Y</enabled><name>DEPDATE</name></field><field><id>REPLAYDATE</id><enabled>Y</enabled><name>REPLAYDATE</name></field><field><id>LOG_FIELD</id><enabled>Y</enabled><name>LOG_FIELD</name></field></trans-log-table>
<perf-log-table><connection/>
<schema/>
<table/>
<interval/>
<timeout_days/>
<field><id>ID_BATCH</id><enabled>Y</enabled><name>ID_BATCH</name></field><field><id>SEQ_NR</id><enabled>Y</enabled><name>SEQ_NR</name></field><field><id>LOGDATE</id><enabled>Y</enabled><name>LOGDATE</name></field><field><id>TRANSNAME</id><enabled>Y</enabled><name>TRANSNAME</name></field><field><id>STEPNAME</id><enabled>Y</enabled><name>STEPNAME</name></field><field><id>STEP_COPY</id><enabled>Y</enabled><name>STEP_COPY</name></field><field><id>LINES_READ</id><enabled>Y</enabled><name>LINES_READ</name></field><field><id>LINES_WRITTEN</id><enabled>Y</enabled><name>LINES_WRITTEN</name></field><field><id>LINES_UPDATED</id><enabled>Y</enabled><name>LINES_UPDATED</name></field><field><id>LINES_INPUT</id><enabled>Y</enabled><name>LINES_INPUT</name></field><field><id>LINES_OUTPUT</id><enabled>Y</enabled><name>LINES_OUTPUT</name></field><field><id>LINES_REJECTED</id><enabled>Y</enabled><name>LINES_REJECTED</name></field><field><id>ERRORS</id><enabled>Y</enabled><name>ERRORS</name></field><field><id>INPUT_BUFFER_ROWS</id><enabled>Y</enabled><name>INPUT_BUFFER_ROWS</name></field><field><id>OUTPUT_BUFFER_ROWS</id><enabled>Y</enabled><name>OUTPUT_BUFFER_ROWS</name></field></perf-log-table>
<channel-log-table><connection/>
<schema/>
<table/>
<timeout_days/>
<field><id>ID_BATCH</id><enabled>Y</enabled><name>ID_BATCH</name></field><field><id>CHANNEL_ID</id><enabled>Y</enabled><name>CHANNEL_ID</name></field><field><id>LOG_DATE</id><enabled>Y</enabled><name>LOG_DATE</name></field><field><id>LOGGING_OBJECT_TYPE</id><enabled>Y</enabled><name>LOGGING_OBJECT_TYPE</name></field><field><id>OBJECT_NAME</id><enabled>Y</enabled><name>OBJECT_NAME</name></field><field><id>OBJECT_COPY</id><enabled>Y</enabled><name>OBJECT_COPY</name></field><field><id>REPOSITORY_DIRECTORY</id><enabled>Y</enabled><name>REPOSITORY_DIRECTORY</name></field><field><id>FILENAME</id><enabled>Y</enabled><name>FILENAME</name></field><field><id>OBJECT_ID</id><enabled>Y</enabled><name>OBJECT_ID</name></field><field><id>OBJECT_REVISION</id><enabled>Y</enabled><name>OBJECT_REVISION</name></field><field><id>PARENT_CHANNEL_ID</id><enabled>Y</enabled><name>PARENT_CHANNEL_ID</name></field><field><id>ROOT_CHANNEL_ID</id><enabled>Y</enabled><name>ROOT_CHANNEL_ID</name></field></channel-log-table>
<step-log-table><connection/>
<schema/>
<table/>
<timeout_days/>
<field><id>ID_BATCH</id><enabled>Y</enabled><name>ID_BATCH</name></field><field><id>CHANNEL_ID</id><enabled>Y</enabled><name>CHANNEL_ID</name></field><field><id>LOG_DATE</id><enabled>Y</enabled><name>LOG_DATE</name></field><field><id>TRANSNAME</id><enabled>Y</enabled><name>TRANSNAME</name></field><field><id>STEPNAME</id><enabled>Y</enabled><name>STEPNAME</name></field><field><id>STEP_COPY</id><enabled>Y</enabled><name>STEP_COPY</name></field><field><id>LINES_READ</id><enabled>Y</enabled><name>LINES_READ</name></field><field><id>LINES_WRITTEN</id><enabled>Y</enabled><name>LINES_WRITTEN</name></field><field><id>LINES_UPDATED</id><enabled>Y</enabled><name>LINES_UPDATED</name></field><field><id>LINES_INPUT</id><enabled>Y</enabled><name>LINES_INPUT</name></field><field><id>LINES_OUTPUT</id><enabled>Y</enabled><name>LINES_OUTPUT</name></field><field><id>LINES_REJECTED</id><enabled>Y</enabled><name>LINES_REJECTED</name></field><field><id>ERRORS</id><enabled>Y</enabled><name>ERRORS</name></field><field><id>LOG_FIELD</id><enabled>N</enabled><name>LOG_FIELD</name></field></step-log-table>
    </log>
    <maxdate>
      <connection/>
      <table/>
      <field/>
      <offset>0.0</offset>
      <maxdiff>0.0</maxdiff>
    </maxdate>
    <size_rowset>__rowset__</size_rowset>
    <sleep_time_empty>10</sleep_time_empty>
    <sleep_time_full>10</sleep_time_full>
    <unique_connections>N</unique_connections>
    <feedback_shown>Y</feedback_shown>
    <feedback_size>500000</feedback_size>
    <using_thread_priorities>Y</using_thread_priorities>
    <shared_objects_file/>
    <capture_step_performance>Y</capture_step_performance>
    <step_performance_capturing_delay>1000</step_performance_capturing_delay>
    <step_performance_capturing_size_limit>100</step_performance_capturing_size_limit>
    <dependencies>
    </dependencies>
    <partitionschemas>
    </partitionschemas>
    <slaveservers>
    </slaveservers>
    <clusterschemas>
    </clusterschemas>
  <created_user>-</created_user>
  <created_date>2013&#47;02&#47;28 14:04:49.560</created_date>
  <modified_user>-</modified_user>
  <modified_date>2013&#47;03&#47;01 12:35:39.999</modified_date>
  </info>
  <notepads>
  </notepads>
  <connection>
    <name>__oracle_db__</name>
    <server>__oracle_host__</server>
    <type>ORACLE</type>
    <access>Native</access>
    <database>__oracle_instance__</database>
    <port>__oracle_port__</port>
    <username>__oracle_username__</username>
    <password>__oracle_password__</password>
    <servername/>
    <data_tablespace/>
    <index_tablespace/>
    <attributes>
      <attribute><code>EXTRA_OPTION_ORACLE.defaultRowPrefetch</code><attribute>10000</attribute></attribute>
      <attribute><code>EXTRA_OPTION_ORACLE.fetchSize</code><attribute>1000</attribute></attribute>
      <attribute><code>FORCE_IDENTIFIERS_TO_LOWERCASE</code><attribute>N</attribute></attribute>
      <attribute><code>FORCE_IDENTIFIERS_TO_UPPERCASE</code><attribute>N</attribute></attribute>
      <attribute><code>IS_CLUSTERED</code><attribute>N</attribute></attribute>
      <attribute><code>PORT_NUMBER</code><attribute>__oracle_port__</attribute></attribute>
      <attribute><code>QUOTE_ALL_FIELDS</code><attribute>N</attribute></attribute>
      <attribute><code>SUPPORTS_BOOLEAN_DATA_TYPE</code><attribute>N</attribute></attribute>
      <attribute><code>USE_POOLING</code><attribute>N</attribute></attribute>
    </attributes>
  </connection>
  <connection>
    <name>__postgres_db__</name>
    <server>__postgres_host__</server>
    <type>POSTGRESQL</type>
    <access>Native</access>
    <database>__postgres_database_name__</database>
    <port>__postgres_port__</port>
    <username>__postgres_username__</username>
    <password>__postgres_password__</password>
    <servername/>
    <data_tablespace/>
    <index_tablespace/>
    <attributes>
      <attribute><code>FORCE_IDENTIFIERS_TO_LOWERCASE</code><attribute>N</attribute></attribute>
      <attribute><code>FORCE_IDENTIFIERS_TO_UPPERCASE</code><attribute>N</attribute></attribute>
      <attribute><code>IS_CLUSTERED</code><attribute>N</attribute></attribute>
      <attribute><code>PORT_NUMBER</code><attribute>__postgres_port__</attribute></attribute>
      <attribute><code>QUOTE_ALL_FIELDS</code><attribute>N</attribute></attribute>
      <attribute><code>SUPPORTS_BOOLEAN_DATA_TYPE</code><attribute>Y</attribute></attribute>
      <attribute><code>USE_POOLING</code><attribute>N</attribute></attribute>
      <attribute><code>EXTRA_OPTION_POSTGRESQL.synchronous_commit</code><attribute>__sync_commit_onoff__</attribute></attribute>
    </attributes>
  </connection>
  <order>
  <hop> <from>Table input</from><to>Modified Java Script Value</to><enabled>Y</enabled> </hop>  <hop> <from>Modified Java Script Value</from><to>Table output</to><enabled>Y</enabled> </hop>

  </order>
  <step>
    <name>Table input</name>
    <type>TableInput</type>
    <description/>
    <distribute>Y</distribute>
    <copies>__select_copies__</copies>
         <partitioning>
           <method>none</method>
           <schema_name/>
           </partitioning>
    <connection>__oracle_db__</connection>
    <sql>__select_query__</sql>
    <limit>0</limit>
    <lookup/>
    <execute_each_row>N</execute_each_row>
    <variables_active>N</variables_active>
    <lazy_conversion_active>N</lazy_conversion_active>
     <cluster_schema/>
 <remotesteps>   <input>   </input>   <output>   </output> </remotesteps>    <GUI>
      <xloc>122</xloc>
      <yloc>160</yloc>
      <draw>Y</draw>
      </GUI>
    </step>

  <step>
    <name>Table output</name>
    <type>TableOutput</type>
    <description/>
    <distribute>Y</distribute>
    <copies>__insert_copies__</copies>
         <partitioning>
           <method>none</method>
           <schema_name/>
           </partitioning>
    <connection>__postgres_db__</connection>
    <schema/>
    <table>__postgres_table_name__</table>
    <commit>__commit_size__</commit>
    <truncate>__truncate__</truncate>
    <ignore_errors>Y</ignore_errors>
    <use_batch>Y</use_batch>
    <specify_fields>N</specify_fields>
    <partitioning_enabled>N</partitioning_enabled>
    <partitioning_field/>
    <partitioning_daily>N</partitioning_daily>
    <partitioning_monthly>Y</partitioning_monthly>
    <tablename_in_field>N</tablename_in_field>
    <tablename_field/>
    <tablename_in_table>Y</tablename_in_table>
    <return_keys>N</return_keys>
    <return_field/>
    <fields>
    </fields>
     <cluster_schema/>
 <remotesteps>   <input>   </input>   <output>   </output> </remotesteps>    <GUI>
      <xloc>369</xloc>
      <yloc>155</yloc>
      <draw>Y</draw>
      </GUI>
    </step>

  <step>
    <name>Modified Java Script Value</name>
    <type>ScriptValueMod</type>
    <description/>
    <distribute>Y</distribute>
    <copies>__js_copies__</copies>
         <partitioning>
           <method>none</method>
           <schema_name/>
           </partitioning>
    <compatible>N</compatible>
    <optimizationLevel>9</optimizationLevel>
    <jsScripts>      <jsScript>        <jsScript_type>0</jsScript_type>
        <jsScript_name>Script 1</jsScript_name>
        <jsScript_script>for (var i=0;i&lt;getInputRowMeta().size();i++) { 
  var valueMeta = getInputRowMeta().getValueMeta(i);
  if (valueMeta.getTypeDesc().equals(&quot;String&quot;)) {
    row[i]=replace(row[i],&quot;\\00&quot;,&apos;&apos;);
  }
} </jsScript_script>
      </jsScript>    </jsScripts>    <fields>    </fields>     <cluster_schema/>
 <remotesteps>   <input>   </input>   <output>   </output> </remotesteps>    <GUI>
      <xloc>243</xloc>
      <yloc>166</yloc>
      <draw>Y</draw>
      </GUI>
    </step>

  <step_error_handling>
  </step_error_handling>
   <slave-step-copy-partition-distribution>
</slave-step-copy-partition-distribution>
   <slave_transformation>N</slave_transformation>
</transformation>
EOF

}

# Constants for creating kettle files from the template
sub create_kettle_output
{
	my ($self, $table, $output_dir) = @_;

	my $oracle_host = 'localhost';
	if ($self->{oracle_dsn} =~ /host=([^;]+)/) {
		$oracle_host = $1;
	}
	my $oracle_port = 1521;
	if ($self->{oracle_dsn} =~ /port=(\d+)/) {
		$oracle_port = $1;
	}
	my $oracle_instance='';
	if ($self->{oracle_dsn} =~ /sid=([^;]+)/) {
		$oracle_instance = $1;
	} elsif ($self->{oracle_dsn} =~ /dbi:Oracle:([^:]+)/) {
		$oracle_instance = $1;
	}
	if ($self->{oracle_dsn} =~ /\/\/([^:]+):(\d+)\/(.*)/) {
		$oracle_host = $1;
		$oracle_port = $2;
		$oracle_instance = $3;
	} elsif ($self->{oracle_dsn} =~ /\/\/([^\/]+)\/(.*)/) {
		$oracle_host = $1;
		$oracle_instance = $2;
	}

	my $pg_host = 'localhost';
	if ($self->{pg_dsn} =~ /host=([^;]+)/) {
		$pg_host = $1;
	}
	my $pg_port = 5432;
	if ($self->{pg_dsn} =~ /port=(\d+)/) {
		$pg_port = $1;
	}
	my $pg_dbname = '';
	if ($self->{pg_dsn} =~ /dbname=([^;]+)/) {
		$pg_dbname = $1;
	}

	my $select_query = "SELECT * FROM $table";
	if ($self->{schema}) {
		$select_query = "SELECT * FROM $self->{schema}.$table";
	}
	my $select_copies = $self->{oracle_copies} || 1;
	if (($self->{oracle_copies} > 1) && $self->{defined_pk}{"\L$table\E"}) {
		my $colpk = $self->{defined_pk}{"\L$table\E"};
		if ($self->{preserve_case}) {
			$colpk = '"' . $colpk . '"';
		}
		if ($self->{schema}) {
			$select_query = "SELECT * FROM $self->{schema}.$table WHERE ";
		} else {
			$select_query = "SELECT * FROM $table WHERE ";
		}
		if ($self->{is_mssql}) {
			$select_query = "ABS($colpk % \${Internal.Step.Unique.Count})=\${Internal.Step.Unique.Number}";
		} else {
			$select_query = "ABS(MOD($colpk,\${Internal.Step.Unique.Count}))=\${Internal.Step.Unique.Number}";
		}
	} else {
		$select_copies = 1;
	}

	my $insert_copies = $self->{jobs} || 4;
	my $js_copies = $insert_copies;
	my $rowset = $self->{data_limit} || 10000;
	if (exists $self->{local_data_limit}{$table}) {
		$rowset  = $self->{local_data_limit}{$table};
	}
	my $commit_size = 500;
	my $sync_commit_onoff = 'off';
	my $truncate = 'Y';
	$truncate = 'N' if (!$self->{truncate_table});

	my $pg_table = $table;
	if ($self->{export_schema}) {
		if ($self->{pg_schema}) {
			$pg_table = "$self->{pg_schema}.$table";
		} elsif ($self->{schema}) {
			$pg_table = "$self->{schema}.$table";
		}
	}

	my $xml = &get_kettle_xml();
	$xml =~ s/__oracle_host__/$oracle_host/gs;
	$xml =~ s/__oracle_instance__/$oracle_instance/gs;
	$xml =~ s/__oracle_port__/$oracle_port/gs;
	$xml =~ s/__oracle_username__/$self->{oracle_user}/gs;
	$xml =~ s/__oracle_password__/$self->{oracle_pwd}/gs;
	$xml =~ s/__postgres_host__/$pg_host/gs;
	$xml =~ s/__postgres_database_name__/$pg_dbname/gs;
	$xml =~ s/__postgres_port__/$pg_port/gs;
	$xml =~ s/__postgres_username__/$self->{pg_user}/gs;
	$xml =~ s/__postgres_password__/$self->{pg_pwd}/gs;
	$xml =~ s/__select_copies__/$select_copies/gs;
	$xml =~ s/__select_query__/$select_query/gs;
	$xml =~ s/__insert_copies__/$insert_copies/gs;
	$xml =~ s/__js_copies__/$js_copies/gs;
	$xml =~ s/__truncate__/$truncate/gs;
	$xml =~ s/__transformation_name__/$table/gs;
	$xml =~ s/__postgres_table_name__/$pg_table/gs;
	$xml =~ s/__rowset__/$rowset/gs;
	$xml =~ s/__commit_size__/$commit_size/gs;
	$xml =~ s/__sync_commit_onoff__/$sync_commit_onoff/gs;

	my $fh = new IO::File;
	$fh->open(">$output_dir$table.ktr") or $self->logit("FATAL: can't write to $output_dir$table.ktr, $!\n", 0, 1);
	$fh->print($xml);
	$self->close_export_file($fh);

	return "JAVAMAXMEM=4096 ./pan.sh -file \$KETTLE_TEMPLATE_PATH/$output_dir$table.ktr -level Detailed\n";
}

# Normalize SQL queries by removing parameters
sub normalize_query
{
	my ($self, $orig_query) = @_;

	return if (!$orig_query);

	# Remove comments
	$orig_query =~ s/\/\*(.*?)\*\///gs;

	# Set the entire query lowercase
	$orig_query = lc($orig_query);

	# Remove extra space, new line and tab characters by a single space
	$orig_query =~ s/\s+/ /gs;

	# Removed start of transaction 
	if ($orig_query !~ /^\s*begin\s*;\s*$/) {
		$orig_query =~ s/^\s*begin\s*;\s*//gs
	}

	# Remove string content
	$orig_query =~ s/\\'//g;
	$orig_query =~ s/'[^']*'/''/g;
	$orig_query =~ s/''('')+/''/g;

	# Remove NULL parameters
	$orig_query =~ s/=\s*NULL/=''/g;

	# Remove numbers
	$orig_query =~ s/([^a-z_\$-])-?([0-9]+)/${1}0/g;

	# Remove hexadecimal numbers
	$orig_query =~ s/([^a-z_\$-])0x[0-9a-f]{1,10}/${1}0x/g;

	# Remove IN values
	$orig_query =~ s/in\s*\([\'0x,\s]*\)/in (...)/g;

	return $orig_query;
}

sub _escape_lob
{
	my ($self, $col, $generic_type, $cond, $isnested, $dest_type) = @_;

	if ($self->{type} eq 'COPY')
	{
		if ($generic_type eq 'BLOB')
		{
			# Get an hexa representation of the blob data
			$col = unpack("H*",$col);
			$col = "\\\\x" . $col;
		}
		elsif ($generic_type eq 'RAW')
		{
			# RAW data are already returned in hexa by DBD::Oracle 
			if ($dest_type eq 'uuid')
			{
				# we do nothing, the value will be cast into uuid automatically
			}
			elsif ($dest_type eq 'bytea' && $cond->{long})
			{
				$col = unpack("H*",$col);
				$col = "\\\\x" . $col;
			}
			elsif ($dest_type eq 'bytea')
			{
				$col = "\\\\x" . $col;
			}
		}
		elsif (($generic_type eq 'CLOB') || $cond->{istext})
		{
			$col = $self->escape_copy($col, $isnested);
		}
	}
	else
	{
		if ($generic_type eq 'BLOB')
		{
			# Get an hexa representation of the blob data
			$col = unpack("H*",$col);
			if (!$self->{standard_conforming_strings}) {
				$col = "'$col'";
			} else {
				$col = "E'$col'";
			}
			if (!$self->{blob_to_lo})
			{
				if (!$self->{pg_dsn}) {
					$col = "decode($col, 'hex')";
				}
				else
				{
					# with prepare just send the data
					$col =~ s/^[E]?'//;
					$col =~ s/'$//;
				}
			}
			elsif (!$self->{pg_dsn})
			{
				$col = "lo_from_bytea(0, decode($col, 'hex'))";
			}
		}
		elsif ($generic_type eq 'RAW')
		{
			# RAW data are already returned in hexa by DBD::Oracle
			if ($dest_type eq 'uuid')
			{
				# we do nothing, the value will be cast into uuid automatically
				$col = "'$col'";
			}
			elsif ($dest_type eq 'bytea')
			{
				if (!$self->{standard_conforming_strings}) {
					$col = "'$col'";
				} else {
					$col = "E'$col'";
				}
				$col = "decode($col, 'hex')";
			}
		}
		elsif (($generic_type eq 'CLOB') || $cond->{istext})
		{
			$col = $self->escape_insert($col, $isnested);
		}
	}

	return $col;
}

sub escape_copy
{
	my ($self, $col, $isnested) = @_;

	my $q = "'";
	$q = '"' if ($isnested);

	if ($self->{has_utf8_fct}) {
		utf8::encode($col) if (!utf8::valid($col));
	}
	# Escape some character for COPY output
	$col =~ s/(\0|\\|\r|\n|\t)/$ESCAPE_COPY->{$1}/gs;
	if (!$self->{noescape}) {
		$col =~ s/\f/\\f/gs;
		$col =~ s/([\1-\10\13-\14\16-\37])/sprintf("\\%03o", ord($1))/egs;
	}

	return $col;
}

sub escape_insert
{
	my ($self, $col, $isnested) = @_;

	my $q = "'";
	$q = '"' if ($isnested);

	if (!$self->{standard_conforming_strings})
	{
		$col =~ s/'/''/gs; # double single quote
		if ($isnested) {
			$col =~ s/"/\\"/gs; # escape double quote
		}
		$col =~ s/\\/\\\\/gs;
		$col =~ s/\0//gs;
		$col = "$q$col$q";
	}
	else
	{
		$col =~ s/\0//gs;
		$col =~ s/\\/\\\\/gs;
		$col =~ s/\r/\\r/gs;
		$col =~ s/\n/\\n/gs;
		if ($isnested)
		{
			$col =~ s/'/''/gs; # double single quote
			$col =~ s/"/\\"/gs; # escape double quote
			$col = "$q$col$q";
		}
		else
		{
			$col =~ s/'/\\'/gs; # escape single quote
			$col = "E'$col'";
		}
	}
	return $col;
}

sub clear_global_declaration
{
	my ($self, $pname, $str, $is_pkg_body) = @_;

	# Remove comment
	$str =~ s/\%ORA2PG_COMMENT\d+\%//igs;

	# remove pragma restrict_references
	$str =~ s/PRAGMA\s+RESTRICT_REFERENCES\s*\([^;]+;//igs;

	# Remove all function/procedure declaration from the content
	if (!$is_pkg_body) {
		$str =~ s/\b(PROCEDURE|FUNCTION)\s+[^;]+;//igs;
	} else {
		while ($str =~ s/\b(PROCEDURE|FUNCTION)\s+.*?END[^;]*;((?:(?!\bEND\b).)*\s+(?:PROCEDURE|FUNCTION)\s+)/$2/is) {
		};
		$str =~ s/(PROCEDURE|FUNCTION).*END[^;]*;//is;
	}
	# Remove end of the package declaration
	$str =~ s/\s+END[^;]*;\s*$//igs;
	# Eliminate extra newline
	$str =~ s/[\r\n]+/\n/isg;

	my @cursors = ();
	while ($str =~ s/(CURSOR\s+[^;]+\s+RETURN\s+[^;]+;)//is) {
		push(@cursors, $1);
	}
	# Extract TYPE/SUBTYPE declaration
	my $i = 0;
	while ($str =~ s/\b(SUBTYPE|TYPE)\s+([^\s\(\)]+)\s+(AS|IS)\s+([^;]+;)//is) {
		$self->{pkg_type}{$pname}{$2} = "$pname.$2";
		my $code = "$1 $self->{pkg_type}{$pname}{$2} AS $4";
		push(@{$self->{types}}, { ('name' => $2, 'code' => $code, 'pos' => $i++) });
	}

	return ($str, @cursors);
}


sub register_global_variable
{
	my ($self, $pname, $glob_vars) = @_;

	$glob_vars = $self->_replace_sql_type($glob_vars);

	# Replace PL/SQL code into PL/PGSQL similar code
	$glob_vars = Ora2Pg::PLSQL::convert_plsql_code($self, $glob_vars);

	my @vars = split(/\s*(\%ORA2PG_COMMENT\d+\%|;)\s*/, $glob_vars);
	map { s/^\s+//; s/\s+$//; } @vars;
	my $ret = '';
	foreach my $l (@vars)
	{
		if ($l eq ';' || $l =~ /ORA2PG_COMMENT/ || $l =~ /^CREATE\s+/i) {
			$ret .= $l if ($l ne ';');
			next;
		}
		next if (!$l);
		$l =~ s/\-\-[^\r\n]+//sg;
		$l =~ s/\s*:=\s*/ := /igs;
		my ($n, $type, @others) = split(/\s+/, $l);
		$ret .= $l, next if (!$type);
		if (!$n)
		{
			$n = $type;
			$type = $others[0] || '';
		}
		if (uc($type) eq 'EXCEPTION')
		{
			$n = lc($n);
			if (!exists $self->{custom_exception}{$n}) {
				$self->{custom_exception}{$n} = $self->{exception_id}++;
			}
			next;
		}
		next if (!$pname);
		my $v = lc($pname . '.' . $n);
		$self->{global_variables}{$v}{name} = lc($n);
		if (uc($type) eq 'CONSTANT')
		{
			$type = '';
			$self->{global_variables}{$v}{constant} = 1;
			for (my $j = 0; $j < $#others; $j++)
			{
				$type .= $others[$j] if ($others[$j] ne ':=' and uc($others[$j]) ne 'DEFAULT');
			}
		}
		# extract the default value from the declaration
		for (my $j = 0; $j < $#others; $j++)
		{
			if ($others[$j] eq ':=' or uc($others[$j]) eq 'DEFAULT')
			{
				# Append the rest of the definition to the default value
				for (my $l = $j+1; $l <= $#others; $l++) {
					$self->{global_variables}{$v}{default} .= " " if ($l > $j+1);
					$self->{global_variables}{$v}{default} .= $others[$l];
				}
				last;
			}
		}
		if (exists $self->{global_variables}{$v}{default})
		{
			#$self->{global_variables}{$v}{default} =~ s/^\s+//;
			$self->{global_variables}{$v}{default} = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{global_variables}{$v}{default});
			$self->_restore_text_constant_part(\$self->{global_variables}{$v}{default});
			if ($self->{global_variables}{$v}{default} !~ /^'/) {
				$self->{global_variables}{$v}{default} =~ s/'/\\'/gs;
			}
			$self->{global_variables}{$v}{default} =~ s/^'//s;
			$self->{global_variables}{$v}{default} =~ s/'\s*$//s;
		}
		$self->{global_variables}{$v}{type} = $type;

		# Handle Oracle user defined error code
		if ($self->{global_variables}{$v}{constant} && ($type =~ /bigint|int|numeric|double/)
			&& $self->{global_variables}{$v}{default} <= -20000 && $self->{global_variables}{$v}{default} >= -20999)
		{
			$self->{global_variables}{$v}{default} =~ s/^\s+//;
			# Change the type into char(5) for SQLSTATE type
			$self->{global_variables}{$v}{type} = 'char(5)';
			# Transform the value to match PostgreSQL user defined exceptions starting with 45
			$self->{global_variables}{$v}{default} =~ s/^-20/45/;
		}
	}

	return $ret;
}

sub remove_newline
{
	my $str = shift;

	$str =~ s/[\n\r]+\s*/ /gs;

	return $str;
}

sub _ask_username {
  my $self = shift;
  my $target = shift;
  
  print 'Enter ' . $target . ' username: ';
  my $username = ReadLine(0);
  chomp($username);
  
  return $username;
}

sub _ask_password {
  my $self = shift;
  my $target = shift;
  
  print 'Enter ' . $target . ' password: ';
  ReadMode(2);
  my $password = ReadLine(0);
  ReadMode(0);
  chomp($password);
  print "\n";
  
  return $password;
}

##############
# Prefix function calls with their package name when necessary
##############
sub normalize_function_call
{
	my ($self, $str) = @_;

	return if (!$self->{current_package});

	my $p = lc($self->{current_package});

	# foreach function declared in a package qualify its callis with the package name
	foreach my $f (keys %{$self->{package_functions}{$p}})
	{
		# If the package is already prefixed to the function name in the hash take it from here
		if (lc($self->{package_functions}{$p}{$f}{name}) ne lc($f)) {
			$$str =~ s/([^\.])\b$f\s*([\(;])/$1$self->{package_functions}{$p}{$f}{name}$2/igs;
		} elsif (exists $self->{package_functions}{$p}{$f}{package}) {
			# otherwise use the package name from the hash and the function name from the string
			$$str =~ s/([^\.])\b($f\s*[\(;])/$1$self->{package_functions}{$p}{$f}{package}\.$2/igs;
		}

		# Append parenthesis to functions without parameters
		$$str =~ s/\b($self->{package_functions}{$p}{$f}{package}\.$f)\b((?!\s*\())/$1()$2/igs;
	}
	# Fix unwanted double parenthesis
	#$$str =~ s/\(\)\s*(\()/ $1/gs;

}

##############
# Requalify function calls
##############
sub requalify_function_call
{
	my ($self, $str) = @_;

	# Loop through package 
	foreach my $p (keys %{$self->{package_functions}}) {
		# foreach function declared in a package qualify its callis with the package name
		foreach my $f (keys %{$self->{package_functions}{$p}}) {
			$$str =~ s/\b$p\.$f\s*([\(;])/$self->{package_functions}{$p}{$f}{name}$1/igs;
		}
	}
}


sub _make_WITH
{
    my ($with_oid, $table_info) = @_;
    my @withs =();
    push @withs, 'OIDS' if ($with_oid);
    push @withs, 'fillfactor=' . $table_info->{fillfactor} if (exists $table_info->{fillfactor});
    my $WITH='';
    if (@withs>0) {
	$WITH .= 'WITH (' . join(",",@withs) . ')';
    }
    return $WITH;
}

sub _create_foreign_server
{
	my $self = shift;

	# Verify that the oracle_fdw or mysql_fdw extension is created, create it if not
	my $sth = $self->{dbhdest}->prepare("SELECT * FROM pg_extension WHERE extname=?") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	my $extension = 'oracle_fdw';
	$extension = 'mysql_fdw' if ($self->{is_mysql});
	$extension = 'tds_fdw' if ($self->{is_mssql});
	$sth->execute($extension) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . ", SQL: SELECT * FROM pg_extension WHERE extname='$extension'\n", 0, 1);
	my $row = $sth->fetch;
	$sth->finish;
	if (not defined $row)
	{
		# try to create the extension
		$self->{dbhdest}->do("CREATE EXTENSION IF NOT EXISTS $extension") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	}

	# Check if the server already exists or need to be created
	$sth = $self->{dbhdest}->prepare("SELECT * FROM pg_foreign_server WHERE srvname=?") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	$sth->execute($self->{fdw_server}) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	$row = $sth->fetch;
	$sth->finish;
	if (not defined $row)
	{
		# try to create the foreign server
		if (!defined $self->{oracle_pwd})
		{
			eval("use Term::ReadKey;") unless $self->{oracle_user} eq '/';
			if (!$@) {
				$self->{oracle_user} = $self->_ask_username('Oracle') unless (defined $self->{oracle_user});
				$self->{oracle_pwd} = $self->_ask_password('Oracle') unless ($self->{oracle_user} eq '/');
			}
		}
		my $ora_session_mode = ($self->{oracle_user} eq "/" || $self->{oracle_user} eq "sys") ? 2 : undef;

		$self->logit("ORACLE_HOME = $ENV{ORACLE_HOME}\n", 1);
		$self->logit("NLS_LANG = $ENV{NLS_LANG}\n", 1);
		$self->logit("NLS_NCHAR = $ENV{NLS_NCHAR}\n", 1);
		$self->logit("Trying to connect to database: $self->{oracle_dsn}\n", 1) if (!$quiet);

		my $sql = '';
		my $extension = 'oracle_fdw';
		if (!$self->{fdw_server}) {
			$self->logit("FATAL: a foreign server name must be set using FDW_SERVER\n", 0, 1);
		}
		if (!$self->{is_mysql} && !$self->{is_mssql} &&
			($self->{oracle_dsn} =~ /(\/\/.*\/.*)/ || $self->{oracle_dsn} =~ /dbi:Oracle:([^=:;]+)$/i)
		)
		{
			$self->{oracle_fwd_dsn} = "dbserver '$1'";
		}
		else
		{
			$self->{oracle_dsn} =~ /(?:host|server)=([^;]+)/;
			my $host = $1 || 'localhost';
			$self->{oracle_dsn} =~ /port=(\d+)/;
			my $port = $1;
			if (!$port && $self->{is_mysql}) {
				$port = 3306;
			} elsif (!$port && $self->{is_mssql}) {
				$port = 1433;
			} elsif (!$port)  {
				$port = 1521;
			}
			my $sid = '';
			if ($self->{is_mysql}) {
				$extension = 'mysql_fdw';
				$self->{oracle_dsn} =~ /(database)=([^;]+)/;
				$self->{mysql_fwd_db} = $2 || '';
				$self->{oracle_fwd_dsn} = "host '$host', port '$port'";
			} elsif ($self->{is_mssql}) {
				$extension = 'tds_fdw';
				$self->{oracle_dsn} =~ /(database)=([^;]+)/;
				$self->{mssql_fwd_db} = $2 || '';
				$self->{oracle_fwd_dsn} = "servername '$host', port '$port', database '$self->{mssql_fwd_db}'";
			} else {
				$self->{oracle_dsn} =~ /(service_name|sid)=([^;]+)/;
				$sid = $2 || '';
				$self->{oracle_fwd_dsn} = "dbserver '//$host:$port/$sid'";
			}
		}
		$sql = "CREATE SERVER $self->{fdw_server} FOREIGN DATA WRAPPER $extension OPTIONS ($self->{oracle_fwd_dsn});";
		$self->{dbhdest}->do($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	}

	# Create the user mapping if it not exists
	my $usrlbl = 'user';
	$usrlbl = 'username' if ($self->{is_mysql} || $self->{is_mssql});
	my $sql = "CREATE USER MAPPING IF NOT EXISTS FOR \"$self->{pg_user}\" SERVER $self->{fdw_server} OPTIONS ($usrlbl '$self->{oracle_user}', password '$self->{oracle_pwd}');";
	if ($self->{oracle_user} eq "__SEPS__" && $self->{oracle_pwd} eq "__SEPS__")  # Replace with empty credentials for an Oracle Wallet connection
	{
		$sql =~ s/__SEPS__//g;
	}
	$self->{dbhdest}->do($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
}

sub _select_foreign_objects
{
	my $self = shift;

	# With reports we don't have object name limitation
	return if ($self->{type} ne 'TEST_DATA');

	my $str = '';
	my @limit_to = ();
	my @except = ();

	for (my $j = 0; $j <= $#{$self->{limited}{TABLE}}; $j++) {
		push(@limit_to, '"' . uc($self->{limited}{TABLE}->[$j]) . '"');
	}

	if ($#limit_to == -1)
	{
		for (my $j = 0; $j <= $#{$self->{excluded}{TABLE}}; $j++) {
			push(@except, '"'. uc($self->{excluded}{TABLE}->[$j] . '"'));
		}
		if ($#except > -1) {
			$str = " EXCEPT ( " .  join(', ', @except) . ")";
		}
	} else {
		$str = " LIMIT TO ( " .  join(', ', @limit_to) . ")";
	}

	return $str;
}

sub _import_foreign_schema
{
	my $self = shift;

	# Drop and recreate the import schema
	$self->{dbhdest}->do("DROP SCHEMA $self->{pg_supports_ifexists} $self->{fdw_import_schema} CASCADE") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	$self->{dbhdest}->do("CREATE SCHEMA $self->{fdw_import_schema}") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	# Import foreign table into the dedicated schema $self->{fdw_import_schema}
	my $sql = "IMPORT FOREIGN SCHEMA \"\U$self->{schema}\E\"";
	if ($self->{is_mysql}) {
		$sql = "IMPORT FOREIGN SCHEMA $self->{schema}";
	}
	# ALLOW/EXCLUDE must be applied for data validation
	$sql .= $self->_select_foreign_objects();
	$sql .= " FROM SERVER $self->{fdw_server} INTO $self->{fdw_import_schema}";
	if ($self->{is_mssql}) {
		$sql .= " OPTIONS (import_default 'true')";
	} elsif (!$self->{is_mysql}) {
		$sql .= " OPTIONS (case 'keep', readonly 'true')";
	}
	$self->{dbhdest}->do($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . ", SQL: $sql\n", 0, 1);
}

sub _data_validation
{
	my $self = shift;

	# Get all tables information specified by the DBI method table_info
	$self->logit("Data validation between source database and PostgreSQL...\n", 1);

	my $unique_clause = ' AND i.indkey IS NOT NULL AND i.indisunique ORDER BY i.indisprimary DESC';
	$unique_clause = '' if (!$self->{data_validation_ordering});

	# First of all extract all tables from PostgreSQL database with the
	# unique index column list they must be part of a single schema.
	my $schema_clause = $self->get_schema_condition();
	my $sql = qq{
SELECT c.relname,n.nspname,i.indkey
FROM pg_catalog.pg_class c
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
     LEFT JOIN pg_catalog.pg_index i ON i.indrelid = c.oid
WHERE c.relkind IN ('r','p') AND regexp_match(i.indkey::text, '(^0 | 0 | 0\$|^0\$)') IS NULL
    $schema_clause
    $unique_clause
};
	$self->logit("Get list of table with unique key: $sql\n", 1);
	my $s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	if (not $s->execute())
	{
		push(@errors, "Can not extract information from catalog about tables.");
		next;
	}
	my %list_tables  = ();
	while ( my @row = $s->fetchrow())
	{
		$row[2] =~ s/ /,/g;
		$list_tables{"\L$row[0]\E"}{ucols} = $row[2];
		$list_tables{"\L$row[0]\E"}{schema} = $row[1];
	}
	$s->finish();
	

	my @foreign_tables  = ();
	if ($self->{fdw_server})
	{
		# Extract all foreign tables imported in schema $self->{fdw_import_schema}.
		# Normally the table list have already been filtered.
		$sql = qq{
SELECT c.relname
FROM pg_catalog.pg_class c
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'f' and n.nspname = '$self->{fdw_import_schema}'
};
		$self->logit("Get list of foreign tables imported: $sql\n") if ($self->{debug});
		$s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		if (not $s->execute())
		{
			push(@errors, "Can not extract information from foreign tables.");
			next;
		}
		while ( my @row = $s->fetchrow())
		{
			push(@foreign_tables, $row[0]);
		}
		$s->finish();
	}
	else
	{
		# Retrieve tables informations
		foreach my $table (keys %{$self->{tables}})
		{
			push(@foreign_tables, $table);
		}
	}

	
	# Sort foreign_tables to always have the same ordered iteration
	@foreign_tables = sort @foreign_tables;

	my $total_tables = 0;
	foreach my $f (@foreign_tables)
	{
		next if (!exists $list_tables{"\L$f\E"});
		$total_tables++;
	}
	$self->{child_count} = 0;

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	unlink("${dirprefix}data_validation.log");
	print STDERR "Result will be written to file ${dirprefix}data_validation.log\n";
	my $tfh = $self->append_export_file($dirprefix . 'data_validation.log', 1);
	flock($tfh, 2) || die "FATAL: can't lock file ${dirprefix}data_validation.log\n";
	$tfh->print("[DATA VALIDATION]\n");
	$self->close_export_file($tfh, 1);
	my $q = 1;
	foreach my $f (@foreign_tables)
	{
		next if (!exists $list_tables{"\L$f\E"});

		if ($self->{parallel_tables} > 1)
		{
			while ($self->{child_count} >= $self->{parallel_tables})
			{
				my $kid = waitpid(-1, WNOHANG);
				if ($kid > 0)
				{
					$self->{child_count}--;
					delete $RUNNING_PIDS{$kid};
				}
				usleep(50000);
			}
			spawn sub {
				$self->compare_data($f, $list_tables{"\L$f\E"}{ucols}, $list_tables{"\L$f\E"}{schema});
			};
			$self->{child_count}++;
		}
		else
		{
			$self->compare_data($f, $list_tables{"\L$f\E"}{ucols}, $list_tables{"\L$f\E"}{schema});
		}
		if (!$self->{quiet} && !$self->{debug}) {
			print STDERR $self->progress_bar($q, $total_tables, 25, '=', 'tables', "checked table: $f" ), "\r";
		}
		$q++;
	}

	if ($self->{parallel_tables} > 1)
	{
		# Wait for all child end
		while ($self->{child_count} > 0)
		{
			my $kid = waitpid(-1, WNOHANG);
			if ($kid > 0)
			{
				$self->{child_count}--;
				delete $RUNNING_PIDS{$kid};
			}
			usleep(50000);
		}
	}

	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($q-1, $total_tables, 25, '=', 'tables', "checked" ), "\n\n";
	}
}

sub compare_data
{
	my ($self, $tb, $ucols, $schema) = @_;

	my $lbl = 'ORACLEDB';
	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});
	my $dbhora = undef;
	my $dbhpg = undef;

	if ($self->{fdw_server})
	{
		$self->logit("DEBUG: cloning connection to PostgreSQL.\n", 1);
		$dbhora = $self->{dbhdest}->clone();
		$dbhpg = $self->{dbhdest}->clone();
		# Force execution of initial command on both side
		$self->_ora_initial_command($dbhora);
		$self->_pg_initial_command($dbhpg);
	}
	else
	{
		$self->logit("DEBUG: cloning connection to Oracle.\n", 1);
		$dbhora = $self->{dbh}->clone();
		$dbhpg = $self->{dbhdest}->clone();
		# Force execution of initial command on both side
		$self->_ora_initial_command($dbhora);
		$self->_pg_initial_command($dbhpg);
		if (!$self->{is_mysql} && !$self->{is_mssql})
		{
			$dbhora->{'LongReadLen'} = $self->{longreadlen};
			$dbhora->{'LongTruncOk'} = $self->{longtruncok};
			$dbhora->{'ora_objects'} = 0;
			$self->_datetime_format($dbhora);
			$self->_numeric_format($dbhora);
		}
		else
		{
			if ($self->{nls_lang})
			{
				if ($self->{debug} && !$quiet) {
					$self->logit("Set default encoding to '$self->{nls_lang}' and collate to '$self->{nls_nchar}'\n", 1);
				}
				my $collate = '';
				$collate = " COLLATE '$self->{nls_nchar}'" if ($self->{nls_nchar});
				$sth = $dbhora->prepare("SET NAMES '$self->{nls_lang}'$collate") or $self->logit("FATAL: " . $dbhora->errstr . "\n", 0, 1);
				$sth->execute or $self->logit("FATAL: " . $dbhora->errstr . "\n", 0, 1);
				$sth->finish;
			}
		}
	}

	my $search_path = $self->set_search_path();
	if ($search_path) {
		$dbhpg->do($search_path) or $self->logit("FATAL: " . $dbhpg->errstr . "\n", 0, 1);
	}

	my $sth = undef;
	if ($self->{fdw_server})
	{
		# Oracle lookup through foreign table
		$sql = "SELECT a.* FROM $self->{fdw_import_schema}.\"$tb\" a";
		if (exists $self->{where}{"\L$table\E"} && $self->{where}{"\L$table\E"})
		{
			$sql .= ' WHERE ';
			if (!$self->{is_mysql} || ($self->{where}{"\L$table\E"} !~ /\s+LIMIT\s+\d/)) {
				$sql .= '(' . $self->{where}{"\L$table\E"} . ')';
			} else {
				$sql .= $self->{where}{"\L$table\E"};
			}
		}
		elsif ($self->{global_where})
		{
			$sql .= ' WHERE ';
			if (!$self->{is_mysql} || ($self->{global_where} !~ /\s+LIMIT\s+\d/)) {
				$sql .= '(' . $self->{global_where} . ')';
			} else {
				$sql .= $self->{global_where};
			}
		}
		$sql .= " ORDER BY " . $ucols if ($self->{data_validation_ordering});
		$sql .= " LIMIT $self->{data_validation_rows}" if ($self->{data_validation_rows});
		$self->logit("Get rows from foreign tables: $sql\n", 1);
		$sth = $dbhora->prepare($sql)  or $self->logit("ERROR: " . $dbhora->errstr . "\n", 0, 0);
		$sth->execute or $self->logit("FATAL: " . $dbhora->errstr . "\n", 0, 0);
	}
	else
	{
		# Extract all column information used to determine data export.
		# This hash will be used in function _howto_get_data()
		%{$self->{colinfo}} = $self->_column_attributes($tb, $self->{schema}, 'TABLE');
		$sql = $self->_get_oracle_test_data($tb);
		# Oracle data lookup
		if ($self->{data_validation_rows})
		{
			if ($self->{db_version} =~ /Release (8|9|10|11)/)
			{
				$sql .= " ORDER BY " . $ucols if ($self->{data_validation_ordering});
				$sql = "SELECT * FROM ( $sql ) WHERE ROWNUM <= $self->{data_validation_rows}";
			}
			else
			{
				$sql .= " ORDER BY " . $ucols if ($self->{data_validation_ordering});
				if (!$self->{is_mysql}) {
					$sql .= " FETCH FIRST $self->{data_validation_rows} ROWS ONLY";
				} else {
					$sql .= " LIMIT $self->{data_validation_rows}";
				}
			}
		} elsif ($self->{data_validation_ordering}) {
			$sql .= " ORDER BY " . $ucols;
		}
		$self->logit("Get rows from oracle tables: $sql\n", 1);
		$sth = $dbhora->prepare($sql)  or $self->logit("ERROR: " . $dbhora->errstr . "\n", 0, 0);
		$sth->execute or $self->logit("FATAL: " . $dbhora->errstr . "\n", 0, 0);
	}

	####
	# PostgreSQL lookup
	####

	# Compute the target list to be able to call functions to format some data type like geometry
	my $tname = $self->quote_object_name($schema) . '.'. $self->quote_object_name($tb);
	my $tmp = qq{
SELECT attname,attnum,atttypid::regtype
FROM pg_attribute
WHERE attrelid = '$tname'::regclass AND attnum > 0 AND NOT attisdropped
ORDER BY attnum};
	my $tmpsth = $dbhpg->prepare($tmp);
	$tmpsth->execute();
	my @tlist = ();
	my @dest_types = ();
	while ( my @crow = $tmpsth->fetchrow())
	{
		next if (!$self->is_in_struct($tb, $crow[0]));
		if ($crow[2] eq 'geometry')
		{
			if ($self->{is_mysql}) {
				push(@tlist, "$self->{st_astext_function}($crow[0])");
			} else {
				push(@tlist, $crow[0]);
			}
		}
		else
		{
			push(@tlist, $crow[0]);
		}
		push(@dest_types, $crow[2]);
	}
	$tmpsth->finish();

	# Quote column name when required
	for (my $i = 0; $i <= $#tlist; $i++)
	{
		if ($tlist[$i] !~ /"/ && $self->is_reserved_words($tlist[$i])) {
		       $tlist[$i] = '"' . $tlist[$i] . '"';
		}
		if ($self->{preserve_case}) {
			$tlist[$i] =~ s/^(["]*)/"/;
			$tlist[$i] =~ s/(["]*)$/"/;
		}
	}

	# Now get the data
	my $sql2 = "SELECT " . join(',', @tlist) . " FROM " . $self->quote_object_name($schema) . '.'. $self->quote_object_name($tb);
	$sql2 .= " ORDER BY " . $ucols if ($self->{data_validation_ordering});
	$sql2 .= " LIMIT $self->{data_validation_rows}" if ($self->{data_validation_rows});
	$self->logit("Get rows from migrated tables: $sql2\n") if ($self->{debug});
	my $sth2 = $dbhpg->prepare($sql2)  or $self->logit("ERROR: " . $dbhpg->errstr . "\n", 0, 0);
	$sth2->execute or $self->logit("FATAL: " . $dbhpg->errstr . "\n", 0, 0);
	my $i = 1;

	$self->logit("Checking data validation for table $f\n", 1);
	my $nerror = 0;
	my $error_msg = '';
	while ( my @orow = $sth->fetchrow())
	{
		my @prow = $sth2->fetchrow();
		# There is an issue to compare timestamp, try to fix it
		for (my $i = 0; $i <= $#orow; $i++)
		{
			# We must adjust the microsecond information on PG data
			if ( $self->{enable_microsecond} && exists $self->{colinfo}{$tb} &&
					$self->{colinfo}{$tb}{data_type}{$i+1} =~ /^(DATE|TIMESTAMP)/i )
			{
				if ($orow[$i] =~ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+$/
					&& $prow[$i] =~ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/) {
					$prow[$i] .= '.000000';
				} elsif ($prow[$i] =~ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+$/) {
					while (length($prow[$i]) < 26) { $prow[$i] .= '0' };
				}
			}
			# RAW(16) and RAW(32) might have been replaced by uuid
			if ($dest_types[$i] eq 'uuid' && $self->{colinfo}{$tb}{data_type}{$i+1} eq 'RAW') {
				$orow[$i] =~ s/^([A-F0-9]{8})([A-F0-9]{4})([A-F0-9]{4})([A-F0-9]{4})([A-F0-9]{12})$/\L$1-$2-$3-$4-$5\E/i;
			}
			# Cover boolean transformation
			if ($dest_types[$i] eq 'boolean')
			{
				foreach my $k (keys %{ $self->{ora_boolean_values} })
				{
					if ($orow[$i] =~ /^$k$/i)
					{
						$orow[$i] = $self->{ora_boolean_values}{$k};
						last;
					}
				}
				if ($prow[$i] == 1) {
					$prow[$i] = 't';
				} elsif (defined $prow[$i]) {
					$prow[$i] = 'f';
				}
			}
			# Remove extra zero following the decimal when needed
			if ($dest_types[$i] =~ /(double precision|real|numeric)/i)
			{
				$orow[$i] =~ s/\.[0]+$// if ($prow[$i] !~ /\.[0]+$/);
				if ($prow[$i] !~ /\.[0-9]+[0]+$/) {
					while ($orow[$i] =~ s/(\.[0-9]+)[0]+$/$1/) {};
				}
				$prow[$i] =~ s/\.[0]+$// if ($orow[$i] !~ /\.[0]+$/);
				if ($orow[$i] !~ /\.[0-9]+[0]+$/) {
					while ($prow[$i] =~ s/(\.[0-9]+)[0]+$/$1/) {};
				}
			}

			# MySQL remove the trailing space at end of char(n) -> take care of that in your app
			# PostgreSQL keep the trailing spaces in respect to SQL standard.
			if ($self->{is_mysql} && $self->{colinfo}{$tb}{data_type}{$i+1} =~ /^CHAR/i) {
				$prow[$i] =~ s/[ ]+$//;
			}
			# Oracle can report decimal as .nn, PG always have a 0 at startup
			if ($self->{colinfo}{$tb}{data_type}{$i+1} eq 'NUMBER') {
				$orow[$i] =~ s/^([\-]?)(\.\d+)/${1}0$2/;
			}
		}
		my $ora_data = join('|', @orow);
		my $pg_data = join('|', @prow);
		if ($ora_data ne $pg_data)
		{
			$error_msg .= "$lbl:$tb:$i:[$ora_data]\n";
			$error_msg .= "POSTGRES:$tb:$i:[$pg_data]\n";
			$nerror++;
		}
		$i++;
		last if ($nerror == $self->{data_validation_error});
	}
	$sth->finish;
	$sth2->finish;
	$dbhora->disconnect() if ($dbhora);
	$dbhpg->disconnect() if ($dbhpg);

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	my $tfh = $self->append_export_file($dirprefix . 'data_validation.log', 1);
	flock($tfh, 2) || die "FATAL: can't lock file data_validation.log\n";
	$tfh->print("Data validation for table $tb: " . ((!$nerror) ? "OK\n" : "$nerror FAIL\n"));
	if ($error_msg)
	{
		$tfh->print("-----------------------------------------------------------------\n");
		$tfh->print($error_msg);
		$tfh->print("-----------------------------------------------------------------\n");
	}
	$self->close_export_file($tfh, 1);
}

####
# Set query to extract Oracle table content for data comparison
####
sub _get_oracle_test_data
{
	my ($self, $table, $part_name, $is_subpart) = @_;

	# Rename table and double-quote it if required
	my $tmptb = '';

	# Prefix partition name with tablename, if pg_supports_partition is enabled
	# direct import to partition is not allowed so import to main table.
	if (!$self->{pg_supports_partition} && $part_name && $self->{rename_partition}) {
		$tmptb = $self->get_replaced_tbname($table . '_' . $part_name);
	} elsif (!$self->{pg_supports_partition} && $part_name) {
		$tmptb = $self->get_replaced_tbname($part_name || $table);
	} else {
		$tmptb = $self->get_replaced_tbname($table);
	}

	# Build the header of the query
	my @tt = ();
	my @stt = ();
	my @nn = ();
	my $col_list = '';

	# Extract column information following the Oracle position order
	my @fname = ();
	my (@pg_colnames_nullable, @pg_colnames_notnull, @pg_colnames_pkey);
	foreach my $i ( 0 .. $#{$self->{tables}{$table}{field_name}} )
	{
		my $fieldname = ${$self->{tables}{$table}{field_name}}[$i];
		next if (!$self->is_in_struct($table, $fieldname));

		my $f = $self->{tables}{"$table"}{column_info}{"$fieldname"};
		$f->[2] =~ s/\D//g;
		if (!$self->{enable_blob_export} && $f->[1] =~ /blob/i)
		{
			# user don't want to export blob
			next;
		}
		if (!$self->{enable_clob_export} && $f->[1] =~ /clob/i)
		{
			# user don't want to export clob
			next;
		}

		my $is_pk = $self->is_primary_key_column($table, $fieldname);

		# When lo_import is used we only want the PK colmuns and the BLOB
		if ($self->{lo_import} && $f->[1] !~ /blob/i && !$is_pk) {
			next;
		}

		if (!$self->{preserve_case}) {
			push(@fname, lc($fieldname));
		} else {
			push(@fname, $fieldname);
		}

		my $type = $self->_sql_type($f->[1], $f->[2], $f->[5], $f->[6], $f->[4]);
		$type = "$f->[1], $f->[2]" if (!$type);

		if (uc($f->[1]) eq 'ENUM') {
			my $keyname = lc($table . '_' . $colname . '_t');
			$f->[1] = $keyname;
		}
		push(@stt, uc($f->[1]));
		push(@tt, $type);
		push(@nn, $f->[0]);

		# Change column names
		my $colname = $f->[0];
		if ($self->{replaced_cols}{lc($table)}{lc($f->[0])}) {
			$self->logit("\tReplacing column $f->[0] as " . $self->{replaced_cols}{lc($table)}{lc($f->[0])} . "...\n", 1);
			$colname = $self->{replaced_cols}{lc($table)}{lc($f->[0])};
		}
		$colname = $self->quote_object_name($colname);
		if ($colname !~ /"/ && $self->is_reserved_words($colname)) {
			$colname = '"' . $colname . '"';
		}
		$col_list .= "$colname,";
		if ($is_pk) {
			push @pg_colnames_pkey, "$colname";
		} elsif ($f->[3] =~ m/^Y/) {
			push @pg_colnames_nullable, "$colname";
		} else {
			push @pg_colnames_notnull, "$colname";
		}
	}
	$col_list =~ s/,$//;
	$self->{tables}{$table}{pg_colnames_nullable} = \@pg_colnames_nullable;
	$self->{tables}{$table}{pg_colnames_notnull} = \@pg_colnames_notnull;
	$self->{tables}{$table}{pg_colnames_pkey} = \@pg_colnames_pkey;

	# Extract all data from the current table
	my $query = $self->_howto_get_data($table, \@nn, \@tt, \@stt, $part_name, $is_subpart);

	return $query
}

1;

__END__


=head1 AUTHOR

Gilles Darold <gilles _AT_ darold _DOT_ net>


=head1 COPYRIGHT

Copyright (c) 2000-2025 Gilles Darold - All rights reserved.

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program. If not, see < http://www.gnu.org/licenses/ >.


=head1 SEE ALSO

L<DBD::Oracle>, L<DBD::Pg>


=cut
