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-2020 : 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 qw/ tmpdir /;
use File::Temp qw/ tempfile /;
use Benchmark;

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

$VERSION = '21.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_.*');
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_%');

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 = (
	'bool' => 0, 'boolean' => 0, 'bytea' => 4, 'char' => 0, 'name' => 0,
	'int8' => 8, 'int2' => 2, 'int4' => 4, 'text' => 4, 'oid' => 4, 'json' => 4,
	'xml' => 4, 'point' => 8, 'lseg' => 8, 'path' => 8, 'box' => 8,
	'polygon' => 8, 'line' => 8, 'float4' => 4, 'float8' => 8,
	'abstime' => 4, 'reltime' => 4, 'tinterval' => 4, 'circle' => 8,
	'money' => 8, 'macaddr' => 4, 'inet' => 4, 'cidr' => 4, 'bpchar' => 4,
	'varchar' => 4, 'date' => 4, 'time' => 8, 'timestamp' => 8,
	'timestamptz' => 8, 'interval' => 8, 'timetz' => 8, 'bit' => 4,
	'varbit' => 4, 'numeric' => 4, 'uuid' => 0, 'timestamp with time zone' => 8,
	'character varying' => 0, 'timestamp without time zone' => 8,
	'double precision' => 8, 'smallint' => 2, 'integer' => 4, 'bigint' => 8,
	'decimal' => '4', 'real' => 4, 'smallserial' => 2, 'serial' => 4,
	'bigserial' => 8
);

# These definitions can be overriden from configuration file
our %TYPE = (
	# Oracle only has one flexible underlying numeric type, NUMBER.
	# Without precision and scale it is set to the PG type float8
	# to match all needs
	'NUMBER' => 'numeric',
	# CHAR types limit of 2000 bytes with defaults to 1 if no length
	# is specified. PG char type has max length set to 8104 so it
	# should match all needs
	'CHAR' => 'char',
	'NCHAR' => 'char',
	# VARCHAR types the limit is 2000 bytes in Oracle 7 and 4000 in
	# Oracle 8. PG varchar type has max length iset to 8104 so it
	# should match all needs
	'VARCHAR' => 'varchar',
	'NVARCHAR' => 'varchar',
	'VARCHAR2' => 'varchar',
	'NVARCHAR2' => 'varchar',
	'STRING' => 'varchar',
	# The DATE data type is used to store the date and time
	# information. PG type timestamp should match all needs.
	'DATE' => 'timestamp',
	# Type LONG is like VARCHAR2 but with up to 2Gb. PG type text
	# should match all needs or if you want you could use blob
	'LONG' => 'text', # Character data of variable length
	'LONG RAW' => 'bytea',
	# Types LOB and FILE are like LONG but with up to 4Gb. PG type
	# text should match all needs or if you want you could use blob
	# (large object)
	'CLOB' => 'text', # A large object containing single-byte characters
	'NCLOB' => 'text', # A large object containing national character set data
	'BLOB' => 'bytea', # Binary large object
	# The full path to the external file is returned if destination type is text.
	# If the destination type is bytea the content of the external file is returned.
	'BFILE' => 'bytea', # Locator for external large binary file
	# The RAW type is presented as hexadecimal characters. The
	# contents are treated as binary data. Limit of 2000 bytes
	# PG type text should match all needs or if you want you could
	# use blob (large object)
	'RAW' => 'bytea',
	'ROWID' => 'oid',
	'UROWID' => 'oid',
	'FLOAT' => 'double precision',
	'DEC' => 'decimal',
	'DECIMAL' => 'decimal',
	'DOUBLE PRECISION' => 'double precision',
	'INT' => 'numeric',
	'INTEGER' => 'numeric',
	'BINARY_INTEGER' => 'integer',
	'PLS_INTEGER' => 'integer',
	'REAL' => 'real',
	'SMALLINT' => 'smallint',
	'BINARY_FLOAT' => 'double precision',
	'BINARY_DOUBLE' => 'double precision',
	'TIMESTAMP' => 'timestamp',
	'BOOLEAN' => 'boolean',
	'INTERVAL' => 'interval',
	'XMLTYPE' => 'xml',
	'TIMESTAMP WITH TIME ZONE' => 'timestamp with time zone',
	'TIMESTAMP WITH LOCAL TIME ZONE' => 'timestamp with time zone',
	'SDO_GEOMETRY' => 'geometry',
);

our %ORA2PG_SDO_GTYPE = (
	'0' => 'GEOMETRY',
	'1' => 'POINT',
	'2' => 'LINESTRING',
	'3' => 'POLYGON',
	'4' => 'GEOMETRYCOLLECTION',
	'5' => 'MULTIPOINT',
	'6' => 'MULTILINESTRING',
	'7' => 'MULTIPOLYGON',
	'8' => 'SOLID',
	'9' => 'MULTISOLID'
);

our %GTYPE = (
	'UNKNOWN_GEOMETRY' => 'GEOMETRY',
	'GEOMETRY' => 'GEOMETRY',
	'POINT' => 'POINT',
	'LINE' => 'LINESTRING',
	'CURVE' => 'LINESTRING',
	'POLYGON' => 'POLYGON',
	'SURFACE' => 'POLYGON',
	'COLLECTION' => 'GEOMETRYCOLLECTION',
	'MULTIPOINT' => 'MULTIPOINT',
	'MULTILINE' => 'MULTILINESTRING',
	'MULTICURVE' => 'MULTILINESTRING',
	'MULTIPOLYGON' => 'MULTIPOLYGON',
	'MULTISURFACE' => 'MULTIPOLYGON',
	'SOLID' => 'SOLID',
	'MULTISOLID' => 'MULTISOLID'
);
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 COLLATE COLLATION COLUMN CONCURRENTLY CONSTRAINT CREATE
	CROSS 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 LATERAL LEADING LEFT LIKE LIMIT
	LOCALTIME LOCALTIMESTAMP NATURAL NOT NOTNULL NULL OFFSET ON ONLY OR ORDER OUTER
	OVERLAPS PLACING PRIMARY REFERENCES 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'
);

$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');
}


=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->{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 ($self->{output_dir} && !$noprefix) {
				$outfile = $self->{output_dir} . '/' . $outfile;
			}
		}
		# 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);
		} 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);
		} 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 '-')
		{
			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);
			}
			# 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;
			}
		}

		# 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 '-')
	{
		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);
		}
		# 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;
		}
		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 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$/) {
		$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 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));
	}
	if ($obj_name =~ /^\d+/) {
		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"/;
				}
			}
		# 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"/;
		}
		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 _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});

	# 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->{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->{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;

	# 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 prefix partition tablename with main tablename
	$self->{prefix_partition} = 0;
	$self->{prefix_part_subpartition} = 1;

	# 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;

	# 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;

	# 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} = ();

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

	# 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 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
	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');

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

	# 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';

	# 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;

	# Default degree for Oracle parallelism
	if ($self->{default_parallelism_degree} eq '') {
		$self->{default_parallelism_degree} = 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;
	}

	# 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 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}));
	}

	# Overwrite configuration with all given parameters
	# and try to preserve backward compatibility
	foreach my $k (keys %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 '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 ($options{$k} ne '') {
			$self->{"\L$k\E"} = $options{$k};
		}
	}

	# 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)/);
		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)/);
		push(@{$self->{excluded}{$typ}}, $i);
	}
	delete $self->{excluded}{ALL};

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

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

	# 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');

	# 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;
	}
	
	# 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;

	# Enable BLOB data export by default
	$self->{no_blob_export} ||= 0;

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

	# 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} = '';
	}

	$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;
	}

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

	if ($self->{is_mysql}) {
		# MySQL 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();

	# Multiple Oracle connection
	$self->{oracle_copies} ||= 0;
	$self->{ora_conn_count} = 0;
	$self->{data_limit} ||= 10000;
	$self->{blob_limit} ||= 0;
	$self->{disable_partition} ||= 0;
	$self->{parallel_tables} ||= 0;
	$self->{no_lob_locator} = 1 if ($self->{no_lob_locator} ne '0');

	# 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
		%{$self->{data_type}} = %TYPE;
		if ($self->{is_mysql}) {
			%{$self->{data_type}} = %Ora2Pg::MySQL::MYSQL_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
		%{$self->{data_type}} = %TYPE;
		if ($self->{is_mysql}) {
			%{$self->{data_type}} = %Ora2Pg::MySQL::MYSQL_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_sheet} ||= 0;
	$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;

	# 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 as 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;
	}

	# Compatibility with PostgreSQL versions
	if ($self->{pg_version} >= 9.0) {
		$self->{pg_supports_when} = 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;
	}

	# 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->{longtruncok} = 0 if (not defined $self->{longtruncok});
	# With lob locators LONGREADLEN must at least be 1MB
	if (!$self->{longreadlen} || !$self->{no_lob_locator}) {
		$self->{longreadlen} = (1023*1024);
	}

	# 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);
		}
	}

	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;

	# Allow multiple or chained extraction export type
	$self->{export_type} = ();
	if ($self->{type}) {
		@{$self->{export_type}} = split(/[\s,;]+/, $self->{type});
		# Assume backward comaptibility 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");
	}

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

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

	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->{dbh} = $self->_mysql_connection();

			$self->{is_mysql} = 1;

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

		} else {
			$self->{dbh} = $self->_oracle_connection();

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

			# Compile again all objects in the schema
			if ($self->{compile_schema}) {
				$self->_compile_schema(uc($self->{compile_schema}));
			}
		}
		if (!grep(/^$self->{type}$/, 'COPY', 'INSERT', 'SEQUENCE', '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});
						} 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});
				} 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;

		if (grep(/^$self->{type}$/, 'TABLE', 'SEQUENCE', 'GRANT', 'TABLESPACE', 'VIEW', 'TRIGGER', 'QUERY', 'FUNCTION','PROCEDURE','PACKAGE','TYPE','SYNONYM', 'DIRECTORY', 'DBLINK','LOAD')) {
			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)$/) {
		for my $t (keys %{$self->{'modify_struct'}}) {
			$self->modify_struct($t, @{$self->{'modify_struct'}{$t}});
		}
	}

	# 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;
			$self->_tables();
			# 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->_get_partitions_list();
			}
		} 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->_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') {
			$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->{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'));
				$self->_count_object($o);
			}
			# count function/procedure/package function
			$self->_test_function();
			# Count row in each table
			if ($self->{count_rows}) {
				$self->_table_row_count();
			}
			$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->{dbh}->disconnect() if ($self->{dbh}); 
			exit 0;
		} else {
			warn "type option must be (TABLE, VIEW, GRANT, SEQUENCE, 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_VIEW), 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});
		}
	}

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

}


sub _oracle_connection
{
	my ($self, $quiet) = @_;
 
	if (!defined $self->{oracle_pwd})
	{
		eval("use Term::ReadKey;") unless $self->{oracle_user} eq '/';
		$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("Trying to connect to database: $self->{oracle_dsn}\n", 1) if (!$quiet);

	my $dbh = DBI->connect($self->{oracle_dsn}, $self->{oracle_user}, $self->{oracle_pwd},
		{
			ora_envhp => 0,
			LongReadLen=>$self->{longreadlen},
			LongTruncOk=>$self->{longtruncok},
			AutoInactiveDestroy => 1,
			PrintError => 0,
			ora_session_mode => $ora_session_mode
		}
	);

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

	# Get Oracle version, needed to set date/time format
	my $sth = $dbh->prepare( "SELECT BANNER FROM v\$version" ) or return undef;
	$sth->execute or return undef;
	while ( my @row = $sth->fetchrow()) {
		$self->{db_version} = $row[0];
		last;
	}
	$sth->finish();
	chomp($self->{db_version});
	$self->{db_version} =~ s/ \- .*//;

	# Check if the connection user has the DBA privilege
	$sth = $dbh->prepare( "SELECT 1 FROM DBA_ROLE_PRIVS" );
	if (!$sth) {
		my $ret = $dbh->err;
		if ($ret == 942 && $self->{prefix} eq 'DBA') {
			$self->logit("HINT: you should activate USER_GRANTS for a connection without DBA privilege. Continuing with USER privilege.\n");
			# No DBA privilege, set use of ALL_* tables instead of DBA_* tables
			$self->{prefix} = 'ALL';
			$self->{user_grants} = 1;
		}
	} else {
		$sth->finish();
	}

	# Fix a problem when exporting type LONG and LOB
	$dbh->{'LongReadLen'} = $self->{longreadlen};
	$dbh->{'LongTruncOk'} = $self->{longtruncok};
	# Embedded object (user defined type) must be returned as an
	# array rather than an instance. This is normally the default.
	$dbh->{'ora_objects'} = 0;

	# Force datetime format
	$self->_datetime_format($dbh);
	# Force numeric format
	$self->_numeric_format($dbh);

	# Use consistent reads for concurrent dumping...
	$dbh->begin_work || $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	if ($self->{debug} && !$quiet) {
		$self->logit("Isolation level: $self->{transaction}\n", 1);
	}
	$sth = $dbh->prepare($self->{transaction}) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	$sth->execute or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	$sth->finish;

	# Force execution of initial command
	$self->_ora_initial_command($dbh);

	return $dbh;
}

sub _mysql_connection
{
	my ($self, $quiet) = @_;

	use Ora2Pg::MySQL;

	$self->logit("Trying to connect to database: $self->{oracle_dsn}\n", 1) if (!$quiet);
 
	if (!defined $self->{oracle_pwd})
	{
		eval("use Term::ReadKey;");
		$self->{oracle_user} = $self->_ask_username('MySQL') unless (defined $self->{oracle_user});
		$self->{oracle_pwd} = $self->_ask_password('MySQL');
	}

	my $dbh = DBI->connect("$self->{oracle_dsn}", $self->{oracle_user}, $self->{oracle_pwd}, { 'RaiseError' => 1, AutoInactiveDestroy => 1, mysql_enable_utf8 => 1});
	# Check for connection failure
	if (!$dbh) {
		$self->logit("FATAL: $DBI::err ... $DBI::errstr\n", 0, 1);
	}

	# Use consistent reads for concurrent dumping...
	#$dbh->do('START TRANSACTION WITH CONSISTENT SNAPSHOT;') || $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	if ($self->{debug} && !$quiet) {
		$self->logit("Isolation level: $self->{transaction}\n", 1);
	}
	my $sth = $dbh->prepare($self->{transaction}) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	$sth->execute or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	$sth->finish;

	# Get SQL_MODE from the MySQL database
	$sth = $dbh->prepare('SELECT @@sql_mode') or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	$sth->execute or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	while (my $row = $sth->fetch) {
		$self->{mysql_mode} = $row->[0];
	}
	$sth->finish;

	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 = $dbh->prepare("SET NAMES '$self->{nls_lang}'$collate") or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
		$sth->execute or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
		$sth->finish;
	}
	# Force execution of initial command
	$self->_ora_initial_command($dbh);

	if ($self->{mysql_mode} =~ /PIPES_AS_CONCAT/) {
		$self->{mysql_pipes_as_concat} = 1;
	}

	# Instruct Ora2Pg that the database engine is mysql
	$self->{is_mysql} = 1;

	return $dbh;
}

# 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} = 'AMERICAN_AMERICA.AL32UTF8';
		} else {
			$self->{nls_lang} = 'utf8';
		}
	}
	if (!$self->{nls_nchar}) {
		if (!$self->{is_mysql}) {
			$self->{nls_nchar} = 'AL32UTF8';
		} else {
			$self->{nls_nchar} = 'utf8_general_ci';
		}
	}
	$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/) ) {
		$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'}';" or die "FATAL: can't use open layer $self->{'binmode'}\n";
        } elsif ($self->{'binmode'}) {
                eval "use open 'encoding($self->{'binmode'})';" or die "FATAL: can't use open layer :encoding($self->{'binmode'})\n";
        }
        # 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/^://;
		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;");
		$self->{pg_user} = $self->_ask_username('PostgreSQL') unless (defined($self->{pg_user}));
		$self->{pg_pwd} = $self->_ask_password('PostgreSQL');
	}

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

	# 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();

}

=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) = @_;

	$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 gets the following information in the DBI object to affect the
main hash of the database structure :

    $self->{tables}{$class_name}{field_name} = $sth->{NAME};
    $self->{tables}{$class_name}{field_type} = $sth->{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) = @_;

	# 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();

	# 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')
			{
				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')
		{
			$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->{is_mysql})
			{
				$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(/^$t$/, @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};
		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 !~ /\./ ) {
					$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\"";
				}
			}
			my $query = "SELECT * FROM $tmp_tbname WHERE 1=0";
			if ($tables_infos{$t}{nested} eq 'YES') {
				$query = "SELECT /*+ nested_table_get_refs */ * FROM $tmp_tbname WHERE 1=0";
			}
			my $sth = $self->{dbh}->prepare($query);
			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}{$t}{type} = 'table';
			$self->{tables}{$t}{field_name} = $sth->{NAME};
			$self->{tables}{$t}{field_type} = $sth->{TYPE};
		}
		$i++;
	}

	if (!$self->{quiet} && !$self->{debug}) {
		print STDERR $self->progress_bar($i - 1, $num_total_table, 25, '=', 'tables', 'end of scanning.'), "\n";
	}
 
	# Try to search requested TABLE names in the VIEW names if not found in
	# real TABLE names
	if ($#{$self->{view_as_table}} >= 0)
	{
		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});
			next if (!grep($view =~ /^$_$/i, @{$self->{view_as_table}}));
			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, @{$self->{view_as_table}}));
			$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');
			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();
	}
}

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 %tmp = ($1 => $2);
		$self->{tables}{$tb_name}{check_constraint}{constraint}{$1}{condition} = $2;
		if ($c =~ /NOVALIDATE/is) {
			$self->{tables}{$tb_name}{check_constraint}{constraint}{$1}{validate} = 'NOT VALIDATED';
		}
	} elsif ($c =~ /^([^\s]+)\s+FOREIGN KEY (\([^\)]+\))?\s*REFERENCES ([^\(\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_dml_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_dml_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; 

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

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

		# Remove some unwanted and unused keywords from the statements
		$content =~ s/\s+(PARALLEL|COMPRESS)\b//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;
			# 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 ora2pg_ukey_$tbn $1/is;
				$c =~ s/^(CHECK[^,;]+)DEFERRABLE\s+INITIALLY\s+DEFERRED/$1/is;
				$c =~ s/^CHECK\b/CONSTRAINT ora2pg_ckey_$tbn CHECK/is;
				$c =~ s/^FOREIGN KEY/CONSTRAINT ora2pg_fkey_$tbn FOREIGN KEY/is;

				if (!$self->{preserve_case}) {
					$c =~ s/"//gs;
				}
				$c =~ s/\(\s+/\(/gs;

				# Get column name
				if ($c =~ s/^\s*([^\s]+)\s*//s) {
					my $c_name = $1;
					$c_name =~ s/"//g;
					# 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/(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;
							}
						} 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/^ENUM\s*(\([^\(\)]+\))\s*//is) {
							$c_type = 'varchar';
							my $ck_name = 'ora2pg_ckey_' . $c_name;
							$self->_parse_constraint($tb_name, $c_name, "$ck_name CHECK ($c_name IN $1)");
						} 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;
						}
						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//s) {
							$c_nullable = 0;
						} elsif ($c =~ s/NOT NULL//) {
							$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 = 'ora2pg_ukey_' . $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 = 'ora2pg_ckey_' . $c_name; 
							my $chk_search = $1;
							if ($2) {
								$pk_name = $1;
								$chk_search = $2;
							}
							$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 = 'ora2pg_fkey_' . $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-00-00/) {
							if ($self->{replace_zero_date}) {
								$c_default = $self->{replace_zero_date};
							} else {
								$c_default =~ s/^'0000-00-00/'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); 
						}
					}
				}
				$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 - min(90, $1);
				}
			}
			if ($tb_param =~ /\bNOLOGGING\b/is) {
				$self->{tables}{$tb_name}{table_info}{nologging} = 1;
			}

			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;
			# 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;
			}
			$idx_def =~ s/\)[^\)]*$//s;
			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;
			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;
			$tb_name =~ s/"//g;
			my $tb_def = $2;
			# 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/^[^\(]+//;
				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 PRIMARY KEY $constname " . 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 PRIMARY KEY $constname " . 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;
				}
			}
		}

	}

	# 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_dml_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]+)\s+IS\s+'([^;]+);//is) {
		my $tb_name = $1;
		my $tb_comment = $2;
		$tb_name =~ s/"//g;
		$tb_comment =~ s/'\s*$//g;
		if ($tb_name =~ s/\.([^\.]+)$//) {
			if (exists $self->{tables}{$tb_name}) {
					$self->{tables}{$tb_name}{column_comments}{"\L$1\E"} = $tb_comment;
			} elsif (exists $self->{views}{$tb_name}) {
					$self->{views}{$tb_name}{column_comments}{"\L$1\E"} = $tb_comment;
			}
		}
	}

}

sub read_view_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->_get_dml_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+/ /gs;
	$content =~ s/\s+FORCE\s+/ /gs;
	$content =~ s/\s+OR\s+REPLACE\s+/ /gs;
	$content =~ s/CREATE\s+VIEW\s+([^\s]+)\s+OF\s+(.*?)\s+AS\s+/CREATE VIEW $1 AS /sg;
	# 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\sVIEW[\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_dml_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}{$table}{type} = 'PACKAGE BODY';
		} else {
			$self->{grants}{$table}{type} = 'TABLE';
		}
	}

}

sub read_trigger_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->_get_dml_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) {
		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) {
			my $t_name = $1;
			my $t_pos = $2;
			my $t_event = $3;
			my $tb_name = $4;
			my $trigger = $5 . $6;
			my $t_type = '';
			$t_name =~ s/"//g;
			# Remove referencing clause, not supported by PostgreSQL
			$trigger =~ s/REFERENCING\s+(.*?)(FOR\s+EACH\s+)/$2/is;

			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
			$trigger =~ s/\bEND\s+[^\s]+\s+$/END/is;
			push(@{$self->{triggers}}, [($t_name, $t_pos, $t_event, $tb_name, $trigger, $t_when_cond, '', $t_type)]);

		}
	};

}

sub read_sequence_from_file
{
	my $self = shift;

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

	my $tid = 0; 

	# Sequences 
	while ($content =~ s/CREATE\s+SEQUENCE[\s]+([^\s;]+)\s*([^;]+);//i) {
		my $s_name = $1;
		$s_name =~ s/"//g;
		my $s_def = $2;
		$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}}, \@seq_info);
	}
}

sub read_tablespace_from_file
{
	my $self = shift;

	# Load file in a single string
	my $content = $self->_get_dml_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_dml_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_dml_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_dml_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}{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
	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, $dirprefix, $sql_header) = @_;

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

	# 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");
				}
			}
		}
	}

	# 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->{pg_dsn} && !$self->{oracle_speed}) {
			if ($search_path) {
				$local_dbh->do($search_path) or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);
			}
			$self->logit("Truncating table $table...\n", 1);
			my $s = $local_dbh->do("TRUNCATE TABLE $tmptb;") 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\nTRUNCATE TABLE $tmptb;\n",  $table);
			} else {
				$self->dump("\n$head$search_path\nTRUNCATE TABLE $tmptb;\n");
			}
		}
	}

	# With partitioned table, load data direct from table partition
	if (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;
			$tbpart_name = $table . '_' . $part_name if ($self->{prefix_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 = "${table}_$sub_tb_name" if ($self->{prefix_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}"));
					}

					$self->logit("Dumping sub partition table $table ($subpart)...\n", 1);
					$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $subpart, 1);
					# Rename temporary filename into final name
					$self->rename_dump_partfile($dirprefix, $sub_tb_name);
				}
				# 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}$/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}_$self->{output}")) {
								$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{subpartitions_default}{$table}{$part_name}, 1);
							}
						} else {
							$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{subpartitions_default}{$table}{$part_name}, 1);
						}
					}
					# Rename temporary filename into final name
					$self->rename_dump_partfile($dirprefix, $self->{subpartitions_default}{$table}{$part_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);
				# Rename temporary filename into final name
				$self->rename_dump_partfile($dirprefix, $part_name, $table);
			}
		}
		# Now load content of the default partition table
		if ($self->{partitions_default}{$table}) {
			if (!$self->{allow_partition} || grep($_ =~ /^$self->{partitions_default}{$table}$/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->{partitions_default}{$table}_$self->{output}")) {
						$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{partitions_default}{$table});
					}
				} else {
					$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{partitions_default}{$table});
				}
				# Rename temporary filename into final name
				$self->rename_dump_partfile($dirprefix, $self->{partitions_default}{$table}, $table);
			}
		}
	} else {

		$total_record = $self->_dump_table($dirprefix, $sql_header, $table);
	}

	# 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 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 rename_dump_partfile
{
	my ($self, $dirprefix, $partname, $tbl) = @_;

	my $filename = "${dirprefix}tmp_${partname}_$self->{output}";
	my $filedest = "${dirprefix}${partname}_$self->{output}";
	if ($tbl && $self->{prefix_partition}) {
		$filename = "${dirprefix}tmp_${tbl}_${partname}_$self->{output}";
		$filedest = "${dirprefix}${tbl}_${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 =~ 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);
					$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-2020 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 function
		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\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-2020 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 =~ 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($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;
			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);
			}

			if ($self->{views}{$view}{text} =~ /SELECT[^\s]*(.*?)\bFROM\b/is) {
				my $clause = $1;
				$self->{views}{$view}{text} =~ s/SELECT[^\s]*(.*?)\bFROM\b/SELECT $clause FROM/is;
			}
			$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.$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}) {
		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 =~ 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 .= "CREATE MATERIALIZED VIEW $view\n";
			$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 $self->{materialized_views}{$view}{text}";
			$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($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 .= "CREATE VIEW \L$view\E_mview AS\n";
				$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 .= "CREATE MATERIALIZED VIEW \L$view\E AS\n";
				$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';
		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', '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', 'SEQUENCE','SCHEMA','TABLESPACE'));
					$grants .= "GRANT ALL ON $obj $realtable TO $ownee$wgrantoption;\n";
				}
			}
			if (grep(/^$self->{grants}{$table}{type}$/, 'FUNCTION', '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 {
			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 ALL ON SCHEMA $realtable TO ROLE $ownee$wgrantoption;\n";
				} else {
					$grants .= "ALTER SCHEMA $realtable OWNER TO $ownee;\n";
					$grants .= "GRANT ALL ON 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', 'SEQUENCE','SCHEMA','TABLESPACE', 'TYPE')) {
					$grants .= "GRANT $agrants ON $obj $realtable TO $usr$wgrantoption;\n";
				} else {
					$grants .= "GRANT $agrants ON $realtable TO $usr$wgrantoption;\n";
				}
			} else {
				$grants .= "GRANT USAGE ON SCHEMA $realtable TO $usr$wgrantoption;\n";
				$grants .= "GRANT EXECUTE ON ALL FUNCTIONS 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 = $#{$self->{sequences}} + 1;
	my $count_seq = 0;
	my $PGBAR_REFRESH = set_refresh_count($num_total_sequence);
	foreach my $seq (sort { $a->[0] cmp $b->[0] } @{$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->[0]" ), "\r";
		}
		$count_seq++;
		my $cache = '';
		$cache = $seq->[5] if ($seq->[5]);
		my $cycle = '';
		$cycle = ' CYCLE' if ($seq->[6] eq 'Y');
		if ($self->{export_schema} && !$self->{schema}) {
			$seq->[0] = $seq->[7] . '.' . $seq->[0];
		}
		$sql_output .= "CREATE SEQUENCE " . $self->quote_object_name($seq->[0]) . " INCREMENT $seq->[3]";
		if ($seq->[1] eq '' || $seq->[1] < (-2**63-1)) {
			$sql_output .= " NO MINVALUE";
		} else {
			$sql_output .= " MINVALUE $seq->[1]";
		}
		# Max value lower than start value are not allowed
		if (($seq->[2] > 0) && ($seq->[2] < $seq->[4])) {
			$seq->[2] = $seq->[4];
		}
		if ($seq->[2] eq '' || $seq->[2] > (2**63-1)) {
			$sql_output .= " NO MAXVALUE";
		} else {
			$seq->[2] = 9223372036854775807 if ($seq->[2] > 9223372036854775807);
			$sql_output .= " MAXVALUE $seq->[2]";
		}
		$sql_output .= " START $seq->[4]";
		$sql_output .= " CACHE $cache" if ($cache ne '');
		$sql_output .= "$cycle;\n";

		if ($self->{force_owner}) {
			my $owner = $seq->[7];
			$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
			$sql_output .= "ALTER SEQUENCE " . $self->quote_object_name($seq->[0])
						. " 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_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";
		}
		$sql_output .= "CREATE SERVER " . $self->quote_object_name($db);
		if (!$self->{is_mysql}) {
			$sql_output .= " FOREIGN DATA WRAPPER oracle_fdw OPTIONS (dbserver '$self->{dblink}{$db}{host}');\n";
		} else {
			$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";
		}
		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}) {
			$sql_output .= "CREATE USER MAPPING FOR " . $self->quote_object_name($self->{dblink}{$db}{username})
						. " SERVER " . $self->quote_object_name($db)
						. " OPTIONS (user '" . $self->quote_object_name($self->{dblink}{$db}{user})
						. "' $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 FOREIGN DATA WRAPPER " . $self->quote_object_name($db)
						. " 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;
}

=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 $f = "$dirprefix$trig->[0]_$self->{output}";
			$f =~ s/\.(?:gz|bz2)$//i;
			$self->dump("\\i$self->{psql_relative_path} $f\n");
			$self->logit("Dumping to one file per trigger : $trig->[0]_$self->{output}\n", 1);
			$fhdl = $self->open_export_file("$trig->[0]_$self->{output}");
			$self->set_binmode($fhdl) if (!$self->{compress});
			$self->save_filetoupdate_list("ORA2PG_$self->{type}", lc($trig->[0]), "$dirprefix$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/;

		$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/\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) {
						if (!$self->{is_mysql}) {
							$parts[0] = Ora2Pg::PLSQL::replace_sql_type($parts[0], $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, %{$self->{data_type}});
						} else {
							$parts[0] = Ora2Pg::MySQL::replace_sql_type($parts[0], $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, %{$self->{data_type}});
						}
					}
					$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->{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});
			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
				$trig->[6] =~ s/REFERENCING\s+(.*?)(FOR\s+EACH\s+)/$2/is;
				$trig->[6] =~ s/^\s*["]*(?:$trig->[0])["]*//is;
				$trig->[6] =~ s/\s+ON\s+([^"\s]+)\s+/" ON " . $self->quote_object_name($1) . " "/ies;
				$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";
				}
				$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 .= "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 ($statement) {
					$sql_output .= "FOR EACH STATEMENT\n";
				} else {
					$sql_output .= "FOR EACH ROW\n";
				}
				$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)/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});
				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 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}} )
	{
		$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);

	$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 = '';
		foreach my $l (@allfct) {
			$l =~ s/\r//g;
			next if ($l =~ /^\/$/);
			next if ($l =~ /^\s*$/);
			if ($old_line) {
				$l = $old_line .= ' ' . $l;
				$old_line = '';
			}
			if ($l =~ /^\s*CREATE\s*(?:OR REPLACE)?\s*(?:EDITIONABLE|NONEDITIONABLE)?\s*$/i) {
				$old_line = $l;
				next;
			}
			if ($l =~ /^\s*(?:EDITIONABLE|NONEDITIONABLE)?\s*(FUNCTION|PROCEDURE)$/i) {
				$old_line = $l;
				next;
			}
			if ($l =~ /^\s*CREATE\s*(?:OR REPLACE)?\s*(?:EDITIONABLE|NONEDITIONABLE)?\s*(FUNCTION|PROCEDURE)\s*$/i) {
				$old_line = $l;
				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;
			}
			$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}} ) {
		$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);

	$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 hearder
		$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});

		# 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 =~ 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}{text}, $fhdl);
				$self->close_export_file($fhdl);
			} else {
				$pkgbody = $self->{packages}{$pkg}{text};
			}

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

			# 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});
					$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);

	$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 {
			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});
	}
	$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}}) {
					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}}) {
						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_sort_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}};
	}

	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 $ipos = 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 $count_part = 0;
		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} && ($count_part % $PGBAR_REFRESH) == 0)
			{
				print STDERR $self->progress_bar($ipos++, $total_partition, 25, '=', 'partitions', "generating $table/$part" ), "\r";
			}
			$count_part++;
			my $create_table_tmp = '';
			my $create_table_index_tmp = '';
			my $tb_name = '';
			if ($self->{prefix_partition}) {
				$tb_name = $table . "_" . $part;
			} else {
				if ($self->{export_schema} && !$self->{schema} && ($table =~ /^([^\.]+)\./)) {
					$tb_name =  $1 . '.' . $part;
				} else {
					$tb_name =  $part;
				}
			}
			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 \L$table\E\n";
				$create_table_tmp .= "FOR VALUES";
			}

			my @condition = ();
			my @ind_col = ();
			for (my $i = 0; $i <= $#{$self->{partitions}{$table}{$pos}{info}}; $i++)
			{
				if ($self->{partitions}{$table}{$pos}{info}[$i]->{type} eq 'LIST')
				{
					if (!$self->{pg_supports_partition})
					{
						$check_cond .= "\t$self->{partitions}{$table}{$pos}{info}[$i]->{column} IN ("
							. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$table}{$pos}{info}[$i]->{value})
							. ")";
					}
					else
					{
						$check_cond .= " IN ("
							. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$table}{$pos}{info}[$i]->{value})
							. ")";
					}
				}
				elsif ($self->{partitions}{$table}{$pos}{info}[$i]->{type} eq 'RANGE')
				{
					if ($#{$self->{partitions}{$table}{$pos}{info}} == 0)
					{
						if (!$self->{pg_supports_partition})
						{
							if ($old_part eq '')
							{
								$check_cond .= "\t$self->{partitions}{$table}{$pos}{info}[$i]->{column} < "
									. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$table}{$pos}{info}[$i]->{value})
							}
							else
							{
								$check_cond .= "\t$self->{partitions}{$table}{$pos}{info}[$i]->{column} >= "
									. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$table}{$old_pos}{info}[$i]->{value})
									. " AND $self->{partitions}{$table}{$pos}{info}[$i]->{column} < "
									. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$table}{$pos}{info}[$i]->{value})
							}
						}
						else
						{
							if ($old_part eq '')
							{
								$check_cond .= " FROM (MINVALUE) TO ("
									. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$table}{$pos}{info}[$i]->{value})
									. ")";
							}
							else
							{
								$check_cond .= " FROM ("
									. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$table}{$old_pos}{info}[$i]->{value})
									. ") TO ("
									. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$table}{$pos}{info}[$i]->{value})
									. ")";
							}
						}
					}
					else
					{
						my @values = split(/,\s/, Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$table}{$pos}{info}[$i]->{value}));
						if (!$self->{pg_supports_partition})
						{
							# multicolumn partitioning
							$check_cond .= "\t$self->{partitions}{$table}{$pos}{info}[$i]->{column} < $values[$i]";
						}
						else
						{
							$check_cond .= " FROM (MINVALUE) TO ($values[$i])";
						}
					}
				}
				elsif ($self->{partitions}{$table}{$pos}{info}[$i]->{type} eq 'HASH')
				{
					if ($self->{pg_version} < 11)
					{
						print STDERR "WARNING: Hash partitioning not supported, skipping partitioning of table $table\n";
						$function = '';
						$create_table_tmp = '';
						$create_table_index_tmp = '';
						next;
					}
					else
					{
						$check_cond .= " WITH (MODULUS " . (scalar keys %{$self->{partitions}{$table}}) . ", REMAINDER " . ($pos-1) . ")";
					}
				}
				else
				{
					print STDERR "WARNING: Unknown partitioning type $self->{partitions}{$table}{$pos}{info}[$i]->{type}, skipping partitioning of table $table\n";
					$create_table_tmp = '';
					$create_table_index_tmp = '';
					next;
				}
				if (!$self->{pg_supports_partition})
				{
					$check_cond .= " AND" if ($i < $#{$self->{partitions}{$table}{$pos}{info}});
				}
				my $fct = '';
				my $colname = $self->{partitions}{$table}{$pos}{info}[$i]->{column};
				if ($colname =~ s/([^\(]+)\(([^\)]+)\)/$2/)
				{
					$fct = $1;
				}
				my $cindx = $self->{partitions}{$table}{$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}{$table}{$part})
				{
					foreach my $p (sort {$a <=> $b} keys %{$self->{subpartitions}{$table}{$part}})
					{
						for (my $j = 0; $j <= $#{$self->{subpartitions}{$table}{$part}{$p}{info}}; $j++)
						{
							if ($self->{subpartitions}{$table}{$part}{$p}{info}[$j]->{type} eq 'HASH')
							{
								$has_hash_subpartition = 1;
								last;
							}
						}
						last if ($has_hash_subpartition);
					}
				}

				if (!exists $self->{subpartitions}{$table}{$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($table, 0, %{$self->{tables}{$table}{indexes}});
						my $tb_name2 = $self->quote_object_name($tb_name);
						$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/ $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 /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($table, $self->{tables}{$table}{unique_key});
						if ($idx)
						{
							$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 /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 = '';
				$deftb = "${table}_" if ($self->{prefix_partition});
				if ($self->{partitions_default}{$table} && ($create_table{$table}{index} !~ /ON $deftb$self->{partitions_default}{$table} /)) {
					$cindx = $self->{partitions}{$table}{$pos}{info}[$i]->{column} || '';
					$cindx = lc($cindx) if (!$self->{preserve_case});
					$cindx = Ora2Pg::PLSQL::convert_plsql_code($self, $cindx);
					$create_table_index_tmp .= "CREATE INDEX " . $self->quote_object_name("$deftb$self->{partitions_default}{$table}_$colname") . " ON " . $self->quote_object_name("$deftb$self->{partitions_default}{$table}") . " ($cindx);\n";
				}
				push(@ind_col, $self->{partitions}{$table}{$pos}{info}[$i]->{column}) if (!grep(/^$self->{partitions}{$table}{$pos}{info}[$i]->{column}$/, @ind_col));
				if ($self->{partitions}{$table}{$pos}{info}[$i]->{type} eq 'LIST') {
					if (!$fct) {
						push(@condition, "NEW.$self->{partitions}{$table}{$pos}{info}[$i]->{column} IN (" . Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$table}{$pos}{info}[$i]->{value}) . ")");
					} else {
						push(@condition, "$fct(NEW.$colname) IN (" . Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$table}{$pos}{info}[$i]->{value}) . ")");
					}
				} elsif ($self->{partitions}{$table}{$pos}{info}[$i]->{type} eq 'RANGE') {
					if (!$fct) {
						push(@condition, "NEW.$self->{partitions}{$table}{$pos}{info}[$i]->{column} < " . Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$table}{$pos}{info}[$i]->{value}));
					} else {
						push(@condition, "$fct(NEW.$colname) < " . Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions}{$table}{$pos}{info}[$i]->{value}));
					}
				}
				$owner = $self->{partitions}{$table}{$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} . " (";
					for (my $j = 0; $j <= $#{$self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{columns}}; $j++)
					{
						$create_table_tmp .= ', ' if ($j > 0);
						$create_table_tmp .= $self->quote_object_name($self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{columns}[$j]);
					}
					$create_table_tmp .= ")";
				}
				$create_table_tmp .= ";\n";
			}
			# 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 $count_subpart = 0;
				my $total_subpartition = scalar %{$self->{subpartitions}{$table}{$part}} || 0;
				my $PGBAR_REFRESH_S = set_refresh_count($total_subpartition);
				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->{prefix_partition}) {
						if ($self->{prefix_sub_partition}) {
							$sub_tb_name = "${tb_name}_$sub_tb_name";
						} else {
							$sub_tb_name = "${table}_$sub_tb_name";
						}
					}
					if (!$self->{quiet} && !$self->{debug} && ($count_subpart % $PGBAR_REFRESH_S) == 0) {
						print STDERR $self->progress_bar($ipos++, $total_subpartition, 25, '=', 'subpartitions', "generating $table/$part/$subpart" ), "\r";
					}
					$count_subpart++;
					$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) . "\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++) {
						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 ($self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{value})";
							} else {
								$sub_check_cond_tmp .= " IN ($self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{value})";
							}
						} elsif ($self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{type} eq 'RANGE') {
							if ($#{$self->{subpartitions}{$table}{$part}{$p}{info}} == 0) {
								if (!$self->{pg_supports_partition}) {
									if ($sub_old_part eq '') {
										$sub_check_cond_tmp .= "$self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{column} < "
											. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{value});
									} else {
										$sub_check_cond_tmp .= "$self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{column} >= "
											. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$sub_old_pos}{info}[$i]->{value})
											. " AND $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{column} < "
											. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{value});
									}
								} else {
									if ($sub_old_part eq '') {
										$sub_check_cond_tmp .= " FROM (MINVALUE) TO ("
											. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{value})
											. ")";
									} else {
										$sub_check_cond_tmp .= " FROM ("
											. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$sub_old_pos}{info}[$i]->{value});
										$sub_check_cond_tmp .= ") TO ("
											. Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{value})
											. ")";
									}
								}
							} else {
								my @values = split(/,\s/, Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{value}));
								if (!$self->{pg_supports_partition}) {
									# multicolumn partitioning
									$sub_check_cond_tmp .= "\t$self->{subpartitions}{$table}{$part}{$p}{info}[$i]->{column} < $values[$i]";
								} else {
									$sub_check_cond_tmp .= " FROM (MINVALUE) TO ($values[$i])";
								}
							}
						} 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 {
								$sub_check_cond_tmp .= " WITH (MODULUS " . $self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{count} . ", REMAINDER " . ($p-1) . ")";
							}
						} 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);
							$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$p /gs;
									$fts_idx =~ s/(CREATE(?:.*?)INDEX ([^\s]+)) /$1$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});
							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$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 = '';

			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}) {
							my $deftb = '';
							$deftb = "${table}_" if ($self->{prefix_partition});
							$funct_cond .= "\t\tELSE INSERT INTO " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}")
										. " VALUES (NEW.*);\n\t\tEND IF;\n";
							$create_table_tmp .= "CREATE TABLE " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}")
										. " () INHERITS ($table);\n";
							$create_table_index_tmp .= "CREATE INDEX " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}_$pos")
										. " ON " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}") . " ($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};
						if ($self->{prefix_partition}) {
							$tb_name = $table . "_" . $self->{subpartitions_default}{$table}{$part};
						} elsif ($self->{export_schema} && !$self->{schema} && ($table =~ /^([^\.]+)\./)) {
							$tb_name =  $1 . '.' . $self->{subpartitions_default}{$table}{$part};
						}
						if ($self->{pg_version} >= 11) {
							$create_table_tmp .= "CREATE TABLE " . $self->quote_object_name($tb_name)
									. " PARTITION OF \L$table\E 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 \L$table\E FOR VALUES FROM ($self->{subpartitions}{$table}{$part}{$sub_old_pos}{info}[-1]->{value}) TO (MAX_VALUE);\n";
						}
					}

					if ($self->{subpartitions_default}{$table}{$part} && ($create_table{$table}{index} !~ /ON $self->{subpartitions_default}{$table}{$part} /)) {
					}
				}
				$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 ($self->{partitions_default}{$table})
				{
					my $deftb = '';
					$deftb = "${table}_" if ($self->{prefix_partition});
					my $pname = $self->quote_object_name("$deftb$self->{partitions_default}{$table}");
					$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}) {
					my $tb_name = '';
					if ($self->{prefix_partition}) {
						$tb_name = $table . "_" . $self->{partitions_default}{$table};
					} else {
						if ($self->{export_schema} && !$self->{schema} && ($table =~ /^([^\.]+)\./)) {
							$tb_name =  $1 . '.' . $self->{partitions_default}{$table};
						} else {
							$tb_name =  $self->{partitions_default}{$table};
						}
					}
					if ($self->{pg_version} >= 11) {
						$create_table{$table}{table} .= "CREATE TABLE " . $self->quote_object_name($tb_name)
								. " PARTITION OF \L$table\E DEFAULT;\n";
					} else {
						$create_table{$table}{table} .= "CREATE TABLE " . $self->quote_object_name($tb_name)
								. " PARTITION OF \L$table\E 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};
			$defname = $table . '_' .  $defname if ($self->{prefix_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});
				$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})
								. " OWNER TO " . $self->quote_object_name($owner) . ";\n"
						if ($self->{partitions_default}{$table});
					$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($ipos - 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});
	}
	$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-2020 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} (see DBLINK and FDW export type)\n";
		}
		$sql_output .= "CREATE VIEW " . $self->quote_object_name("$self->{synonyms}{$syn}{owner}.$syn")
			. " AS SELECT * FROM " . $self->quote_object_name("$self->{synonyms}{$syn}{table_owner}.$self->{synonyms}{$syn}{table_name}") . ";\n";
		my $owner = $self->{synonyms}{$syn}{table_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";
		$sql_output .= "GRANT ALL ON " . $self->quote_object_name("$self->{synonyms}{$syn}{owner}.$syn")
					. " TO " . $self->quote_object_name($self->{synonyms}{$syn}{owner}) . ";\n\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->{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 \L$owner\E;\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 = '';

	# 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' 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}) {
			if ( grep(/^$table$/i, keys %{$self->{external_table}}) ) {
					$sql_header .= "CREATE EXTENSION IF NOT EXISTS file_fdw;\n\n" if ($sql_header !~ /CREATE EXTENSION .* file_fdw;/is);
					$sql_header .= "CREATE SERVER \L$self->{external_table}{$table}{directory}\E FOREIGN DATA WRAPPER file_fdw;\n\n" if ($sql_header !~ /CREATE SERVER $self->{external_table}{$table}{directory} FOREIGN DATA WRAPPER file_fdw;/is);
			}
		}

		my $tbname = $self->get_replaced_tbname($table);
		my $foreign = '';
		if ( ($self->{type} eq 'FDW') || ($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 (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});
			}
			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 = ();
		if (exists $self->{tables}{$table}{column_info}) {
			my $schem = '';
			$sql_output .= "\nCREATE$foreign $obj_type $tbname (\n";

			# Extract column information following the Oracle 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/\(.*//;
						$TYPALIGN{$typb} <=> $TYPALIGN{$typa};
					}
				} 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/\D//g;
				my $type = $self->_sql_type($f->[1], $f->[2], $f->[5], $f->[6], $f->[4]);
				$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/;
						}
					}
					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 . '"';
						}
						$serial_sequence .= "ALTER SEQUENCE $seqname RESTART WITH $self->{tables}{$table}{table_info}{auto_increment};\n" if (exists $self->{tables}{$table}{table_info}{auto_increment});
					}
				}

				# Check if this column should be replaced by a boolean following table/column name
				if ($f->[1] =~ /ENUM/i) {
					$f->[1] =~ s/^ENUM\(//i;
					$f->[1] =~ s/\)$//;
					my $keyname = $tbname . '_' . $fname . '_chk';
					$keyname =~ s/(.*)"(_${fname}_chk)/$1$2"/; # used when preserve_case is enable
					$enum_str .= "ALTER TABLE $tbname ADD CONSTRAINT $keyname CHECK ($fname IN ($f->[1]));\n";
					$type = 'varchar';
				}
				my $typlen = $f->[5];
				$typlen ||= $f->[2];
				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_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);
				$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->[9].$f->[8]";
				}
				if (exists $self->{identity_info}{$f->[8]}{$f->[0]} and $self->{type} ne 'FDW') {
					$sql_output =~ s/ NOT NULL\s*$//s; # IDENTITY or serial column are NOT NULL by default
					if ($self->{pg_supports_identity}) {
						$sql_output .= " GENERATED $self->{identity_info}{$f->[8]}{$f->[0]}{generation} AS IDENTITY";
						$sql_output .= " (" . $self->{identity_info}{$f->[8]}{$f->[0]}{options} . ')' if (exists $self->{identity_info}{$f->[8]}{$f->[0]}{options} && $self->{identity_info}{$f->[8]}{$f->[0]}{options} ne '');
					} 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";
					$sequence_output .= "SELECT ora2pg_upd_autoincrement_seq('$f->[8]','$f->[0]');\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 this is a virtual column before proceeding to default value export
					if ($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')) {
							if ($type eq 'boolean') {
								my $found = 0;
								foreach my $k (sort {$b cmp $a} %{ $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|ENUM/i) {
										$f->[4] = "'$f->[4]'" if ($f->[4] !~ /[']/ && $f->[4] !~ /\(.*\)/);
									} elsif ($type =~ /DATE|TIME/i) {
										if ($f->[4] =~ /0000-00-00/) {
											if ($self->{replace_zero_date}) {
												$f->[4] = $self->{replace_zero_date};
											} else {
												$f->[4] =~ s/^0000-00-00/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|ENUM/i) {
											$f->[4] = "'$f->[4]'" if ($f->[4] !~ /[']/ && $f->[4] !~ /\(.*\)/);
										} elsif ($type =~ /DATE|TIME/i) {
											if ($f->[4] =~ /0000-00-00/) {
												if ($self->{replace_zero_date}) {
													$f->[4] = $self->{replace_zero_date};
												} else {
													$f->[4] =~ s/^0000-00-00/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 {
											$f->[4] = "'$f->[4]'";
										}
									}
								}
								$f->[4] = 'NULL' if ($f->[4] eq "''" && $type =~ /int|double|numeric/i);
								$sql_output .= " DEFAULT $f->[4]";
							}
						}
					}
				}
				$sql_output .= ",\n";
			}
			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}) {
				$sql_output .= " PARTITION BY " . $self->{partitions_list}{"\L$table\E"}{type} . " (";
				for (my $j = 0; $j <= $#{$self->{partitions_list}{"\L$table\E"}{columns}}; $j++)
				{
					$sql_output .= ', ' if ($j > 0);
			       		$sql_output .= $self->quote_object_name($self->{partitions_list}{"\L$table\E"}{columns}[$j]);
				}
			       $sql_output .= 	")";
			}
			if ( ($self->{type} ne 'FDW') && (!$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}'" if ($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";
			} 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');\n";
			}
		}
		$sql_output .= $serial_sequence;
		$sql_output .= $enum_str;

		# 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 $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.$fname") . " IS E'" . $self->{tables}{$table}{column_comments}{$f} .  "';\n";
			}
		}

		# Change ownership
		if ($self->{force_owner}) {
			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} " .  $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++;
	}
	if (!$self->{quiet} && !$self->{debug})
	{
		print STDERR $self->progress_bar($ib - 1, $num_total_table, 25, '=', 'tables', 'end of table export.'), "\n";
	}

	if ($sequence_output && $self->{type} ne 'FDW')
	{
		my $fhdl = undef;
		$sequence_output = 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(' || colname || ')+1 FROM ' || tbname;
        EXECUTE query INTO maxval;
        IF (maxval IS NOT NULL) THEN
                query := \$\$SELECT (string_to_array(adsrc,''''))[2] FROM pg_attrdef WHERE adrelid = '\$\$
                        || tbname || \$\$'::regclass AND adnum = (SELECT attnum FROM pg_attribute WHERE attrelid = '\$\$
                        || tbname || \$\$'::regclass AND attname = '\$\$ || colname || \$\$') AND adsrc LIKE 'nextval%'\$\$;
                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;
		$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 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 functions only
	elsif ($self->{type} eq 'FUNCTION')
	{
		$self->export_function();
	}

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

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

	# 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'))
	{

		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->{dbh} = $self->_mysql_connection();
		} else {
			$self->{dbh} = $self->_oracle_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};
				}
			}
		}
		# 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_sort_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}};
		}

		# 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})
		{
			# 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
		my $load_file = "\n";
		foreach my $table (@ordered_tables)
		{
			# 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]/));
			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}");
			}
			# Rename table and double-quote it if required
			my $tmptb = $self->get_replaced_tbname($table);

			#### 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');
				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 =~ 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 . '_' . $tb_name if ($self->{prefix_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
							$sub_tb_name = "${tb_name}$sub_tb_name" if ($self->{prefix_partition});
							if ($self->{file_per_table} && !$self->{pg_dsn}) {
								my $file_name = "$dirprefix${sub_tb_name}_$self->{output}";
								$file_name =~ s/\.(gz|bz2)$//;
								$load_file .=  "\\i$self->{psql_relative_path} $file_name\n";
							}
						}
						# Now load content of the default partion table
						if ($self->{subpartitions_default}{$table}{$part_name})
						{
							if (!$self->{allow_partition} || grep($_ =~ /^$self->{subpartitions_default}{$table}{$part_name}$/i, @{$self->{allow_partition}}))
							{
								if ($self->{file_per_table} && !$self->{pg_dsn})
								{
									my $part_name = $self->{subpartitions_default}{$table}{$part_name};
									$part_name = "${tb_name}$part_name" if ($self->{prefix_partition});
									my $file_name = "$dirprefix${part_name}_$self->{output}";
									$file_name =~ s/\.(gz|bz2)$//;
									$load_file .=  "\\i$self->{psql_relative_path} $file_name\n";
								}
							}
						}
					}
					else
					{
						if ($self->{file_per_table} && !$self->{pg_dsn})
						{
							my $file_name = "$dirprefix${tb_name}_$self->{output}";
							$file_name =~ s/\.(gz|bz2)$//;
							$load_file .=  "\\i$self->{psql_relative_path} $file_name\n";
						}
					}
				}
				# Now load content of the default partion table
				if ($self->{partitions_default}{$table})
				{
					if (!$self->{allow_partition} || grep($_ =~ /^$self->{partitions_default}{$table}$/i, @{$self->{allow_partition}}))
					{
						if ($self->{file_per_table} && !$self->{pg_dsn})
						{
							my $part_name = $self->{partitions_default}{$table};
							$part_name = $table . '_' . $part_name if ($self->{prefix_partition});
							my $file_name = "$dirprefix${part_name}_$self->{output}";
							$file_name =~ s/\.(gz|bz2)$//;
							$load_file .=  "\\i$self->{psql_relative_path} $file_name\n";
						}
					}
				}
			}

			# 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";

			}

		}

		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} && !$self->{oracle_speed}) {
			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)
		{
			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');

			my $total_record = 0;
			if ($self->{parallel_tables} > 1)
			{
				spawn sub {
					$self->logit("Creating new connection to Oracle database to export table $table...\n", 1);
					$self->_export_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 {
				$total_record = $self->_export_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 $numchild = 1; # will not wait for progressbar process
			$numchild = 0 if ($self->{debug}); # in debug there is no progressbar
			while (scalar keys %RUNNING_PIDS > $numchild) {
				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->{dbh} = $self->_mysql_connection();
			} else {
				$self->{dbh} = $self->_oracle_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
		my $footer = '';
		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);
			
			# 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 . "\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 . "\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 an 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);
		}
		return;
	}
}

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;
        }

	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) = @_;

	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 = '';

	# 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->{prefix_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);
	}
	
	# 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 $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];
		if (!$self->{preserve_case}) {
			if (exists $self->{modify}{"\L$table\E"}) {
				next if (!grep(/^$fieldname$/i, @{$self->{modify}{"\L$table\E"}}));
			}
		} else {
			if (exists $self->{modify}{"$table"}) {
				next if (!grep(/^$fieldname$/i, @{$self->{modify}{"$table"}}));
			}
		}

		my $f = $self->{tables}{"$table"}{column_info}{"$fieldname"};
		$f->[2] =~ s/\D//g;
		if ($self->{no_blob_export} && $f->[1] =~ /blob/i) {
			# user don't want to export blob
			next;
		}

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

		if ($f->[1] =~ /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') {
			$f->[1] = 'varchar';
		}
		push(@stt, uc($f->[1]));
		push(@tt, $type);
		push(@nn,  $self->{tables}{$table}{column_info}{$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 . '"';
		}
		$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/,$//;
	$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 = '';
	$overriding_system = ' OVERRIDING SYSTEM VALUE' if ($has_identity);

	my $s_out = "INSERT INTO $tmptb ($col_list";
	if ($self->{type} eq 'COPY') {
		$s_out = "\nCOPY $tmptb ($col_list";
	}

	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') {
			$s_out .= '?,' foreach (@fname);
			$s_out =~ s/,$//;
			$s_out .= ")";
			$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} = '';

}

=head2 _column_comments

This function return comments associated to columns

=cut
sub _column_comments
{
	my ($self, $table) = @_;

	return Ora2Pg::MySQL::_column_comments($self, $table) if ($self->{is_mysql});

	my $condition = '';

	my $sql = "SELECT A.COLUMN_NAME,A.COMMENTS,A.TABLE_NAME,A.OWNER FROM $self->{prefix}_COL_COMMENTS A $condition";
	if ($self->{schema}) {
		$sql .= "WHERE A.OWNER='$self->{schema}' ";
	} else {
		$sql .= " WHERE A.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	}
	$sql .= "AND A.TABLE_NAME='$table' " if ($table);
	if ($self->{db_version} !~ /Release 8/) {
		$sql .= " AND (A.OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, TABLE_NAME FROM ALL_OBJECT_TABLES)";
		$sql .= " AND (A.OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)" if ($self->{type} ne 'FDW');
	}
	if (!$table) {
		$sql .= $self->limit_to_objects('TABLE','TABLE_NAME');
	} else {
		@{$self->{query_bind_params}} = ();
	}

	my $sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	my %data = ();
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[2] = "$row->[3].$row->[2]";
		}
		if (!$self->{preserve_case}) {
			next if (exists $self->{modify}{"\L$row->[2]\E"} && !grep(/^$row->[0]$/i, @{$self->{modify}{"\L$row->[2]\E"}}));
		} else {
			next if (exists $self->{modify}{$row->[2]} && !grep(/^$row->[0]$/i, @{$self->{modify}{$row->[2]}}));
		}
		$data{$row->[2]}{$row->[0]} = $row->[1];
	}

	return %data;
}


=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 = ();
	# 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->{$objtyp}{$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);
			}
		}

		# 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 is found
		if (!$self->{input_file}) {
			for ($i = 0; $i <= $#{$indexes{$idx}}; $i++) {
				if ( ($indexes{$idx}->[$i] =~ /\s/) && ($indexes{$idx}->[$i] !~ /^[^\.\s]+\s+DESC$/i) ) {
					$indexes{$idx}->[$i] = '(' . $indexes{$idx}->[$i] . ')';
				}
			}
		}
		my $columns = '';
		foreach (@{$indexes{$idx}})
		{
			$columns .= ((exists $opclass_type{$_}) ? $opclass_type{$_} : $_) . ", ";
			# 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/, $//;
		$columns =~ s/\s+/ /g;
		my $colscompare = $columns;
		$colscompare =~ s/"//gs;
		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;
			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 $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;
				my @collist = @{$indexes{$idx}};
				# Remove double quote, DESC and parenthesys
				map { s/"//g; s/.*\(([^\)]+)\).*/$1/; s/\s+DESC//i; s/::.*//; } @collist;
				$idxname = $idxname . '_' . join('_', @collist);
				$idxname =~ s/\s+//g;
				if ($self->{indexes_suffix}) {
					$idxname = substr($idxname,0,59);
				} else {
					$idxname = substr($idxname,0,63);
				}
			}
			$idxname = $schm . '.' . $idxname if ($schm);
			$idxname = $self->quote_object_name($idxname);
			my $tb = $self->quote_object_name($table);
			if ($self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_name} =~ /SPATIAL_INDEX/) {
				$str .= "CREATE INDEX$concurrently " . $self->quote_object_name("$idxname$self->{indexes_suffix}")
						. " ON $tb USING gist($columns)";
			} elsif ($self->{bitmap_as_gin} && $self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_name} eq 'BITMAP') {
				$str .= "CREATE INDEX$concurrently " . $self->quote_object_name("$idxname$self->{indexes_suffix}")
						. " 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 " . $self->quote_object_name("$idxname$self->{indexes_suffix}")
						. " 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 " . $self->quote_object_name("$idxname$self->{indexes_suffix}")
						. " 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 " . $self->quote_object_name("$idxname$self->{indexes_suffix}")
						 . " ON $table USING gin(tsv_$newcolname)";
				} else {
					$fts_str .= "CREATE$unique INDEX$concurrently " . $self->quote_object_name("$idxname$self->{indexes_suffix}")
						. " 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 " . $self->quote_object_name("$idxname$self->{indexes_suffix}")
						. " ON $table ($columns)";
			} else {
				$str .= "CREATE$unique INDEX$concurrently " . $self->quote_object_name("$idxname$self->{indexes_suffix}")
						. " ON $table ($columns)";
			}
			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);
		}
	}

	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 ($_ !~ /\(.*\)/) { $_ = $self->quote_object_name($_) } } @{$indexes{$idx}};

                my $columns = '';
                foreach (@{$indexes{$idx}})
                {
                        $columns .= ((exists $opclass_type{$_}) ? $opclass_type{$_} : $_) . ", ";
                        # 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)
		{
			if ($self->{indexes_renaming})
			{
				map { s/"//g; } @{$indexes{$idx}};
				$idx = $self->quote_object_name($table.'_'.join('_', @{$indexes{$idx}}));
				$idx =~ s/\s+//g;
				if ($self->{indexes_suffix}) {
					$idx = substr($idx,0,59);
				} else {
					$idx = substr($idx,0,63);
				}
			}
			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} \L$idx$self->{indexes_suffix}\E;");
			} else {
				push(@out, "DROP INDEX $self->{pg_supports_ifexists} \L$idx$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) = @_;

	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"};
			}
		}
		map { $_ = $self->quote_object_name($_) } @conscols;

		my $columnlist = join(',', @conscols);
		if ($columnlist) {
			if (!$self->{keep_pkey_names} || ($constgen eq 'GENERATED NAME')) {
				$out .= "ALTER TABLE $table ADD $constypename ($columnlist)";
			} else {
				$out .= "ALTER TABLE $table ADD CONSTRAINT \L$consname\E $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 (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);
		my $skip_create = 0;
		if (exists $check_constraint->{notnull}) {
			foreach my $col (@{$check_constraint->{notnull}}) {
				$skip_create = 1, last if (lc($chkconstraint) eq lc("\"$col\" IS NOT NULL"));
			}
		}
		if (!$skip_create) {
			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) {
				# Force lower case
				my $ret = $self->quote_object_name($c);
				$chkconstraint =~ s/"$c"/$ret/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) {
				$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)});

			# 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 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;

	return Ora2Pg::MySQL::_extract_sequence_info($self) if ($self->{is_mysql});

	my $sql = "SELECT DISTINCT SEQUENCE_NAME, MIN_VALUE, MAX_VALUE, INCREMENT_BY, CYCLE_FLAG, ORDER_FLAG, CACHE_SIZE, LAST_NUMBER,SEQUENCE_OWNER FROM $self->{prefix}_SEQUENCES";
	if ($self->{schema}) {
		$sql .= " WHERE SEQUENCE_OWNER='$self->{schema}'";
	} else {
		$sql .= " WHERE SEQUENCE_OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	}
	$sql .= $self->limit_to_objects('SEQUENCE','SEQUENCE_NAME');

	my @script = ();

	my $sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr ."\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	while (my $seq_info = $sth->fetchrow_hashref) {

		my $seqname = $seq_info->{SEQUENCE_NAME};
		if (!$self->{schema} && $self->{export_schema}) {
			$seqname = $seq_info->{SEQUENCE_OWNER} . '.' . $seq_info->{SEQUENCE_NAME};
		}

		my $nextvalue = $seq_info->{LAST_NUMBER} + $seq_info->{INCREMENT_BY};
		my $alter = "ALTER SEQUENCE $self->{pg_supports_ifexists} " .  $self->quote_object_name($seqname) . " RESTART WITH $nextvalue;";
		push(@script, $alter);
		$self->logit("Extracted sequence information for sequence \"$seqname\"\n", 1);
	}
	$sth->finish();

	return @script;

}


=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) = @_;

	# 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 ";
	if ($self->{tables}{$table}{table_info}{nested} eq 'YES') {
		$str = "SELECT /*+ nested_table_get_refs */ ";
	}

	my $extraStr = "";
	my $dateformat = 'YYYY-MM-DD HH24:MI:SS';
	my $timeformat = $dateformat;
	if ($self->{enable_microsecond}) {
		my $dim = 6;
		$dim = '' if ($self->{db_version} =~ /Release [89]/);
		$timeformat = "YYYY-MM-DD HH24:MI:SS.FF$dim";
	}
	my $timeformat_tz = $timeformat . ' TZH:TZM';
	# 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 ( ( $src_type->[$k] =~ /^char/i) && ($type->[$k] =~ /(varchar|text)/i)) {
				$str .= "trim($self->{trim_type} '$self->{trim_char}' FROM $name->[$k]->[0]) AS $name->[$k]->[0],";
			} elsif ($self->{is_mysql} && $src_type->[$k] =~ /bit/i) {
				$str .= "BIN($name->[$k]->[0]),";
			# 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($name->[$k]->[0]),";
			# 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($name->[$k]->[0]),";
			# 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($name->[$k]->[0]),";
			} elsif ( $src_type->[$k] =~ /xmltype/i) {
				if ($self->{xml_pretty}) {
					$str .= "$alias.$name->[$k]->[0].extract('/').getStringVal(),";
				} else {
					$str .= "$alias.$name->[$k]->[0].extract('/').getClobVal(),";
				}
			} elsif ( !$self->{is_mysql} && $src_type->[$k] =~ /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 $name->[$k]->[0] IS NOT NULL THEN SDO_UTIL.TO_WKBGEOMETRY($name->[$k]->[0]) ELSE NULL END,";
					} elsif ($self->{geometry_extract_type} eq 'INTERNAL') {
						$str .= "CASE WHEN $name->[$k]->[0] IS NOT NULL THEN $name->[$k]->[0] ELSE NULL END,";
					} else {
						$str .= "CASE WHEN $name->[$k]->[0] IS NOT NULL THEN 'ST_GeomFromText('''||SDO_UTIL.TO_WKTGEOMETRY($name->[$k]->[0])||''','||($spatial_srid)||')' ELSE NULL END,";
					}
				} else {
					if ($self->{geometry_extract_type} eq 'WKB') {
						$str .= "CASE WHEN $name->[$k]->[0] IS NOT NULL THEN SDO_UTIL.TO_WKBGEOMETRY($name->[$k]->[0]) ELSE NULL END,";
					} elsif ($self->{geometry_extract_type} eq 'INTERNAL') {
						$str .= "CASE WHEN $name->[$k]->[0] IS NOT NULL THEN $name->[$k]->[0] ELSE NULL END,";
					} else {
						$str .= "CASE WHEN $name->[$k]->[0] IS NOT NULL THEN SDO_UTIL.TO_WKTGEOMETRY($name->[$k]->[0]) ELSE NULL END,";
					}
				}

			} elsif ( $self->{is_mysql} && $src_type->[$k] =~ /geometry/i) {

				if ($self->{geometry_extract_type} eq 'WKB') {
					$str .= "CASE WHEN $name->[$k]->[0] IS NOT NULL THEN CONCAT('SRID=',SRID($name->[$k]->[0]),';', AsBinary($name->[$k]->[0])) ELSE NULL END,";
				} else {
					$str .= "CASE WHEN $name->[$k]->[0] IS NOT NULL THEN CONCAT('SRID=',SRID($name->[$k]->[0]),';',AsText($name->[$k]->[0])) ELSE NULL END,";
				}

			} elsif ( !$self->{is_mysql} && (($src_type->[$k] =~ /clob/i) || ($src_type->[$k] =~ /blob/i)) ) {
				if ($self->{no_blob_export} && $src_type->[$k] =~ /blob/i) {
					# user don't want to export blob
					next;
				}
				if ($self->{empty_lob_null}) {
					$str .= "CASE WHEN dbms_lob.getlength($name->[$k]->[0]) = 0 THEN NULL ELSE $name->[$k]->[0] END,";
				} else {
					$str .= "$name->[$k]->[0],";
				}

			} else {

				$str .= "$name->[$k]->[0],";

			}
			push(@{$self->{spatial_srid}{$table}}, $spatial_srid);
			
			if ($type->[$k] =~ /bytea/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 all_directories WHERE directory_name = l_dir;
      l_dir := rtrim(l_path,'/');
      RETURN l_dir || '/' || 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' || 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->_oracle_connection();
		my $sth2 =  $local_dbh->do($bfile_function);
		$local_dbh->disconnect() if ($local_dbh);
	}

	# Fix empty column list with nested table
	$str =~ s/ ""$/ \*/;

	if ($part_name) {
		if ($is_subpart) {
			$alias = "SUBPARTITION(" . $self->quote_object_name($part_name) . ") a";
		} else {
			$alias = "PARTITION(". $self->quote_object_name($part_name) . ") a";
		}
	}
	# Force parallelism on Oracle side
	if ($self->{default_parallelism_degree} > 1) {
		# Only if the number of rows is upper than PARALLEL_MIN_ROWS
		$self->{tables}{$table}{table_info}{num_rows} ||= 0;
		if ($self->{tables}{"\L$table\E"}{table_info}{num_rows} > $self->{parallel_min_rows}) {
			$str =~ s#^SELECT #SELECT /*+ FULL(a) PARALLEL(a, $self->{default_parallelism_degree}) */ #;
		}
	}
	$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"}) {
			if ($str =~ / WHERE /) {
				$str .= " AND ABS(MOD(" . $self->{defined_pk}{"\L$table\E"} . ", $self->{oracle_copies})) = ?";
			} else {
				$str .= " WHERE ABS(MOD(" . $self->{defined_pk}{"\L$table\E"} . ", $self->{oracle_copies})) = ?";
			}
		}
	}

	$self->logit("DEGUG: Query sent to Oracle: $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) = @_;

	$type = uc($type); # Force uppercase

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_sql_type($self, $type, $len, $precision, $scale);
	}

	my $data_type = '';

	# Simplify timestamp type
	$type =~ s/TIMESTAMP\(\d+\)/TIMESTAMP/;

	# Interval precision for year/month/day is not supported by PostgreSQL
	if ($type =~ /INTERVAL/) {
		$type =~ s/(INTERVAL\s+YEAR)\s*\(\d+\)/$1/;
		$type =~ s/(INTERVAL\s+YEAR\s+TO\s+MONTH)\s*\(\d+\)/$1/;
		$type =~ s/(INTERVAL\s+DAY)\s*\(\d+\)/$1/;
		# maximum precision allowed for seconds is 6
		if ($type =~ /INTERVAL\s+DAY\s+TO\s+SECOND\s*\((\d+)\)/) {
			if ($1 > 6) {
				$type =~ s/(INTERVAL\s+DAY\s+TO\s+SECOND)\s*\(\d+\)/$1(6)/;
			}
		}
	}

        # Overide the length
	if ( ($type eq 'NUMBER') && $precision ) {
		$len = $precision;
		return $self->{data_type}{'NUMBER(*)'} if ($scale eq '0' && exists $self->{data_type}{'NUMBER(*)'});
	} elsif ( ($type eq 'NUMBER') && ($len == 38) ) {
		if ($scale eq '0' && $precision eq '') {
			# Allow custom type rewrite for NUMBER(*,0)
			return $self->{data_type}{'NUMBER(*,0)'} if (exists $self->{data_type}{'NUMBER(*,0)'});
		}
		$precision = $len;
	} elsif ( $type =~ /CHAR/ && $len && exists $self->{data_type}{"$type($len)"}) {
		return $self->{data_type}{"$type($len)"};
	} elsif ( $type =~ /RAW/ && $len && exists $self->{data_type}{"$type($len)"}) {
		return $self->{data_type}{"$type($len)"};
	} elsif ( $type =~ /RAW/ && $len && $default =~ /sys_guid/i) {
		return 'uuid';
	}

        if (exists $self->{data_type}{$type}) {
		if ($len) {

			if ( ($type eq "CHAR") || ($type eq "NCHAR") || ($type =~ /VARCHAR/) ) {
				# Type CHAR have default length set to 1
				# Type VARCHAR(2) must have a specified length
				$len = 1 if (!$len && (($type eq "CHAR") || ($type eq "NCHAR")) );
                		return "$self->{data_type}{$type}($len)";
			} elsif ($type eq "NUMBER") {
				# This is an integer
				if (!$scale) {
					if ($precision) {
						if (exists $self->{data_type}{"$type($precision)"}) {
							return $self->{data_type}{"$type($precision)"};
						}
						if ($self->{pg_integer_type}) {
							if ($precision < 5) {
								return 'smallint';
							} elsif ($precision <= 9) {
								return 'integer'; # The speediest in PG
							} elsif ($precision <= 19) {
								return 'bigint';
							} else {
								return "numeric($precision)";
							}
						}
						return "numeric($precision)";
					} elsif ($self->{pg_integer_type}) {
						# Most of the time interger should be enought?
						return $self->{default_numeric} || 'bigint';
					}
				} else {
					if (exists $self->{data_type}{"$type($precision,$scale)"}) {
						return $self->{data_type}{"$type($precision,$scale)"};
					}
					if ($self->{pg_numeric_type}) {
						if ($precision <= 6) {
							return 'real';
						} elsif ($precision <= 15) {
							return 'double precision';
						}
					}
					return "decimal($precision,$scale)";
				}
			}
			return "$self->{data_type}{$type}";
		} else {
			if (($type eq 'NUMBER') && $self->{pg_integer_type}) {
				return $self->{default_numeric};
			} else {
				return $self->{data_type}{$type};
			}
		}
        }

	#$type =~ s/\%ROWTYPE//i;

        return $type;
}


=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, $recurs) = @_;

	return Ora2Pg::MySQL::_column_info($self,'',$owner,'TABLE') if ($self->{is_mysql});

	$objtype ||= 'TABLE';

	my $condition = '';
	$condition .= "AND A.TABLE_NAME='$table' " if ($table);
	if ($owner) {
		$condition .= "AND A.OWNER='$owner' ";
	} else {
		$condition .= " AND A.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	if (!$table) {
		$condition .= $self->limit_to_objects('TABLE', 'A.TABLE_NAME');
	} else {
		@{$self->{query_bind_params}} = ();
	}

	my $sth = '';
	if ($self->{db_version} !~ /Release 8/) {
		my $exclude_mview = " AND (A.OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, TABLE_NAME FROM ALL_OBJECT_TABLES)";
		$exclude_mview .= " AND (A.OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)" if ($self->{type} ne 'FDW');
		$sth = $self->{dbh}->prepare(<<END);
SELECT A.COLUMN_NAME, A.DATA_TYPE, A.DATA_LENGTH, A.NULLABLE, A.DATA_DEFAULT,
    A.DATA_PRECISION, A.DATA_SCALE, A.CHAR_LENGTH, A.TABLE_NAME, A.OWNER, V.VIRTUAL_COLUMN
FROM $self->{prefix}_TAB_COLUMNS A, ALL_OBJECTS O, ALL_TAB_COLS V
WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='$objtype'
    AND A.OWNER=V.OWNER AND A.TABLE_NAME=V.TABLE_NAME AND A.COLUMN_NAME=V.COLUMN_NAME $condition
    $exclude_mview
ORDER BY A.COLUMN_ID
END
		if (!$sth) {
			my $ret = $self->{dbh}->err;
			if (!$recurs && ($ret == 942) && ($self->{prefix} eq 'DBA')) {
				$self->logit("HINT: Please activate USER_GRANTS or connect using a user with DBA privilege.\n");
				$self->{prefix} = 'ALL';
				return $self->_column_info($table, $owner, $objtype, 1);
			}
			$self->logit("FATAL: _column_info() " . $self->{dbh}->errstr . "\n", 0, 1);
		}
	} else {
		# an 8i database.
		$sth = $self->{dbh}->prepare(<<END);
SELECT A.COLUMN_NAME, A.DATA_TYPE, A.DATA_LENGTH, A.NULLABLE, A.DATA_DEFAULT,
    A.DATA_PRECISION, A.DATA_SCALE, A.DATA_LENGTH, A.TABLE_NAME, A.OWNER, 'NO' as "VIRTUAL_COLUMN"
FROM $self->{prefix}_TAB_COLUMNS A, ALL_OBJECTS O
WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='$objtype'
    $condition
ORDER BY A.COLUMN_ID
END
		if (!$sth) {
			my $ret = $self->{dbh}->err;
			if (!$recurs && ($ret == 942) && ($self->{prefix} eq 'DBA')) {
				$self->logit("HINT: Please activate USER_GRANTS or connect using a user with DBA privilege.\n");
				$self->{prefix} = 'ALL';
				return $self->_column_info($table, $owner, $objtype, 1);
			}
			$self->logit("FATAL: _column_info() " . $self->{dbh}->errstr . "\n", 0, 1);
		}
	}
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: _column_info() " . $self->{dbh}->errstr . "\n", 0, 1);

	# Default number of line to scan to grab the geometry type of the column.
	# If it not limited, the query will scan the entire table which may take a very long time.
	my $max_lines = 50000;
	$max_lines = $self->{autodetect_spatial_type} if ($self->{autodetect_spatial_type} > 1);
	my $spatial_gtype =  'SELECT DISTINCT c.%s.SDO_GTYPE FROM %s c WHERE ROWNUM < ' . $max_lines;
	# Set query to retrieve the SRID
	my $spatial_srid = "SELECT SRID FROM ALL_SDO_GEOM_METADATA WHERE TABLE_NAME=? AND COLUMN_NAME=? AND OWNER=?";
	if ($self->{convert_srid}) {
		# Translate SRID to standard EPSG SRID, may return 0 because there's lot of Oracle only SRID.
		$spatial_srid = 'SELECT sdo_cs.map_oracle_srid_to_epsg(SRID) FROM ALL_SDO_GEOM_METADATA WHERE TABLE_NAME=? AND COLUMN_NAME=? AND OWNER=?';
	}
	# Get the dimension of the geometry by looking at the number of element in the SDO_DIM_ARRAY
	my $spatial_dim = "SELECT t.SDO_DIMNAME, t.SDO_LB, t.SDO_UB FROM ALL_SDO_GEOM_METADATA m, TABLE (m.diminfo) t WHERE m.TABLE_NAME=? AND m.COLUMN_NAME=? AND OWNER=?";

	my %data = ();
	my $pos = 0;
	while (my $row = $sth->fetch) {

		$row->[2] = $row->[7] if $row->[1] =~ /char/i;

		# Seems that for a NUMBER with a DATA_SCALE to 0, no DATA_PRECISION and a DATA_LENGTH of 22
		# Oracle use a NUMBER(38) instead
		if ( ($row->[1] eq 'NUMBER') && ($row->[6] eq '0') && ($row->[5] eq '') && ($row->[2] == 22) ) {
			$row->[2] = 38;
		}

		my $tmptable = $row->[8];
		if ($self->{export_schema} && !$self->{schema}) {
			$tmptable = "$row->[9].$row->[8]";
		}

		# check if this is a spatial column (srid, dim, gtype)
		my @geom_inf = ();
		if ($row->[1] eq 'SDO_GEOMETRY') {

			# Get the SRID of the column
			if ($self->{convert_srid} > 1) {
				push(@geom_inf, $self->{convert_srid});
			} else {
				my @result = ();
				my $sth2 = $self->{dbh}->prepare($spatial_srid);
				if (!$sth2) {
					if ($self->{dbh}->errstr !~ /ORA-01741/) {
						$self->logit("FATAL: _column_info() " . $self->{dbh}->errstr . "\n", 0, 1);
					} else {
						# No SRID defined, use default one
						$self->logit("WARNING: Error retreiving SRID, no matter default SRID will be used: $spatial_srid\n", 0);
					}
				} else {
					$sth2->execute($row->[8],$row->[0],$row->[9]) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
					while (my $r = $sth2->fetch) {
						push(@result, $r->[0]) if ($r->[0] =~ /\d+/);
					}
					$sth2->finish();
				}
				if ($#result == 0) {
					push(@geom_inf, $result[0]);
				} elsif ($self->{default_srid}) {
					push(@geom_inf, $self->{default_srid});
				} else {
					push(@geom_inf, 0);
				}
			}

			# Grab constraint type and dimensions from index definition
			my $found_contraint = 0;
			foreach my $idx (keys %{$self->{tables}{$tmptable}{idx_type}}) {
				if (exists $self->{tables}{$tmptable}{idx_type}{$idx}{type_constraint}) {
					foreach my $c (@{$self->{tables}{$tmptable}{indexes}{$idx}}) {
						if ($c eq $row->[0]) {
							if ($self->{tables}{$tmptable}{idx_type}{$idx}{type_dims}) {
								$found_dims = $self->{tables}{$tmptable}{idx_type}{$idx}{type_dims};
							}
							if ($self->{tables}{$tmptable}{idx_type}{$idx}{type_constraint}) {
								$found_contraint = $GTYPE{$self->{tables}{$tmptable}{idx_type}{$idx}{type_constraint}} || $self->{tables}{$tmptable}{idx_type}{$idx}{type_constraint};
							}
						}
					}
				}
			}

			# Get the dimension of the geometry column
			if (!$found_dims) {
				$sth2 = $self->{dbh}->prepare($spatial_dim);
				if (!$sth2) {
					$self->logit("FATAL: _column_info() " . $self->{dbh}->errstr . "\n", 0, 1);
				}
				$sth2->execute($row->[8],$row->[0],$row->[9]) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
				my $count = 0;
				while (my $r = $sth2->fetch) {
					$count++;
				}
				$sth2->finish();
				push(@geom_inf, $count);
			} else {
				push(@geom_inf, $found_dims);
			}

			# Set dimension and type of the spatial column
			if (!$found_contraint && $self->{autodetect_spatial_type}) {

				# Get spatial information
				my $colname = $row->[9] . "." . $row->[8];
				my $squery = sprintf($spatial_gtype, $row->[0], $colname);
				my $sth2 = $self->{dbh}->prepare($squery);
				if (!$sth2) {
					$self->logit("FATAL: _column_info() " . $self->{dbh}->errstr . "\n", 0, 1);
				}
				$sth2->execute or $self->logit("FATAL: _column_info() " . $self->{dbh}->errstr . "\n", 0, 1);
				my @result = ();
				while (my $r = $sth2->fetch) {
					if ($r->[0] =~ /(\d)$/) {
						push(@result, $ORA2PG_SDO_GTYPE{$1});
					}
				}
				$sth2->finish();
				if ($#result == 0) {
					push(@geom_inf, $result[0]);
				} else {
					push(@geom_inf, join(',', @result));
				}
			} elsif ($found_contraint) {
				push(@geom_inf, $found_contraint);

			} else {
				push(@geom_inf, $ORA2PG_SDO_GTYPE{0});
			}
		}

		if (!$self->{schema} && $self->{export_schema}) {
			next if (exists $self->{modify}{"\L$tmptable\E"} && !grep(/^$row->[0]$/i, @{$self->{modify}{"\L$tmptable\E"}}));
			push(@{$data{$tmptable}{"$row->[0]"}}, (@$row, $pos, @geom_inf));
		} else {
			if (!$self->{preserve_case}) {
				next if (exists $self->{modify}{"\L$row->[8]\E"} && !grep(/^$row->[0]$/i, @{$self->{modify}{"\L$row->[8]\E"}}));
			} else {
				next if (exists $self->{modify}{$row->[8]} && !grep(/^$row->[0]$/i, @{$self->{modify}{$row->[8]}}));
			}
			push(@{$data{"$row->[8]"}{"$row->[0]"}}, (@$row, $pos, @geom_inf));
		}

		$pos++;
	}

	return %data;	
}

sub _column_attributes
{
	my ($self, $table, $owner, $objtype) = @_;

	return Ora2Pg::MySQL::_column_attributes($self,'',$owner,'TABLE') if ($self->{is_mysql});

	$objtype ||= 'TABLE';

	my $condition = '';
	$condition .= "AND A.TABLE_NAME='$table' " if ($table);
	if ($owner) {
		$condition .= "AND A.OWNER='$owner' ";
	} else {
		$condition .= " AND A.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	if (!$table) {
		$condition .= $self->limit_to_objects('TABLE', 'A.TABLE_NAME');
	} else {
		@{$self->{query_bind_params}} = ();
	}

	my $sth = '';
	if ($self->{db_version} !~ /Release 8/) {
		$sth = $self->{dbh}->prepare(<<END);
SELECT A.COLUMN_NAME, A.NULLABLE, A.DATA_DEFAULT, A.TABLE_NAME, A.OWNER, A.COLUMN_ID
FROM $self->{prefix}_TAB_COLUMNS A, ALL_OBJECTS O WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='$objtype' $condition
ORDER BY A.COLUMN_ID
END
		if (!$sth) {
			$self->logit("FATAL: _column_attributes() " . $self->{dbh}->errstr . "\n", 0, 1);
		}
	} else {
		# an 8i database.
		$sth = $self->{dbh}->prepare(<<END);
SELECT A.COLUMN_NAME, A.NULLABLE, A.DATA_DEFAULT, A.TABLE_NAME, A.OWNER, A.COLUMN_ID
FROM $self->{prefix}_TAB_COLUMNS A, ALL_OBJECTS O WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='$objtype' $condition
ORDER BY A.COLUMN_ID
END
		if (!$sth) {
			$self->logit("FATAL: _column_attributes() " . $self->{dbh}->errstr . "\n", 0, 1);
		}
	}
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: _column_attributes() " . $self->{dbh}->errstr . "\n", 0, 1);

	my %data = ();
	while (my $row = $sth->fetch) {
		if ($self->{export_schema} && !$self->{schema}) {
			$data{"$row->[4].$row->[3]"}{"$row->[0]"}{nullable} = $row->[1];
			$data{"$row->[4].$row->[3]"}{"$row->[0]"}{default} = $row->[2];
		} else {
			$data{$row->[3]}{"$row->[0]"}{nullable} = $row->[1];
			$data{$row->[3]}{"$row->[0]"}{default} = $row->[2];
		}
		my $f = $self->{tables}{"$table"}{column_info}{"$row->[0]"};
		if ( ($f->[1] =~ /GEOMETRY/i) && ($self->{convert_srid} <= 1) ) {
			$spatial_srid = "SELECT COALESCE(SRID, $self->{default_srid}) FROM ALL_SDO_GEOM_METADATA WHERE TABLE_NAME='\U$table\E' AND COLUMN_NAME='$row->[0]' AND OWNER='\U$self->{tables}{$table}{table_info}{owner}\E'";
			if ($self->{convert_srid} == 1) {
				$spatial_srid = "SELECT COALESCE(sdo_cs.map_oracle_srid_to_epsg(SRID), $self->{default_srid}) FROM ALL_SDO_GEOM_METADATA WHERE TABLE_NAME='\U$table\E' AND COLUMN_NAME='$row->[0]' AND OWNER='\U$self->{tables}{$table}{table_info}{owner}\E'";
			}
			my $sth2 = $self->{dbh}->prepare($spatial_srid);
			if (!$sth2) {
				if ($self->{dbh}->errstr !~ /ORA-01741/) {
					$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
				} else {
					# No SRID defined, use default one
					$spatial_srid = $self->{default_srid} || '0';
					$self->logit("WARNING: Error retreiving SRID, no matter default SRID will be used: $spatial_srid\n", 0);
				}
			} else {
				$sth2->execute() or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
				my @result = ();
				while (my $r = $sth2->fetch) {
					push(@result, $r->[0]) if ($r->[0] =~ /\d+/);
				}
				$sth2->finish();
				if ($self->{export_schema} && !$self->{schema}) {
					  $data{"$row->[4].$row->[3]"}{"$row->[0]"}{spatial_srid} = $result[0] || $self->{default_srid} || '0';
				} else {
					  $data{$row->[3]}{"$row->[0]"}{spatial_srid} = $result[0] || $self->{default_srid} || '0';
				}
			}
		}
	}

	return %data;	
}

sub _encrypted_columns
{
	my ($self, $table, $owner) = @_;

	return Ora2Pg::MySQL::_encrypted_columns($self,'',$owner) if ($self->{is_mysql});

	# Encryption appears in version 10 only
	return if ($self->{db_version} =~ /Release [8|9]/);

	my $condition = '';
	$condition .= "AND A.TABLE_NAME='$table' " if ($table);
	if ($owner) {
		$condition .= "AND A.OWNER='$owner' ";
	} else {
		$condition .= " AND A.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	if (!$table) {
		$condition .= $self->limit_to_objects('TABLE', 'A.TABLE_NAME');
	} else {
		@{$self->{query_bind_params}} = ();
	}
	$condition =~ s/^\s*AND /WHERE /s;

	my $sth = $self->{dbh}->prepare(<<END);
SELECT A.COLUMN_NAME, A.TABLE_NAME, A.OWNER, A.ENCRYPTION_ALG
FROM $self->{prefix}_ENCRYPTED_COLUMNS A
$condition
END
	if (!$sth) {
		$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	}
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %data = ();
	while (my $row = $sth->fetch) {
		if ($self->{export_schema} && !$self->{schema}) {
			$data{"$row->[2].$row->[1].$row->[0]"} = $row->[3];
		} else {
			$data{"$row->[1].$row->[0]"} = $row->[3];
		}
	}

	return %data;	
}



=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) = @_;

	return Ora2Pg::MySQL::_unique_key($self,$table,$owner) if ($self->{is_mysql});

	my %result = ();

        my @accepted_constraint_types = ();
	if ($type) {
		push @accepted_constraint_types, "'$type'";
	} else {
		push @accepted_constraint_types, "'P'" unless($self->{skip_pkeys});
		push @accepted_constraint_types, "'U'" unless($self->{skip_ukeys});
	}
        return %result unless(@accepted_constraint_types);

        my $cons_types = '('. join(',', @accepted_constraint_types) .')';

	my $indexname = "'' AS INDEX_NAME";
	if ($self->{db_version} !~ /Release 8/) {
		$indexname = 'B.INDEX_NAME';
	}
	# Get columns of all the table in the specified schema or excluding the list of system schema
	my $sql = qq{SELECT DISTINCT A.COLUMN_NAME,A.CONSTRAINT_NAME,A.OWNER,A.POSITION,B.CONSTRAINT_NAME,B.CONSTRAINT_TYPE,B.DEFERRABLE,B.DEFERRED,B.GENERATED,B.TABLE_NAME,B.OWNER,$indexname
FROM $self->{prefix}_CONS_COLUMNS A JOIN $self->{prefix}_CONSTRAINTS B ON (B.CONSTRAINT_NAME = A.CONSTRAINT_NAME AND B.OWNER = A.OWNER)
};
	if ($owner) {
		$sql .= " WHERE A.OWNER = '$owner'";
	} else {
		$sql .= " WHERE A.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	}
	$sql .= " AND B.CONSTRAINT_TYPE IN $cons_types";
	$sql .= " AND B.TABLE_NAME='$table'" if ($table);
	$sql .= " AND B.STATUS='ENABLED' ";
	if ($self->{db_version} !~ /Release 8/) {
		$sql .= " AND (B.OWNER, B.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)" if ($self->{type} ne 'FDW');
		$sql .= " AND (B.OWNER, B.TABLE_NAME) NOT IN (SELECT OWNER, TABLE_NAME FROM ALL_OBJECT_TABLES)";
	}

	# Get the list of constraints in the specified schema or excluding the list of system schema
	my @tmpparams = ();
	if ($self->{type} ne 'SHOW_REPORT')
	{
		$sql .= $self->limit_to_objects('UKEY|TABLE', 'B.CONSTRAINT_NAME|B.TABLE_NAME');
		push(@tmpparams, @{$self->{query_bind_params}});
		$sql .= $self->limit_to_objects('UKEY', 'B.CONSTRAINT_NAME');
		push(@tmpparams, @{$self->{query_bind_params}});
	}
	$sql .=  " ORDER BY A.POSITION";

	my $sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@tmpparams) or $self->logit("FATAL: " . $sth->errstr . "\n", 0, 1);

	while (my $row = $sth->fetch)
	{
		my $name = $row->[9];
		if (!$self->{schema} && $self->{export_schema})
		{
			$name = "$row->[10].$row->[9]";
		}
		if (!exists $result{$name}{$row->[4]})
		{
			$result{$name}{$row->[4]} = { (type => $row->[5], 'generated' => $row->[8], 'index_name' => $row->[11], 'deferrable' => $row->[6], 'deferred' => $row->[7], columns => ()) };
			push(@{ $result{$name}{$row->[4]}->{columns} }, $row->[0]) if ($row->[4] !~ /^SYS_NC/i);
		}
		elsif ($row->[4] !~ /^SYS_NC/i)
		{
			push(@{ $result{$name}{$row->[4]}->{columns} }, $row->[0]);
		}
	}
	return %result;
}

=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) = @_;

	my $condition = '';
	$condition .= "AND TABLE_NAME='$table' " if ($table);
	if ($owner) {
		$condition .= "AND OWNER = '$owner' ";
	} else {
		$condition .= "AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	$condition .= $self->limit_to_objects('CKEY|TABLE', 'CONSTRAINT_NAME|TABLE_NAME');

	my $sql = qq{
SELECT A.CONSTRAINT_NAME,A.R_CONSTRAINT_NAME,A.SEARCH_CONDITION,A.DELETE_RULE,A.DEFERRABLE,A.DEFERRED,A.R_OWNER,A.TABLE_NAME,A.OWNER,A.VALIDATED
FROM $self->{prefix}_CONSTRAINTS A
WHERE A.CONSTRAINT_TYPE='C' $condition
AND A.STATUS='ENABLED'
};

	if ($self->{db_version} !~ /Release 8/) {
		$sql .= " AND (A.OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)" if ($self->{type} ne 'FDW');
		$sql .= " AND (A.OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, TABLE_NAME FROM ALL_OBJECT_TABLES)";
	}
	my $sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %data = ();
	while (my $row = $sth->fetch) {
		if ($self->{export_schema} && !$self->{schema}) {
			$row->[7] = "$row->[8].$row->[7]";
		}
		$data{$row->[7]}{constraint}{$row->[0]}{condition} = $row->[2];
		$data{$row->[7]}{constraint}{$row->[0]}{validate}  = $row->[9];
	}

	return %data;
}

=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) = @_;

	return Ora2Pg::MySQL::_foreign_key($self,$table,$owner) if ($self->{is_mysql});

	my @tmpparams = ();
	my $condition = '';
	$condition .= "AND CONS.TABLE_NAME='$table' " if ($table);
	if ($owner) {
		$condition .= "AND CONS.OWNER = '$owner' ";
	} else {
		$condition .= "AND CONS.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	$condition .= $self->limit_to_objects('FKEY|TABLE','CONS.CONSTRAINT_NAME|CONS.TABLE_NAME');

	my $deferrable = $self->{fkey_deferrable} ? "'DEFERRABLE' AS DEFERRABLE" : "DEFERRABLE";
	my $defer = $self->{fkey_deferrable} ? "'DEFERRABLE' AS DEFERRABLE" : "CONS.DEFERRABLE";

	my $sql = <<END;
SELECT
    CONS.TABLE_NAME,
    CONS.CONSTRAINT_NAME,
    COLS.COLUMN_NAME,
    CONS_R.TABLE_NAME R_TABLE_NAME,
    CONS.R_CONSTRAINT_NAME,
    COLS_R.COLUMN_NAME R_COLUMN_NAME,
    CONS.SEARCH_CONDITION,CONS.DELETE_RULE,$defer,CONS.DEFERRED,
    CONS.OWNER,CONS.R_OWNER,
    COLS.POSITION,COLS_R.POSITION,
    CONS.VALIDATED
FROM $self->{prefix}_CONSTRAINTS CONS
    LEFT JOIN $self->{prefix}_CONS_COLUMNS COLS ON (COLS.CONSTRAINT_NAME = CONS.CONSTRAINT_NAME AND COLS.OWNER = CONS.OWNER AND COLS.TABLE_NAME = CONS.TABLE_NAME)
    LEFT JOIN $self->{prefix}_CONSTRAINTS CONS_R ON (CONS_R.CONSTRAINT_NAME = CONS.R_CONSTRAINT_NAME AND CONS_R.OWNER = CONS.R_OWNER)
    LEFT JOIN $self->{prefix}_CONS_COLUMNS COLS_R ON (COLS_R.CONSTRAINT_NAME = CONS.R_CONSTRAINT_NAME AND COLS_R.POSITION=COLS.POSITION AND COLS_R.OWNER = CONS.R_OWNER)
WHERE CONS.CONSTRAINT_TYPE = 'R' $condition
END
	$sql .= "\nAND (CONS.OWNER, CONS.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)" if ($self->{type} ne 'FDW');
	$sql .= " AND (CONS.OWNER, CONS.TABLE_NAME) NOT IN (SELECT OWNER, TABLE_NAME FROM ALL_OBJECT_TABLES)";

	$sql .= "\nORDER BY CONS.TABLE_NAME, CONS.CONSTRAINT_NAME, COLS.POSITION";

	if ($self->{db_version} =~ /Release 8/) {
		$sql = <<END;
SELECT
    CONS.TABLE_NAME,
    CONS.CONSTRAINT_NAME,
    COLS.COLUMN_NAME,
    CONS_R.TABLE_NAME R_TABLE_NAME,
    CONS.R_CONSTRAINT_NAME,
    COLS_R.COLUMN_NAME R_COLUMN_NAME,
    CONS.SEARCH_CONDITION,CONS.DELETE_RULE,$defer,CONS.DEFERRED,
    CONS.OWNER,CONS.R_OWNER,
    COLS.POSITION,COLS_R.POSITION,
    CONS.VALIDATED
FROM $self->{prefix}_CONSTRAINTS CONS,  $self->{prefix}_CONS_COLUMNS COLS, $self->{prefix}_CONSTRAINTS CONS_R, $self->{prefix}_CONS_COLUMNS COLS_R
WHERE CONS_R.CONSTRAINT_NAME = CONS.R_CONSTRAINT_NAME AND CONS_R.OWNER = CONS.R_OWNER
    AND COLS.CONSTRAINT_NAME = CONS.CONSTRAINT_NAME AND COLS.OWNER = CONS.OWNER AND COLS.TABLE_NAME = CONS.TABLE_NAME
    AND COLS_R.CONSTRAINT_NAME = CONS.R_CONSTRAINT_NAME AND COLS_R.POSITION=COLS.POSITION AND COLS_R.OWNER = CONS.R_OWNER
    AND CONS.CONSTRAINT_TYPE = 'R' $condition
ORDER BY CONS.TABLE_NAME, CONS.CONSTRAINT_NAME, COLS.POSITION
END
	}
	my $sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $sth->errstr . "\n", 0, 1);

	my %data = ();
	my %link = ();
	#my @tab_done = ();
	while (my $row = $sth->fetch) {
		my $local_table = $row->[0];
		my $remote_table = $row->[3];
		if (!$self->{schema} && $self->{export_schema}) {
			$local_table = "$row->[10].$row->[0]";
			$remote_table = "$row->[11].$row->[3]";
		}
		if (!$self->{preserve_case}) {
			next if (exists $self->{modify}{"\L$local_table\E"} && !grep(/^$row->[2]$/i, @{$self->{modify}{"\L$local_table\E"}}));
			next if (exists $self->{modify}{"\L$remote_table\E"} && !grep(/^$row->[5]$/i, @{$self->{modify}{"\L$remote_table\E"}}));
		} else {
			next if (exists $self->{modify}{$local_table} && !grep(/^$row->[2]$/i, @{$self->{modify}{$local_table}}));
			next if (exists $self->{modify}{$remote_table} && !grep(/^$row->[5]$/i, @{$self->{modify}{$remote_table}}));
		}
		push(@{$data{$local_table}}, [ ($row->[1],$row->[4],$row->[6],$row->[7],$row->[8],$row->[9],$row->[11],$row->[0],$row->[10],$row->[14]) ]);
		#            TABLENAME     CONSTNAME           COLNAME
		push(@{$link{$local_table}{$row->[1]}{local}}, $row->[2]);
		#            TABLENAME     CONSTNAME          TABLENAME        COLNAME
		push(@{$link{$local_table}{$row->[1]}{remote}{$remote_table}}, $row->[5]);
	}

	return \%link, \%data;
}


=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;
	}

	return Ora2Pg::MySQL::_get_privilege($self) if ($self->{is_mysql});

	my %privs = ();
	my %roles = ();

	# Retrieve all privilege per table defined in this database
	my $str = "SELECT b.GRANTEE,b.OWNER,b.TABLE_NAME,b.PRIVILEGE,a.OBJECT_TYPE,b.GRANTABLE FROM DBA_TAB_PRIVS b, DBA_OBJECTS a";
	if ($self->{schema}) {
		$str .= " WHERE b.GRANTOR = '$self->{schema}'";
	} else {
		$str .= " WHERE b.GRANTOR NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	}
	$str .= " AND b.TABLE_NAME=a.OBJECT_NAME AND a.OWNER=b.GRANTOR";
	if ($self->{grant_object} && $self->{grant_object} ne 'USER') {
		$str .= " AND a.OBJECT_TYPE = '\U$self->{grant_object}\E'"; 
	} else {
		$str .= " AND a.OBJECT_TYPE <> 'TYPE'";
	}
	$str .= " " . $self->limit_to_objects('GRANT|TABLE|VIEW|FUNCTION|PROCEDURE|SEQUENCE', 'b.GRANTEE|b.TABLE_NAME|b.TABLE_NAME|b.TABLE_NAME|b.TABLE_NAME|b.TABLE_NAME');
	
	if (!$self->{export_invalid}) {
		$str .= " AND a.STATUS='VALID'";
	}
	#$str .= " ORDER BY b.TABLE_NAME, b.GRANTEE";
	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	while (my $row = $sth->fetch) {
		next if ($row->[0] eq 'PUBLIC');
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[2] = "$row->[1].$row->[2]";
		}
		$privs{$row->[2]}{type} = $row->[4];
		$privs{$row->[2]}{owner} = $row->[1] if (!$privs{$row->[2]}{owner});
		if ($row->[5] eq 'YES') {
			$privs{$row->[2]}{grantable} = $row->[5];
		}
		push(@{$privs{$row->[2]}{privilege}{$row->[0]}}, $row->[3]);
		push(@{$roles{owner}}, $row->[1]) if (!grep(/^$row->[1]$/, @{$roles{owner}}));
		push(@{$roles{grantee}}, $row->[0]) if (!grep(/^$row->[0]$/, @{$roles{grantee}}));
	}
	$sth->finish();

	# Retrieve all privilege per column table defined in this database
	$str = "SELECT b.GRANTEE,b.OWNER,b.TABLE_NAME,b.PRIVILEGE,b.COLUMN_NAME FROM DBA_COL_PRIVS b, DBA_OBJECTS a";
	if ($self->{schema}) {
		$str .= " WHERE b.GRANTOR = '$self->{schema}'";
	} else {
		$str .= " WHERE b.GRANTOR NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	}
	if (!$self->{export_invalid}) {
		$str .= " AND a.STATUS='VALID'";
	}
	$str .= " AND b.TABLE_NAME=a.OBJECT_NAME AND a.OWNER=b.GRANTOR AND a.OBJECT_TYPE <> 'TYPE'";
	if ($self->{grant_object} && $self->{grant_object} ne 'USER') {
		$str .= " AND a.OBJECT_TYPE = '\U$self->{grant_object}\E'"; 
	} else {
		$str .= " AND a.OBJECT_TYPE <> 'TYPE'";
	}
	$str .= " " . $self->limit_to_objects('GRANT|TABLE|VIEW|FUNCTION|PROCEDURE|SEQUENCE', 'b.GRANTEE|b.TABLE_NAME|b.TABLE_NAME|b.TABLE_NAME|b.TABLE_NAME|b.TABLE_NAME');

	$sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[2] = "$row->[1].$row->[2]";
		}
		$privs{$row->[2]}{owner} = $row->[1] if (!$privs{$row->[2]}{owner});
		push(@{$privs{$row->[2]}{column}{$row->[4]}{$row->[0]}}, $row->[3]);
		push(@{$roles{owner}}, $row->[1]) if (!grep(/^$row->[1]$/, @{$roles{owner}}));
		push(@{$roles{grantee}}, $row->[0]) if (!grep(/^$row->[0]$/, @{$roles{grantee}}));
	}
	$sth->finish();

	# Search if users have admin rights
	my @done = ();
	foreach my $r (@{$roles{owner}}, @{$roles{grantee}}) {
		next if (grep(/^$r$/, @done));
		push(@done, $r);
		# Get all system priviledge given to a role
		$str = "SELECT PRIVILEGE,ADMIN_OPTION FROM DBA_SYS_PRIVS WHERE GRANTEE = '$r'";
		$str .= " " . $self->limit_to_objects('GRANT', 'GRANTEE');
		#$str .= " ORDER BY PRIVILEGE";
		$sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		while (my $row = $sth->fetch) {
			push(@{$roles{admin}{$r}{privilege}}, $row->[0]);
			push(@{$roles{admin}{$r}{admin_option}}, $row->[1]);
		}
		$sth->finish();
	}
	# Now try to find if it's a user or a role 
	foreach my $u (@done) {
		$str = "SELECT GRANTED_ROLE FROM DBA_ROLE_PRIVS WHERE GRANTEE = '$u'";
		$str .= " " . $self->limit_to_objects('GRANT', 'GRANTEE');
		#$str .= " ORDER BY GRANTED_ROLE";
		$sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		while (my $row = $sth->fetch) {
			push(@{$roles{role}{$u}}, $row->[0]);
		}
		$str = "SELECT USERNAME FROM DBA_USERS WHERE USERNAME = '$u'";
		$str .= " " . $self->limit_to_objects('GRANT', 'USERNAME');
		#$str .= " ORDER BY USERNAME";
		$sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		while (my $row = $sth->fetch) {
			$roles{type}{$u} = 'USER';
		}
		next if  $roles{type}{$u};
		$str = "SELECT ROLE,PASSWORD_REQUIRED FROM DBA_ROLES WHERE ROLE='$u'";
		$str .= " " . $self->limit_to_objects('GRANT', 'ROLE');
		#$str .= " ORDER BY ROLE";
		$sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		while (my $row = $sth->fetch) {
			$roles{type}{$u} = 'ROLE';
			$roles{password_required}{$u} = $row->[1];
		}
		$sth->finish();
	}

	return (\%privs, \%roles);
}

=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) = @_;

	return Ora2Pg::MySQL::_get_security_definer($self, $type) if ($self->{is_mysql});

	my %security = ();

	# This table does not exists before 10g
	return if ($self->{db_version} =~ /Release [89]/);

	# Retrieve security privilege per function defined in this database
	# Version of Oracle 10 does not have the OBJECT_TYPE column.
	my $str = "SELECT AUTHID,OBJECT_TYPE,OBJECT_NAME,OWNER FROM $self->{prefix}_PROCEDURES";
	if ($self->{db_version} =~ /Release 10/) {
		$str = "SELECT AUTHID,'ALL' AS OBJECT_TYPE,OBJECT_NAME,OWNER FROM $self->{prefix}_PROCEDURES";
	}
	if ($self->{schema}) {
		$str .= " WHERE OWNER = '$self->{schema}'";
	} else {
		$str .= " WHERE OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	}
	if ( $type && ($self->{db_version} !~ /Release 10/) ) {
		$str .= " AND OBJECT_TYPE='$type'";
	}
	$str .= " " . $self->limit_to_objects('FUNCTION|PROCEDURE|PACKAGE|TRIGGER', 'OBJECT_NAME|OBJECT_NAME|OBJECT_NAME|OBJECT_NAME');
	#$str .= " ORDER BY OBJECT_NAME";

	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	while (my $row = $sth->fetch) {
		next if (!$row->[0]);
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[2] = "$row->[3].$row->[2]";
		}
		$security{$row->[2]}{security} = $row->[0];
		$security{$row->[2]}{owner} = $row->[3];
	}
	$sth->finish();

	return (\%security);
}




=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) = @_;

	return Ora2Pg::MySQL::_get_indexes($self,$table,$owner) if ($self->{is_mysql});

	# Retrieve FTS indexes information before.
	my %idx_info = ();
	%idx_info = $self->_get_fts_indexes_info($owner) if ($self->_table_exists('CTXSYS', 'CTX_INDEX_VALUES'));

	my $sub_owner = '';
	if ($owner) {
		$sub_owner = "AND A.INDEX_OWNER=B.TABLE_OWNER";
	}

	my $condition = '';
	$condition .= "AND A.TABLE_NAME='$table' " if ($table);
	if ($owner) {
		$condition .= "AND A.INDEX_OWNER='$owner' ";
	} else {
		$condition .= " AND A.INDEX_OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	if (!$table) {
		$condition .= $self->limit_to_objects('TABLE|INDEX', "A.TABLE_NAME|A.INDEX_NAME");
	} else {
		@{$self->{query_bind_params}} = ();
	}

	# When comparing number of index we need to retrieve generated index (mostly PK)
        my $generated = '';
        $generated = " B.GENERATED = 'N' AND" if (!$generated_indexes);

	# Retrieve all indexes 
	my $sth = '';
	if ($self->{db_version} !~ /Release 8/) {
		my $no_mview = " AND (A.INDEX_OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)" if ($self->{type} ne 'FDW');
		$no_mview .= " AND (A.INDEX_OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, TABLE_NAME FROM ALL_OBJECT_TABLES)";
		$no_mview = '' if ($self->{type} eq 'MVIEW');
		$sth = $self->{dbh}->prepare(<<END) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
SELECT DISTINCT A.INDEX_NAME,A.COLUMN_NAME,B.UNIQUENESS,A.COLUMN_POSITION,B.INDEX_TYPE,B.TABLE_TYPE,B.GENERATED,B.JOIN_INDEX,A.TABLE_NAME,A.INDEX_OWNER,B.TABLESPACE_NAME,B.ITYP_NAME,B.PARAMETERS,A.DESCEND
FROM $self->{prefix}_IND_COLUMNS A
JOIN $self->{prefix}_INDEXES B ON (B.INDEX_NAME=A.INDEX_NAME AND B.OWNER=A.INDEX_OWNER)
WHERE$generated B.TEMPORARY = 'N' $condition $no_mview
ORDER BY A.COLUMN_POSITION
END
	} else {
		# an 8i database.
		$sth = $self->{dbh}->prepare(<<END) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
SELECT DISTINCT A.INDEX_NAME,A.COLUMN_NAME,B.UNIQUENESS,A.COLUMN_POSITION,B.INDEX_TYPE,B.TABLE_TYPE,B.GENERATED, 'NO', A.TABLE_NAME,A.INDEX_OWNER,B.TABLESPACE_NAME,B.ITYP_NAME,B.PARAMETERS,A.DESCEND
FROM $self->{prefix}_IND_COLUMNS A, $self->{prefix}_INDEXES B
WHERE B.INDEX_NAME=A.INDEX_NAME AND B.OWNER=A.INDEX_OWNER $condition
AND$generated B.TEMPORARY = 'N'
ORDER BY A.COLUMN_POSITION
END
	}

	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my $idxnc = qq{SELECT IE.COLUMN_EXPRESSION FROM $self->{prefix}_IND_EXPRESSIONS IE, $self->{prefix}_IND_COLUMNS IC
WHERE  IE.INDEX_OWNER = IC.INDEX_OWNER
AND    IE.INDEX_NAME = IC.INDEX_NAME
AND    IE.TABLE_OWNER = IC.TABLE_OWNER
AND    IE.TABLE_NAME = IC.TABLE_NAME
AND    IE.COLUMN_POSITION = IC.COLUMN_POSITION
AND    IC.COLUMN_NAME = ?
AND    IE.TABLE_NAME = ?
AND    IC.TABLE_OWNER = ?
};
	my $sth2 = $self->{dbh}->prepare($idxnc);
	my %data = ();
	my %unique = ();
	my %idx_type = ();
	while (my $row = $sth->fetch)
	{
		# Exclude log indexes of materialized views, there must be a better
		# way to exclude then than looking at index name, fill free to fix it.
		next if ($row->[0] =~ /^I_SNAP\$_/);

		my $save_tb = $row->[8];
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[8] = "$row->[9].$row->[8]";
		}
		if (!$self->{preserve_case}) {
			next if (exists $self->{modify}{"\L$row->[8]\E"} && !grep(/^$row->[1]$/i, @{$self->{modify}{"\L$row->[8]\E"}}));
		} else {
			next if (exists $self->{modify}{$row->[8]} && !grep(/^$row->[1]$/i, @{$self->{modify}{$row->[8]}}));
		}
		# Show a warning when an index has the same name as the table
		if ( !$self->{indexes_renaming} && !$self->{indexes_suffix} && (lc($row->[0]) eq lc($table)) ) {
			 print STDERR "WARNING: index $row->[0] has the same name as the table itself. Please rename it before export or enable INDEXES_RENAMING.\n"; 
		}
		$unique{$row->[8]}{$row->[0]} = $row->[2];

		# Save original column name
		my $colname = $row->[1];

		# Replace function based index type
		if ( ($row->[4] =~ /FUNCTION-BASED/i) && ($colname =~ /^SYS_NC\d+\$$/) ) {
			$sth2->execute($colname,$save_tb,$row->[-5]) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
			my $nc = $sth2->fetch();
			$row->[1] = $nc->[0];
			$row->[1] =~ s/"//g;
			# Enclose with double quote if required when is is not an index function
			if ($row->[1] !~ /\(.*\)/ && $row->[4] !~ /FUNCTION-BASED/i) {
				$row->[1] = $self->quote_object_name($row->[1]);
			}
			# Append DESC sort order when not default to ASC
			if ($row->[13] eq 'DESC') {
				$row->[1] .= " DESC";
			}
		} else {
                        # Quote column with unsupported symbols
                        $row->[1] = $self->quote_object_name($row->[1]);
		}

		$row->[1] =~ s/SYS_EXTRACT_UTC\s*\(([^\)]+)\)/$1/isg;

		# Index with DESC are declared as FUNCTION-BASED, fix that
		if (($row->[4] =~ /FUNCTION-BASED/i) && ($row->[1] !~ /\(.*\)/)) {
			$row->[4] =~ s/FUNCTION-BASED\s*//;
		}
		$idx_type{$row->[8]}{$row->[0]}{type_name} = $row->[11];
		if (($#{$row} > 6) && ($row->[7] eq 'Y')) {
			$idx_type{$row->[8]}{$row->[0]}{type} = $row->[4] . ' JOIN';
		} else {
			$idx_type{$row->[8]}{$row->[0]}{type} = $row->[4];
		}
		my $idx_name = $row->[0];
		if (!$self->{schema} && $self->{export_schema}) {
			$idx_name = "$row->[9].$row->[0]";
		}
		if (exists $idx_info{$idx_name}) {
			$idx_type{$row->[8]}{$row->[0]}{stemmer} = $idx_info{$idx_name}{stemmer};
		}
		if ($row->[11] =~ /SPATIAL_INDEX/) {
			$idx_type{$row->[8]}{$row->[0]}{type} = 'SPATIAL INDEX';
			if ($row->[12] =~ /layer_gtype=([^\s,]+)/i) {
				$idx_type{$row->[9]}{$row->[0]}{type_constraint} = uc($1);
			}
			if ($row->[12] =~ /sdo_indx_dims=(\d+)/i) {
				$idx_type{$row->[8]}{$row->[0]}{type_dims} = $1;
			}
		}
		if ($row->[4] eq 'BITMAP') {
			$idx_type{$row->[8]}{$row->[0]}{type} = $row->[4];
		}
		push(@{$data{$row->[8]}{$row->[0]}}, $row->[1]);
		$index_tablespace{$row->[8]}{$row->[0]} = $row->[10];

	}
	$sth->finish();
	$sth2->finish();

	return \%unique, \%data, \%idx_type, \%index_tablespace;
}

=head2 _get_fts_indexes_info

This function retrieve FTS index attributes informations

Returns a hash of containing all useful attribute values for all FTS indexes

=cut

sub _get_fts_indexes_info
{
	my ($self, $owner) = @_;

	my $condition = '';
	$condition .= "AND IXV_INDEX_OWNER='$owner' " if ($owner);
	$condition .= $self->limit_to_objects('INDEX', "IXV_INDEX_NAME");

	# Retrieve all indexes informations
	my $sth = $self->{dbh}->prepare(<<END) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
SELECT DISTINCT IXV_INDEX_OWNER,IXV_INDEX_NAME,IXV_CLASS,IXV_ATTRIBUTE,IXV_VALUE
FROM CTXSYS.CTX_INDEX_VALUES
WHERE (IXV_CLASS='WORDLIST' AND IXV_ATTRIBUTE='STEMMER') $condition
END

	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	my %indexes_info = ();
	while (my $row = $sth->fetch) {
		my $save_idx = $row->[1];
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[1] = "$row->[0].$row->[1]";
		}
		$indexes_info{$row->[1]}{"\L$row->[3]\E"} = $row->[4];
	}

	return %indexes_info;
}



=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) = @_;

	return Ora2Pg::MySQL::_get_sequences($self) if ($self->{is_mysql});

	# Retrieve all indexes 
	my $str = "SELECT DISTINCT SEQUENCE_NAME, MIN_VALUE, MAX_VALUE, INCREMENT_BY, LAST_NUMBER, CACHE_SIZE, CYCLE_FLAG, SEQUENCE_OWNER FROM $self->{prefix}_SEQUENCES";
	if (!$self->{schema}) {
		$str .= " WHERE SEQUENCE_OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " WHERE SEQUENCE_OWNER = '$self->{schema}'";
	}
	$str .= $self->limit_to_objects('SEQUENCE', 'SEQUENCE_NAME');
	#$str .= " ORDER BY SEQUENCE_NAME";


	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my @seqs = ();
	while (my $row = $sth->fetch) {

		push(@seqs, [ @$row ]);
	}

	return \@seqs;
}

=head2 _get_identities

This function retrieve information about IDENTITY columns that must be
exported as PostgreSQL serial.

=cut

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

	return Ora2Pg::MySQL::_get_identities($self) if ($self->{is_mysql});

	# Identity column appears in version 12 only
	return if ($self->{db_version} =~ /Release (8|9|10|11)/);

	# Retrieve all indexes 
	my $str = "SELECT OWNER, TABLE_NAME, COLUMN_NAME, GENERATION_TYPE, IDENTITY_OPTIONS FROM $self->{prefix}_TAB_IDENTITY_COLS";
	if (!$self->{schema}) {
		$str .= " WHERE OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " WHERE OWNER = '$self->{schema}'";
	}
	$str .= $self->limit_to_objects('TABLE', 'TABLE_NAME');
	#$str .= " ORDER BY OWNER, TABLE_NAME, COLUMN_NAME";

	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %seqs = ();
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[1] = "$row->[0].$row->[1]";
		}
		# GENERATION_TYPE can be ALWAYS, BY DEFAULT and BY DEFAULT ON NULL
		$seqs{$row->[1]}{$row->[2]}{generation} = $row->[3];
		# SEQUENCE options
		$seqs{$row->[1]}{$row->[2]}{options} = $row->[4];
		$seqs{$row->[1]}{$row->[2]}{options} =~ s/(START WITH):/$1/;
		$seqs{$row->[1]}{$row->[2]}{options} =~ s/(INCREMENT BY):/$1/;
		$seqs{$row->[1]}{$row->[2]}{options} =~ s/MAX_VALUE:/MAXVALUE/;
		$seqs{$row->[1]}{$row->[2]}{options} =~ s/MIN_VALUE:/MINVALUE/;
		$seqs{$row->[1]}{$row->[2]}{options} =~ s/CYCLE_FLAG: N/NO CYCLE/;
		$seqs{$row->[1]}{$row->[2]}{options} =~ s/CYCLE_FLAG: Y/CYCLE/;
		$seqs{$row->[1]}{$row->[2]}{options} =~ s/CACHE_SIZE:/CACHE/;
		$seqs{$row->[1]}{$row->[2]}{options} =~ s/CACHE_SIZE:/CACHE/;
		$seqs{$row->[1]}{$row->[2]}{options} =~ s/ORDER_FLAG: .//;
		$seqs{$row->[1]}{$row->[2]}{options} =~ s/,//g;
		$seqs{$row->[1]}{$row->[2]}{options} =~ s/\s$//;
		$seqs{$row->[1]}{$row->[2]}{options} =~ s/CACHE\s+0/CACHE 1/;
		# For default values don't use option at all
		if ( $seqs{$row->[1]}{$row->[2]}{options} eq 'START WITH 1 INCREMENT BY 1 MAXVALUE 9999999999999999999999999999 MINVALUE 1 NO CYCLE CACHE 20') {
			delete $seqs{$row->[1]}{$row->[2]}{options};
		}
		# Limit the sequence value to bigint max
		$seqs{$row->[1]}{$row->[2]}{options} =~ s/MAXVALUE 9999999999999999999999999999/MAXVALUE 9223372036854775807/;
	}

	return %seqs;
}

=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) = @_;

	# Retrieve all database link from dba_db_links table
	my $str = "SELECT a.*,b.DIRECTORY_PATH,c.LOCATION,a.OWNER FROM $self->{prefix}_EXTERNAL_TABLES a, $self->{prefix}_DIRECTORIES b, $self->{prefix}_EXTERNAL_LOCATIONS c";
	if (!$self->{schema}) {
		$str .= " WHERE a.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " WHERE a.OWNER = '$self->{schema}'";
	}
	$str .= " AND a.DEFAULT_DIRECTORY_NAME = b.DIRECTORY_NAME AND a.TABLE_NAME=c.TABLE_NAME AND a.DEFAULT_DIRECTORY_NAME=c.DIRECTORY_NAME AND a.OWNER=c.OWNER";
	$str .= $self->limit_to_objects('TABLE', 'a.TABLE_NAME');
	#$str .= " ORDER BY a.TABLE_NAME";
	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	
	my %data = ();
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[1] = "$row->[0].$row->[1]";
		}
		$data{$row->[1]}{directory} = $row->[5];
		$data{$row->[1]}{directory_path} = $row->[10];
		if ($data{$row->[1]}{directory_path} =~ /([\/\\])/) {
			$data{$row->[1]}{directory_path} .= $1 if ($data{$row->[1]}{directory_path} !~ /$1$/); 
		}
		$data{$row->[1]}{location} = $row->[11];
		$data{$row->[1]}{delimiter} = ',';
		if ($row->[8] =~ /FIELDS TERMINATED BY '(.)'/is) {
			$data{$row->[1]}{delimiter} = $1;
		}
		if ($row->[8] =~ /PREPROCESSOR EXECDIR\s*:\s*'([^']+)'/is) {
			$data{$row->[1]}{program} = $1;
		}
	}
	$sth->finish();

	return %data;
}

=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) = @_;

	# Retrieve all database link from dba_db_links table
	my $str = "SELECT d.DIRECTORY_NAME, d.DIRECTORY_PATH, d.OWNER, p.GRANTEE, p.PRIVILEGE FROM $self->{prefix}_DIRECTORIES d, $self->{prefix}_TAB_PRIVS p";
	$str .= " WHERE d.DIRECTORY_NAME = p.TABLE_NAME";
	if (!$self->{schema}) {
		$str .= " AND p.GRANTEE NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " AND p.GRANTEE = '$self->{schema}'";
	}
	$str .= $self->limit_to_objects('TABLE', 'd.DIRECTORY_NAME');
	#$str .= " ORDER BY d.DIRECTORY_NAME";

	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	
	my %data = ();
	while (my $row = $sth->fetch) {

		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[2].$row->[0]";
		}
		$data{$row->[0]}{path} = $row->[1];
		if ($row->[1] !~ /\/$/) {
			$data{$row->[0]}{path} .= '/';
		}
		$data{$row->[0]}{grantee}{$row->[3]} .= $row->[4];
	}
	$sth->finish();

	return %data;
}

=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) = @_;

	return Ora2Pg::MySQL::_get_dblink($self) if ($self->{is_mysql});

	# Retrieve all database link from dba_db_links table
	my $str = "SELECT OWNER,DB_LINK,USERNAME,HOST FROM $self->{prefix}_DB_LINKS";
	if (!$self->{schema}) {
		$str .= " WHERE OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " WHERE OWNER = '$self->{schema}'";
	}
	$str .= $self->limit_to_objects('DBLINK', 'DB_LINK');
	#$str .= " ORDER BY DB_LINK";

	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %data = ();
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[1] = "$row->[0].$row->[1]";
		}
		$data{$row->[1]}{owner} = $row->[0];
		$data{$row->[1]}{user} = $row->[2];
		$data{$row->[1]}{username} = $self->{pg_user} || $row->[2];
		$data{$row->[1]}{host} = $row->[3];
	}

	return %data;
}

=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) = @_;

	return Ora2Pg::MySQL::_get_job($self) if ($self->{is_mysql});

	# Jobs appears in version 10 only
	return if ($self->{db_version} =~ /Release [8|9]/);

	# Retrieve all database job from user_jobs table
	my $str = "SELECT JOB,WHAT,INTERVAL,SCHEMA_USER FROM $self->{prefix}_JOBS";
	if (!$self->{schema}) {
		$str .= " WHERE SCHEMA_USER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " WHERE SCHEMA_USER = '$self->{schema}'";
	}
	$str .= $self->limit_to_objects('JOB', 'JOB');
	#$str .= " ORDER BY JOB";
	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %data = ();
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[3].$row->[0]";
		}
		$data{$row->[0]}{what} = $row->[1];
		$data{$row->[0]}{interval} = $row->[2];
	}

	# Retrieve all database jobs from view [ALL|DBA]_SCHEDULER_JOBS
	$str = "SELECT job_name AS JOB, job_action AS WHAT, repeat_interval AS INTERVAL, owner AS SCHEMA_USER";
	$str .= " FROM $self->{prefix}_SCHEDULER_JOBS";
	$str .= " WHERE repeat_interval IS NOT NULL";
	$str .= " AND client_id IS NULL";
	if (!$self->{schema}) {
		$str .= " AND owner NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " AND owner = '$self->{schema}'";
	}
	$sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	while ($row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[3].$row->[0]";
		}
		$data{$row->[0]}{what} = $row->[1];
		$data{$row->[0]}{interval} = $row->[2];
	}

	return %data;
}


=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) = @_;


	return Ora2Pg::MySQL::_get_views($self) if ($self->{is_mysql});

	my $owner = '';
	if ($self->{schema}) {
		$owner = "AND A.OWNER='$self->{schema}' ";
	} else {
		$owner = "AND A.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}

	my %comments = ();
	if ($self->{type} ne 'SHOW_REPORT')
	{
		my $sql = "SELECT A.TABLE_NAME,A.COMMENTS,A.TABLE_TYPE,A.OWNER FROM ALL_TAB_COMMENTS A, ALL_OBJECTS O WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='VIEW' $owner";
		$sql .= $self->limit_to_objects('VIEW', 'A.TABLE_NAME');
		my $sth = $self->{dbh}->prepare( $sql ) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		while (my $row = $sth->fetch)
		{
			if (!$self->{schema} && $self->{export_schema})
			{
				$row->[0] = "$row->[3].$row->[0]";
			}
			$comments{$row->[0]}{comment} = $row->[1];
			$comments{$row->[0]}{table_type} = $row->[2];
		}
		$sth->finish();
	}

	# Retrieve all views
	my $str = "SELECT v.VIEW_NAME,v.TEXT,v.OWNER FROM $self->{prefix}_VIEWS v";
	if (!$self->{export_invalid}) {
		$str .= ", $self->{prefix}_OBJECTS a";
	}

	if (!$self->{schema}) {
		$str .= " WHERE v.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " WHERE v.OWNER = '$self->{schema}'";
	}

	if (!$self->{export_invalid}) {
		$str .= " AND a.OBJECT_TYPE='VIEW' AND a.STATUS='VALID' AND v.VIEW_NAME=a.OBJECT_NAME AND a.OWNER=v.OWNER";
	}
	$str .= $self->limit_to_objects('VIEW', 'v.VIEW_NAME');
	#$str .= " ORDER BY v.OWNER,v.VIEW_NAME";


	# Compute view order, where depended view appear before using view
	my %view_order = ();
	if ($self->{type} ne 'SHOW_REPORT')
	{
		if ($self->{db_version} !~ /Release (8|9|10|11\.1)/)
		{
			if ($self->{schema}) {
				$owner = "AND o.OWNER='$self->{schema}' ";
			} else {
				$owner = "AND o.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
			}
			$sql = qq{
WITH x (ITER, OWNER, OBJECT_NAME) AS
( SELECT 1 , o.OWNER, o.OBJECT_NAME FROM ALL_OBJECTS o WHERE OBJECT_TYPE = 'VIEW' $owner
  AND NOT EXISTS (SELECT 1 FROM ALL_DEPENDENCIES d WHERE TYPE LIKE 'VIEW' AND REFERENCED_TYPE = 'VIEW'
  AND REFERENCED_OWNER = o.OWNER AND d.OWNER = o.OWNER and o.OBJECT_NAME=d.NAME)
UNION ALL
  SELECT ITER + 1, d.OWNER, d.NAME FROM ALL_DEPENDENCIES d
     JOIN x ON d.REFERENCED_OWNER = x.OWNER and d.REFERENCED_NAME = x.OBJECT_NAME
    WHERE TYPE LIKE 'VIEW' AND REFERENCED_TYPE = 'VIEW'
)
SELECT max(ITER) ITER, OWNER, OBJECT_NAME FROM x
GROUP BY OWNER, OBJECT_NAME
ORDER BY ITER ASC, 2, 3
};

			my $sth = $self->{dbh}->prepare( $sql ) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
			$sth->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
			while (my $row = $sth->fetch) {
				$view_order{"\U$row->[1].$row->[2]\E"} = $row->[0];
			}
			$sth->finish();
		}
	}

	$sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %data = ();
	while (my $row = $sth->fetch)
	{
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[2].$row->[0]";
		}
		$data{$row->[0]}{text} = $row->[1];
		$data{$row->[0]}{owner} = $row->[2];
		$data{$row->[0]}{comment} = $comments{$row->[0]}{comment} || '';
		if ($self->{type} ne 'SHOW_REPORT')
		{
			@{$data{$row->[0]}{alias}} = $self->_alias_info ($row->[0]);
		}
		if ($self->{type} ne 'SHOW_REPORT' && exists $view_order{"\U$row->[2].$row->[0]\E"})
		{
			$data{$row->[0]}{iter} = $view_order{"\U$row->[2].$row->[0]\E"};
		}
	}

	return %data;
}

=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) = @_;

	# Retrieve all views
	my $str = "SELECT MVIEW_NAME,QUERY,UPDATABLE,REFRESH_MODE,REFRESH_METHOD,USE_NO_INDEX,REWRITE_ENABLED,BUILD_MODE,OWNER FROM $self->{prefix}_MVIEWS";
	if ($self->{db_version} =~ /Release 8/) {
		$str = "SELECT MVIEW_NAME,QUERY,UPDATABLE,REFRESH_MODE,REFRESH_METHOD,'',REWRITE_ENABLED,BUILD_MODE,OWNER FROM $self->{prefix}_MVIEWS";
	}
	if (!$self->{schema}) {
		$str .= " WHERE OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " WHERE OWNER = '$self->{schema}'";
	}
	$str .= $self->limit_to_objects('MVIEW', 'MVIEW_NAME');
	#$str .= " ORDER BY MVIEW_NAME";
	my $sth = $self->{dbh}->prepare($str);
	if (not defined $sth) {
		$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	}
	if (not $sth->execute(@{$self->{query_bind_params}})) {
		$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		return ();
	}

	my %data = ();
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[8].$row->[0]";
		}
		$data{$row->[0]}{text} = $row->[1];
		$data{$row->[0]}{updatable} = ($row->[2] eq 'Y') ? 1 : 0;
		$data{$row->[0]}{refresh_mode} = $row->[3];
		$data{$row->[0]}{refresh_method} = $row->[4];
		$data{$row->[0]}{no_index} = ($row->[5] eq 'Y') ? 1 : 0;
		$data{$row->[0]}{rewritable} = ($row->[6] eq 'Y') ? 1 : 0;
		$data{$row->[0]}{build_mode} = $row->[7];
		$data{$row->[0]}{owner} = $row->[8];
	}

	return %data;
}

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

	# Retrieve all views
	my $str = "SELECT MVIEW_NAME,OWNER FROM $self->{prefix}_MVIEWS";
	if (!$self->{schema}) {
		$str .= " WHERE OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " WHERE OWNER = '$self->{schema}'";
	}
	$str .= $self->limit_to_objects('MVIEW', 'MVIEW_NAME');
	#$str .= " ORDER BY MVIEW_NAME";
	my $sth = $self->{dbh}->prepare($str);
	if (not defined $sth) {
		$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	}
	if (not $sth->execute(@{$self->{query_bind_params}})) {
		$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	}

	my @data = ();
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[1].$row->[0]";
		}
		push(@data, uc($row->[0]));
	}

	return @data;
}


=head2 _alias_info

This function implements an Oracle-native column information.

Returns a list of array references containing the following information
for each alias of the specified view:

[(
  column name,
  column id
)]

=cut

sub _alias_info
{
        my ($self, $view) = @_;

	my $str = "SELECT COLUMN_NAME, COLUMN_ID, OWNER FROM $self->{prefix}_TAB_COLUMNS WHERE TABLE_NAME='$view'";
	if ($self->{schema}) {
		$str .= " AND OWNER = '$self->{schema}'";
	} else {
		$str .= " AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	}
	$str .= " ORDER BY COLUMN_ID ASC";
        my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
        $sth->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
        my $data = $sth->fetchall_arrayref();
	$self->logit("View $view column aliases:\n", 1);
	foreach my $d (@$data) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[2].$row->[0]";
		}
		$self->logit("\t$d->[0] =>  column id:$d->[1]\n", 1);
	}

        return @$data; 

}

=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) = @_;

	return Ora2Pg::MySQL::_get_triggers($self) if ($self->{is_mysql});

	# Retrieve all indexes 
	my $str = "SELECT TRIGGER_NAME, TRIGGER_TYPE, TRIGGERING_EVENT, TABLE_NAME, TRIGGER_BODY, WHEN_CLAUSE, DESCRIPTION, ACTION_TYPE, OWNER FROM $self->{prefix}_TRIGGERS WHERE STATUS='ENABLED'";
	if (!$self->{schema}) {
		$str .= " AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " AND OWNER = '$self->{schema}'";
	}
	$str .= " " . $self->limit_to_objects('TABLE|VIEW|TRIGGER','TABLE_NAME|TABLE_NAME|TRIGGER_NAME');

	#$str .= " ORDER BY TABLE_NAME, TRIGGER_NAME";
	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my @triggers = ();
	while (my $row = $sth->fetch) {
		push(@triggers, [ @$row ]);
	}

	return \@triggers;
}

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

	return Ora2Pg::MySQL::_list_triggers($self) if ($self->{is_mysql});

	# Retrieve all indexes 
	my $str = "SELECT TRIGGER_NAME, TABLE_NAME, OWNER FROM $self->{prefix}_TRIGGERS WHERE STATUS='ENABLED'";
	if (!$self->{schema}) {
		$str .= " AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " AND OWNER = '$self->{schema}'";
	}
	$str .= " " . $self->limit_to_objects('TABLE|VIEW|TRIGGER','TABLE_NAME|TABLE_NAME|TRIGGER_NAME');

	#$str .= " ORDER BY TABLE_NAME, TRIGGER_NAME";
	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %triggers = ();
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			push(@{$triggers{"$row->[2].$row->[1]"}}, $row->[0]);
		} else {
			push(@{$triggers{$row->[1]}}, $row->[0]);
		}
	}

	return %triggers;
}

=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;

	return Ora2Pg::MySQL::_get_plsql_metadata($self, $owner) if ($self->{is_mysql});

	# Retrieve all functions 
	my $str = "SELECT DISTINCT OBJECT_NAME,OWNER,OBJECT_TYPE FROM $self->{prefix}_OBJECTS WHERE (OBJECT_TYPE = 'FUNCTION' OR OBJECT_TYPE = 'PROCEDURE' OR  OBJECT_TYPE = 'PACKAGE BODY')";
	$str .= " AND STATUS='VALID'" if (!$self->{export_invalid});
	if ($owner) {
		$str .= " AND OWNER = '$owner'";
		$self->logit("Looking forward functions declaration in schema $owner.\n", 1) if (!$quiet);
	} elsif (!$self->{schema}) {
		$str .= " AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
		$self->logit("Looking forward functions declaration in all schema.\n", 1) if (!$quiet);
	} else {
		$str .= " AND OWNER = '$self->{schema}'";
		$self->logit("Looking forward functions declaration in schema $self->{schema}.\n", 1) if (!$quiet);
	}
	#$str .= " ORDER BY OBJECT_NAME";
	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %functions = ();
	my @fct_done = ();
	push(@fct_done, @EXCLUDED_FUNCTION);
        while (my $row = $sth->fetch) {
                next if (grep(/^$row->[1].$row->[0]$/i, @fct_done));
                push(@fct_done, "$row->[1].$row->[0]");
		$self->{function_metadata}{$row->[1]}{'none'}{$row->[0]}{type} = $row->[2];
        }
        $sth->finish();

	# Get content of package body
	my $sql = "SELECT NAME, OWNER, TYPE, TEXT FROM $self->{prefix}_SOURCE";
	if ($owner) {
		$sql .= " WHERE OWNER = '$owner'";
	} elsif (!$self->{schema}) {
		$sql .= " WHERE OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$sql .= " WHERE OWNER = '$self->{schema}'";
	}
	$sql .= " AND TYPE <> 'PACKAGE'";
	$sql .= " ORDER BY OWNER, NAME, LINE";
	$sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute or $self->logit("FATAL: " . $sth->errstr . "\n", 0, 1);
	while (my $row = $sth->fetch) {
		next if (!exists $self->{function_metadata}{$row->[1]}{'none'}{$row->[0]});
		$self->{function_metadata}{$row->[1]}{'none'}{$row->[0]}{text} .= $row->[3];
	}
        $sth->finish();

	# For each schema in the Oracle instance
	foreach my $sch (sort keys %{ $self->{function_metadata} }) {
		next if ( ($owner && ($sch ne $owner)) || (!$owner && $self->{schema} && ($sch ne $self->{schema})) );
		# Look for functions/procedures
		foreach my $name (sort keys %{$self->{function_metadata}{$sch}{'none'}}) {
			if ($self->{function_metadata}{$sch}{'none'}{$name}{type} ne 'PACKAGE BODY') {
				# Retrieve metadata for this function after removing comments
				$self->_remove_comments(\$self->{function_metadata}{$sch}{'none'}{$name}{text}, 1);
				$self->{comment_values} = ();
				$self->{function_metadata}{$sch}{'none'}{$name}{text} =~  s/\%ORA2PG_COMMENT\d+\%//gs;
				my %fct_detail = $self->_lookup_function($self->{function_metadata}{$sch}{'none'}{$name}{text});
				if (!exists $fct_detail{name}) {
					delete $self->{function_metadata}{$sch}{'none'}{$name};
					next;
				}
				delete $fct_detail{code};
				delete $fct_detail{before};
				%{$self->{function_metadata}{$sch}{'none'}{$name}{metadata}} = %fct_detail;
				delete $self->{function_metadata}{$sch}{'none'}{$name}{text};
			} else {
				$self->_remove_comments(\$self->{function_metadata}{$sch}{'none'}{$name}{text}, 1);
				$self->{comment_values} = ();
				$self->{function_metadata}{$sch}{'none'}{$name}{text} =~  s/\%ORA2PG_COMMENT\d+\%//gs;
				my %infos = $self->_lookup_package($self->{function_metadata}{$sch}{'none'}{$name}{text});
				delete $self->{function_metadata}{$sch}{'none'}{$name};
				$name =~ s/"//g;
				foreach my $f (sort keys %infos) {
					next if (!$f);
					my $fn = lc($f);
					delete $infos{$f}{code};
					delete $infos{$f}{before};
					%{$self->{function_metadata}{$sch}{$name}{$fn}{metadata}} = %{$infos{$f}};
					my $res_name = $f;
					$res_name =~ s/^([^\.]+)\.//;
					$f =~ s/^([^\.]+)\.//;
					if ($self->{package_as_schema}) {
						$res_name = $name . '.' . $res_name;
					} else {
						$res_name = $name . '_' . $res_name;
					}
					$res_name =~ s/"_"/_/g;
					$f =~ s/"//g;
					$self->{package_functions}{"\L$name\E"}{"\L$f\E"}{name}    = $self->quote_object_name($res_name);
					$self->{package_functions}{"\L$name\E"}{"\L$f\E"}{package} = $name;
				}
			}
		}
	}
}


=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;

	return Ora2Pg::MySQL::_get_package_function_list($self, $owner) if ($self->{is_mysql});

	# Retrieve all package information
	my $str = "SELECT DISTINCT OBJECT_NAME,OWNER FROM $self->{prefix}_OBJECTS WHERE OBJECT_TYPE = 'PACKAGE BODY'";
	$str .= " AND STATUS='VALID'" if (!$self->{export_invalid});
	if ($owner) {
		$str .= " AND OWNER = '$owner'";
		$self->logit("Looking forward functions declaration in schema $owner.\n", 1) if (!$quiet);
	} elsif (!$self->{schema}) {
		$str .= " AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
		$self->logit("Looking forward functions declaration in all schema.\n", 1) if (!$quiet);
	} else {
		$str .= " AND OWNER = '$self->{schema}'";
		$self->logit("Looking forward functions declaration in schema $self->{schema}.\n", 1) if (!$quiet);
	}
	#$str .= " ORDER BY OBJECT_NAME";
	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my @packages = ();
        while (my $row = $sth->fetch) {
                next if (grep(/^$row->[0]$/i, @packages));
                push(@packages, $row->[0]);
        }
        $sth->finish();

	# Get content of all packages definition
	my $sql = "SELECT NAME, OWNER, TYPE, TEXT FROM $self->{prefix}_SOURCE";
	if ($owner) {
		$sql .= " WHERE OWNER = '$owner'";
	} elsif (!$self->{schema}) {
		$sql .= " WHERE OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$sql .= " WHERE OWNER = '$self->{schema}'";
	}
	$sql .= " AND TYPE <> 'PACKAGE'";
	$sql .= " AND NAME IN ('" . join("','", @packages) . "')" if ($#packages >= 0);
	$sql .= " ORDER BY OWNER, NAME, LINE";
	$sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute or $self->logit("FATAL: " . $sth->errstr . "\n", 0, 1);
	my %function_metadata = ();
	while (my $row = $sth->fetch) {
		$function_metadata{$row->[1]}{$row->[0]}{text} .= $row->[3];
	}
        $sth->finish();

	my @fct_done = ();
	push(@fct_done, @EXCLUDED_FUNCTION);
	foreach my $sch (sort keys %function_metadata) {
		next if ( ($owner && ($sch ne $owner)) || (!$owner && $self->{schema} && ($sch ne $self->{schema})) );
		foreach my $name (sort keys %{$function_metadata{$sch}}) {
			$self->_remove_comments(\$function_metadata{$sch}{$name}{text}, 1);
			$self->{comment_values} = ();
			$function_metadata{$sch}{$name}{text} =~  s/\%ORA2PG_COMMENT\d+\%//gs;
			my %infos = $self->_lookup_package($function_metadata{$sch}{$name}{text});
			delete $function_metadata{$sch}{$name};
			foreach my $f (sort keys %infos) {
				next if (!$f);
				my $fn = lc($f);
				my $res_name = $f;
				if ($res_name =~ s/^([^\.]+)\.//) {
					next if (lc($1) ne lc($name));
				}
				if ($self->{package_as_schema}) {
					$res_name = $name . '.' . $res_name;
				} else {
					$res_name = $name . '_' . $res_name;
				}
				$res_name =~ s/"_"/_/g;
				$f =~ s/"//gs;
				if ($res_name) {
					$self->{package_functions}{"\L$name\E"}{"\L$f\E"}{name}    = $self->quote_object_name($res_name);
					$self->{package_functions}{"\L$name\E"}{"\L$f\E"}{package} = $name;
				}
			}
		}
	}
}

=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;

	return Ora2Pg::MySQL::_get_functions($self) if ($self->{is_mysql});

	# Retrieve all functions 
	my $str = "SELECT DISTINCT OBJECT_NAME,OWNER FROM $self->{prefix}_OBJECTS WHERE OBJECT_TYPE='FUNCTION'";
	$str .= " AND STATUS='VALID'" if (!$self->{export_invalid});
	if (!$self->{schema}) {
		$str .= " AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " AND OWNER = '$self->{schema}'";
	}
	$str .= " " . $self->limit_to_objects('FUNCTION','OBJECT_NAME');
	#$str .= " ORDER BY OBJECT_NAME";
	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %functions = ();
	my @fct_done = ();
	push(@fct_done, @EXCLUDED_FUNCTION);
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[1].$row->[0]";
		}
		next if (grep(/^$row->[0]$/i, @fct_done));
		push(@fct_done, $row->[0]);
		$functions{"$row->[0]"}{owner} = $row->[1];
	}
	$sth->finish();

	my $sql = "SELECT NAME,OWNER,TEXT FROM $self->{prefix}_SOURCE";
	if (!$self->{schema}) {
		$sql .= " WHERE OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$sql .= " WHERE OWNER = '$self->{schema}'";
	}
	$sql .= " " . $self->limit_to_objects('FUNCTION','NAME');
	$sql .= " ORDER BY OWNER,NAME,LINE";
	$sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $sth->errstr . "\n", 0, 1);
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[1].$row->[0]";
		}
		# Remove some bargage when migrating from 8i
		$row->[2] =~ s/\bAUTHID\s+[^\s]+\s+//is;
		if (exists $functions{"$row->[0]"}) {
			$functions{"$row->[0]"}{text} .= $row->[2];
		}
	}

	return \%functions;
}

sub _get_functions2
{
	my $self = shift;

	return Ora2Pg::MySQL::_get_functions($self) if ($self->{is_mysql});

	# Retrieve all functions 
	my $str = "SELECT DISTINCT OBJECT_NAME,OWNER FROM $self->{prefix}_OBJECTS WHERE OBJECT_TYPE='FUNCTION'";
	$str .= " AND STATUS='VALID'" if (!$self->{export_invalid});
	if (!$self->{schema}) {
		$str .= " AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " AND OWNER = '$self->{schema}'";
	}
	$str .= " " . $self->limit_to_objects('FUNCTION','OBJECT_NAME');
	#$str .= " ORDER BY OBJECT_NAME";
	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %functions = ();
	my @fct_done = ();
	push(@fct_done, @EXCLUDED_FUNCTION);
	while (my $row = $sth->fetch) {
		my $sql = "SELECT TEXT FROM $self->{prefix}_SOURCE WHERE OWNER='$row->[1]' AND NAME='$row->[0]' ORDER BY LINE";
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[1].$row->[0]";
		}
		next if (grep(/^$row->[0]$/i, @fct_done));
		push(@fct_done, $row->[0]);
		my $sth2 = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		$sth2->execute or $self->logit("FATAL: " . $sth2->errstr . "\n", 0, 1);
		while (my $r = $sth2->fetch) {
			$functions{"$row->[0]"}{text} .= $r->[0];
		}
		$functions{"$row->[0]"}{owner} .= $row->[1];
	}

	return \%functions;
}

=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;

	return Ora2Pg::MySQL::_get_functions($self) if ($self->{is_mysql});

	# Retrieve all functions 
	my $str = "SELECT DISTINCT OBJECT_NAME,OWNER FROM $self->{prefix}_OBJECTS WHERE OBJECT_TYPE='PROCEDURE'";
	$str .= " AND STATUS='VALID'" if (!$self->{export_invalid});
	if (!$self->{schema}) {
		$str .= " AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " AND OWNER = '$self->{schema}'";
	}
	$str .= " " . $self->limit_to_objects('PROCEDURE','OBJECT_NAME');
	#$str .= " ORDER BY OBJECT_NAME";
	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %procedures = ();
	my @fct_done = ();
	push(@fct_done, @EXCLUDED_FUNCTION);
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[1].$row->[0]";
		}
		next if (grep(/^$row->[0]$/i, @fct_done));
		push(@fct_done, $row->[0]);
		$procedures{"$row->[0]"}{owner} = $row->[1];
	}
	$sth->finish();

	my $sql = "SELECT NAME,OWNER,TEXT FROM $self->{prefix}_SOURCE";
	if (!$self->{schema}) {
		$sql .= " WHERE OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$sql .= " WHERE OWNER = '$self->{schema}'";
	}
	$sql .= " " . $self->limit_to_objects('PROCEDURE','NAME');
	$sql .= " ORDER BY OWNER,NAME,LINE";
	$sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $sth->errstr . "\n", 0, 1);
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[1].$row->[0]";
		}
		# Remove some bargage when migrating from 8i
		$row->[2] =~ s/\bAUTHID\s+[^\s]+\s+//is;
		if (exists $procedures{"$row->[0]"}) {
			$procedures{"$row->[0]"}{text} .= $row->[2];
		}
	}

	return \%procedures;
}

=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) = @_;

	# Retrieve all indexes 
	my $str = "SELECT DISTINCT OBJECT_NAME,OWNER FROM $self->{prefix}_OBJECTS WHERE OBJECT_TYPE = 'PACKAGE BODY'";
	$str .= " AND STATUS='VALID'" if (!$self->{export_invalid});
	if (!$self->{schema}) {
		$str .= " AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	} else {
		$str .= " AND OWNER = '$self->{schema}'";
	}
	$str .= " " . $self->limit_to_objects('PACKAGE','OBJECT_NAME');
	#$str .= " ORDER BY OBJECT_NAME";

	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %packages = ();
	my @fct_done = ();
	while (my $row = $sth->fetch)
	{
		$self->logit("\tFound Package: $row->[0]\n", 1);
		next if (grep(/^$row->[0]$/, @fct_done));
		push(@fct_done, $row->[0]);
		# Get package definition first
		my $sql = "SELECT TEXT FROM $self->{prefix}_SOURCE WHERE OWNER='$row->[1]' AND NAME='$row->[0]' AND TYPE='PACKAGE' ORDER BY LINE";
		my $sth2 = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		$sth2->execute or $self->logit("FATAL: " . $sth2->errstr . "\n", 0, 1);
		while (my $r = $sth2->fetch) {
			$packages{$row->[0]}{desc} .= 'CREATE OR REPLACE ' if ($r->[0] =~ /^PACKAGE\s+/is);
			$packages{$row->[0]}{desc} .= $r->[0];
		}
		$sth2->finish();
		$packages{$row->[0]}{desc} .= "\n" if (exists $packages{$row->[0]});

		# Then package body code
		$sql = "SELECT TEXT FROM $self->{prefix}_SOURCE WHERE OWNER='$row->[1]' AND NAME='$row->[0]' AND TYPE='PACKAGE BODY' ORDER BY LINE";
		$sth2 = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		$sth2->execute or $self->logit("FATAL: " . $sth2->errstr . "\n", 0, 1);
		while (my $r = $sth2->fetch) {
			$packages{$row->[0]}{text} .= 'CREATE OR REPLACE ' if ($r->[0] =~ /^PACKAGE\s+/is);
			$packages{$row->[0]}{text} .= $r->[0];
		}
		$packages{$row->[0]}{owner} = $row->[1];
	}

	return \%packages;
}

=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) = @_;

	# Retrieve all user defined types
	my $str = "SELECT DISTINCT OBJECT_NAME,OWNER,OBJECT_ID FROM $self->{prefix}_OBJECTS WHERE OBJECT_TYPE='TYPE'";
	$str .= " AND STATUS='VALID'" if (!$self->{export_invalid});
	$str .= " AND OBJECT_NAME='$name'" if ($name);
	$str .= " AND GENERATED='N'";
	if ($self->{schema}) {
		$str .= "AND OWNER='$self->{schema}' ";
	} else {
		$str .= "AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	if (!$name) {
		$str .= $self->limit_to_objects('TYPE', 'OBJECT_NAME');
	} else {
		@{$self->{query_bind_params}} = ();
	}
	#$str .= " ORDER BY OBJECT_NAME";

	# use a separeate connection
	my $local_dbh = $self->_oracle_connection();

	my $sth = $local_dbh->prepare($str) or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);

	my @types = ();
	my @fct_done = ();
	while (my $row = $sth->fetch) {
		my $sql = "SELECT TEXT FROM $self->{prefix}_SOURCE WHERE OWNER='$row->[1]' AND NAME='$row->[0]' AND (TYPE='TYPE' OR TYPE='TYPE BODY') ORDER BY TYPE, LINE";
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[1].$row->[0]";
		}
		$self->logit("\tFound Type: $row->[0]\n", 1);
		next if (grep(/^$row->[0]$/, @fct_done));
		push(@fct_done, $row->[0]);
		my %tmp = ();
		my $sth2 = $local_dbh->prepare($sql) or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);
		$sth2->execute or $self->logit("FATAL: " . $sth2->errstr . "\n", 0, 1);
		while (my $r = $sth2->fetch) {
			$tmp{code} .= $r->[0];
		}
		$sth2->finish();
		$tmp{name} = $row->[0];
		$tmp{owner} = $row->[1];
		$tmp{pos} = $row->[2];
		if (!$self->{preserve_case}) {
			$tmp{code} =~ s/(TYPE\s+)"[^"]+"\."[^"]+"/$1\L$row->[0]\E/is;
			$tmp{code} =~ s/(TYPE\s+)"[^"]+"/$1\L$row->[0]\E/is;
		}
		push(@types, \%tmp);
	}
	$sth->finish();

	$local_dbh->disconnect() if ($local_dbh);

	return \@types;
}

=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;

	return Ora2Pg::MySQL::_table_info($self) if ($self->{is_mysql});

	my $owner = '';
	if ($self->{schema}) {
		$owner .= "AND A.OWNER='$self->{schema}' ";
	} else {
            $owner .= "AND A.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}

	my %comments = ();
	if ($self->{type} eq 'TABLE')
	{
		my $sql = "SELECT A.TABLE_NAME,A.COMMENTS,A.TABLE_TYPE,A.OWNER FROM ALL_TAB_COMMENTS A, ALL_OBJECTS O WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='TABLE' $owner";
		if ($self->{db_version} !~ /Release 8/) {
			$sql .= " AND (A.OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)" if ($self->{type} ne 'FDW');
			$sql .= " AND (A.OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, TABLE_NAME FROM ALL_OBJECT_TABLES)";
		}
		$sql .= $self->limit_to_objects('TABLE', 'A.TABLE_NAME');
		my $sth = $self->{dbh}->prepare( $sql ) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		while (my $row = $sth->fetch) {
			if (!$self->{schema} && $self->{export_schema}) {
				$row->[0] = "$row->[3].$row->[0]";
			}
			$comments{$row->[0]}{comment} = $row->[1];
			$comments{$row->[0]}{table_type} = $row->[2];
		}
		$sth->finish();
	}

	my $sql = "SELECT A.OWNER,A.TABLE_NAME,NVL(num_rows,1) NUMBER_ROWS,A.TABLESPACE_NAME,A.NESTED,A.LOGGING,A.PARTITIONED,A.PCT_FREE FROM $self->{prefix}_TABLES A, ALL_OBJECTS O WHERE A.OWNER=O.OWNER AND A.TABLE_NAME=O.OBJECT_NAME AND O.OBJECT_TYPE='TABLE' $owner";
	$sql .= " AND A.TEMPORARY='N' AND (A.NESTED != 'YES' OR A.LOGGING != 'YES') AND A.SECONDARY = 'N'";
	if ($self->{db_version} !~ /Release [89]/) {
		$sql .= " AND (A.DROPPED IS NULL OR A.DROPPED = 'NO')";
	}
	if ($self->{db_version} !~ /Release 8/) {
		$sql .= " AND (A.OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)" if ($self->{type} ne 'FDW');
		$sql .= " AND (A.OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, TABLE_NAME FROM ALL_OBJECT_TABLES)";
	}
	$sql .= $self->limit_to_objects('TABLE', 'A.TABLE_NAME');
        $sql .= " AND (A.IOT_TYPE IS NULL OR A.IOT_TYPE = 'IOT')";
        #$sql .= " ORDER BY A.OWNER, A.TABLE_NAME";

        my $sth = $self->{dbh}->prepare( $sql ) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
        $sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	my %tables_infos = ();
	while (my $row = $sth->fetch)
	{
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[1] = "$row->[0].$row->[1]";
		}
		$tables_infos{$row->[1]}{owner} = $row->[0] || '';
		$tables_infos{$row->[1]}{num_rows} = $row->[2] || 0;
		$tables_infos{$row->[1]}{tablespace} = $row->[3] || 0;
		$tables_infos{$row->[1]}{comment} =  $comments{$row->[1]}{comment} || '';
		$tables_infos{$row->[1]}{type} =  $comments{$row->[1]}{table_type} || '';
		$tables_infos{$row->[1]}{nested} = $row->[4] || '';
		if ($row->[5] eq 'NO') {
			$tables_infos{$row->[1]}{nologging} = 1;
		} else {
			$tables_infos{$row->[1]}{nologging} = 0;
		}
		if ($row->[6] eq 'NO') {
			$tables_infos{$row->[1]}{partitioned} = 0;
		} else {
			$tables_infos{$row->[1]}{partitioned} = 1;
		}
		# Only take care of PCTFREE upper than the Oracle default value
		if (($row->[7] || 0) > 10) {
			$tables_infos{$row->[1]}{fillfactor} = 100 - min(90, $row->[7]);
		}
		if ($do_real_row_count)
		{
			$self->logit("DEBUG: looking for real row count for table ($row->[0]) $row->[1] (aka using count(*))...\n", 1);
			$sql = "SELECT COUNT(*) FROM $row->[1]";
			if ($self->{schema}) {
				$sql = "SELECT COUNT(*) FROM $row->[0].$row->[1]";
			}
			my $sth2 = $self->{dbh}->prepare( $sql ) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
			$sth2->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
			my $size = $sth2->fetch();
			$sth2->finish();
			$tables_infos{$row->[1]}{num_rows} = $size->[0];
		}
	}
	$sth->finish();

	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;

	return Ora2Pg::MySQL::_global_temp_table_info($self) if ($self->{is_mysql});

	my $owner = '';
	if ($self->{schema}) {
		$owner .= "AND A.OWNER='$self->{schema}' ";
	} else {
            $owner .= "AND A.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}

	# Get comment on global temporary table
	my %comments = ();
	if ($self->{type} eq 'TABLE')
	{
		my $sql = "SELECT A.TABLE_NAME,A.COMMENTS,A.TABLE_TYPE,A.OWNER FROM ALL_TAB_COMMENTS A, ALL_OBJECTS O WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='TABLE' $owner";
		if ($self->{db_version} !~ /Release 8/) {
			$sql .= " AND (A.OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)";
			$sql .= " AND (A.OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, TABLE_NAME FROM ALL_OBJECT_TABLES)";
		}
		$sql .= $self->limit_to_objects('TABLE', 'A.TABLE_NAME');
		my $sth = $self->{dbh}->prepare( $sql ) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
		while (my $row = $sth->fetch) {
			if (!$self->{schema} && $self->{export_schema}) {
				$row->[0] = "$row->[3].$row->[0]";
			}
			$comments{$row->[0]}{comment} = $row->[1];
			$comments{$row->[0]}{table_type} = $row->[2];
		}
		$sth->finish();
	}

	$sql = "SELECT A.OWNER,A.TABLE_NAME,NVL(num_rows,1) NUMBER_ROWS,A.TABLESPACE_NAME,A.NESTED,A.LOGGING FROM $self->{prefix}_TABLES A, ALL_OBJECTS O WHERE A.OWNER=O.OWNER AND A.TABLE_NAME=O.OBJECT_NAME AND O.OBJECT_TYPE='TABLE' $owner";
	$sql .= " AND A.TEMPORARY='Y'";
	if ($self->{db_version} !~ /Release [89]/) {
		$sql .= " AND (A.DROPPED IS NULL OR A.DROPPED = 'NO')";
	}
	$sql .= $self->limit_to_objects('TABLE', 'A.TABLE_NAME');
        $sql .= " AND (A.IOT_TYPE IS NULL OR A.IOT_TYPE = 'IOT')";
        #$sql .= " ORDER BY A.OWNER, A.TABLE_NAME";

        $sth = $self->{dbh}->prepare( $sql ) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
        $sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	my %tables_infos = ();
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[1] = "$row->[0].$row->[1]";
		}
		$tables_infos{$row->[1]}{owner} = $row->[0] || '';
		$tables_infos{$row->[1]}{num_rows} = $row->[2] || 0;
		$tables_infos{$row->[1]}{tablespace} = $row->[3] || 0;
		$tables_infos{$row->[1]}{comment} =  $comments{$row->[1]}{comment} || '';
		$tables_infos{$row->[1]}{type} =  $comments{$row->[1]}{table_type} || '';
		$tables_infos{$row->[1]}{nested} = $row->[4] || '';
		if ($row->[5] eq 'NO') {
			$tables_infos{$row->[1]}{nologging} = 1;
		} else {
			$tables_infos{$row->[1]}{nologging} = 0;
		}
		$tables_infos{$row->[1]}{num_rows} = 0;
	}
	$sth->finish();

	return %tables_infos;
}


=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;
	}

	return Ora2Pg::MySQL::_get_audit_queries($self) if ($self->{is_mysql});

	my @users = ();
	push(@users, split(/[,;\s]/, uc($self->{audit_user})));

	# Retrieve all object with tablespaces.
	my $str = "SELECT SQL_TEXT FROM DBA_AUDIT_TRAIL WHERE ACTION_NAME IN ('INSERT','UPDATE','DELETE','SELECT')";
	if (($#users >= 0) && !grep(/^ALL$/, @users)) {
		$str .= " AND USERNAME IN ('" . join("','", @users) . "')";
	}
	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %tmp_queries = ();
	while (my $row = $sth->fetch) {
		$self->_remove_comments(\$row->[0], 1);
		$self->{comment_values} = ();
		$row->[0] =~  s/\%ORA2PG_COMMENT\d+\%//gs;
		$row->[0] =  $self->normalize_query($row->[0]);
		$tmp_queries{$row->[0]}++;
	}
	$sth->finish;

	my %queries = ();
	my $i = 1;
	foreach my $q (keys %tmp_queries) {
		$queries{$i} = $q;
		$i++;
	}

	return %queries;
}


=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;
	}

	return Ora2Pg::MySQL::_get_tablespaces($self) if ($self->{is_mysql});

	# Retrieve all object with tablespaces.
my $str = qq{
SELECT a.SEGMENT_NAME,a.TABLESPACE_NAME,a.SEGMENT_TYPE,c.FILE_NAME, a.OWNER
FROM DBA_SEGMENTS a, $self->{prefix}_OBJECTS b, DBA_DATA_FILES c
WHERE a.SEGMENT_TYPE IN ('INDEX', 'TABLE', 'INDEX PARTITION', 'TABLE PARTITION')
AND a.SEGMENT_NAME = b.OBJECT_NAME
AND a.SEGMENT_TYPE = b.OBJECT_TYPE
AND a.OWNER = b.OWNER
AND a.TABLESPACE_NAME = c.TABLESPACE_NAME
};
	if ($self->{schema}) {
		$str .= " AND a.OWNER='$self->{schema}'";
	} else {
		$str .= " AND a.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	}
	$str .= $self->limit_to_objects('TABLESPACE|TABLE', 'a.TABLESPACE_NAME|a.SEGMENT_NAME');
	#$str .= " ORDER BY TABLESPACE_NAME";
	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %tbs = ();
	while (my $row = $sth->fetch) {
		# TYPE - TABLESPACE_NAME - FILEPATH - OBJECT_NAME
		if ($self->{export_schema} && !$self->{schema}) {
			$row->[0] = "$row->[4].$row->[0]";
		}
		push(@{$tbs{$row->[2]}{$row->[1]}{$row->[3]}}, $row->[0]);
	}
	$sth->finish;

	return \%tbs;
}

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

	# If the user is given as not DBA, do not look at tablespace
	if ($self->{user_grants}) {
		return;
	}

	return Ora2Pg::MySQL::_list_tablespaces($self) if ($self->{is_mysql});

	# list tablespaces.
	my $str = qq{
SELECT c.FILE_NAME, c.TABLESPACE_NAME, a.OWNER, ROUND(c.BYTES/1024000) MB
FROM DBA_DATA_FILES c, DBA_SEGMENTS a
WHERE a.TABLESPACE_NAME = c.TABLESPACE_NAME
};
	if ($self->{schema}) {
		$str .= " AND a.OWNER='$self->{schema}'";
	} else {
		$str .= " AND a.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	}
	$str .= $self->limit_to_objects('TABLESPACE', 'c.TABLESPACE_NAME');
	#$str .= " ORDER BY c.TABLESPACE_NAME";
	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %tbs = ();
	while (my $row = $sth->fetch) {
		$tbs{$row->[1]}{path} = $row->[0];
		$tbs{$row->[1]}{owner} = $row->[2];
	}
	$sth->finish;

	return \%tbs;
}


=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) = @_;

	return Ora2Pg::MySQL::_get_partitions($self) if ($self->{is_mysql});

	my $highvalue = 'A.HIGH_VALUE';
	if ($self->{db_version} =~ /Release [89]/) {
		$highvalue = "'' AS HIGH_VALUE";
	}
	my $condition = '';
	if ($self->{schema}) {
		$condition .= "AND A.TABLE_OWNER='$self->{schema}' ";
	} else {
		$condition .= " AND A.TABLE_OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	# Retrieve all partitions.
	my $str = qq{
SELECT
	A.TABLE_NAME,
	A.PARTITION_POSITION,
	A.PARTITION_NAME,
	$highvalue,
	A.TABLESPACE_NAME,
	B.PARTITIONING_TYPE,
	C.NAME,
	C.COLUMN_NAME,
	C.COLUMN_POSITION,
	A.TABLE_OWNER
FROM $self->{prefix}_TAB_PARTITIONS A, $self->{prefix}_PART_TABLES B, $self->{prefix}_PART_KEY_COLUMNS C
WHERE
	a.table_name = b.table_name AND
	(b.partitioning_type = 'RANGE' OR b.partitioning_type = 'LIST' OR b.partitioning_type = 'HASH')
	AND a.table_name = c.name
	$condition
};

	if ($self->{db_version} !~ /Release 8/) {
		$str .= " AND (A.TABLE_OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)";
	}
	$str .= $self->limit_to_objects('TABLE|PARTITION', 'A.TABLE_NAME|A.PARTITION_NAME');

	if ($self->{prefix} ne 'USER') {
		if ($self->{schema}) {
			$str .= "\tAND A.TABLE_OWNER ='$self->{schema}' AND B.OWNER=A.TABLE_OWNER AND C.OWNER=A.TABLE_OWNER\n";
		} else {
			$str .= "\tAND A.TABLE_OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') AND B.OWNER=A.TABLE_OWNER AND C.OWNER=A.TABLE_OWNER\n";
		}
	}
	$str .= "ORDER BY A.TABLE_OWNER,A.TABLE_NAME,A.PARTITION_POSITION,C.COLUMN_POSITION\n";

	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %parts = ();
	my %default = ();
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[9].$row->[0]";
		}
		if ( ($row->[3] eq 'DEFAULT')) {
			$default{$row->[0]} = $row->[2];
			next;
		}
		$parts{$row->[0]}{$row->[1]}{name} = $row->[2];
		push(@{$parts{$row->[0]}{$row->[1]}{info}}, { 'type' => $row->[5], 'value' => $row->[3], 'column' => $row->[7], 'colpos' => $row->[8], 'tablespace' => $row->[4], 'owner' => $row->[9]});
	}
	$sth->finish;

	return \%parts, \%default;
}

=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) = @_;

	return Ora2Pg::MySQL::_get_subpartitions($self) if ($self->{is_mysql});

	my $highvalue = 'A.HIGH_VALUE';
	if ($self->{db_version} =~ /Release [89]/) {
		$highvalue = "'' AS HIGH_VALUE";
	}
	my $condition = '';
	if ($self->{schema}) {
		$condition .= "AND A.TABLE_OWNER='$self->{schema}' ";
	} else {
		$condition .= " AND A.TABLE_OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	# Retrieve all partitions.
	my $str = qq{
SELECT
	A.TABLE_NAME,
	A.SUBPARTITION_POSITION,
	A.SUBPARTITION_NAME,
	$highvalue,
	A.TABLESPACE_NAME,
	B.SUBPARTITIONING_TYPE,
	C.NAME,
	C.COLUMN_NAME,
	C.COLUMN_POSITION,
	A.TABLE_OWNER,
	A.PARTITION_NAME
FROM $self->{prefix}_tab_subpartitions A, $self->{prefix}_part_tables B, $self->{prefix}_subpart_key_columns C
WHERE
	a.table_name = b.table_name AND
	(b.subpartitioning_type = 'RANGE' OR b.subpartitioning_type = 'LIST' OR b.subpartitioning_type = 'HASH')
	AND a.table_name = c.name
	$condition
};
	$str .= $self->limit_to_objects('TABLE|PARTITION', 'A.TABLE_NAME|A.SUBPARTITION_NAME');

	if ($self->{prefix} ne 'USER') {
		if ($self->{schema}) {
			$str .= "\tAND A.TABLE_OWNER ='$self->{schema}' AND B.OWNER=A.TABLE_OWNER AND C.OWNER=A.TABLE_OWNER\n";
		} else {
			$str .= "\tAND A.TABLE_OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') AND B.OWNER=A.TABLE_OWNER AND C.OWNER=A.TABLE_OWNER\n";
		}
	}
	if ($self->{db_version} !~ /Release 8/) {
		$str .= " AND (A.TABLE_OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)";
	}
	$str .= "ORDER BY A.TABLE_OWNER,A.TABLE_NAME,A.PARTITION_NAME,A.SUBPARTITION_POSITION,C.COLUMN_POSITION\n";

	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %subparts = ();
	my %default = ();
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[9].$row->[0]";
		}
		if ( ($row->[3] eq 'MAXVALUE') || ($row->[3] eq 'DEFAULT')) {
			$default{$row->[0]}{$row->[10]} = $row->[2];
			next;
		}

		$subparts{$row->[0]}{$row->[10]}{$row->[1]}{name} = $row->[2];
		push(@{$subparts{$row->[0]}{$row->[10]}{$row->[1]}{info}}, { 'type' => $row->[5], 'value' => $row->[3], 'column' => $row->[7], 'colpos' => $row->[8], 'tablespace' => $row->[4], 'owner' => $row->[9]});
	}
	$sth->finish;

	return \%subparts, \%default;
}


=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) = @_;

	return Ora2Pg::MySQL::_get_synonyms($self) if ($self->{is_mysql});

	# Retrieve all synonym
	my $str = "SELECT OWNER,SYNONYM_NAME,TABLE_OWNER,TABLE_NAME,DB_LINK FROM $self->{prefix}_SYNONYMS";
	if ($self->{schema}) {
		$str .= " WHERE (owner='$self->{schema}' OR owner='PUBLIC') AND table_owner NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	} else {
		$str .= " WHERE (owner='PUBLIC' OR owner NOT IN ('" . join("','", @{$self->{sysusers}}) . "')) AND table_owner NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	$str .= $self->limit_to_objects('SYNONYM','SYNONYM_NAME');
	#$str .= " ORDER BY SYNONYM_NAME\n";

	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %synonyms = ();
	while (my $row = $sth->fetch) {
		next if ($row->[1] =~ /^\//); # Some not fully deleted synonym start with a slash
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[1] = "$row->[0].$row->[1]";
		}
		$synonyms{$row->[1]}{owner} = $row->[0];
		$synonyms{$row->[1]}{table_owner} = $row->[2];
		$synonyms{$row->[1]}{table_name} = $row->[3];
		$synonyms{$row->[1]}{dblink} = $row->[4];
	}
	$sth->finish;

	return %synonyms;
}

=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) = @_;

	return Ora2Pg::MySQL::_get_partitions_list($self) if ($self->{is_mysql});

	my $highvalue = 'A.HIGH_VALUE';
	if ($self->{db_version} =~ /Release [89]/) {
		$highvalue = "'' AS HIGH_VALUE";
	}
	my $condition = '';
	if ($self->{schema}) {
		$condition .= "AND A.TABLE_OWNER='$self->{schema}' ";
	} else {
		$condition .= " AND A.TABLE_OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	# Retrieve all partitions.
	my $str = qq{
SELECT
	A.TABLE_NAME,
	A.PARTITION_POSITION,
	A.PARTITION_NAME,
	$highvalue,
	A.TABLESPACE_NAME,
	B.PARTITIONING_TYPE,
	A.TABLE_OWNER
FROM $self->{prefix}_TAB_PARTITIONS A, $self->{prefix}_PART_TABLES B
WHERE A.TABLE_NAME = B.TABLE_NAME
$condition
};
	if ($self->{db_version} !~ /Release 8/) {
		$str .= " AND (A.TABLE_OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)";
	}
	$str .= $self->limit_to_objects('TABLE|PARTITION','A.TABLE_NAME|A.PARTITION_NAME');

	if ($self->{prefix} ne 'USER') {
		if ($self->{schema}) {
			$str .= "\tAND A.TABLE_OWNER ='$self->{schema}'\n";
		} else {
			$str .= "\tAND A.TABLE_OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')\n";
		}
	}
	#$str .= "ORDER BY A.TABLE_NAME\n";

	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %parts = ();
	while (my $row = $sth->fetch) {
		$parts{$row->[5]}++;
	}
	$sth->finish;

	return %parts;
}

=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) = @_;

	return Ora2Pg::MySQL::_get_partitioned_table($self) if ($self->{is_mysql});

	my $highvalue = 'A.HIGH_VALUE';
	if ($self->{db_version} =~ /Release [89]/) {
		$highvalue = "'' AS HIGH_VALUE";
	}
	my $condition = '';
	if ($self->{schema}) {
		$condition .= "AND B.OWNER='$self->{schema}' ";
	} else {
		$condition .= " AND B.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	# Retrieve all partitions.
	my $str = "SELECT B.TABLE_NAME, B.PARTITIONING_TYPE, B.OWNER, B.PARTITION_COUNT, B.SUBPARTITIONING_TYPE";
	if ($self->{type} !~ /SHOW|TEST/)
	{
		$str .= ", C.COLUMN_NAME, C.COLUMN_POSITION";
		$str .= " FROM $self->{prefix}_PART_TABLES B, $self->{prefix}_PART_KEY_COLUMNS C";
		$str .= " WHERE B.TABLE_NAME = C.NAME AND (B.PARTITIONING_TYPE = 'RANGE' OR B.PARTITIONING_TYPE = 'LIST' OR B.PARTITIONING_TYPE = 'HASH')";
	}
	else
	{
		$str .= " FROM $self->{prefix}_PART_TABLES B WHERE (B.PARTITIONING_TYPE = 'RANGE' OR B.PARTITIONING_TYPE = 'LIST' OR B.PARTITIONING_TYPE = 'HASH') AND B.SUBPARTITIONING_TYPE <> 'SYSTEM' ";
	}
	$str .= $self->limit_to_objects('TABLE','B.TABLE_NAME');

	if ($self->{prefix} ne 'USER')
	{
		if ($self->{type} !~ /SHOW|TEST/)
		{
			if ($self->{schema}) {
				$str .= "\tAND B.OWNER ='$self->{schema}' AND C.OWNER=B.OWNER\n";
			} else {
				$str .= "\tAND B.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') AND B.OWNER=C.OWNER\n";
			}
		} else {
			if ($self->{schema}) {
				$str .= "\tAND B.OWNER ='$self->{schema}'\n";
			} else {
				$str .= "\tAND B.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')\n";
			}
		}
	}
	if ($self->{db_version} !~ /Release 8/) {
		$str .= " AND (B.OWNER, B.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)"  if ($self->{type} ne 'FDW');
	}
	if ($self->{type} !~ /SHOW|TEST/) {
		$str .= "ORDER BY B.OWNER,B.TABLE_NAME,C.COLUMN_POSITION\n";
	} else {
		$str .= "ORDER BY B.OWNER,B.TABLE_NAME\n";
	}

	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %parts = ();
	while (my $row = $sth->fetch)
	{
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[2].$row->[0]";
		}
		# when this is not a composite partition the count is defined
		# when this is not the default number of subpartition
		$parts{"\L$row->[0]\E"}{count} = 0;
		$parts{"\L$row->[0]\E"}{composite} = 0;
		if (exists $subpart{"\L$row->[0]\E"})
		{
			$parts{"\L$row->[0]\E"}{composite} = 1;
			foreach my $k (keys %{$subpart{"\L$row->[0]\E"}}) {
				$parts{"\L$row->[0]\E"}{count} += $subpart{"\L$row->[0]\E"}{$k}{count};
			}
			$parts{"\L$row->[0]\E"}{count} = $row->[3] if (!$parts{"\L$row->[0]\E"}{count});
		} else {
			$parts{"\L$row->[0]\E"}{count} = $row->[3];
		}
		$parts{"\L$row->[0]\E"}{type} = $row->[1];
		if ($self->{type} !~ /SHOW|TEST/) {
			push(@{ $parts{"\L$row->[0]\E"}{columns} }, $row->[5]);
		}
	}
	$sth->finish;

	return %parts;
}

=head2 _get_subpartitioned_table

Return a hash of the partitioned table list with the number of partition.

=cut

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

	return Ora2Pg::MySQL::_get_subpartitioned_table($self) if ($self->{is_mysql});

	my $highvalue = 'A.HIGH_VALUE';
	if ($self->{db_version} =~ /Release [89]/) {
		$highvalue = "'' AS HIGH_VALUE";
	}
	my $condition = '';
	if ($self->{schema}) {
		$condition .= "AND A.TABLE_OWNER='$self->{schema}' ";
	} else {
		$condition .= " AND A.TABLE_OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	# Retrieve all partitions.
	my $str = "SELECT A.TABLE_NAME, A.PARTITION_NAME, A.SUBPARTITION_NAME, A.SUBPARTITION_POSITION, B.SUBPARTITIONING_TYPE, A.TABLE_OWNER, B.PARTITION_COUNT";
	if ($self->{type} !~ /SHOW|TEST/) {
		$str .= ", C.COLUMN_NAME, C.COLUMN_POSITION";
		$str .= " FROM $self->{prefix}_TAB_SUBPARTITIONS A, $self->{prefix}_PART_TABLES B, $self->{prefix}_SUBPART_KEY_COLUMNS C";
	} else {
		$str .= " FROM $self->{prefix}_TAB_SUBPARTITIONS A, $self->{prefix}_PART_TABLES B";
	}
	$str .= " WHERE A.TABLE_NAME = B.TABLE_NAME AND (B.SUBPARTITIONING_TYPE = 'RANGE' OR B.SUBPARTITIONING_TYPE = 'LIST' OR B.SUBPARTITIONING_TYPE = 'HASH')";

        $str .= " AND A.TABLE_NAME = C.NAME" if ($self->{type} !~ /SHOW|TEST/);

	$str .= $self->limit_to_objects('TABLE|PARTITION','A.TABLE_NAME|A.PARTITION_NAME');

	if ($self->{prefix} ne 'USER') {
		if ($self->{type} !~ /SHOW|TEST/) {
			if ($self->{schema}) {
				$str .= "\tAND A.TABLE_OWNER ='$self->{schema}' AND B.OWNER=A.TABLE_OWNER AND C.OWNER=A.TABLE_OWNER\n";
			} else {
				$str .= "\tAND A.TABLE_OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') AND B.OWNER=A.TABLE_OWNER AND C.OWNER=A.TABLE_OWNER\n";
			}
		} else {
			if ($self->{schema}) {
				$str .= "\tAND A.TABLE_OWNER ='$self->{schema}' AND B.OWNER=A.TABLE_OWNER\n";
			} else {
				$str .= "\tAND A.TABLE_OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') AND B.OWNER=A.TABLE_OWNER\n";
			}
		}
	}
	if ($self->{db_version} !~ /Release 8/) {
		$str .= " AND (A.TABLE_OWNER, A.TABLE_NAME) NOT IN (SELECT OWNER, MVIEW_NAME FROM ALL_MVIEWS UNION ALL SELECT LOG_OWNER, LOG_TABLE FROM ALL_MVIEW_LOGS)";
	}
	if ($self->{type} !~ /SHOW|TEST/) {
		$str .= "ORDER BY A.TABLE_OWNER,A.TABLE_NAME,A.PARTITION_NAME,A.SUBPARTITION_POSITION,C.COLUMN_POSITION\n";
	} else {
		$str .= "ORDER BY A.TABLE_OWNER,A.TABLE_NAME,A.PARTITION_NAME,A.SUBPARTITION_POSITION\n";
	}

	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %parts = ();
	while (my $row = $sth->fetch)
	{
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[5].$row->[0]";
		}
		$parts{"\L$row->[0]\E"}{"\L$row->[1]\E"}{type} = $row->[4];
		$parts{"\L$row->[0]\E"}{"\L$row->[1]\E"}{count}++;
		push(@{ $parts{"\L$row->[0]\E"}{"\L$row->[1]\E"}{columns} }, $row->[7]) if (!grep(/^$row->[7]$/, @{ $parts{"\L$row->[0]\E"}{"\L$row->[1]\E"}{columns} }));
	}
	$sth->finish;

	return %parts;
}

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

	# Copy the type translation hash
	my %all_types = %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*([^\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) = @_;

	for (my $idx = 0; $idx <= $#{$data_types}; $idx++) {
		my $data_type = $data_types->[$idx] || '';
		if ($row->[$idx] && $src_data_types->[$idx] =~ /GEOMETRY/) {
			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]);
					$row->[$idx] = $geom_obj->parse_sdo_geometry($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 '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}) {
					use Ora2Pg::GEOM;
					my $geom_obj = new Ora2Pg::GEOM('srid' => $self->{spatial_srid}{$table}->[$idx]);
					$row->[$idx] = $geom_obj->parse_sdo_geometry($row->[$idx]);
					$row->[$idx] = "ST_GeomFromText('" . $row->[$idx] . "', $self->{spatial_srid}{$table}->[$idx])";
				} else {
					$row->[$idx] =~ s/^SRID=(\d+);//;
					$row->[$idx] = "ST_GeomFromText('" . $row->[$idx] . "', $1)";
				}
			}

		} 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);

		}
	}
}

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] !~ /geometry/i) {
					my $dtype = uc($user_type->{src_types}[$i][$j]) || '';
					$dtype =~ s/\(.*//; # remove any precision
					my $utype = {};
					%{$utype} = $self->custom_type_definition($dtype);
					$col_ref->[$i][$j] =  $self->set_custom_type_value($dtype, $utype, $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')]
		$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 no_lob_locator is enabled) where null values fetch as
	# empty string for certain types.
	if ($self->{no_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 =~ /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}) {
			$col = $self->_escape_lob($col, $cond->{raw} ? 'RAW' : 'BLOB', $cond, $isnested);
		} elsif ($cond->{istext}) {
			if ($cond->{clob}) {
				$col = $self->_escape_lob($col, 'CLOB', $cond, $isnested);
			} elsif (!$sprep) {
				$col = $self->escape_insert($col, $isnested);
			}
		} elsif ($cond->{isbit}) {
			$col = "B$q" . $col . "$q";
		} elsif ($cond->{isdate}) {
			if ($col =~ /^0000-00-00/) {
				$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 ($data_type eq 'boolean') {
			if (exists $self->{ora_boolean_values}{lc($col)}) {
				$col = $q . $self->{ora_boolean_values}{lc($col)} . $q;
			}
		} else {
			$col =~ s/([\-]*)(\~|Inf)/'$1Infinity'/i;
			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 ($data_type eq 'boolean') {
			if (exists $self->{ora_boolean_values}{lc($col)}) {
				$col = $self->{ora_boolean_values}{lc($col)};
			}
		} elsif ($cond->{isnum}) {
			$col =~ s/([\-]*)(\~|Inf)/$1Infinity/i;
			$col = '\N' if ($col eq '');
		} elsif ($cond->{isbytea}) {
			$col = $self->_escape_lob($col, $cond->{raw} ? 'RAW' : 'BLOB', $cond, $isnested);
		} elsif ($cond->{istext}) {
			$cond->{clob} ? $col = $self->_escape_lob($col, 'CLOB', $cond, $isnested) : $col = $self->escape_copy($col, $isnested);
		} elsif ($cond->{isdate}) {
			if ($col =~ /^0000-00-00/) {
				$col = $self->{replace_zero_date} || '\N';
			} elsif ($col =~ /^(\d+-\d+-\d+ \d+:\d+:\d+)\.$/) {
				$col = $1;
			}
		} elsif ($cond->{isbit}) {
			$col = $col;
		}
	}
	return $col;
}

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] =~ /GEOMETRY/i ? 1 : 0;
		$hs->{isnum} =    $data_types->[$idx] !~ /^(char|varchar|date|time|text|bytea|xml|uuid|citext)/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->{isbit} = $data_types->[$idx] =~ /bit/i ? 1 : 0;
		$hs->{isnotnull} = 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);
	}
}

=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 writind 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 = $pname || $tname;
	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
		{
			$self->{cfhout} = $self->open_export_file($filename) if (!defined $self->{cfhout});
			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} && not defined $self->{cfhout});
				$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;
	$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*$//;
		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;
				}
			}
		# 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','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')) {
			$AConfig{$var} = $val;
		} elsif ($var eq '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') {
			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(/[\s,]+/, $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(/:/, lc($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 '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;
			}
		}
	}
	$self->close_export_file($fh);

}

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;
			$fcname =~ s/^.*\.//;
			$fcname =~ s/"//g;
			$type = 'FUNCTION' if (!$self->{pg_supports_procedure});
			if ($before) {
				push(@functions, "$before\n");
				$functions[-1] .= "$type $2 $3\n";
			} else {
				push(@functions, "$type $fcname $3\n");
			}
			$before = '';
		} elsif ($fcname) {
			$functions[-1] .= "$lines[$i]\n";
		} else {
			$before .= "$lines[$i]\n";
		}
		$fcname = '' if ($lines[$i] =~ /^\s*END\s+$fcname\b/i);
	}

	map { s/\bEND\s+(?!IF|LOOP|CASE|INTO|FROM|,)[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}{text});

	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*(.*)/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);
		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}...\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}...\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 = lc("$dirprefix$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
	while ($$content =~ s/('[^';\n]*)\/\*([^';\n]*')/$1\%OPEN_COMMENT\%$2/s) {};

	# Fix unterminated comment at end of the code
	$$content =~ s/(\/\*(?:(?!\*\/).)*)$/$1 \*\//s;

	# 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 some other cases that are breaking the parser (presence of -- in constant string)
	my @lines = split(/([\n\r]+)/, $$content);
	for (my $i = 0; $i <= $#lines; $i++)
	{
		next if ($lines[$i] !~ /\S/);

		# 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);

	# 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}++;
	}

	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/^.*\.//;
	$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});
	$fname =~ s/"_"/_/gs;

	$fct_detail{args} =~ s/\s+IN\s+/ /igs; # Remove default IN keyword
	# 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$2/gs;
	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) {
				$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 reordering if needed
	$fct_detail{args} = '(' . join(',', @args_sorted) . ')';

	# 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 ($self->{pg_supports_procedure} && ($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;
	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";
		} 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";
	}

	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${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})
		{
			$function = "\n${fct_warning}CREATE$self->{create_or_replace} $type " . $self->quote_object_name("$owner.$fname") . " $fct_detail{args}";
			$name =  $self->quote_object_name("$owner.$fname");
			$self->logit("Parsing function " . $self->quote_object_name("$owner.$fname") . "...\n", 1);
		}
		elsif ($self->{export_schema} && $self->{schema})
		{
			$function = "\n${fct_warning}CREATE$self->{create_or_replace} $type " . $self->quote_object_name("$self->{schema}.$fname") . " $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
	{
		$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;
};
		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 := 'SELECT * FROM $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 := 'SELECT * FROM $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 := 'SELECT true FROM $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";

		# Remove parameters to RETURN call when the function has no RETURNS clause
		if ($function !~ /\s+RETURNS\s+/s || ($function =~ /\s+RETURNS VOID\s+/s || ($type eq 'PROCEDURE' && $self->{pg_supports_procedures}))) {
			$function =~ s/(RETURN)\s+[^;]+;/$1;/igs;
		}
		$revoke = "-- REVOKE ALL ON $type $name $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}) {
					$function .= "SECURITY DEFINER\n" if ($self->{security}{"\U$fct_detail{name}\E"}{security} eq 'DEFINER');
				} else  {
					$function .= "SECURITY DEFINER\n" if ($fct_detail{security} eq 'DEFINER');
				}
			} else {
				$function .= "SECURITY DEFINER\n" if ($self->{security}{"\U$pname\E"}{security} eq 'DEFINER');
			}
		}
		$fct_detail{immutable} = '' if ($fct_detail{code} =~ /\b(UPDATE|INSERT|DELETE)\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 $fname $fct_detail{args} OWNER TO";
			$function .= " " . $self->quote_object_name($owner) . ";\n";
		}
	}
	$function .= "\nCOMMENT ON FUNCTION $fname$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-2020 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();
		$sql_header .= "SET check_function_bodies = false;\n\n" if (!$self->{function_check});
		$sql_header = '' if ($self->{no_header});

		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 =~ 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;
	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, $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;
			}
		}
	}

	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 $unsupported = "-- Unsupported, please edit to match PostgreSQL syntax\n";
	my $content = '';
	my $type_name = '';

	# Replace SUBTYPE declaration into DOMAIN declaration
        if ($plsql =~ s/SUBTYPE\s+/CREATE DOMAIN /i) {
		$plsql =~ s/\s+IS\s+/ AS /;
		$plsql = Ora2Pg::PLSQL::replace_sql_type($plsql, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, %{$self->{data_type}});
		return $plsql;
	}

	$plsql =~ s/\s*INDEX\s+BY\s+([^\s;]+)//is;
	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;
		my $internal_name = $type_name;
		if ($self->{export_schema} && !$self->{schema} && $owner) {
			$type_name = "$owner.$type_name";
		}
		$internal_name  =~ 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 = Ora2Pg::PLSQL::replace_sql_type($type_of, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, %{$self->{data_type}});
			$self->{type_of_type}{'Nested Tables'}++;
			$content = "CREATE TYPE \L$type_name\E AS (\L$internal_name\E $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 ($self->{export_schema} && !$self->{schema} && $owner) {
			$type_name = "$owner.$type_name";
		}
		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 = Ora2Pg::PLSQL::replace_sql_type($description, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, %{$self->{data_type}});
		$type_name =~ s/"//g;
		$type_name = $self->get_replaced_tbname($type_name);
		if ($notfinal =~ /FINAL/is) {
			$content = "-- Inherited types are not supported in PostgreSQL, replacing with inherited table\n";
			$content .= qq{CREATE TABLE $type_name (
$declar
);
};
			$self->{type_of_type}{'Type inherited'}++;
		} else {
			$content = qq{
CREATE TYPE $type_name AS (
$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 ($self->{export_schema} && !$self->{schema} && $owner) {
			$type_name = "$owner.$type_name";
		}
		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 = Ora2Pg::PLSQL::replace_sql_type($description, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, %{$self->{data_type}});
		$type_name =~ s/"//g;
		$type_name = $self->get_replaced_tbname($type_name);
		$content = qq{
CREATE TABLE $type_name (
$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;
		$tbname =~ s/;//g;
		my $internal_name = $type_name;
		chomp($tbname);
		if ($self->{export_schema} && !$self->{schema} && $owner) {
			$type_name = "$owner.$type_name";
		}
		$internal_name  =~ s/^[^\.]+\.//;
		my $declar = Ora2Pg::PLSQL::replace_sql_type($tbname, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, %{$self->{data_type}});
		$declar =~ s/[\n\r]+//s;
		$content = qq{
CREATE TYPE \L$type_name\E AS ($internal_name $declar\[$size\]);
};
		$self->{type_of_type}{Varrays}++;
	} 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}{$stt->[$idx]}) {
				%{ $self->{user_type}{$stt->[$idx]} } = $self->custom_type_definition($stt->[$idx]);
			}
		}
	}

	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} =~ /\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 $cond = " ABS(MOD(" . $self->{defined_pk}{"\L$table\E"} . ", $self->{oracle_copies})) = ?";
			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} = ();

	if ($self->{is_mysql}) {
		my %col_info = Ora2Pg::MySQL::_column_info($self, $rname);
		foreach my $col (keys %{$col_info{$rname}}) {
			push(@{$self->{data_cols}{$table}}, $col);
		}
	}

	# 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}) {
			# 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}) {
			if ($self->{no_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);
			}
			foreach (@{$sth->{NAME}}) {
				push(@{$self->{data_cols}{$table}}, $_);
			}
		} else {
			#$query .= " LIMIT ?, ?";
			$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);
		}

	} 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})
		{
			# 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->{no_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);
			}
			foreach (@{$sth->{NAME}}) {
				push(@{$self->{data_cols}{$table}}, $_);
			}
		} else {
			#$query .= " LIMIT ?, ?";
			$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);
		}

	}

	# 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 . "\n", 0, 1);
	} else {
		$sth->execute(@params) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\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->{no_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
				next if ($self->{oracle_speed} && !$self->{ora2pg_speed});

				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] !~ /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 ($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}++;

				# Do we just want to test Oracle output speed
				next if ($self->{oracle_speed} && !$self->{ora2pg_speed});

				push(@rows, [ @row ] );

				if ($#rows == $data_limit)
				{
					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
			next if ($self->{oracle_speed} && !$self->{ora2pg_speed});

			# 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});
#				if ( ($self->{parallel_tables} > 1) || (($self->{oracle_copies} > 1) && $self->{defined_pk}{"\L$table\E"}) ) {
#					my $max_jobs = $self->{jobs};
#					while ($self->{child_count} >= $max_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 = ();
			}
		}

		if (@rows && (!$self->{oracle_speed} || $self->{ora2pg_speed})) {
			$total_record += @rows;
			$self->{current_total_row} += @rows;
#			if ( ($self->{parallel_tables} > 1) || (($self->{oracle_copies} > 1) && $self->{defined_pk}{"\L$table\E"}) ) {
#				my $max_jobs = $self->{jobs};
#				while ($self->{child_count} >= $max_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);
#			}
		}
	}

	$sth->finish();

	# 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->{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 _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}) {
			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) . ");\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-00-00/) {
							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->{prefix_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);

	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);
	}

	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) {
			$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')
	{
		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);
			my ($my_encoding, $my_client, $pg_encoding, $my_timestamp_format, $my_date_format) = &Ora2Pg::MySQL::_get_encoding($self, $self->{dbh});
			$self->logit("Showing current MySQL encoding and possible PostgreSQL client encoding:\n", 0);
			$self->logit("\tMySQL database and client encoding: $my_encoding\n", 0);
			$self->logit("\tMySQL collation encoding: $my_client\n", 0);
			$self->logit("\tPostgreSQL CLIENT_ENCODING: $pg_encoding\n", 0);
			$self->logit("MySQL SQL mode: $self->{mysql_mode}\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}) {
				my $dim = 6;
				$dim = '' if ($self->{db_version} =~ /Release  [89]/);
				$self->logit("\tOracle NLS_TIMESTAMP_FORMAT YYYY-MM-DD HH24:MI:SS.FF$dim\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);

			my ($ora_encoding, $ora_charset, $pg_encoding, $nls_timestamp_format, $nls_date_format) = $self->_get_encoding($self->{dbh});
			$self->logit("Showing current Oracle encoding and possible PostgreSQL client encoding:\n", 0);
			$self->logit("\tOracle NLS_LANG $ora_encoding\n", 0);
			$self->logit("\tOracle NLS_NCHAR $ora_charset\n", 0);
			$self->logit("\tOracle NLS_TIMESTAMP_FORMAT $nls_timestamp_format\n", 0);
			$self->logit("\tOracle NLS_DATE_FORMAT $nls_date_format\n", 0);
			$self->logit("\tPostgreSQL CLIENT_ENCODING $pg_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')
	{
		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';
		}
		# 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} = ();
		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;
				# Set the table information for each class found
				foreach my $t (sort keys %{$self->{tables}})
				{
					# Set the total number of rows
					$total_row_num += $self->{tables}{$t}{table_info}{num_rows};

					# 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}})
					{
						$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 (!$self->{is_mysql}) {
							if (!exists $self->{data_type}{uc($self->{tables}{$t}{column_info}{$k}[1])}) {
								$table_detail{'unknown types'}++;
							}
						} else {
							if (!exists $Ora2Pg::MySQL::MYSQL_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}}) {
					$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});
				}
				my %largest_table = ();
				%largest_table = $self->_get_largest_tables() if ($self->{is_mysql});
				if ((scalar keys %largest_table > 0) || !$self->{is_mysql}) {
					$i = 1;
					if (!$self->{is_mysql}) {
						$report_info{'Objects'}{$typ}{'detail'} .= "Top $self->{top_max} of largest tables:\n";
						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";
							$i++;
							last if ($i > $self->{top_max});
						}
					} else {
						$report_info{'Objects'}{$typ}{'detail'} .= "Top $self->{top_max} of largest tables:\n";
						foreach my $t (sort {$self->{tables}{$b}{table_info}{size} <=> $self->{tables}{$a}{table_info}{size}} keys %{$self->{tables}}) {
							$report_info{'Objects'}{$typ}{'detail'} .= "\L$t\E: $self->{tables}{$t}{table_info}{size} MB (" . $self->{tables}{$t}{table_info}{num_rows} . " rows)\n";
							$i++;
							last if ($i > $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'};
					}
				}
				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 = 0;
				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}) {
					$total_size += length($trig->[4]);
					if ($self->{estimate_cost}) {
						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $trig->[4]);
						$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}) {
					$total_size += length($functions->{$fct}{text});
					if ($self->{estimate_cost}) {
						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $functions->{$fct}{text});
						$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})
				{
					$total_size += length($procedures->{$proc}{text});
					if ($self->{estimate_cost}) {
						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $procedures->{$proc}{text});
						$report_info{'Objects'}{$typ}{'cost_value'} += $cost;
						$report_info{'Objects'}{$typ}{'detail'} .= "\L$proc: $cost\E\n";
						$report_info{full_function_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_function_details}{"\L$proc\E"}{info} .= "\t$d => $cost_detail{$d}";
							$report_info{full_function_details}{"\L$proc\E"}{info} .= " (cost: ${$uncovered_score}{$d})" if (${$uncovered_score}{$d});
							$report_info{full_function_details}{"\L$proc\E"}{info} .= "\n";
							push(@{$report_info{full_function_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} = ();
					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});
								$report_info{'Objects'}{$typ}{'cost_value'} += $cost;
								$report_info{'Objects'}{$typ}{'detail'} .= "\L$f: $cost\E\n";
								$report_info{full_function_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_function_details}{"\L$f\E"}{info} .= "\t$d => $cost_detail{$d}";
									$report_info{full_function_details}{"\L$f\E"}{info} .= " (cost: ${$uncovered_score}{$d})" if (${$uncovered_score}{$d});
									$report_info{full_function_details}{"\L$f\E"}{info} .= "\n";
									push(@{$report_info{full_function_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$synonyms{$t}{owner}.$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'} .= " $partitions{$t} $t partitions.\n";
				}
				$report_info{'Objects'}{$typ}{'comment'} = "Partitions are exported using table inheritance and check constraint. Hash and Key partitions are not supported by PostgreSQL and 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_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});
				}
			}
			$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();

		# 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}};
			}
		}

		# 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_sort_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;
		}

		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(/^$t$/, @done)) {
				$self->logit("Duplicate entry found: $t\n", 1);
				next;
			} else {
				push(@done, $t);
			}
			my $warning = '';

			# 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';
			}
			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}}) {
					# 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/\D//g;
					my $type = $self->_sql_type($d->[1], $d->[2], $d->[5], $d->[6], $d->[4]);
					$type = "$d->[1], $d->[2]" if (!$type);

					# Check if we need auto increment
					$warning = '';
					if ($d->[12] eq 'auto_increment' || $d->[12] eq '1') {
						if ($type !~ s/bigint/bigserial/) {
							if ($type !~ s/smallint/smallserial/) {
								$type =~ s/integer/serial/;
							}
						}
						if ($type =~ /serial/) {
							$warning = " - Seq last value: $tables_infos{$t}{auto_increment}";
						}
					}
					$type = $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];
					}
					$self->logit("\t$d->[0] : $d->[1]");
					if ($d->[1] !~ /SDO_GEOMETRY/) {
						if ($d->[2] && !$d->[5]) {
							$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 = $type;
							$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_SDO_GTYPE{0};
						} else {
							$gtypes = $d->[14];
						}
						$type = "geometry($gtypes$suffix";
						if ($d->[12]) {
							$type .= ",$d->[12]";
						}
						$type .= ")";
						$type .= " - $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)}})) {
						$type = '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)) {
						$type = '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}) {
							$type .= " GENERATED $self->{identity_info}{$d->[8]}{$d->[0]}{generation} AS IDENTITY";
							$type .= " (" . $self->{identity_info}{$d->[8]}{$d->[0]}{options} . ')' if (exists $self->{identity_info}{$d->[8]}{$d->[0]}{options} && $self->{identity_info}{$d->[8]}{$d->[0]}{options} ne '');
						} else {
							$type =~ s/bigint$/bigserial/;
							$type =~ s/smallint/smallserial/;
							$type =~ 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');
					$self->logit(" => $type$warning$align$virtual$encrypted\n");
				}
			}
			$i++;
		}
		$self->logit("----------------------------------------------------------\n", 0);
		$self->logit("Total number of rows: $total_row_num\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) {
			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_mysql}) {
			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++;
			}
		} else {
			foreach my $t (sort {$tables_infos{$b}{size} <=> $tables_infos{$a}{size}} keys %tables_infos) {
				last if ($i > $self->{top_max});
				my $tname = $t;
				$self->logit("\t[$i] TABLE $tname: $tables_infos{$t}{size} 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 "$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 $orig = '';
	$orig = " (origin: $table)" if (lc($tbmod) ne lc($table));
	my $tbname = $tbmod;
	$tbname =~ s/[^"\.]+\.//;
	if ($self->{pg_schema} && $self->{export_schema}) {
		return ($tbmod, $orig, $self->{pg_schema}, "$self->{pg_schema}.$tbname");
	} elsif ($self->{schema} && $self->{export_schema}) {
		return ($tbmod, $orig, $self->{schema}, "$self->{schema}.$tbname");
	}

	return ($tbmod, $orig, '', $tbmod);
}

sub get_schema_condition
{
	my ($self, $attrname, $visible) = @_;

	$attrname ||= 'n.nspname';

	if ($self->{pg_schema} && $self->{export_schema}) {
		return " AND $attrname IN ('" . join("','", split(/\s*,\s*/, lc($self->{pg_schema}))) . "')";
	} elsif ($self->{schema} && $self->{export_schema}) {
		return "AND $attrname = '\L$self->{schema}\E'";
	}

	my $cond = '';
	if ($visible && !$self->{pg_schema}) {
		$cond = " AND $visible";
	}
	$cond .= " AND $attrname <> 'pg_catalog' AND $attrname <> 'information_schema' AND $attrname !~ '^pg_toast'";

	return $cond;
}


sub _table_row_count
{
	my $self = shift;

	my $lbl = 'ORACLEDB';
	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});

	# 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) {
		print "$lbl:$t:$tables_infos{$t}{num_rows}\n";
		if ($self->{pg_dsn}) {
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
			my $s = $self->{dbhdest}->prepare("SELECT count(*) FROM $both;") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
			if (not $s->execute) {
				push(@errors, "Table $both$orig does not exists in PostgreSQL database.") if ($s->state eq '42P01');
				next;
			}
			while ( my @row = $s->fetchrow()) {
				print "POSTGRES:$both$orig:$row[0]\n";
				if ($row[0] != $tables_infos{$t}{num_rows}) {
					push(@errors, "Table $both$orig doesn't have the same number of line in source database ($tables_infos{$t}{num_rows}) and in PostgreSQL ($row[0]).");
				}
				last;
			}
			$s->finish();
		}
	}
	$self->show_test_errors('rows', @errors);
}

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($self->{count_rows});

	my $lbl = 'ORACLEDB';
	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});

	####
	# Test number of index in tables
	####
	print "[TEST INDEXES COUNT]\n";
	my ($uniqueness, $indexes, $idx_type, $idx_tbsp) = $self->_get_indexes('', $self->{schema}, 1);
	if ($self->{is_mysql}) {
		$indexes = Ora2Pg::MySQL::_count_indexes($self, '', $self->{schema});
	}
	foreach my $t (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);
			$schema = $self->get_schema_condition('schemaname');
			$tbmod =~ s/^([^\.]+\.)//; # Remove schema part from table name
			my $s = $self->{dbhdest}->prepare("SELECT count(*) FROM pg_indexes WHERE tablename = '\L$tbmod\E'$schema;") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
			$tbmod = $1 . $tbmod  if ($1);
			if (not $s->execute) {
				push(@errors, "Can not extract information from catalog table pg_indexes.");
				next;
			}
			while ( my @row = $s->fetchrow()) {
				print "POSTGRES:$both$orig:$row[0]\n";
				if ($row[0] != $numixd) {
					push(@errors, "Table $both$orig doesn't have the same number of indexes in source database ($numixd) and in PostgreSQL ($row[0]).");
				}
				last;
			}
			$s->finish();
		}
	}
	$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');
	my $schema_cond = $self->get_schema_condition('pg_indexes.schemaname');
	my $sql = qq{
SELECT count(*)
FROM pg_indexes
JOIN pg_class ON (pg_class.relname=pg_indexes.indexname)
JOIN pg_constraint ON (pg_constraint.conname=pg_class.relname AND pg_constraint.connamespace=pg_class.relnamespace)
WHERE pg_indexes.tablename=?
AND pg_constraint.contype IN ('u')
 $schema_cond
};

	my $s = undef;
	if ($self->{pg_dsn}) {
		$s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	}
	foreach my $t (keys %unique_keys) {
		next if (!exists $tables_infos{$t});
		my $numixd = scalar keys %{$unique_keys{$t}};
		print "$lbl:$t:$numixd\n";
		if ($self->{pg_dsn}) {
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
			$tbmod =~ s/^([^\.]+\.)//; # Remove schema part from table name
			if (not $s->execute(lc($tbmod))) {
				push(@errors, "Can not extract information from catalog about unique constraints.");
				next;
			}
			$tbmod = $1 . $tbmod  if ($1);
			while ( my @row = $s->fetchrow()) {
				print "POSTGRES:$both$orig:$row[0]\n";
				if ($row[0] != $numixd) {
					push(@errors, "Table $both$orig doesn't have the same number of unique constraints in source database ($numixd) and in PostgreSQL ($row[0]).");
				}
				last;
			}
		}
	}
	$s->finish() if ($self->{pg_dsn});
	$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_indexes.schemaname');
	$sql = qq{
SELECT count(*)
FROM pg_indexes
JOIN pg_class ON (pg_class.relname=pg_indexes.indexname)
JOIN pg_constraint ON (pg_constraint.conname=pg_class.relname AND pg_constraint.connamespace=pg_class.relnamespace)
WHERE pg_indexes.tablename=?
AND pg_constraint.contype IN ('p')
 $schema_cond
};
	if ($self->{pg_dsn}) {
		$s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	}
	foreach my $t (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);
			$tbmod =~ s/^([^\.]+\.)//; # Remove schema part from table name
			if (not $s->execute(lc($tbmod))) {
				push(@errors, "Can not extract information from catalog about primary keys.");
				next;
			}
			$tbmod = $1 . $tbmod  if ($1);
			while ( my @row = $s->fetchrow()) {
				print "POSTGRES:$both$orig:$row[0]\n";
				if ($row[0] != $nbpk) {
					push(@errors, "Table $both$orig doesn't have the same number of primary keys in source database ($nbpk) and in PostgreSQL ($row[0]).");
				}
				last;
			}
		}
	}
	$s->finish() if ($self->{pg_dsn});
	%unique_keys = ();
	$self->show_test_errors('primary keys', @errors);
	@errors = ();

	####
	# Test check constraints
	####
	if (!$self->{is_mysql}) {
		print "\n";
		print "[TEST CHECK CONSTRAINTS COUNT]\n";
		my %check_constraints = $self->_check_constraint('',$self->{schema});
		$schema_cond = $self->get_schema_condition();
		$sql = qq{
SELECT 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 c.relname = ? AND r.contype = 'c'
$schema_cond
};
		if ($self->{pg_dsn}) {
			$s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
		}
		foreach my $t (keys %check_constraints) {
			next if (!exists $tables_infos{$t});
			my $nbcheck = 0;
			foreach my $cn (keys %{$check_constraints{$t}{constraint}}) {
				$nbcheck++ if ($check_constraints{$t}{constraint}{$cn}{condition} !~ /IS NOT NULL$/);
			}
			print "$lbl:$t:$nbcheck\n";
			if ($self->{pg_dsn}) {
				my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
				$tbmod =~ s/^([^\.]+\.)//; # Remove schema part from table name
				if (not $s->execute(lc($tbmod))) {
					push(@errors, "Can not extract information from catalog about check constraints.");
					next;
				}
				$tbmod = $1 . $tbmod  if ($1);
				while ( my @row = $s->fetchrow()) {
					print "POSTGRES:$both$orig:$row[0]\n";
					if ($row[0] != $nbcheck) {
						push(@errors, "Table $both$orig doesn't have the same number of check constraints in source database ($nbcheck) and in PostgreSQL ($row[0]).");
					}
					last;
				}
			}
		}
		$s->finish() if ($self->{pg_dsn});
		%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();
	$sql = qq{
SELECT 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 e.relname = ?
  AND a.attnum > 0
  AND NOT a.attisdropped AND a.attnotnull 
 $schema_cond
};
	if ($self->{pg_dsn}) {
		$s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	}
	foreach my $t (keys %column_infos) {
		next if (!exists $tables_infos{$t});
		my $nbnull = 0;
		foreach my $cn (keys %{$column_infos{$t}}) {
			if ($column_infos{$t}{$cn}{nullable} =~ /^N/) {
				$nbnull++;
			}
		}
		print "$lbl:$t:$nbnull\n";
		if ($self->{pg_dsn}) {
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
			$tbmod =~ s/^([^\.]+\.)//; # Remove schema part from table name
			if (not $s->execute(lc($tbmod))) {
				push(@errors, "Can not extract information from catalog about not null constraints.");
				next;
			}
			$tbmod = $1 . $tbmod  if ($1);
			while ( my @row = $s->fetchrow()) {
				print "POSTGRES:$both$orig:$row[0]\n";
				if ($row[0] != $nbnull) {
					push(@errors, "Table $both$orig doesn't have the same number of not null constraints in source database ($nbnull) and in PostgreSQL ($row[0]).");
				}
				last;
			}
		}
	}
	$s->finish() if ($self->{pg_dsn});
	$self->show_test_errors('not null constraints', @errors);
	@errors = ();

	####
	# Test NOT NULL constraints
	####
	print "\n";
	print "[TEST COLUMN DEFAULT VALUE COUNT]\n";
	$schema_cond = $self->get_schema_condition();
	$sql = qq{
SELECT a.attname,
  (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)
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 e.relname = ? AND a.attnum > 0 AND NOT a.attisdropped
 $schema_cond
};
	if ($self->{pg_dsn}) {
		$s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	}
	my @seqs = ();
	if ($self->{is_mysql}) {
		@seqs = Ora2Pg::MySQL::_count_sequences($self);
	}
	foreach my $t (keys %column_infos) {
		next if (!exists $tables_infos{$t});
		my $nbdefault = 0;
		foreach my $cn (keys %{$column_infos{$t}}) {
			if ($column_infos{$t}{$cn}{default} ne '' && uc($column_infos{$t}{$cn}{default}) ne 'NULL') {
				$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);
			$tbmod =~ s/^([^\.]+\.)//; # Remove schema part from table name
			if (not $s->execute(lc($tbmod))) {
				push(@errors, "Can not extract information from catalog about column default value.");
				next;
			}
			$tbmod = $1 . $tbmod  if ($1);
			my $pgdef = 0;
			while ( my @row = $s->fetchrow()) {
				$pgdef++ if ($row[1] ne '');
			}
			print "POSTGRES:$both$orig:$pgdef\n";
			if ($pgdef != $nbdefault) {
				push(@errors, "Table $both$orig doesn't have the same number of column default value in source database ($nbdefault) and in PostgreSQL ($pgdef).");
			}
		}
	}
	$s->finish() if ($self->{pg_dsn});
	%column_infos = ();
	$self->show_test_errors('column default value', @errors);
	@errors = ();

	####
	# 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();
	$sql = qq{
SELECT 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 c.relname = ?
  AND r.contype = 'f'
 $schema_cond
};
	if ($self->{pg_dsn}) {
		$s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	}
	foreach my $t (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);
			$tbmod =~ s/^([^\.]+\.)//; # Remove schema part from table name
			if (not $s->execute(lc($tbmod))) {
				push(@errors, "Can not extract information from catalog about foreign key constraints.");
				next;
			}
			$tbmod = $1 . $tbmod  if ($1);
			while ( my @row = $s->fetchrow()) {
				print "POSTGRES:$both$orig:$row[0]\n";
				if ($row[0] != $nbfk) {
					push(@errors, "Table $both$orig doesn't have the same number of foreign key constraints in source database ($nbfk) and in PostgreSQL ($row[0]).");
				}
				last;
			}
		}
	}
	$s->finish() if ($self->{pg_dsn});
	$self->show_test_errors('foreign keys', @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 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 c.relname = ?
  AND (NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D'))
 $schema_cond
};
	if ($self->{pg_dsn}) {
		$s = $self->{dbhdest}->prepare($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
	}
	foreach my $t (keys %triggers) {
		next if (!exists $tables_infos{$t});
		my $nbtrg = $#{$triggers{$t}}+1;
		print "$lbl:$t:$nbtrg\n";
		if ($self->{pg_dsn}) {
			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
			$tbmod =~ s/^([^\.]+\.)//; # Remove schema part from table name
			if (not $s->execute(lc($tbmod))) {
				push(@errors, "Can not extract information from catalog about foreign key constraints.");
				next;
			}
			$tbmod = $1 . $tbmod  if ($1);
			while ( my @row = $s->fetchrow()) {
				print "POSTGRES:$both$orig:$row[0]\n";
				if ($row[0] != $nbtrg) {
					push(@errors, "Table $both$orig doesn't have the same number of triggers in source database ($nbtrg) and in PostgreSQL ($row[0]).");
				}
				last;
			}
		}
	}
	$s->finish() if ($self->{pg_dsn});
	$self->show_test_errors('table triggers', @errors);
	@errors = ();

	####
	# Test partitions
	####
	print "\n";
	print "[TEST PARTITION COUNT]\n";
	my %partitions = $self->_get_partitioned_table();
	$schema_cond = $self->get_schema_condition('nmsp_parent.nspname');
	$schema_cond =~ s/^ AND/ WHERE/;
	$sql = qq{
SELECT
    nmsp_parent.nspname     AS parent_schema,
    parent.relname          AS parent,
    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
$schema_cond
GROUP BY                                                                    
    parent_schema,
    parent;
};
	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()) {
			$pg_part{$row[1]} = $row[2];
		}
		$s->finish();
	}
	foreach my $t (keys %partitions) {
		next if (!exists $tables_infos{$t});
		print "$lbl:$t:", $partitions{"\L$t\E"}{count}, "\n";
		my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
		if (exists $pg_part{$tbmod}) {
			print "POSTGRES:$both$orig:$pg_part{$tbmod}\n";
			if ($pg_part{$tbmod} != $partitions{"\L$t\E"}{count}) {
				push(@errors, "Table $both$orig doesn't have the same number of partitions in source database (" . $partitions{"\L$t\E"}{count} . ") and in PostgreSQL ($pg_part{$tbmod}).");
			}
		} else {
			push(@errors, "Table $both$orig doesn't have the same number of partitions in source database (" . $partitions{"\L$t\E"}{count} . ") and in PostgreSQL (0).");
		}
	}
	$self->show_test_errors('PARTITION', @errors);
	@errors = ();

	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','')
 $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 = ();

	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 IN ('v','')
      $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()) {
			$list_views{$row[0]} = $row[1];
		}
		$s->finish();
	}

	my $lbl = 'ORACLEDB';
	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});

	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
		$sql = "SELECT count(*) FROM $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:$v:", join('|', @row), "\n";
		$sth->finish;
		# Execute view in the PostgreSQL database
		$sql = "SELECT count(*) FROM $v;";
		$sth = $self->{dbhdest}->prepare($sql)  or $self->logit("ERROR: " . $self->{dbhdest}->errstr . "\n", 0, 0);
		$sth->execute or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 0);
		@row = $sth->fetchrow();
		$sth->finish;
		my $pg_ct = $row[0];
		print "POSTGRES:$v:", 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});

	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','')
      $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','')
      $schema_clause
};
	} elsif ($obj_type eq 'SEQUENCE') {
		my $obj_infos = ();
		if (!$self->{is_mysql}) {
			$obj_infos = $self->_get_sequences();
		} else {
			$obj_infos = Ora2Pg::MySQL::_count_sequences($self);
		}
		$nbobj = $#{$obj_infos} + 1;
		$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','')
      $schema_clause
};
	} elsif ($obj_type eq 'TYPE') {
		my $obj_infos = $self->_get_types();
		$nbobj = $#{$obj_infos} + 1;
		$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)
  $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','')
      $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});

	####
	# Test number of function
	####
	print "\n";
	print "[TEST FUNCTION COUNT]\n";
	my @fct_infos = $self->_list_all_funtions();
	#my $schema_clause = $self->get_schema_condition('', 'pg_catalog.pg_function_is_visible(p.oid)');
	my $schema_clause = "    AND n.nspname NOT IN ('pg_catalog','information_schema')";
	$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'
$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 funtion
		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 =~ /^[^\.]+\.$f$/i);
				} else {
					$found = 1, last if ($pgf =~ /^$f$/i);
				}
			}
			push(@errors, "Function $f is missing in PostgreSQL database.") if (!$found);
		}
	}
	$self->show_test_errors('FUNCTION', @errors);
	@errors = ();
	print "\n";
}

=head2 _get_version

This function retrieves the Oracle version information

=cut

sub _get_version
{
	my $self = shift;

	return Ora2Pg::MySQL::_get_version($self) if ($self->{is_mysql});

	my $oraver = '';
	my $sql = "SELECT BANNER FROM v\$version";

        my $sth = $self->{dbh}->prepare( $sql ) or return undef;
        $sth->execute or return undef;
	while ( my @row = $sth->fetchrow()) {
		$oraver = $row[0];
		last;
	}
	$sth->finish();

	chomp($oraver);
	$oraver =~ s/ \- .*//;

	return $oraver;
}

=head2 _get_database_size

This function retrieves the size of the Oracle database in MB

=cut

sub _get_database_size
{
	my $self = shift;

	return Ora2Pg::MySQL::_get_database_size($self) if ($self->{is_mysql});

	my $mb_size = '';
	my $sql = "SELECT sum(bytes)/1024/1024 FROM USER_SEGMENTS";
	if (!$self->{user_grants}) {
		$sql = "SELECT sum(bytes)/1024/1024 FROM DBA_SEGMENTS";
		if ($self->{schema}) {
			$sql .= " WHERE OWNER='$self->{schema}' ";
		} else {
			$sql .= " WHERE OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
		}
	}
        my $sth = $self->{dbh}->prepare( $sql ) or return undef;
        $sth->execute or return undef;
	while ( my @row = $sth->fetchrow()) {
		$mb_size = sprintf("%.2f MB", $row[0]);
		last;
	}
	$sth->finish();

	return $mb_size;
}

=head2 _get_objects

This function retrieves all object the Oracle information
except SYNONYM and temporary objects

=cut

sub _get_objects
{
	my $self = shift;

	return Ora2Pg::MySQL::_get_objects($self) if ($self->{is_mysql});

	my $oraver = '';
	# OWNER|OBJECT_NAME|SUBOBJECT_NAME|OBJECT_ID|DATA_OBJECT_ID|OBJECT_TYPE|CREATED|LAST_DDL_TIME|TIMESTAMP|STATUS|TEMPORARY|GENERATED|SECONDARY
	my $sql = "SELECT OBJECT_NAME,OBJECT_TYPE,STATUS FROM $self->{prefix}_OBJECTS WHERE TEMPORARY='N' AND GENERATED='N' AND SECONDARY='N' AND OBJECT_TYPE <> 'SYNONYM'";
        if ($self->{schema}) {
                $sql .= " AND OWNER='$self->{schema}'";
        } else {
                $sql .= " AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
        }
	my @infos = ();
        my $sth = $self->{dbh}->prepare( $sql ) or return undef;
	push(@infos, join('|', @{$sth->{NAME}}));
        $sth->execute or return undef;
	my %count = ();
	while ( my @row = $sth->fetchrow())
	{
		my $valid = ($row[2] eq 'VALID') ? 0 : 1;
		push(@{$infos{$row[1]}}, { ( name => $row[0], invalid => $valid ) });
		$count{$row[1]}{$valid}++;
	}
	$sth->finish();

	if ($self->{debug})
	{
		foreach my $k (sort keys %count)
		{
			print STDERR "\tFound $count{$k}{0} valid and ", ($count{$k}{1}||0), " invalid object $k\n";
		}
	}

	return %infos;
}

sub _list_all_funtions
{
	my $self = shift;

	return Ora2Pg::MySQL::_list_all_funtions($self) if ($self->{is_mysql});

	my $oraver = '';
	# OWNER|OBJECT_NAME|PROCEDURE_NAME|OBJECT_TYPE
	my $sql = qq{
SELECT p.owner,p.object_name,p.procedure_name,o.object_type
  FROM $self->{prefix}_PROCEDURES p
  JOIN $self->{prefix}_OBJECTS o ON p.owner = o.owner
   AND p.object_name = o.object_name 
 WHERE o.object_type IN ('PROCEDURE','PACKAGE','FUNCTION')
   AND o.TEMPORARY='N' AND o.GENERATED='N' AND o.SECONDARY='N'
   AND o.STATUS = 'VALID'
};
	if ($self->{db_version} =~ /Release 8/) {
		$sql = qq{
SELECT p.owner,p.object_name,p.procedure_name,o.object_type
  FROM $self->{prefix}_PROCEDURES p, $self->{prefix}_OBJECTS o
 WHERE o.object_type IN ('PROCEDURE','PACKAGE','FUNCTION')
   AND p.owner = o.owner AND p.object_name = o.object_name
   AND o.TEMPORARY='N' AND o.GENERATED='N' AND o.SECONDARY='N'
   AND o.STATUS = 'VALID'
};
	}
        if ($self->{schema}) {
                $sql .= " AND p.OWNER='$self->{schema}'";
        } else {
                $sql .= " AND p.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
        }
	my @infos = ();
        my $sth = $self->{dbh}->prepare( $sql ) or return undef;
        $sth->execute or return undef;
	while ( my @row = $sth->fetchrow()) {
		next if (($row[3] eq 'PACKAGE') && !$row[2]);
		if ( $row[2] ) {
			# package_name.fct_name
			push(@infos, lc("$row[1].$row[2]"));
		} else {
			# owner.fct_name
			push(@infos, lc($row[1]));
		}
	}
	$sth->finish();

	return @infos;
}

=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;

	return Ora2Pg::MySQL::_schema_list($self) if ($self->{is_mysql});

	my $sql = "SELECT DISTINCT OWNER FROM ALL_TABLES WHERE OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ORDER BY OWNER";

        my $sth = $self->{dbh}->prepare( $sql ) or return undef;
        $sth->execute or return undef;
        $sth;
}

=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) = @_;

	return Ora2Pg::MySQL::_table_exists($self, $schema, $table) if ($self->{is_mysql});

	my $ret = '';

	my $sql = "SELECT TABLE_NAME FROM $self->{prefix}_TABLES WHERE OWNER = '$schema' AND TABLE_NAME = '$table'";
        my $sth = $self->{dbh}->prepare( $sql ) or return undef;
        $sth->execute or return undef;
	while ( my @row = $sth->fetchrow()) {
		$ret = $row[0];
	}
        $sth->finish();
	return $ret;
}



=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;

	return Ora2Pg::MySQL::_get_largest_tables($self) if ($self->{is_mysql});

	my %table_size = ();

	my $prefix = 'USER';
	my $owner_segment = '';
	$owner_segment = " AND A.OWNER='$self->{schema}'";
	if (!$self->{user_grants}) {
		$prefix = 'DBA';
		$owner_segment = ' AND S.OWNER=A.OWNER';
	}

	my $sql = "SELECT * FROM ( SELECT S.SEGMENT_NAME, ROUND(S.BYTES/1024/1024) SIZE_MB FROM ${prefix}_SEGMENTS S JOIN ALL_TABLES A ON (S.SEGMENT_NAME=A.TABLE_NAME$owner_segment) WHERE S.SEGMENT_TYPE LIKE 'TABLE%' AND A.SECONDARY = 'N'";
	if ($self->{db_version} =~ /Release 8/) {
		$sql = "SELECT * FROM ( SELECT A.SEGMENT_NAME, ROUND(A.BYTES/1024/1024) SIZE_MB FROM ${prefix}_SEGMENTS A WHERE A.SEGMENT_TYPE LIKE 'TABLE%'";
	}
	if ($self->{db_version} !~ /Release 8/ || !$self->{user_grants}) {
		if ($self->{schema}) {
			$sql .= " AND A.OWNER='$self->{schema}'";
		} else {
			$sql .= " AND A.OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
		}
	}
	if ($self->{db_version} =~ /Release 8/) {
		$sql .= $self->limit_to_objects('TABLE', 'A.SEGMENT_NAME');
	} else {
		$sql .= $self->limit_to_objects('TABLE', 'A.TABLE_NAME');
	}

	if ($self->{db_version} =~ /Release 8/) {
		$sql .= " ORDER BY A.BYTES DESC, A.SEGMENT_NAME ASC) WHERE ROWNUM <= $self->{top_max}";
	} else {
		$sql .= " ORDER BY S.BYTES DESC, S.SEGMENT_NAME ASC) WHERE ROWNUM <= $self->{top_max}";
	}

        my $sth = $self->{dbh}->prepare( $sql ) or return undef;
        $sth->execute(@{$self->{query_bind_params}}) or return undef;
	while ( my @row = $sth->fetchrow()) {
		$table_size{$row[0]} = $row[1];
	}
	$sth->finish();

	return %table_size;
}


=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) = @_;

	my $sql = "SELECT * FROM NLS_DATABASE_PARAMETERS";
        my $sth = $dbh->prepare($sql) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
        $sth->execute() or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	my $language = '';
	my $territory = '';
	my $charset = '';
	my $nls_timestamp_format = '';
	my $nls_date_format = '';
	while ( my @row = $sth->fetchrow()) {
		if ($row[0] eq 'NLS_LANGUAGE') {
			$language = $row[1];
		} elsif ($row[0] eq 'NLS_TERRITORY') {
			$territory = $row[1];
		} elsif ($row[0] eq 'NLS_CHARACTERSET') {
			$charset = $row[1];
		} elsif ($row[0] eq 'NLS_TIMESTAMP_FORMAT') {
			$nls_timestamp_format = $row[1];
		} elsif ($row[0] eq 'NLS_DATE_FORMAT') {
			$nls_date_format = $row[1];
		}
	}
	$sth->finish();
	$sql = "SELECT * FROM NLS_SESSION_PARAMETERS";
        $sth = $dbh->prepare($sql) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
        $sth->execute() or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	my $ora_encoding = '';
	while ( my @row = $sth->fetchrow()) {
		#$self->logit("SESSION PARAMETERS: $row[0] $row[1]\n", 1);
		if ($row[0] eq 'NLS_LANGUAGE') {
			$language = $row[1];
		} elsif ($row[0] eq 'NLS_TERRITORY') {
			$territory = $row[1];
		} elsif ($row[0] eq 'NLS_TIMESTAMP_FORMAT') {
			$nls_timestamp_format = $row[1];
		} elsif ($row[0] eq 'NLS_DATE_FORMAT') {
			$nls_date_format = $row[1];
		}
	}
	$sth->finish();

	$ora_encoding = $language . '_' . $territory . '.' . $charset;
	my $pg_encoding = auto_set_encoding($charset);

	return ($ora_encoding, $charset, $pg_encoding, $nls_timestamp_format, $nls_date_format);
}


=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) {
		foreach my $schm (@to_compile) {
			$self->logit("Force Oracle to compile schema $schm before code extraction\n", 1);
			my $sth = $self->{dbh}->do("BEGIN\nDBMS_UTILITY.compile_schema(schema => '$schm');\nEND;")
						or $self->logit("FATAL: " . $self->{dbh}->errstr . "\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;

	return $str;
}

=head2 auto_set_encoding

This function is used to find the PostgreSQL charset corresponding to the
Oracle NLS_LANG value

=cut

sub auto_set_encoding
{
	my $oracle_charset = shift;

	my %ENCODING = (
		"AL32UTF8" => "UTF8",
		"JA16EUC" => "EUC_JP",
		"JA16SJIS" => "EUC_JIS_2004",
		"ZHT32EUC" => "EUC_TW",
		"CL8ISO8859P5" => "ISO_8859_5",
		"AR8ISO8859P6" => "ISO_8859_6",
		"EL8ISO8859P7" => "ISO_8859_7",
		"IW8ISO8859P8" => "ISO_8859_8",
		"CL8KOI8R" => "KOI8R",
		"CL8KOI8U" => "KOI8U",
		"WE8ISO8859P1" => "LATIN1",
		"EE8ISO8859P2" => "LATIN2",
		"SE8ISO8859P3" => "LATIN3",
		"NEE8ISO8859P4"=> "LATIN4",
		"WE8ISO8859P9" => "LATIN5",
		"NE8ISO8859P10"=> "LATIN6",
		"BLT8ISO8859P13"=> "LATIN7",
		"CEL8ISO8859P14"=> "LATIN8",
		"WE8ISO8859P15" => "LATIN9",
		"RU8PC866" => "WIN866",
		"EE8MSWIN1250" => "WIN1250",
		"CL8MSWIN1251" => "WIN1251",
		"WE8MSWIN1252" => "WIN1252",
		"EL8MSWIN1253" => "WIN1253",
		"TR8MSWIN1254" => "WIN1254",
		"IW8MSWIN1255" => "WIN1255",
		"AR8MSWIN1256" => "WIN1256",
		"BLT8MSWIN1257"=> "WIN1257"
	);

	foreach my $k (keys %ENCODING) {
		return $ENCODING{$k} if (uc($oracle_charset) eq $k);
	}

	return '';
}

# Return 0 if the object should be exported, 1 if it not found in allow list
# and 2 if it is found in the exclude list
sub skip_this_object
{
	my ($self, $obj_type, $name) = @_;

return 0;

	# Exclude object in Recycle Bin from the export
	return 3 if ($name =~ /^BIN\$/);

	$obj_type = uc($obj_type);
	if (!$obj_type) {
		$obj_type = 'ALL';
	} else {
		$obj_type .= '|ALL';
	}


	my $found = 0;
	foreach my $obj (split(/\|/, $obj_type)) {
		# Check if this object is in the allowed list of object to export.
		$found = 1 if (($#{$self->{limited}{$obj}} >= 0) && !grep($name =~ /^$_$/i, @{$self->{limited}{$obj}}));

		# Check if this object is in the exlusion list of object to export.
		$found = 2 if (($#{$self->{excluded}{$obj}} >= 0) && grep($name =~ /^$_$/i, @{$self->{excluded}{$obj}}));
	}

	return $found;
}

# 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) = @_;

	# With reports we don't have object name limitation
	return if ($self->{type} eq 'SHOW_REPORT');

	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 ?" ;
					} else {
						$str .= "REGEXP_LIKE(upper($colname), ?)" ;
					}
					push(@{$self->{query_bind_params}}, uc("\^$self->{limited}{$arr_type[$i]}->[$j]\$"));
					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 ?" ;
						} else {
							$str .= " AND NOT REGEXP_LIKE(upper($colname), ?)" ;
						}
						push(@{$self->{query_bind_params}}, uc("\^$1\$"));
					}
				}

			}
			$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 ?" ;
					} else {
						$str .= "NOT REGEXP_LIKE(upper($colname), ?)" ;
					}
					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} && !$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_mysql}) {
						$str .= " upper($colname) NOT RLIKE ?" ;
					} 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 _lookup_check_constraint

This function return an array of the SQL code of the check constraints of a table

=cut

sub _lookup_check_constraint
{
	my ($self, $table, $check_constraint, $field_name, $nonotnull) = @_;

	my  @chk_constr = ();

	my $tbsaved = $table;
	$table = $self->get_replaced_tbname($table);

	# Set the check constraint definition 
	foreach my $k (keys %{$check_constraint->{constraint}})
	{
		my $chkconstraint = $check_constraint->{constraint}->{$k}{condition};
		next if (!$chkconstraint);
		my $skip_create = 0;
		if (exists $check_constraint->{notnull}) {
			foreach my $col (@{$check_constraint->{notnull}}) {
				$skip_create = 1, last if (lc($chkconstraint) eq lc("\"$col\" IS NOT NULL"));
			}
		}
		if (!$skip_create)
		{
			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);
			}
			next if ($nonotnull && ($chkconstraint =~ /IS NOT NULL/));
			foreach my $c (@$field_name) {
				# Force lower case
				my $ret = $self->quote_object_name($c);
				$chkconstraint =~ s/"$c"/$ret/igs;
				$chkconstraint =~ s/\b$c\b/$ret/igs;
			}
			$k = $self->quote_object_name($k);
			my $validate = '';
			$validate = ' NOT VALID' if ($check_constraint->{constraint}->{$k}{validate} eq 'NOT VALIDATED');
			push(@chk_constr,  "ALTER TABLE $table ADD CONSTRAINT $k CHECK ($chkconstraint)$validate;\n");
		}
	}

	return @chk_constr;
}

=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);
		my $skip_create = 0;
		if (exists $check_constraint->{notnull})
		{
			foreach my $col (@{$check_constraint->{notnull}})
			{
				$skip_create = 1, last if (lc($chkconstraint) eq lc("\"$col\" IS NOT NULL"));
			}
		}
		if (!$skip_create)
		{
			$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});
			$fct_detail{name} =~ s/^.*\.//;
			$fct_detail{name} =~ s/"//g;
			%{$infos{"$pname.$fct_detail{name}"}} = %fct_detail;
		}
	}

	return %infos;
}

=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) = @_;

	if ($self->{is_mysql}) {
		return Ora2Pg::MySQL::_lookup_function($self, $plsql, $pname);
	}

	my %fct_detail = ();

	$fct_detail{func_ret_type} = 'OPAQUE';

	# Split data into declarative and code part
	($fct_detail{declare}, $fct_detail{code}) = split(/\bBEGIN\b/i, $plsql, 2);

	return if (!$fct_detail{code});

	@{$fct_detail{param_types}} = ();
	$fct_detail{declare} =~ s/(\b(?:FUNCTION|PROCEDURE)\s+(?:[^\s\(]+))(\s*\%ORA2PG_COMMENT\d+\%\s*)+/$2$1 /is;
	if ( ($fct_detail{declare} =~ s/(.*?)\b(FUNCTION|PROCEDURE)\s+([^\s\(]+)\s*(\([^\)]*\))//is) || 
			($fct_detail{declare} =~ s/(.*?)\b(FUNCTION|PROCEDURE)\s+([^\s\(]+)\s+(RETURN|IS|AS)/$4/is) )
	{
		$fct_detail{before} = $1;
		$fct_detail{type} = uc($2);
		$fct_detail{name} = $3;
		$fct_detail{args} = $4;

		$fct_detail{fct_name} = $3;
		$fct_detail{fct_name} =~ s/^[^\.]+\.//;
		$fct_detail{fct_name} =~ s/"//g;

		# When the function comes from a package remove global declaration
		# outside comments. They have already been extracted before.
		if ($pname && $fct_detail{before}) {
			$self->_remove_comments(\$fct_detail{before});
			my $cmt = '';
			while ($fct_detail{before} =~ s/(\s*\%ORA2PG_COMMENT\d+\%\s*)//is) {
				# only keep comment
				$cmt .= $1;
			}
			$fct_detail{before} = $cmt;
		}

		if ($fct_detail{args} =~ /\b(RETURN|IS|AS)\b/is) {
			$fct_detail{args} = '()';
		}
		my $clause = '';
		my $code = '';
		$fct_detail{name} =~ s/"//g;

		$fct_detail{immutable} = 1 if ($fct_detail{declare} =~ s/\bDETERMINISTIC\b//is);
		$fct_detail{setof} = 1 if ($fct_detail{declare} =~ s/\bPIPELINED\b//is);
		$fct_detail{declare} =~ s/\bDEFAULT/:=/igs;
		if ($fct_detail{declare} =~ s/(.*?)RETURN\s+self\s+AS RESULT IS//is) {
			$fct_detail{args} .= $1;
			$fct_detail{hasreturn} = 1;
			$fct_detail{func_ret_type} = 'OPAQUE';
		} elsif ($fct_detail{declare} =~ s/(.*?)RETURN\s+([^\s]+)//is) {
			$fct_detail{args} .= $1;
			$fct_detail{hasreturn} = 1;
			$fct_detail{func_ret_type} = $self->_sql_type($2) || 'OPAQUE';
		}
		if ($fct_detail{declare} =~ s/(.*?)(USING|AS|IS)//is) {
			$fct_detail{args} .= $1 if (!$fct_detail{hasreturn});
			$clause = $2;
		}
		$fct_detail{args} =~ s/;.*//s;

		if ($fct_detail{declare} =~ /LANGUAGE\s+([^\s="'><\!\(\)]+)/is) {
			$fct_detail{language} = $1;
			if ($fct_detail{declare} =~ /LIBRARY\s+([^\s="'><\!\(\)]+)/is) {
				$fct_detail{library} = $1;
			}
			if ($fct_detail{declare} =~ /NAME\s+"([^"]+)"/is) {
				$fct_detail{library_fct} = $1;
			}
		}
		# 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;

		# Replace DEFAULT EMPTY_BLOB() from function/procedure arguments by DEFAULT NULL
		$fct_detail{args} =~ s/\s+DEFAULT\s+EMPTY_[CB]LOB\(\)/DEFAULT NULL/igs;

		# Now convert types
		$fct_detail{args} = Ora2Pg::PLSQL::replace_sql_type($fct_detail{args}, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, %{$self->{data_type}});
		$fct_detail{declare} = Ora2Pg::PLSQL::replace_sql_type($fct_detail{declare}, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, %{$self->{data_type}});

		# Sometime variable used in FOR ... IN SELECT loop is not declared
		# Append its RECORD declaration in the DECLARE section.
		my $tmp_code = $fct_detail{code};
		while ($tmp_code =~ s/\bFOR\s+([^\s]+)\s+IN(.*?)LOOP//is)
		{
			my $varname = quotemeta($1);
			my $clause = $2;
			if ($fct_detail{declare} !~ /\b$varname\s+/is) {
				chomp($fct_detail{declare});
				# When the cursor is refereing to a statement, declare
				# it as record otherwise it don't need to be replaced
				if ($clause =~ /\bSELECT\b/is) {
					$fct_detail{declare} .= "\n  $varname RECORD;\n";
				}
			}
		}

		# Set parameters for AUTONOMOUS TRANSACTION
		$fct_detail{args} =~ s/\s+/ /gs;
		push(@{$fct_detail{at_args}}, split(/\s*,\s*/, $fct_detail{args}));
		# Remove type parts to only get parameter's name
		push(@{$fct_detail{param_types}}, @{$fct_detail{at_args}});
		map { s/\s(IN|OUT|INOUT)\s/ /i; } @{$fct_detail{at_args}};
		map { s/^\(//; } @{$fct_detail{at_args}};
		map { s/^\s+//; } @{$fct_detail{at_args}};
		map { s/\s.*//; } @{$fct_detail{at_args}};
		map { s/\)$//; } @{$fct_detail{at_args}};
		@{$fct_detail{at_args}} = grep(/^.+$/, @{$fct_detail{at_args}});
		# Store type used in parameter list to lookup later for custom types
		map { s/^\(//; } @{$fct_detail{param_types}};
		map { s/\)$//; } @{$fct_detail{param_types}};
		map { s/\%ORA2PG_COMMENT\d+\%//gs; }  @{$fct_detail{param_types}};
		map { s/^\s*[^\s]+\s+(IN|OUT|INOUT)/$1/i; s/^((?:IN|OUT|INOUT)\s+[^\s]+)\s+[^\s]*$/$1/i; s/\(.*//; s/\s*\)\s*$//; s/\s+$//; } @{$fct_detail{param_types}};
	} else {
		delete $fct_detail{func_ret_type};
		delete $fct_detail{declare};
		$fct_detail{code} = $plsql;
	}

	# PostgreSQL procedure do not support OUT parameter, translate them into INOUT params
	if ($self->{pg_supports_procedure} && ($fct_detail{args} =~ /\bOUT\s+[^,\)]+/i)) {
		$fct_detail{args} =~ s/\bOUT(\s+[^,\)]+)/INOUT$1/igs;
	}

	# Mark the function as having out parameters if any
	my @nout = $fct_detail{args} =~ /\bOUT\s+([^,\)]+)/igs;
	my @ninout = $fct_detail{args} =~ /\bINOUT\s+([^,\)]+)/igs;
	my $nbout = $#nout+1 + $#ninout+1;
	$fct_detail{inout} = 1 if ($nbout > 0);

	# Mark function as having custom type in parameter list
	if ($fct_detail{inout} and $nbout > 1) {
		foreach my $t (@{$fct_detail{param_types}}) {
			# Consider column type reference to never be a composite type this
			# is clearly not right but the false positive case might be very low
			next if ($t =~ /\%TYPE/i || ($t !~ s/^(OUT|INOUT)\s+//i));
			# Mark out parameter as using composite type
			if (!grep(/^\Q$t\E$/i, 'int', 'bigint', 'date', values %TYPE, values %ORA2PG_SDO_GTYPE)) {
				$fct_detail{inout}++;
			}
		}
	}

	# Collect user defined function
	while ($fct_detail{declare} =~ s/\b([^\s]+)\s+EXCEPTION\s*;//) {
		my $e = lc($1);
		if (!exists $self->{custom_exception}{$e}) {
			$self->{custom_exception}{$e} = $self->{exception_id}++;
		}
	}
	$fct_detail{declare} =~ s/PRAGMA\s+EXCEPTION_INIT[^;]*;//igs;

	# Replace call to global variables declared in this package
	foreach my $n (keys %{$self->{global_variables}}) {
		next if (!$n || ($pname && (uc($n) !~ /^\U$pname\E\./)));
		my $tmpname = $n;
		$tmpname =~ s/^$pname\.//i;
		next if ($fct_detail{code} !~ /\b$tmpname\b/is);
		my $i = 0;
		while ($fct_detail{code} =~ s/\b$n\s*:=\s*([^;]+)\s*;/PERFORM set_config('$n', $1, false);/is) { last if ($i++ > 100); };
		$i = 0;
		while ($fct_detail{code} =~ s/([^\.]+)\b$self->{global_variables}{$n}{name}\s*:=\s*([^;]+);/$1PERFORM set_config('$n', $2, false);/is) { last if ($i++ > 100); };
		$i = 0;
		while ($fct_detail{code} =~ s/([^']+)\b$n\b([^']+)/$1current_setting('$n')::$self->{global_variables}{$n}{type}$2/is) { last if ($i++ > 100); };
		$i = 0;
		while ($fct_detail{code} =~ s/([^\.']+)\b$self->{global_variables}{$n}{name}\b([^']+)/$1current_setting('$n')::$self->{global_variables}{$n}{type}$2/is) { last if ($i++ > 100); };
	}

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

	return %fct_detail;
}

####
# Return a string to set the current search path
####
sub set_search_path
{
	my $self = shift;
	my $owner = shift;

	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';
	}
	$local_path .= ',public';
	
	my $search_path = '';
	if (!$self->{schema} && $self->{export_schema} && $owner) {
		$search_path = "SET search_path = " . $self->quote_object_name($owner) . "$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});
		}
		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 man-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 man-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;
		}
	}

	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 $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})
	{
		my $cost_header = '';
		$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_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");
			}
		}
	}
	elsif ($self->{dump_as_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");
	}
	elsif ($self->{dump_as_sheet})
	{
		$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");
	}
	else
	{
		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_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"}) {
		if ($self->{schema}) {
			$select_query = "SELECT * FROM $self->{schema}.$table WHERE ABS(MOD(" . $self->{defined_pk}{"\L$table\E"} . ",\${Internal.Step.Unique.Count}))=\${Internal.Step.Unique.Number}";
		} else {
			$select_query = "SELECT * FROM $table WHERE ABS(MOD(" . $self->{defined_pk}{"\L$table\E"} . ",\${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) = @_;

	if ($self->{type} eq 'COPY') {
		if ( ($generic_type eq 'BLOB') || ($generic_type eq 'RAW') ) {
			# RAW data type is returned in hex
			$col = unpack("H*",$col) if ($generic_type ne 'RAW');
			$col = "\\\\x" . $col;
		} elsif (($generic_type eq 'CLOB') || $cond->{istext}) {
			$col = $self->escape_copy($col, $isnested);
		}
	} else {
		if ( ($generic_type eq 'BLOB') || ($generic_type eq 'RAW') ) {
			#$col = escape_bytea($col);
			# RAW data type is returned in hex
			$col = unpack("H*",$col) if ($generic_type ne 'RAW');
			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 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 = Ora2Pg::PLSQL::replace_sql_type($glob_vars, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, %{$self->{data_type}});

	# 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++)
		{
			$self->{global_variables}{$v}{default} = $others[$j+1] if ($others[$j] eq ':=' or uc($others[$j]) eq 'DEFAULT');
		}
		if (exists $self->{global_variables}{$v}{default})
		{
			$self->_restore_text_constant_part(\$self->{global_variables}{$v}{default});
			$self->{global_variables}{$v}{default} =~ s/^'//s;
			$self->{global_variables}{$v}{default} =~ 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)
		{
			# 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 min
{
	return $_[0] if ($_[0] < $_[1]);

	return $_[1];
}

1;

__END__


=head1 AUTHOR

Gilles Darold <gilles _AT_ darold _DOT_ net>


=head1 COPYRIGHT

Copyright (c) 2000-2020 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
