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-2016 : 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);
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 /;

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

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

$| = 1;

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

# 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_.*');
our @EXCLUDED_TABLES_8I = ('USLOG$_%', 'MLOG$_%', 'RUPD$_%', 'MDXT_%', 'MDRT_%', 'MDRS_%', 'DR$%', 'CLI_SWP$%', 'LOGMNR$%', 'REPCAT$%', 'JAVA$%', 'AQ$%','BIN$%','%$JAVA$%','PROF$%','TOAD_PLAN_%');

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

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 INNER INITIALLY INTERSECT INTO IS ISNULL JOIN LATERAL LEADING LEFT LIKE
	LIMIT LOCALTIME LOCALTIMESTAMP NATURAL NOT NOTNULL NULL OFFSET ON ONLY
	OR ORDER OUTER OVER 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
);

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

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
	if ($self->{type} ne 'LOAD') {
		if (not defined $self->{fhout}) {
			$self->remove_export_file();
			$self->create_export_file();
		} else {
			$self->logit("FATAL: method export_schema() could not be called several time.\n",0,1);
		}
	}

	foreach my $t (@{$self->{export_type}}) {
		next if ($t =~ /^SHOW_/i);
		$self->{type} = $t;
		# Return data as string
		$self->_get_sql_data();
	}

}


=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) {
		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);
			# Force Perl to use utf8 I/O encoding by default
			if ( !$self->{'binmode'} || ($self->{nls_lang} =~ /UTF8/i) ) {
				use open ':utf8';
				$filehdl->binmode(':utf8');
			} elsif ($self->{'binmode'} =~ /^:/) {
				$filehdl->binmode($self->{binmode}) or die "FATAL: can't use open layer $self->{binmode} in append_export_file()\n";
			}
		}
		$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) = @_;

	# Init with configuration OUTPUT filename
	$outfile ||= $self->{output};
	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 ($outfile) {
		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);
			# Force Perl to use utf8 I/O encoding by default
			if ( !$self->{'binmode'} || ($self->{nls_lang} =~ /UTF8/i) ) {
				use open ':utf8';
				$self->{fhout}->binmode(':utf8');
			} elsif ($self->{'binmode'} =~ /^:/) {
				$self->{fhout}->binmode($self->{binmode}) or die "FATAL: can't use open layer $self->{binmode} in append_export_file()\n";
			}
		}
		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 ($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 ($outfile) {
		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}) {
			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);
			# Force Perl to use utf8 I/O encoding by default
			if ( !$self->{'binmode'} || ($self->{nls_lang} =~ /UTF8/i) ) {
				use open ':utf8';
				$filehdl->binmode(':utf8');
			} elsif ($self->{'binmode'} =~ /^:/) {
				$filehdl->binmode($self->{binmode}) or die "FATAL: can't use open layer $self->{binmode} in append_export_file()\n";
			}
		}
	}

	return $filehdl;
}



=head2 close_export_file FILEHANDLE

Close a file handle.

=cut

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


	return if (!defined $filehdl);

	if ($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 quote_reserved_words

Return a quoted object named if it is a PostgreSQL reserved word

=cut

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

	return $obj_name if ($obj_name =~ /^SYS_NC\d+/);

	if ($self->{use_reserved_words}) {
		if ($obj_name && grep(/^$obj_name$/i, @KEYWORDS)) {
			return '"' . $obj_name . '"';
		}
	}
	if (!$self->{preserve_case}) {
		$obj_name = lc($obj_name);
		if ( ($obj_name =~ /^\d+/) || ($obj_name =~ /[^a-zA-Z0-9\_\.]/) ) {
			return '"' . $obj_name . '"';
		}
	}

	return $obj_name;
}

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

	if ($obj_name && grep(/^$obj_name$/i, @KEYWORDS)) {
		return 1;
	}
	if ($obj_name =~ /^\d+$/) {
		return 2;
	}
	if ($obj_name && grep(/^$obj_name$/i, @SYSTEM_FIELDS)) {
		return 3;
	}

	return 0;
}

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

	if (!$self->{preserve_case}) {
		$obj_name = lc($obj_name);
		if ($obj_name =~ /[^a-zA-Z0-9\_]/) {
			return '"' . $obj_name . '"';
		}
	} else {
		return '"' . $obj_name . '"';
	}

	return $obj_name;
}



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

	# Init PostgreSQL DB handle
	$self->{dbhdest} = undef;
	$self->{idxcomment} = 0;
	$self->{standard_conforming_strings} = 1;
	$self->{create_schema} = 1;
	$self->{external_table} = ();
	$self->{function_list} = ();

	# Used to precise if we need to prefix partition tablename with main tablename
	$self->{prefix_partition} = 0;

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

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

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

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

	# 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};
		} elsif (lc($k) eq 'view_as_table') {
			$self->{view_as_table} = $AConfig{VIEW_AS_TABLE};
		} 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_FILES','MDDATA','ORACLE_OCM','SPATIAL_CSW_ADMIN_USR','SPATIAL_WFS_ADMIN_USR','XS$NULL','PERFSTAT','SQLTXPLAIN','DMSYS','TSMSYS','WKSYS','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');

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

	# 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';
	} else {
		$self->{transaction} = 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE';
	}

	# Initial command to execute at Oracle connexion
	$self->{ora_initial_command} ||= '';

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

	# Initialize rewriting of index name
	if (not defined $self->{indexes_renaming} || $self->{indexes_renaming} != 0) {
		$self->{indexes_renaming} = 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;

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

	# Overwrite configuration with all given parameters
	# and try to preserve backward compatibility
	foreach my $k (keys %options) {
		if ((lc($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 ((lc($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 ((lc($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 ((lc($k) eq 'datasource') && $options{datasource}) {
			$self->{oracle_dsn} = $options{datasource};
		} elsif ((lc($k) eq 'user') && $options{user}) {
			$self->{oracle_user} = $options{user};
		} elsif ((lc($k) eq 'password') && $options{password}) {
			$self->{oracle_pwd} = $options{password};
		} elsif ((lc($k) eq 'mysql') && $options{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};

	if ($AConfig{'DEBUG'} == 1) {
		$self->{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';
	}

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

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

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

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

	# 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}));
		# Set default type conversion
		%{$self->{data_type}} = %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;
	}

	# 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->{estimate_cost} = 1 if ($self->{dump_as_sheet});
	$self->{count_rows} ||= 0;

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

	# backward compatibility
	if ($self->{disable_table_triggers}) {
		$self->{disable_triggers} = $self->{disable_table_triggers};
	}
	# Set some default values
	if ($self->{enable_microsecond} eq '') {
		$self->{enable_microsecond} = 1;
	} 
	if ($self->{external_to_fdw} eq '') {
		$self->{external_to_fdw} = 1;
	}
	if ($self->{pg_supports_insteadof} eq '') {
		$self->{pg_supports_insteadof} = 1;
	}
	if ($self->{pg_supports_mview} eq '') {
		$self->{pg_supports_mview} = 1;
	}
	$self->{pg_supports_checkoption} ||= 0;
	if ($self->{pg_supports_ifexists} eq '') {
		$self->{pg_supports_ifexists} = 1;
	}
	if ($self->{pg_supports_ifexists}) {
		$self->{pg_supports_ifexists} = 'IF EXISTS';
	} else {
		$self->{pg_supports_ifexists} = '';
	}
	$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;
	}
	$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 {
		push(@{$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->{debug}) {
		$self->logit("Ora2Pg version: $VERSION\n");
	}

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

	if (!$self->{input_file}) {
		if ($self->{type} eq 'LOAD') {
			$self->logit("FATAL: with LOAD you must provide an 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}) {
				if ($self->{debug} && $self->{compile_schema}) {
					$self->logit("Force Oracle to compile schema before code extraction\n", 1);
				}
				$self->_compile_schema($self->{dbh}, uc($self->{compile_schema}));
			}

			$self->_get_pkg_functions() if (!$self->{package_as_schema} && (!grep(/^$self->{type}$/, 'COPY', 'INSERT', 'SEQUENCE', 'GRANT', 'TABLESPACE', 'QUERY', 'SYNONYM', 'FDW', 'KETTLE', 'DBLINK', 'DIRECTORY')));
			@{$self->{function_list}} = $self->_list_all_funtions() if ($self->{plsql_pgsql} && (!grep(/^$self->{type}$/, 'COPY', 'INSERT', 'SEQUENCE', 'GRANT', 'TABLESPACE', 'QUERY', 'SYNONYM', 'FDW', 'KETTLE', 'DBLINK', 'DIRECTORY')));
			$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;
	}

	# Retreive all table informations
        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();
		} 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;
		} 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), unknown $self->{type}\n";
		}
		# Mofify export structure if required
		if ($self->{type} =~ /^(INSERT|COPY)$/) {
			for my $t (keys %{$self->{'modify_struct'}}) {
				$self->modify_struct($t, @{$self->{'modify_struct'}{$t}});
			}
		}
		$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);
			}
			$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) = @_;

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

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

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

	# Force execution of initial command
	$self->_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);

	my $dbh = DBI->connect("$self->{oracle_dsn}", $self->{oracle_user}, $self->{oracle_pwd}, { 'RaiseError' => 1, AutoInactiveDestroy => 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->_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
	if ( !$self->{'binmode'} || ($self->{nls_lang} =~ /UTF8/i) ) {
		use open ':utf8';
		$self->{'binmode'} = ':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->{client_encoding} = '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});

}


=head2 _send_to_pgdb DEST_DATASRC DEST_USER DEST_PASSWD

Open a DB handle to a PostgreSQL database

=cut

sub _send_to_pgdb
{
	my ($self, $destsrc, $destuser, $destpasswd) = @_;

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

	# Init with configuration options if no parameters
	$destsrc ||= $self->{pg_dsn};
	$destuser ||= $self->{pg_user};
	$destpasswd ||= $self->{pg_pwd};

        # Then connect the destination database
        my $dbhdest = DBI->connect($destsrc, $destuser, $destpasswd, {AutoInactiveDestroy => 1});

	$destsrc =~ /dbname=([^;]*)/;
	$self->{dbname} = $1;
	$destsrc =~ /host=([^;]*)/;
	$self->{dbhost} = $1;
	$self->{dbhost} = 'localhost' if (!$self->{dbhost});
	$destsrc =~ /port=([^;]*)/;
	$self->{dbport} = $1;
	$self->{dbport} = 5432 if (!$self->{dbport});
	$self->{dbuser} = $destuser;
	$self->{dbpwd} = $destpasswd;

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

	return $dbhdest;
}

# Backward Compatibility
sub send_to_pgdb
{
	return &_send_to_pgdb(@_);

}

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

}

=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();
	if ( grep(/^$self->{type}$/, 'TABLE','SHOW_REPORT','COPY','INSERT') && !$self->{skip_indices} && !$self->{skip_indexes}) {

		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) {
		# 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
		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}) {
			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 all unique keys informations
	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 = ();

	# Retrieve check constraints
	if (!$self->{skip_checks} && !$self->{is_mysql}) {
		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;
	foreach my $t (sort keys %tables_infos) {

		if (!$self->{quiet} && !$self->{debug}) {
			print STDERR $self->progress_bar($i, $num_total_table, 25, '=', 'tables', "scanning table $t" ), "\r";
		}

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

		# Set the fields information
		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 _split_table_definition
{
	my $str = shift();

	my $ct = '';
	my @parts = split(/([\(\)])/, $str);
	my $def = '';
	my $param = '';
	my $i = 0;
	for (; $i <= $#parts; $i++) {
		$ct++ if ($parts[$i] =~ /\(/);
		$ct-- if ($parts[$i] =~ /\)/);
		if ( ($def ne '') && ($ct == 0) ) {
			last;
		}
		$def .= $parts[$i] if ($def || ($parts[$i] ne '('));
	}
	$i++;
	for (; $i <= $#parts; $i++) {
		$param .= $parts[$i];
	}

	$def =~ s/\s+/ /g;
	$param =~ s/\s+/ /g;

	return ($def, $param);
}

sub _get_plsql_code
{
	my $str = shift();

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

	$code =~ s/\s+/ /g;
	$other =~ s/\s+/ /g;

	return ($code, $other);
}


sub _parse_constraint
{
	my ($self, $tb_name, $cur_col_name, $c) = @_;

	if ($c =~ /^([^\s]+) (UNIQUE|PRIMARY KEY)\s*\(([^\)]+)\)/i) {
		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]+) CHECK\s*\(([^\)]+)\)/i) {
		my %tmp = ($1 => $2);
		$self->{tables}{$tb_name}{check_constraint}{constraint}{$1} = $2;
	} elsif ($c =~ /^([^\s]+) FOREIGN KEY (\([^\)]+\))?\s*REFERENCES ([^\(]+)\(([^\)]+)\)/i) {
		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/);
		# CONSTRAINT_NAME,R_CONSTRAINT_NAME,SEARCH_CONDITION,DELETE_RULE,$deferrable,DEFERRED,R_OWNER,TABLE_NAME,OWNER
		push(@{$self->{tables}{$tb_name}{foreign_key}}, [ ($c_name,'','','',$deferrable,$deferred,'',$tb_name,'') ]);
	}
}

sub _get_dml_from_file
{
	my ($self, $text_values, $keep_new_line) = @_;

	# Load file in a single string
	if (not open(INFILE, $self->{input_file})) {
		die "FATAL: can't read file $self->{input_file}, $!\n";
	}
	my $content = '';
	while (my $l = <INFILE>) {
		chomp($l) if (!$keep_new_line);
		$l =~ s/\r//gs;
		$l =~ s/\t+/ /gs;
		$l =~ s/\-\-.*// if (!$keep_new_line);
		next if (!$l);
		$content .= $l;
		$content .= (!$keep_new_line) ? ' ' : '';
	}
	close(INFILE);

	$content =~ s/\/\*(.*?)\*\// /gs;
	$content =~ s/CREATE\s+OR\s+REPLACE/CREATE/gs;
	$content =~ s/CREATE\s+EDITIONABLE/CREATE/gs;
	$content =~ s/CREATE\s+NONEDITIONABLE/CREATE/gs;

	if (defined $text_values) {
		my $j = 0;
		while ($content =~ s/'([^']+)'/\%TEXTVALUE-$j\%/s) {
			push(@$text_values, $1);
			$j++;
		}
	}
	return $content;
}

sub read_schema_from_file
{
	my $self = shift;

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

	my $tid = 0; 

	# Remove potential dynamic table creation before parsing
	while ($content =~ s/'(TRUNCATE|CREATE)\s+(GLOBAL|UNIQUE)?\s*(TEMPORARY)?\s*(TABLE|INDEX)([^']+)'//i) {};
	while ($content =~ s/'ALTER\s+TABLE\s*([^']+)'//i) {};

	while ($content =~ s/TRUNCATE TABLE\s+([^;]+);//i) {
		my $tb_name = $1;
		$tb_name =~ s/"//g;
		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;
	}

	while ($content =~ s/CREATE\s+(GLOBAL)?\s*(TEMPORARY)?\s*TABLE[\s]+([^\s]+)\s+AS\s+([^;]+);//i) {
		my $tb_name = $3;
		$tb_name =~ s/"//g;
		my $tb_def = $4;
		$tb_def =~ s/\s+/ /g;
		$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;
	}

	while ($content =~ s/CREATE\s+(GLOBAL)?\s*(TEMPORARY)?\s*TABLE[\s]+([^\s\(]+)\s*([^;]+);//i) {
		my $tb_name = $3;
		my $tb_def  = $4;
		my $tb_param  = '';
		$tb_name =~ s/"//g;
		$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;

		($tb_def, $tb_param) = &_split_table_definition($tb_def);
		my @column_defs = split(/\s*,\s*/, $tb_def);
		map { s/^\s+//; s/\s+$//; } @column_defs;
		# Fix split on scale comma, for example NUMBER(9,4)
		for (my $i = 0; $i <= $#column_defs; $i++) {
			if ($column_defs[$i] =~ /^\d+/) {
				$column_defs[$i-1] .= ",$column_defs[$i]";
				$column_defs[$i] = '';
			}
		}
		# Fix split on multicolumn's constraints, ex: UNIQUE (last_name,first_name) 
		for (my $i = $#column_defs; $i >= 0; $i--) {
			if ( ($column_defs[$i] !~ /\s/) || ($column_defs[$i] =~ /^[^\(]+\) REFERENCES/i) || ($column_defs[$i] =~ /^[^\(]+\) USING INDEX/ii)) {
				$column_defs[$i-1] .= ",$column_defs[$i]";
				$column_defs[$i] = '';
			}
		}
		my $pos = 0;
		my $cur_c_name = '';
		foreach my $c (@column_defs) {
			next if (!$c);
			# Remove things that are not possible with postgres
			$c =~ s/(PRIMARY KEY.*)NOT NULL/$1/i;
			# Rewrite some parts for easiest/generic parsing
			$c =~ s/^(PRIMARY KEY|UNIQUE)/CONSTRAINT ora2pg_ukey_$tb_name $1/i;
			$c =~ s/^(CHECK[^,;]+)DEFERRABLE\s+INITIALLY\s+DEFERRED/$1/i;
			$c =~ s/^CHECK\b/CONSTRAINT ora2pg_ckey_$tb_name CHECK/i;
			$c =~ s/^FOREIGN KEY/CONSTRAINT ora2pg_fkey_$tb_name FOREIGN KEY/i;
			# Get column name
			if ($c =~ s/^\s*([^\s]+)\s*//) {
				my $c_name = $1;
				$c_name =~ s/"//g;
				# Retrieve all columns information
				if (uc($c_name) ne 'CONSTRAINT') {
					$cur_c_name = $c_name;
					my $c_type = '';
					if ($c =~ s/^([^\s\(]+)\s*//) {
						$c_type = $1;
					} else {
						next;
					}
					my $c_length = '';
					my $c_scale = '';
					if ($c =~ s/^\(([^\)]+)\)\s*//) {
						$c_length = $1;
						if ($c_length =~ s/\s*,\s*(\d+)\s*//) {
							$c_scale = $1;
						}
					}
					my $c_nullable = 1;
					if ($c =~ s/CONSTRAINT\s*([^\s]+)?\s*NOT NULL//) {
						$c_nullable = 0;
					} elsif ($c =~ s/NOT NULL//) {
						$c_nullable = 0;
					}

					if (($c =~ s/(UNIQUE|PRIMARY KEY)\s*\(([^\)]+)\)//i) || ($c =~ s/(UNIQUE|PRIMARY KEY)\s*//i)) {
						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*\(([^\)]+)\)//i) || ($c =~ s/CHECK\s*\(([^\)]+)\)//i) ) {
						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+([^\(]+)\(([^\)]+)\)//i) {

						my $pk_name = 'ora2pg_fkey_' . $c_name; 
						my $chk_search = $1 . "($2)";
						$chk_search =~ s/\s+//g;
						$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*//i) {
						$auto_incr = 1;
					}

					my $c_default = '';
					if ($c =~ s/DEFAULT\s+([^\s]+)\s*//) {
						if (!$self->{plsql_pgsql}) {
							$c_default = $1;
						} else {
							$c_default = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $1);
						}
					}
					#COLUMN_NAME, DATA_TYPE, DATA_LENGTH, NULLABLE, DATA_DEFAULT, DATA_PRECISION, DATA_SCALE, CHAR_LENGTH, TABLE_NAME, OWNER
					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, '', $pos, $auto_incr));
				} else {
					$self->_parse_constraint($tb_name, $cur_c_name, $c);
				}
			}
			$pos++;
		}
		map {s/^/\t/; s/$/,\n/; } @column_defs;
		# look for storage information
		if ($tb_param =~ /TABLESPACE[\s]+([^\s]+)/i) {
			$self->{tables}{$tb_name}{table_info}{tablespace} = $1;
			$self->{tables}{$tb_name}{table_info}{tablespace} =~ s/"//g;
		}
		if ($tb_param =~ /PCTFREE\s+(\d+)/i) {
			$self->{tables}{$tb_name}{table_info}{fillfactor} = $1;
		}
		if ($tb_param =~ /\bNOLOGGING\b/i) {
			$self->{tables}{$tb_name}{table_info}{nologging} = 1;
		}

	}

	my $tbspace_move = '';
	while ($content =~ s/ALTER\s+TABLE[\s]+([^\s]+)\s+([^;]+);//i) {
		my $tb_name = $1;
		$tb_name =~ s/"//g;
		my $tb_def = $2;
		$tb_def =~ s/\s+/ /g;
		$tb_def =~ s/(CHECK[^,;]+)\s+DEFERRABLE\s+INITIALLY\s+DEFERRED/$1/i;
		if ( $self->{use_tablespace} && ($tb_def =~ /USING\s+INDEX\s+TABLESPACE\s+([^\s]+)/) ) {
			my $tbspace = $1;
			$tb_def =~ /\s+CONSTRAINT\s+([^\s]+)\s+/;
			$tbspace_move = "ALTER INDEX $1 SET TABLESPACE $tbspace";
		}
		$tb_def =~ s/\s*USING INDEX.*//g;
		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}}, $tb_def) if ($tb_def);
		push(@{$self->{tables}{$tb_name}{alter_index}}, $tbspace_move) if ($tbspace_move);
	}

	while ($content =~ s/CREATE\s+(UNIQUE|BITMAP)?\s*INDEX\s+([^\s]+)\s+ON\s+([^\s\(]+)\s*\(([^;]+);//i) {
		my $is_unique = $1;
		my $idx_name = $2;
		$idx_name =~ s/"//g;
		my $tb_name = $3;
		$tb_name =~ s/\s+/ /g;
		my $idx_def = $4;
		$idx_def =~ s/\s+/ /g;
		$idx_def =~ s/\s*nologging//i;
		$idx_def =~ s/STORAGE\s*\([^\)]+\)\s*//i;
		$idx_def =~ s/COMPRESS(\s+\d+)?\s*//i;
		# look for storage information
		if ($idx_def =~ s/TABLESPACE\s*([^\s]+)\s*//i) {
			$self->{tables}{$tb_name}{idx_tbsp}{$idx_name} = $1;
			$self->{tables}{$tb_name}{idx_tbsp}{$idx_name} =~ s/"//g;
		}
		if ($idx_def =~ s/ONLINE\s*//i) {
			$self->{tables}{$tb_name}{concurrently}{$idx_name} = 1;
		}
		if ($idx_def =~ s/INDEXTYPE\s+IS\s+.*SPATIAL_INDEX//i) {
			$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,]+)//i) {
			$self->{tables}{$tb_name}{idx_type}{$idx_name}{type_constraint} = uc($1);
		}
		if ($idx_def =~ s/sdo_indx_dims=(\d)//i) {
			$self->{tables}{$tb_name}{idx_type}{$idx_name}{type_dims} = $1;
		}
		$idx_def =~ 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 =~ /\(/) {
			$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;
		}

	}
	# 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 @text_values = ();
	my $content = $self->_get_dml_from_file(\@text_values);

	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 VIEW[\s]+([^\s]+)\s+OF\s+(.*?)\s+AS\s+/CREATE VIEW $1 AS /g;
	# Views with aliases
	while ($content =~ s/CREATE\sVIEW[\s]+([^\s]+)\s*\((.*?)\)\s+AS\s+([^;]+);//i) {
		my $v_name = $1;
		my $v_alias = $2;
		my $v_def = $3;
		$v_name =~ s/"//g;
		$v_def =~ s/\s+/ /g;
		$tid++;
	        $self->{views}{$v_name}{text} = $v_def;
	        $self->{views}{$v_name}{iter} = $tid;
		$self->{views}{$v_name}{text} =~ s/\%TEXTVALUE-(\d+)\%/'$text_values[$1]'/gs;
		# Remove constraint
		while ($v_alias =~ s/(,[^,\(]+\(.*)$//) {};
		my @aliases = split(/\s*,\s*/, $v_alias);
		foreach (@aliases) {
			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;
		$v_def =~ s/\s+/ /g;
		$tid++;
	        $self->{views}{$v_name}{text} = $v_def;
		$self->{views}{$v_name}{text} =~ s/\%TEXTVALUE-(\d+)\%/'$text_values[$1]'/gs;
	}

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

	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 @text_values = ();
	my $content = $self->_get_dml_from_file(\@text_values, 1);

	my $tid = 0; 
	my $doloop = 1;
	my @triggers_decl = split(/CREATE(?:\s+OR\s+REPLACE)?\s+TRIGGER\s+/, $content);
	foreach $content (@triggers_decl) {
		if ($content =~ s/^([^\s]+)\s+(BEFORE|AFTER|INSTEAD\s+OF)\s+(.*?)\s+ON\s+([^\s]+)\s+(.*)(END\s*(?!IF|LOOP|CASE|INTO|FROM|,)[a-z0-9_]*;)//is) {
			my $t_name = $1;
			$t_name =~ s/"//g;
			my $t_pos = $2;
			my $t_event = $3;
			my $tb_name = $4;
			my $trigger = $5 . $6;
			my $t_type = '';
			# 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)/) {
					($trigger, $content) = &_get_plsql_code($trigger);
				} else {
					$trigger =~ s/([^;]+;)\s*(.*)/$1/;
					$content = $2;
				}
			} else {
				if ($trigger =~ /^(BEGIN|DECLARE)/) {
					($trigger, $content) = &_get_plsql_code($trigger);
				}
			}
			$tid++;

			# TRIGGER_NAME, TRIGGER_TYPE, TRIGGERING_EVENT, TABLE_NAME, TRIGGER_BODY, WHEN_CLAUSE, DESCRIPTION,ACTION_TYPE
			$trigger =~ s/END\s+[^\s]+\s+$/END/is;
			$trigger =~ s/\%TEXTVALUE-(\d+)\%/'$text_values[$1]'/gs;
			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 = ();

		# 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) {
			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 $tid = 0; 

	# tablespace
	while ($content =~ s/CREATE\s+TABLESPACE\s+([^\s]+)\s+([^;]+);//is) {
		my $t_name = $1;
		my $t_def = $2;
		$t_name =~ s/"//g;
		if ($t_def =~ s/.*DATAFILE\s+'([^']+)'.*/$1/s) {
			$tid++;
			# get path
			my $t_path = dirname($t_def);
			# 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}{username} = $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;
		}
	}

	# 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 matérialized 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();
}

=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)};
	}
	if (!$self->{preserve_case}) {
		$tmptb = lc($tmptb);
		$tmptb =~ s/"//g;
	} elsif ($tmptb !~ /"/) {
		$tmptb = '"' . $tmptb . '"';
	}
	$tmptb = $self->quote_reserved_words($tmptb);

	return $tmptb; 
}

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 = '';
		if (exists $self->{delete}{"\L$table\E"} && $self->{delete}{"\L$table\E"}) {
			$delete_clause = "DELETE 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 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}) {
			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 {
			if ($self->{file_per_table}) {
				$self->data_dump("TRUNCATE TABLE $tmptb;\n",  $table);
			} else {
				$self->dump("\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}}) {
			foreach my $part_name (sort {$self->{partitions}{$table}{$pos}{$a}->{'colpos'} <=> $self->{partitions}{$table}{$pos}{$b}->{'colpos'}} keys %{$self->{partitions}{$table}{$pos}}) {
				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 ($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...\n", 1);
				$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $part_name);
			}
		}
		# 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});
				}
			}
		}
	} 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();
 	}

	# Rename temporary output file
	if (-e "${dirprefix}tmp_${table}_$self->{output}") {
		$self->logit("Renaming temporary file ${dirprefix}tmp_${table}_$self->{output} into ${dirprefix}${table}_$self->{output}\n", 1);
		rename("${dirprefix}tmp_${table}_$self->{output}", "${dirprefix}${table}_$self->{output}");
	}

	return $total_record;
}

=head2 _get_sql_data

Returns a string containing the PostgreSQL compatible SQL Schema
definition.

=cut

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

	my $sql_header = "-- Generated by Ora2Pg, the Oracle database Schema converter, version $VERSION\n";
	$sql_header .= "-- Copyright 2000-2016 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});

	my $sql_output = "";

	# Process view only
	if ($self->{type} eq 'VIEW') {
		$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}};
		foreach my $view (sort sort_view_by_iter keys %ordered_views) {
			$self->logit("\tAdding view $view...\n", 1);
			if (!$self->{quiet} && !$self->{debug}) {
				print STDERR $self->progress_bar($i, $num_total_view, 25, '=', 'views', "generating $view" ), "\r";
			}
			my $fhdl = undef;
			if ($self->{file_per_table}) {
				my $file_name = "$dirprefix${view}_$self->{output}";
				$file_name =~ s/\.(gz|bz2)$//;
				$self->dump("\\i $file_name\n");
				$self->logit("Dumping to one file per view : ${view}_$self->{output}\n", 1);
				$fhdl = $self->open_export_file("${view}_$self->{output}");
			}
			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";
			}
			if (!@{$self->{views}{$view}{alias}}) {
				if (!$self->{preserve_case}) {
					$sql_output .= "CREATE OR REPLACE VIEW \L$tmpv\E AS ";
				} else {
					$tmpv =~ s/\./"."/;
					$sql_output .= "CREATE OR REPLACE VIEW \"$tmpv\" AS ";
				}
				$sql_output .= $self->{views}{$view}{text} . ";\n\n";
			} else {
				if (!$self->{preserve_case}) {
					$sql_output .= "CREATE OR REPLACE VIEW \L$tmpv\E (";
				} else {
					$tmpv =~ s/\./"."/;
					$sql_output .= "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"};
					}
					if (!$self->{preserve_case}) {
						$sql_output .= "\L$fname\E";
					} else {
						$sql_output .= "\"$fname\"";
					}
				}

				if (!$self->{preserve_case}) {
					if ($self->{views}{$view}{text} =~ /SELECT[^\s]*(.*?)\bFROM\b/is) {
						my $clause = $1;
						$clause =~ s/"([^"]+)"/"\L$1\E"/gs;
						$self->{views}{$view}{text} =~ s/SELECT[^\s]*(.*?)\bFROM\b/SELECT $clause FROM/is;
					}
				}
				$sql_output .= ") AS " . $self->{views}{$view}{text} . ";\n\n";
			}

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

			# Add comments on view and columns
			if (!$self->{disable_comment}) {

				if ($self->{views}{$view}{comment}) {
					if (!$self->{preserve_case}) {
						$sql_output .= "COMMENT ON VIEW \L$tmpv\E ";
					} else {
						$sql_output .= "COMMENT ON VIEW \"$tmpv\" ";
					}
					$self->{views}{$view}{comment} =~ s/'/''/gs;
					$sql_output .= " IS E'" . $self->{views}{$view}{comment} . "';\n\n";
				}

				foreach $f (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"};
					}
					if (!$self->{preserve_case}) {
						$sql_output .= "COMMENT ON COLUMN \L$tmpv.$fname\E IS E'" . $self->{views}{$view}{column_comments}{$f} .  "';\n";
					} else {
						my $vname = $tmpv;
						$vname =~ s/\./"."/;
						$sql_output .= "COMMENT ON COLUMN \"$vname\".\"$fname\" IS E'" . $self->{views}{$view}{column_comments}{$f} .  "';\n";
					}
				}
			}

			if ($self->{file_per_table}) {
				$self->dump($sql_header . $sql_output, $fhdl);
				$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";
		} else {
			$sql_output .= "\n";
		}

		$self->dump($sql_output);

		return;
	}

	# Process materialized view only
	if ($self->{type} eq 'MVIEW') {
		$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}};
		foreach my $view (sort { $a cmp $b } keys %{$self->{materialized_views}}) {
			$self->logit("\tAdding materialized view $view...\n", 1);
			if (!$self->{quiet} && !$self->{debug}) {
				print STDERR $self->progress_bar($i, $num_total_mview, 25, '=', 'materialized views', "generating $view" ), "\r";
			}
			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 $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}");
			}
			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");
						if (!$self->{preserve_case}) {
							$sql_output .= "ALTER VIEW \L$view\E_mview OWNER TO \L$owner\E;\n";
						} else {
							$sql_output .= "ALTER VIEW \L$view\E_mview OWNER TO \"$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");
				if (!$self->{preserve_case}) {
					$sql_output .= "ALTER MATERIALIZED VIEW \L$view\E OWNER TO \L$owner\E;\n\n";
				} else {
					$sql_output .= "ALTER MATERIALIZED VIEW \"$view\" OWNER TO \"$owner\";\n\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";
		}

		$self->dump($sql_output);

		return;
	}

	# Process grant only
	if ($self->{type} eq 'GRANT') {
		$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();
		}
		# 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}) {
				if (!$self->{preserve_case}) {
					$realtable =  "\L$self->{schema}.$table\E";
				} else {
					$realtable =  "\"$self->{schema}\".\"$table\"";
				}
			} elsif ($self->{preserve_case}) {
				$realtable =  "\"$table\"";
			}
			$grants .= "-- Set priviledge on $self->{grants}{$table}{type} $table\n";

			my $ownee = $self->{grants}{$table}{owner};
			if (!$self->{preserve_case}) {
				$ownee = lc($self->{grants}{$table}{owner});
			} else {
				$ownee = "\"$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/,$//;
				if (!$self->{preserve_case}) {
					$usr = lc($usr);
				} else {
					$usr = "\"$usr\"";
				}
				if ($self->{grants}{$table}{type} ne 'PACKAGE BODY') {
					if (grep(/^$self->{grants}{$table}{type}$/, 'FUNCTION', 'SEQUENCE','SCHEMA','TABLESPACE')) {
						$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";
		}

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

		$sql_output .= "\n" . $grants . "\n";

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

		return;
	}

	# Process sequences only
	if ($self->{type} eq 'SEQUENCE') {
		$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;

		foreach my $seq (sort { $a->[0] cmp $b->[0] } @{$self->{sequences}}) {
			if (!$self->{quiet} && !$self->{debug}) {
				print STDERR $self->progress_bar($i, $num_total_sequence, 25, '=', 'sequences', "generating $seq->[0]" ), "\r";
			}
			my $cache = 1;
			$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];
			}
			if (!$self->{preserve_case}) {
				$sql_output .= "CREATE SEQUENCE \L$seq->[0]\E INCREMENT $seq->[3]";
			} else {
				$seq->[0] =~ s/\./"."/;
				$sql_output .= "CREATE SEQUENCE \"$seq->[0]\" INCREMENT $seq->[3]";
			}
			if ($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] > (2**63-1)) {
				$sql_output .= " NO MAXVALUE";
			} else {
				$sql_output .= " MAXVALUE $seq->[2]";
			}
			$sql_output .= " START $seq->[4] CACHE $cache$cycle;\n";

			if ($self->{force_owner}) {
				my $owner = $seq->[7];
				$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
				if (!$self->{preserve_case}) {
					$sql_output .= "ALTER SEQUENCE \L$seq->[0]\E OWNER TO \L$owner\E;\n";
				} else {
					$sql_output .= "ALTER SEQUENCE \"$seq->[0]\" OWNER TO \"$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";
		}

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

	# Process dblink only
	if ($self->{type} eq 'DBLINK') {
		$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";
			}
			if (!$self->{preserve_case}) {
				$sql_output .= "CREATE SERVER \L$db\E";
			} else {
				$sql_output .= "CREATE SERVER \"$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}'";
			}
			$self->{dblink}{$db}{user} ||= $self->{dblink}{$db}{username};
			if ($self->{dblink}{$db}{username}) {
				if (!$self->{preserve_case}) {
					$sql_output .= "CREATE USER MAPPING FOR \L$self->{dblink}{$db}{username}\E SERVER \L$db\E OPTIONS (user '\L$self->{dblink}{$db}{user}\E' $self->{dblink}{$db}{password});\n";
				} else {
					$sql_output .= "CREATE USER MAPPING FOR \"$self->{dblink}{$db}{username}\" SERVER \"$db\" OPTIONS (user '$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");
				if (!$self->{preserve_case}) {
					$sql_output .= "ALTER FOREIGN DATA WRAPPER \L$db\E OWNER TO \L$owner\E;\n";
				} else {
					$sql_output .= "ALTER FOREIGN DATA WRAPPER \"$db\" OWNER TO \"$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";
		}

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

	# Process dblink only
	if ($self->{type} eq 'DIRECTORY') {
		$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);
				if (!$self->{preserve_case}) {
					$sql_output .= "INSERT INTO external_file.directory_roles(directory_name,directory_role,directory_read,directory_write) VALUES ('$db','\L$owner\E', true, $write);\n";
				} else {
					$sql_output .= "INSERT INTO external_file.directory_roles(directory_name,directory_role,directory_read,directory_write) VALUES ('$db','$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";
		}

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


	# Process triggers only. PL/SQL code is pre-converted to PL/PGSQL following
	# the recommendation of Roberto Mello, see http://techdocs.postgresql.org/
	# Oracle's PL/SQL to PostgreSQL PL/pgSQL HOWTO  
	if ($self->{type} eq 'TRIGGER') {
		$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;
		foreach my $trig (sort {$a->[0] cmp $b->[0]} @{$self->{triggers}}) {

			if (!$self->{quiet} && !$self->{debug}) {
				print STDERR $self->progress_bar($i, $num_total_trigger, 25, '=', 'triggers', "generating $trig->[0]" ), "\r";
			}
			my $fhdl = undef;
			if ($self->{file_per_function}) {
				$self->dump("\\i $dirprefix$trig->[0]_$self->{output}\n");
				$self->logit("Dumping to one file per trigger : $trig->[0]_$self->{output}\n", 1);
				$fhdl = $self->open_export_file("$trig->[0]_$self->{output}");
			}
			$trig->[1] =~ s/\s*EACH ROW//is;
			chomp($trig->[4]);
			$trig->[4] =~ s/[;\/]$//;
			$self->logit("\tDumping trigger $trig->[0] defined on table $trig->[3]...\n", 1);
			my $tbname = $self->get_replaced_tbname($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 = lc($cname) if (!$self->{preserve_case});
					$trig->[4] =~ s/(OLD|NEW)\.$coln\b/$1\.$cname/igs;
				}
			}
			if ($self->{export_schema} && !$self->{schema}) {
				$sql_output .= $self->set_search_path($trig->[8]) . "\n";
			}
			# Check if it's like a pg rule
			if (!$self->{pg_supports_insteadof} && $trig->[1] =~ /INSTEAD OF/) {
				if (!$self->{preserve_case}) {
					$sql_output .= "CREATE OR REPLACE RULE \L$trig->[0]\E AS\n\tON $trig->[2] TO $tbname\n\tDO INSTEAD\n(\n\t$trig->[4]\n);\n\n";
				} else {
					$sql_output .= "CREATE OR REPLACE RULE \L$trig->[0]\E AS\n\tON $trig->[2] TO $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");
					if (!$self->{preserve_case}) {
						$sql_output .= "ALTER RULE \L$trig->[0]\E OWNER TO \L$owner\E;\n";
					} else {
						$sql_output .= "ALTER RULE \"$trig->[0]\" OWNER TO \"$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::plsql_to_plpgsql($self, $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] !~ /;$/);
							$trig->[4] = "BEGIN\n$trig->[4]\n$ret_kind\nEND;";
						}
						$trig->[4] = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $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\/]*$/$ret_kind\n$1/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});
							} else {
								$parts[0] = Ora2Pg::MySQL::replace_sql_type($parts[0], $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type});
							}
						}
						$trig->[4] = join('BEGIN', @parts);
						$trig->[4] =~ s/\bRETURN\s*;/$ret_kind/igs;
					}
				}
				if (!$self->{preserve_case}) {
					$sql_output .= "DROP TRIGGER $self->{pg_supports_ifexists} \L$trig->[0]\E ON $tbname CASCADE;\n";
				} else {
					$sql_output .= "DROP TRIGGER $self->{pg_supports_ifexists} \"$trig->[0]\" ON $tbname CASCADE;\n";
				}
				my $security = '';
				my $revoke = '';
				if ($self->{security}{"\U$trig->[0]\E"}{security} eq 'DEFINER') {
					$security = " SECURITY DEFINER";
					$revoke = "-- REVOKE ALL ON FUNCTION trigger_fct_\L$trig->[0]()\E FROM PUBLIC;\n";
				}
				if ($self->{pg_supports_when} && $trig->[5]) {
					if (!$self->{preserve_case}) {
						my @text_values = ();
						my $j = 0;
						while ($trig->[4] =~ s/'([^']+)'/\%TEXTVALUE-$j\%/s) {
							push(@text_values, $1);
							$j++;
						}
						$trig->[4] =~ s/"([^"]+)"/\L$1\E/gs;
						$trig->[4] =~ s/\%TEXTVALUE-(\d+)\%/'$text_values[$1]'/gs;
						$trig->[4] =~ s/ALTER TRIGGER\s+[^\s]+\s+ENABLE(;)?//;
					}
					$sql_output .= "CREATE OR REPLACE FUNCTION trigger_fct_\L$trig->[0]\E() 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");
						if (!$self->{preserve_case}) {
							$sql_output .= "ALTER FUNCTION trigger_fct_\L$trig->[0]\E() OWNER TO \L$owner\E;\n\n";
						} else {
							$sql_output .= "ALTER FUNCTION trigger_fct_\L$trig->[0]\E() OWNER TO \"$owner\";\n\n";
						}
					}
					$trig->[6] =~ s/\n+$//s;
					$trig->[6] =~ s/^[^\.\s]+\.//;
					if (!$self->{preserve_case}) {
						my @text_values = ();
						my $j = 0;
						while ($trig->[6] =~ s/'([^']+)'/\%TEXTVALUE-$j\%/s) {
							push(@text_values, $1);
							$j++;
						}
						$trig->[6] =~ s/"([^"]+)"/\L$1\E/gs;
						$trig->[6] =~ s/\%TEXTVALUE-(\d+)\%/'$text_values[$1]'/gs;

						@text_values = ();
						$j = 0;
						while ($trig->[5] =~ s/'([^']+)'/\%TEXTVALUE-$j\%/s) {
							push(@text_values, $1);
							$j++;
						}
						$trig->[5] =~ s/"([^"]+)"/\L$1\E/gs;
						$trig->[5] =~ s/\%TEXTVALUE-(\d+)\%/'$text_values[$1]'/gs;
					}
					chomp($trig->[6]);
					# Remove referencing clause, not supported by PostgreSQL
					$trig->[6] =~ s/REFERENCING\s+(.*?)(FOR\s+EACH\s+)/$2/is;
					$sql_output .= "CREATE TRIGGER $trig->[6]\n";
					if ($trig->[5]) {
						if ($self->{plsql_pgsql}) {
							$trig->[5] = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $trig->[5]);
						}
						$sql_output .= "\tWHEN ($trig->[5])\n";
					}
					$sql_output .= "\tEXECUTE PROCEDURE trigger_fct_\L$trig->[0]\E();\n\n";
				} else {
					if (!$self->{preserve_case}) {
						my @text_values = ();
						my $j = 0;
						while ($trig->[4] =~ s/'([^']+)'/\%TEXTVALUE-$j\%/s) {
							push(@text_values, $1);
							$j++;
						}
						$trig->[4] =~ s/"([^"]+)"/\L$1\E/gs;
						$trig->[4] =~ s/\%TEXTVALUE-(\d+)\%/'$text_values[$1]'/gs;
						$trig->[4] =~ s/ALTER TRIGGER\s+[^\s]+\s+ENABLE(;)?//;
					}
					$sql_output .= "CREATE OR REPLACE FUNCTION trigger_fct_\L$trig->[0]\E() 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");
						if (!$self->{preserve_case}) {
							$sql_output .= "ALTER FUNCTION trigger_fct_\L$trig->[0]\E() OWNER TO \L$owner\E;\n\n";
						} else {
							$sql_output .= "ALTER FUNCTION trigger_fct_\L$trig->[0]\E() OWNER TO \"$owner\";\n\n";
						}
					}
					$sql_output .= "CREATE TRIGGER \L$trig->[0]\E\n\t";
					my $statement = 0;
					$statement = 1 if ($trig->[1] =~ s/ STATEMENT//);
					if (!$self->{preserve_case}) {
						$sql_output .= "$trig->[1] $trig->[2] ON $tbname ";
					} else {
						$sql_output .= "$trig->[1] $trig->[2] ON $tbname ";
					}
					if ($statement) {
						$sql_output .= "FOR EACH STATEMENT\n";
					} else {
						$sql_output .= "FOR EACH ROW\n";
					}
					$sql_output .= "\tEXECUTE PROCEDURE trigger_fct_\L$trig->[0]\E();\n\n";
				}
			}
			if ($self->{file_per_function}) {
				$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_trigger, 25, '=', 'triggers', 'end of output.'), "\n";
		}
		if (!$nothing) {
			$sql_output = "-- Nothing found of type $self->{type}\n";
		}

		$self->dump($sql_output);
		return;
	}

	# Process queries to load
	if ($self->{type} eq 'LOAD') {
		$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);
			open(IN, "$self->{input_file}");
			my @allqueries = <IN>;
			close(IN);
			my $query = 1;
			my $content = join('', @allqueries);
			@allqueries = ();
			# remove comments
			$self->_remove_comments(\$content);
			$content =~  s/\%ORA2PG_COMMENT\d+\%//gs;
			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/^\/$/;/) || ($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});
			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}++;
			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(500000);
			}
			if (!$self->{quiet} && !$self->{debug}) {
				print STDERR "\n";
			}
		}
		return;
	}

	# Process queries only
	if ($self->{type} eq 'QUERY') {
		$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
		#---------------------------------------------------------
		my %comments = ();
		if ($self->{input_file}) {
			$self->{functions} = ();
			$self->logit("Reading input code from file $self->{input_file}...\n", 1);
			open(IN, "$self->{input_file}");
			my @allqueries = <IN>;
			close(IN);
			my $query = 1;
			my $content = join('', @allqueries);
			@allqueries = ();
			$self->{idxcomment} = 0;
			%comments = $self->_remove_comments(\$content);
			foreach my $l (split(/\n/, $content)) {
				chomp($l);
				next if ($l =~ /^\s*$/);
				if ($old_line) {
					$l = $old_line .= ' ' . $l;
					$old_line = '';
				}
				if ( ($l =~ s/^\/$/;/) || ($l =~ /;\s*$/) ) {
						$self->{queries}{$query} .= "$l\n";
						$query++;
				} else {
					$self->{queries}{$query} .= "$l\n";
				}
			}
		} else {
			foreach my $q (keys %{$self->{queries}}) {
				$self->{queries}{$q} =~ s/\%ORA2PG_COMMENT\d+\%//gs;
			}
		}
		foreach my $q (keys %{$self->{queries}}) {
			if ($self->{queries}{$q} !~ /(SELECT|UPDATE|DELETE|INSERT)/is) {
				delete $self->{queries}{$q};
			} else {
				$self->_restore_comments(\$self->{queries}{$q}, \%comments);
			}
		}
		#--------------------------------------------------------

		my $total_size = 0;
		my $cost_value = 0;
		foreach my $q (sort {$a <=> $b} keys %{$self->{queries}}) {
			$self->{idxcomment} = 0;
			my %comments = $self->_remove_comments($self->{queries}{$q});
			$total_size += length($self->{queries}{$q});
			$self->logit("Dumping query $q...\n", 1);
			my $fhdl = undef;
			if ($self->{plsql_pgsql}) {
				my $sql_q = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{queries}{$q});
				$sql_output .= $sql_q;
				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;
					$sql_output .= "\n-- Estimed cost of query [ $q ]: " . sprintf("%2.2f", $cost);
				}
			} else {
				$sql_output .= $self->{queries}{$q};
			}
			$sql_output .= "\n\n";
			$self->_restore_comments(\$sql_output, \%comments);
			$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";
		}
		$self->dump($sql_output);
		$self->{queries} = ();
		return;
	}

	# Process functions only
	if ($self->{type} eq 'FUNCTION') {
		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);
			open(IN, "$self->{input_file}");
			my @allfct = <IN>;
			close(IN);
			my $fcnm = '';
			my $old_line = '';
			my $language = '';
			foreach my $l (@allfct) {
				chomp($l);
				$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*(?:EDITABLE|NONEDITABLE)?\s*$/i) {
					$old_line = $l;
					next;
				}
				if ($l =~ /^\s*(?:EDITABLE|NONEDITABLE)?\s*(FUNCTION|PROCEDURE)$/i) {
					$old_line = $l;
					next;
				}
				if ($l =~ /^\s*CREATE\s*(?:OR REPLACE)?\s*(?:EDITABLE|NONEDITABLE)?\s*(FUNCTION|PROCEDURE)\s*$/i) {
					$old_line = $l;
					next;
				}
				$l =~ s/^\s*CREATE (?:OR REPLACE)?\s*(?:EDITABLE|NONEDITABLE)?\s*(FUNCTION|PROCEDURE)/$1/i;
				$l =~ s/^\s*(?:EDITABLE|NONEDITABLE)?\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 = '';
					}
				}
			}
		}
		#--------------------------------------------------------

		my $total_size = 0;
		my $cost_value = 0;
                my $i = 1;
                my $num_total_function = scalar keys %{$self->{functions}};
		my $fct_cost = '';
		foreach my $fct (sort keys %{$self->{functions}}) {

			if (!$self->{quiet} && !$self->{debug}) {
				print STDERR $self->progress_bar($i, $num_total_function, 25, '=', 'functions', "generating $fct" ), "\r";
			}
			$self->{idxcomment} = 0;
			my %comments = $self->_remove_comments(\$self->{functions}{$fct}{text});
			$total_size += length($self->{functions}->{$fct}{text});
			$self->logit("Dumping function $fct...\n", 1);
			my $fhdl = undef;
			if ($self->{file_per_function}) {
				$self->dump("\\i $dirprefix${fct}_$self->{output}\n");
				$self->logit("Dumping to one file per function : ${fct}_$self->{output}\n", 1);
				$fhdl = $self->open_export_file("${fct}_$self->{output}");
			}
			if ($self->{plsql_pgsql}) {

				my $sql_f = '';
				if ($self->{is_mysql}) {
					$sql_f = $self->_convert_function($self->{functions}{$fct}{owner}, $self->{functions}{$fct}{text}, $fct);
				} else {
					$sql_f = $self->_convert_function($self->{functions}{$fct}{owner}, $self->{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'};
						$cost_value += $cost;
						$self->logit("Function $fct estimated cost: $cost\n", 1);
						$sql_output .= "-- Function $fct estimated cost: $cost\n";
						$fct_cost .= "\t-- Function $fct 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";
						}
					}
				}
			} else {
				$sql_output .= $self->{functions}{$fct}{text} . "\n\n";
			}
			$self->_restore_comments(\$sql_output, \%comments);

			if ($self->{file_per_function}) {
				$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_function, 25, '=', 'functions', 'end of output.'), "\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";
		}
		$self->dump($sql_output);
		$self->{functions} = ();
		return;
	}

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

		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);
			open(IN, "$self->{input_file}");
			my @allfct = <IN>;
			close(IN);
			my $fcnm = '';
			my $old_line = '';
			my $language = '';
			foreach my $l (@allfct) {
				chomp($l);
				$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*(?:EDITABLE|NONEDITABLE)?\s*$/i) {
					$old_line = $l;
					next;
				}
				if ($l =~ /^\s*(?:EDITABLE|NONEDITABLE)?\s*(FUNCTION|PROCEDURE)$/i) {
					$old_line = $l;
					next;
				}
				if ($l =~ /^\s*CREATE\s*(?:OR REPLACE)?\s*(?:EDITABLE|NONEDITABLE)?\s*(FUNCTION|PROCEDURE)\s*$/i) {
					$old_line = $l;
					next;
				}
				$l =~ s/^\s*CREATE (?:OR REPLACE)?\s*(?:EDITABLE|NONEDITABLE)?\s*(FUNCTION|PROCEDURE)/$1/i;
				$l =~ s/^\s*(?:EDITABLE|NONEDITABLE)?\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 = '';
					}
				}
			}
		}
		#--------------------------------------------------------
                my $total_size = 0;
                my $cost_value = 0;
		my $i = 1;
		my $num_total_procedure = scalar keys %{$self->{procedures}};
		my $fct_cost = '';
		foreach my $fct (sort keys %{$self->{procedures}}) {

			if (!$self->{quiet} && !$self->{debug}) {
				print STDERR $self->progress_bar($i, $num_total_procedure, 25, '=', 'procedures', "generating $fct" ), "\r";
			}
			$self->{idxcomment} = 0;
			my %comments = $self->_remove_comments(\$self->{procedures}{$fct}{text});
			$total_size += length($self->{procedures}->{$fct}{text});

			$self->logit("Dumping procedure $fct...\n", 1);
			my $fhdl = undef;
			if ($self->{file_per_function}) {
				$self->dump("\\i $dirprefix${fct}_$self->{output}\n");
				$self->logit("Dumping to one file per procedure : ${fct}_$self->{output}\n", 1);
				$fhdl = $self->open_export_file("${fct}_$self->{output}");
			}
			if ($self->{plsql_pgsql}) {
				my $sql_p = '';
				if ($self->{is_mysql}) {
					$sql_p = $self->_convert_function($self->{procedures}{$fct}{owner}, $self->{procedures}{$fct}{text}, $fct);
				} else {
					$sql_p = $self->_convert_function($self->{procedures}{$fct}{owner}, $self->{procedures}{$fct}{text});
				}
				if ( $sql_p ) {
					$sql_output .= $sql_p . "\n\n";
					if ($self->{estimate_cost}) {
						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $sql_p);
						$cost += $Ora2Pg::PLSQL::OBJECT_SCORE{'PROCEDURE'};
						$cost_value += $cost;
						$self->logit("Function $fct estimated cost: $cost\n", 1);
						$fct_cost .= "\t-- Function $fct 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";
						}
					}
				}
			} else {
				$sql_output .= $self->{procedures}{$fct}{text} . "\n\n";
			}
			$self->_restore_comments(\$sql_output, \%comments);
			$sql_output .= $fct_cost;
			if ($self->{file_per_function}) {
				$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_procedure, 25, '=', 'procedures', 'end of output.'), "\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";
		}
		$self->dump($sql_output);
		$self->{procedures} = ();

		return;
	}

	# Process packages only
	if ($self->{type} eq '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);
			sleep(1);
			open(IN, "$self->{input_file}");
			my $content = '';
			while (my $l = <IN>) {
				$l =~ s/\r//g;
				next if ($l =~ /^\/$/);
				$content .= $l;
			};
			close(IN);
			my $pknm = '';
			my $before = '';
			my $old_line = '';
			my $skip_pkg_header = 0;
			$self->{idxcomment} = 0;
			my %comments = $self->_remove_comments(\$content);
			$content =~ s/(?:CREATE|CREATE OR REPLACE)?\s*(?:EDITABLE|NONEDITABLE)?\s*PACKAGE\s+/CREATE PACKAGE /igs;
			my @allpkg = split(/(CREATE PACKAGE\s+)/, $content);
			$content = '';
			foreach my $l (@allpkg) {
				if (!$old_line && ($l =~ /(CREATE PACKAGE\s+)/)) {
					$old_line = $1;
					next;
				} elsif ($old_line) {
					if ($l =~ s/^BODY\s+([^\s]+)\s+//is) {
						$self->{packages}{"\L$1\E"}{text} .= "PACKAGE BODY $1 $l\n";
					} elsif ($l =~ /^([^\s]+)\s+/s) {
						my $pname = lc($1);
						$pname =~ s/"//g;
						$l =~ s/[\s;]+\s*$/;/gs;
						$self->{packages}{$pname}{text} .= "PACKAGE $l\n";
					}
					$old_line = '';
				}
			}
			foreach my $pkg (sort keys %{$self->{packages}}) {
				$self->_restore_comments(\$self->{packages}{$pkg}{text}, \%comments);
			}
		}
		#--------------------------------------------------------

		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});
			my $pkgbody = '';
			my $fct_cost = '';
			if (!$self->{plsql_pgsql}) {
				$self->logit("Dumping package $pkg...\n", 1);
				if ($self->{file_per_function}) {
					$pkgbody = "\\i $dirprefix\L${pkg}\E_$self->{output}\n";
					my $fhdl = $self->open_export_file("$dirprefix\L${pkg}\E_$self->{output}", 1);
					$self->dump($sql_header . $self->{packages}{$pkg}{text}, $fhdl);
					$self->close_export_file($fhdl);
				} else {
					$pkgbody = $self->{packages}{$pkg}{text};
				}

			} else {
				my @codes = split(/CREATE(?: OR REPLACE)?(?: EDITABLE| NONEDITABLE)? PACKAGE BODY/i, $self->{packages}{$pkg}{text});
				if ($self->{estimate_cost}) {
					$total_size += length($self->{packages}->{$pkg}{text});
					foreach my $txt (@codes) {
						my %infos = $self->_lookup_package("CREATE OR REPLACE PACKAGE BODY$txt");
						foreach my $f (sort keys %infos) {
							next if (!$f);
							$self->{idxcomment} = 0;
							my %comments = $self->_remove_comments(\$infos{$f}{name});
							$total_size_no_comment += (length($infos{$f}{name}) - (17 * $self->{idxcomment}));
							my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $infos{$f}{name});
							$self->_restore_comments(\$infos{$f}{name}, \%comments);
							$cost += $Ora2Pg::PLSQL::OBJECT_SCORE{'FUNCTION'};
							$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";
				}
				foreach my $txt (@codes) {
					$pkgbody .= $self->_convert_package("CREATE OR REPLACE PACKAGE BODY$txt", $self->{packages}{$pkg}{owner});
					$pkgbody =~ s/[\r\n]*END;\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: $total_size_no_comment bytes.\n", 1);
				$self->logit("Total number of functions found inside those packages: $number_fct.\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 .= "-- 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: $total_size_no_comment bytes.\n";
					$sql_output .= "-- Total number of functions found inside those packages: $number_fct.\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->{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";
		}
		$self->dump($sql_output);
		$self->{packages} = ();
		return;
	}

	# Process types only
	if ($self->{type} eq 'TYPE') {
		$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);
			open(IN, "$self->{input_file}");
			my @alltype = <IN>;
			close(IN);
			my $typnm = '';
			my $code = '';
			foreach my $l (@alltype) {
				chomp($l);
				next if ($l =~ /^\s*$/);
				$l =~ s/^\s*CREATE\s*(?:OR REPLACE)?\s*(?:NONEDITABLE|EDITABLE)?\s*//i;
				$l =~ s/^\s*CREATE\s*//i;
				$code .= $l . "\n";
				if ($code =~ /^TYPE\s+([^\s\(]+)/is) {
					$typnm = $1;
				}
				next if (!$typnm);
				if ($code =~ /;/s) {
					push(@{$self->{types}}, { ('name' => $typnm, 'code' => $code) });
					$typnm = '';
					$code = '';
				}
			}
		}
		#--------------------------------------------------------
		my $i = 1;
		foreach my $tpe (sort {length($a->{name}) <=> length($b->{name}) } @{$self->{types}}) {
			$self->logit("Dumping type $tpe->{name}...\n", 1);
			if (!$self->{quiet} && !$self->{debug}) {
				print STDERR $self->progress_bar($i, $#{$self->{types}}+1, 25, '=', 'types', "generating $tpe->{name}" ), "\r";
			}
			if ($self->{plsql_pgsql}) {
				$tpe->{code} = $self->_convert_type($tpe->{code}, $tpe->{owner});
			} else {
				$tpe->{code} = "CREATE OR REPLACE $tpe->{code}\n";
			}
			$sql_output .= $tpe->{code} . "\n";
			$i++;
		}

		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";
		}
		$self->dump($sql_header . $sql_output);
		return;
	}

	# Process TABLESPACE only
	if ($self->{type} eq 'TABLESPACE') {
		$self->logit("Add tablespaces definition...\n", 1);
		$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 $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}}) {
			# 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;
					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) {
							if (!$self->{preserve_case}) {
								$create_tb .= "ALTER TABLESPACE \L$tb_name\E OWNER TO \L$owner\E;\n";
							} else {
								$create_tb .= "ALTER TABLESPACE \"$tb_name\" OWNER TO \"$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'));
						if (!$self->{preserve_case} || ($tb_type eq 'INDEX')) {
							$sql_output .= "ALTER $tb_type \L$obj\E SET TABLESPACE \L$tb_name\E;\n";
						} else {
							$sql_output .= "ALTER $tb_type \"$obj\" SET TABLESPACE \L$tb_name\E;\n";
						}
					}
				}
			}
		}

		if (!$sql_output) {
			$sql_output = "-- Nothing found of type $self->{type}\n";
		}
		$self->dump($sql_header . "$create_tb\n" . $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}");
			$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->dump($sql_header . $sql_output, $fhdl);
			$self->close_export_file($fhdl);
		}
		return;
	}

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

		# 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
		my @ordered_tables = sort { $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;
	}

	# Extract data only
	if (($self->{type} eq 'INSERT') || ($self->{type} eq 'COPY')) {

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

		# 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 (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
		my @ordered_tables = sort { $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";
			}
		} else {
			# 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}) {
				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 $file_name\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}}) {
					foreach my $part_name (sort {$self->{partitions}{$table}{$pos}{$a}->{'colpos'} <=> $self->{partitions}{$table}{$pos}{$b}->{'colpos'}} keys %{$self->{partitions}{$table}{$pos}}) {
						$part_name = $table . '_' . $part_name if ($self->{prefix_partition});
						next if ($self->{allow_partition} && !grep($_ =~ /^$part_name$/i, @{$self->{allow_partition}}));

						if ($self->{file_per_table} && !$self->{pg_dsn}) {
							my $file_name = "$dirprefix${part_name}_$self->{output}";
							$file_name =~ s/\.(gz|bz2)$//;
							$load_file .=  "\\i $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 $file_name\n";
						}
					}
				}
			}
		}

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

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

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

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

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

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

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

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

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

			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(500000);
				}
			} else {
				$total_record = $self->_export_table_data($table, $dirprefix, $sql_header);
			}

			# Close data file
			$self->close_export_file($self->{cfhout}) if (defined $self->{cfhout});
			$self->{cfhout} = undef;

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

		#### Set SQL commands that must be executed after data loading
		my $footer = '';
		foreach my $table (@ordered_tables) {

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

			# disable triggers of current table if requested
			if ($self->{disable_triggers}) {
				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}) {
				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}) {
				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->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";
				}
			}
		}

		# Commit transaction
		if ($self->{pg_dsn}) {
			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);

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

		return;
	}

	# Process PARTITION only
	if ($self->{type} eq 'PARTITION') {
		$self->logit("Add partitions definition...\n", 1);
		$sql_header .= "-- Oracle partitions export.\n";
		$sql_header .= "-- Please take a look at the export to see if order and default table match your need\n";
		my $total_partition = 0;
		foreach my $table (sort keys %{$self->{partitions}}) {
			foreach my $pos (keys %{$self->{partitions}{$table}}) {
				foreach my $part (keys %{$self->{partitions}{$table}{$pos}}) {
					$total_partition++;
				}
			}
		}
		my $i = 1;
		foreach my $table (sort keys %{$self->{partitions}}) {
			my $function = qq{
CREATE OR REPLACE FUNCTION ${table}_insert_trigger()
RETURNS TRIGGER AS \$\$
BEGIN
};
			my $cond = 'IF';
			my $funct_cond = '';
			my %create_table = ();
			my $idx = 0;
			my $old_pos = '';
			my $old_part = '';
			my $owner = '';
			foreach my $pos (sort {$a <=> $b} keys %{$self->{partitions}{$table}}) {
				foreach my $part (sort {$self->{partitions}{$table}{$pos}{$a}->{'colpos'} <=> $self->{partitions}{$table}{$pos}{$b}->{'colpos'}} keys %{$self->{partitions}{$table}{$pos}}) {
					if (!$self->{quiet} && !$self->{debug}) {
						print STDERR $self->progress_bar($i, $total_partition, 25, '=', 'partitions', "generating $part" ), "\r";
					}
					my $tb_name = $part;
					if ($self->{prefix_partition}) {
						$tb_name = $table . "_" . $part;
					} else {
						if ($self->{export_schema} && !$self->{schema} && ($table =~ /^([^\.]+)\./)) {
							$tb_name =  $1 . '.' . $tb_name;
						}
					}
					$create_table{$table}{table} .= "CREATE TABLE $tb_name ( CHECK (\n";
					my $check_cond = '';
					my @condition = ();
					my @ind_col = ();
					for (my $i = 0; $i <= $#{$self->{partitions}{$table}{$pos}{$part}}; $i++) {
						if ($self->{partitions}{$table}{$pos}{$part}[$i]->{type} eq 'LIST') {
							$check_cond .= "\t$self->{partitions}{$table}{$pos}{$part}[$i]->{column} IN ($self->{partitions}{$table}{$pos}{$part}[$i]->{value})";
						} else {
							if ($#{$self->{partitions}{$table}{$pos}{$part}} == 0) {
								if ($old_part eq '') {
									$check_cond .= "\t$self->{partitions}{$table}{$pos}{$part}[$i]->{column} < " . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{partitions}{$table}{$pos}{$part}[$i]->{value});
								} else {
									$check_cond .= "\t$self->{partitions}{$table}{$pos}{$part}[$i]->{column} >= " . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{partitions}{$table}{$old_pos}{$old_part}[$i]->{value}) . " AND $self->{partitions}{$table}{$pos}{$part}[$i]->{column} < " . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{partitions}{$table}{$pos}{$part}[$i]->{value});
								}
							} else {
								my @values = split(/,\s/, Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{partitions}{$table}{$pos}{$part}[$i]->{value}));
								# multicolumn partitioning
								$check_cond .= "\t$self->{partitions}{$table}{$pos}{$part}[$i]->{column} < " .  $values[$i];
							}
						}
						$check_cond .= " AND" if ($i < $#{$self->{partitions}{$table}{$pos}{$part}});
						my $fct = '';
						my $colname = $self->{partitions}{$table}{$pos}{$part}[$i]->{column};
						if ($colname =~ s/([^\(]+)\(([^\)]+)\)/$2/) {
							$fct = $1;
						}
						my $cindx = $self->{partitions}{$table}{$pos}{$part}[$i]->{column} || '';
						$cindx = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $cindx);
						$create_table{$table}{'index'} .= "CREATE INDEX ${tb_name}_$colname ON $tb_name ($cindx);\n";
						if ($self->{partitions_default}{$table} && ($create_table{$table}{'index'} !~ /ON $self->{partitions_default}{$table} /)) {
							$cindx = $self->{partitions}{$table}{$pos}{$part}[$i]->{column} || '';
							$cindx = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $cindx);
							$create_table{$table}{'index'} .= "CREATE INDEX $self->{partitions_default}{$table}_$colname ON $self->{partitions_default}{$table} ($cindx);\n";
						}
						push(@ind_col, $self->{partitions}{$table}{$pos}{$part}[$i]->{column}) if (!grep(/^$self->{partitions}{$table}{$pos}{$part}[$i]->{column}$/, @ind_col));
						if ($self->{partitions}{$table}{$pos}{$part}[$i]->{type} eq 'LIST') {
							if (!$fct) {
								push(@condition, "NEW.$self->{partitions}{$table}{$pos}{$part}[$i]->{column} IN (" . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{partitions}{$table}{$pos}{$part}[$i]->{value}) . ")");
							} else {
								push(@condition, "$fct(NEW.$colname) IN (" . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{partitions}{$table}{$pos}{$part}[$i]->{value}) . ")");
							}
						} else {
							if (!$fct) {
								push(@condition, "NEW.$self->{partitions}{$table}{$pos}{$part}[$i]->{column} < " . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{partitions}{$table}{$pos}{$part}[$i]->{value}));
							} else {
								push(@condition, "$fct(NEW.$colname) < " . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{partitions}{$table}{$pos}{$part}[$i]->{value}));
							}
						}
						$owner = $self->{partitions}{$table}{$pos}{$part}[$i]->{owner} || '';
					}
					$check_cond = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $check_cond);
					$create_table{$table}{table} .= $check_cond;
					$create_table{$table}{table} .= "\n) ) INHERITS ($table);\n";
					$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
					if ($owner) {
						if (!$self->{preserve_case}) {
							$create_table{$table}{table} .= "ALTER TABLE \L$tb_name\E OWNER TO \L$owner\E;\n";
						} else {
							$create_table{$table}{table}.= "ALTER TABLE \"$tb_name\" OWNER TO \"$owner\";\n";
						}
					}
					# Add subpartition if any defined on Oracle
					my $sub_funct_cond = '';
					my $sub_old_part = '';
					if (exists $self->{subpartitions}{$table}) {
						my $sub_cond = 'IF';
						foreach my $p (sort {$a <=> $b} keys %{$self->{subpartitions}{$table}}) {
							foreach my $subpart (sort {$a <=> $b} keys %{$self->{subpartitions}{$table}{$p}}) {
								my $sub_tb_name = $subpart;
								$sub_tb_name = $table . "_" . $subpart if ($self->{prefix_partition});
								$sub_tb_name =~ s/^[^\.]+\.//; # remove schema part if any
								$create_table{$table}{table} .= "CREATE TABLE ${tb_name}_$sub_tb_name ( CHECK (\n";
								my $sub_check_cond = '';
								my @subcondition = ();
								for (my $i = 0; $i <= $#{$self->{subpartitions}{$table}{$p}{$subpart}}; $i++) {
									if ($self->{subpartitions}{$table}{$p}{$subpart}[$i]->{type} eq 'LIST') {
										$sub_check_cond .= "$self->{subpartitions}{$table}{$p}{$subpart}[$i]->{column} IN ($self->{subpartitions}{$table}{$p}{$subpart}[$i]->{value})";
									} else {
										if ($#{$self->{subpartitions}{$table}{$p}{$subpart}} == 0) {
											if ($sub_old_part eq '') {
												$sub_check_cond .= "$self->{subpartitions}{$table}{$p}{$subpart}[$i]->{column} < " . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{subpartitions}{$table}{$p}{$subpart}[$i]->{value});
											} else {
												$sub_check_cond .= "$self->{subpartitions}{$table}{$p}{$subpart}[$i]->{column} >= " . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{subpartitions}{$table}{$old_pos}{$sub_old_part}[$i]->{value}) . " AND $self->{subpartitions}{$table}{$p}{$subpart}[$i]->{column} < " . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{subpartitions}{$table}{$p}{$subpart}[$i]->{value});
											}
										} else {
											my @values = split(/,\s/, Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{subpartitions}{$table}{$p}{$subpart}[$i]->{value}));
											# multicolumn partitioning
											$sub_check_cond .= "\t$self->{subpartitions}{$table}{$p}{$subpart}[$i]->{column} < " .  $values[$i];
										}
									}
									$sub_check_cond .= " AND " if ($i < $#{$self->{subpartitions}{$table}{$p}{$subpart}});
									push(@ind_col, $self->{subpartitions}{$table}{$p}{$subpart}[$i]->{column}) if (!grep(/^$self->{subpartitions}{$table}{$p}{$subpart}[$i]->{column}$/, @ind_col));
									my $fct = '';
									my $colname = $self->{subpartitions}{$table}{$p}{$subpart}[$i]->{column};
									if ($colname =~ s/([^\(]+)\(([^\)]+)\)/$2/) {
										$fct = $1;
									}
									$cindx = join(',', @ind_col);
									$cindx = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $cindx);
									$create_table{$table}{'index'} .= "CREATE INDEX ${tb_name}_${sub_tb_name}_$colname ON ${tb_name}_$sub_tb_name ($cindx);\n";
									if ($self->{subpartitions_default}{$table} && ($create_table{$table}{'index'} !~ /ON $self->{subpartitions_default}{$table} /)) {
										$create_table{$table}{'index'} .= "CREATE INDEX ${tb_name}_$self->{subpartitions_default}{$table}_$colname ON ${tb_name}_$self->{subpartitions_default}{$table} ($cindx);\n";
									}
									if ($self->{subpartitions}{$table}{$p}{$subpart}[$i]->{type} eq 'LIST') {
										if (!$fct) {
											push(@subcondition, "NEW.$self->{subpartitions}{$table}{$p}{$subpart}[$i]->{column} IN (" . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{subpartitions}{$table}{$p}{$subpart}[$i]->{value}) . ")");
										} else {
											push(@subcondition, "$fct(NEW.$colname) IN (" . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{subpartitions}{$table}{$p}{$subpart}[$i]->{value}) . ")");
										}
									} else {
										if (!$fct) {
											push(@subcondition, "NEW.$self->{subpartitions}{$table}{$p}{$subpart}[$i]->{column} < " . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{subpartitions}{$table}{$p}{$subpart}[$i]->{value}));
										} else {
											push(@subcondition, "$fct(NEW.$colname) < " . Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{subpartitions}{$table}{$p}{$subpart}[$i]->{value}));
										}
									}
									$owner = $self->{subpartitions}{$table}{$p}{$subpart}[$i]->{owner} || '';
								}
								$sub_check_cond = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $sub_check_cond);
								$create_table{$table}{table} .= "$check_cond AND $sub_check_cond";
								$create_table{$table}{table} .= "\n) ) INHERITS ($table);\n";
								$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
								if ($owner) {
									if (!$self->{preserve_case}) {
										$create_table{$table}{table} .= "ALTER TABLE \L${tb_name}_$sub_tb_name\E OWNER TO \L$owner\E;\n";
									} else {
										$create_table{$table}{table}.= "ALTER TABLE \"${tb_name}_$sub_tb_name\" OWNER TO \"$owner\";\n";
									}
								}
								$sub_funct_cond .= "\t\t$sub_cond ( " . join(' AND ', @subcondition) . " ) THEN INSERT INTO ${tb_name}_$sub_tb_name VALUES (NEW.*);\n";
								$sub_cond = 'ELSIF';
								$old_part = $part;
							}
						}
					}
					if (!$sub_funct_cond) {
						$funct_cond .= "\t$cond ( " . join(' AND ', @condition) . " ) THEN INSERT INTO $tb_name VALUES (NEW.*);\n";
					} else {
						$sub_funct_cond = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $sub_funct_cond);
						$funct_cond .= "\t$cond ( " . join(' AND ', @condition) . " ) THEN \n";
						$funct_cond .= $sub_funct_cond;
						$funct_cond .= "\t\tELSE INSERT INTO $tb_name VALUES (NEW.*);\n";
						$funct_cond .= "\t\tEND IF;\n";
					}
					$cond = 'ELSIF';
					$old_part = $part;
					$i++;
				}
				$old_pos = $pos;
			}
			if ($self->{partitions_default}{$table}) {
				my $deftb = '';
				$deftb = "${table}_" if ($self->{prefix_partition});
				$function .= $funct_cond . qq{	ELSE
                INSERT INTO $deftb$self->{partitions_default}{$table} VALUES (NEW.*);
};
			} else {
				$function .= $funct_cond . qq{	ELSE
                -- Raise an exception
                RAISE EXCEPTION 'Value out of range. Fix the ${table}_insert_trigger() function!';
};
			}
			$function = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $function);
			$function .= qq{
        END IF;
        RETURN NULL;
END;
\$\$
LANGUAGE plpgsql;
};

			$sql_output .= qq{
$create_table{$table}{table}
};
			$sql_output .= qq{
-- Create default table, where datas are inserted if no condition match
CREATE TABLE $self->{partitions_default}{$table} () INHERITS ($table);
} if ($self->{partitions_default}{$table});
			$sql_output .= qq{
-- Create indexes on each partition table
$create_table{$table}{'index'}

$function

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

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

			$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
			if ($owner) {
				if (!$self->{preserve_case}) {
					$sql_output .= "ALTER TABLE \L$self->{partitions_default}{$table}\E OWNER TO \L$owner\E;\n"if ($self->{partitions_default}{$table});
					$sql_output .= "ALTER FUNCTION \L${table}_insert_trigger\E() OWNER TO \L$owner\E;\n";
				} else {
					$sql_output .= "ALTER TABLE \"$self->{partitions_default}{$table}\"() OWNER TO \"$owner\";\n"if ($self->{partitions_default}{$table});
					$sql_output .= "ALTER FUNCTION \"${table}_insert_trigger\"() OWNER TO \"$owner\";\n";
				}
			}
		}

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

		return;
	}

	# Process synonyms only
	if ($self->{type} eq 'SYNONYM') {
		$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}};

		foreach my $syn (sort { $a cmp $b } keys %{$self->{synonyms}}) {
			if (!$self->{quiet} && !$self->{debug}) {
				print STDERR $self->progress_bar($i, $num_total_synonym, 25, '=', 'synonyms', "generating $syn" ), "\r";
			}
			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";
			}
			if (!$self->{preserve_case}) {
				$sql_output .= "CREATE VIEW \L$self->{synonyms}{$syn}{owner}.$syn\E AS SELECT * FROM \L$self->{synonyms}{$syn}{table_owner}.$self->{synonyms}{$syn}{table_name}\E;\n";
			} else {
				$sql_output .= "CREATE VIEW \"$self->{synonyms}{$syn}{owner}\".\"$syn\" AS SELECT * FROM \"$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"));
			if (!$self->{preserve_case}) {
				$sql_output .= "ALTER VIEW \L$self->{synonyms}{$syn}{owner}.$syn\E OWNER TO \L$owner\E;\n";
				$sql_output .= "GRANT ALL ON \L$self->{synonyms}{$syn}{owner}.$syn\E TO $self->{synonyms}{$syn}{owner};\n\n";
			} else {
				$sql_output .= "ALTER VIEW \"$self->{synonyms}{$syn}{owner}\".\"$syn\" OWNER TO \"$owner\";\n";
				$sql_output .= "GRANT ALL ON \"$self->{synonyms}{$syn}{owner}\".\"$syn\" TO \"$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";
		}

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

	# DATABASE DESIGN - type 'TABLE'
	# Dump the database structure: tables, constraints, indexes, etc.
	if ($self->{export_schema} && $self->{schema}) {
		if ($self->{create_schema}) {
			if (!$self->{preserve_case}) {
				$sql_output .= "CREATE SCHEMA \L$self->{schema}\E;\n";
			} else {
				$sql_output .= "CREATE SCHEMA \"$self->{schema}\";\n";
			}
		}
		my $owner = '';
		$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
		$owner ||= $self->{schema};
		if ($owner && $self->{create_schema}) {
			if (!$self->{preserve_case}) {
				$sql_output .= "ALTER SCHEMA \L$self->{schema}\E OWNER TO \L$owner\E;\n";
			} else {
				$sql_output .= "ALTER SCHEMA \"$self->{schema}\" OWNER TO \"$owner\";\n";
			}
		}
		$sql_output .= "\n";
	} elsif ($self->{export_schema}) {
		if ($self->{create_schema}) {
			my $current_schema = '';
			foreach my $table (sort keys %{$self->{tables}}) {
				if ($table =~ /^([^\.]+)\..*/) {
					if ($1 ne $current_schema) {
						$current_schema = $1;
						if (!$self->{preserve_case}) {
							$sql_output .= "CREATE SCHEMA \L$1\E;\n";
						} else {
							$sql_output .= "CREATE SCHEMA \"$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 = '';
	if ($self->{file_per_index}) {
		$indices .= $self->set_search_path();
		$fts_indices .= $self->set_search_path();
	}

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

	# Dump all table/index/constraints SQL definitions
	my $ib = 1;
	foreach my $table (sort { $self->{tables}{$a}{internal_id} <=> $self->{tables}{$b}{internal_id} } keys %{$self->{tables}}) {

		$self->logit("Dumping table $table...\n", 1);

		if (!$self->{quiet} && !$self->{debug}) {
			print STDERR $self->progress_bar($ib, $num_total_table, 25, '=', 'tables', "exporting $table" ), "\r";
		}
		# Create FDW server if required
		if ($self->{external_to_fdw}) {
			if ( grep(/^$table$/i, keys %{$self->{external_table}}) ) {
					$sql_header .= "CREATE EXTENSION 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}) {
			$obj_type = 'UNLOGGED ' . $obj_type;
		}
		if (exists $self->{tables}{$table}{table_as}) {
			if ($self->{plsql_pgsql}) {
				$self->{tables}{$table}{table_as} = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $self->{tables}{$table}{table_as});
			}
			my $withoid = '';
			$withoid = 'WITH (OIDS)' if ($self->{with_oid});
			$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 = '';
		if (exists $self->{tables}{$table}{column_info}) {
			$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}[10] <=> $self->{tables}{$table}{column_info}{$b}[10];
					} 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]);
						$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]);
						$typb =~ s/\(.*//;
						$TYPALIGN{$typb} <=> $TYPALIGN{$typa};
					}
				} keys %{$self->{tables}{$table}{column_info}}) {

				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]);
				$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') {
					if ($type !~ s/bigint/bigserial/) {
						if ($type !~ s/smallint/smallserial/) {
							$type =~ s/integer/serial/;
						}
					}
					if ($type =~ /serial/) {
						my $seqname = lc($tbname) . '_' . lc($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";
					}
				}

				# Check if this column should be replaced by a boolean following table/column name
				if (uc($f->[1]) eq 'ENUM') {
					$f->[11] =~ s/^enum\(//i;
					$f->[11] =~ s/\)$//;
					my $keyname = $tbname . '_' . $fname . '_chk';
					$enum_str .= "ALTER TABLE $tbname ADD CONSTRAINT $keyname CHECK ($fname IN ($f->[11]));\n";
				}
				my $typlen = $f->[5];
				$typlen ||= $f->[2];
				if (grep(/^$f->[0]$/i, @{$self->{'replace_as_boolean'}{uc($table)}})) {
					$type = 'boolean';
				# Check if this column should be replaced by a boolean following type/precision
				} elsif (exists $self->{'replace_as_boolean'}{uc($f->[1])} && ($self->{'replace_as_boolean'}{uc($f->[1])}[0] == $typlen)) {
					$type = 'boolean';
				}

				if ($f->[1] =~ /SDO_GEOMETRY/) {
					# Set the dimension, array is (srid, dims, gtype)
					my $suffix = '';
					if ($f->[12] == 3) {
						$suffix = 'Z';
					} elsif ($f->[12] == 4) {
						$suffix = 'ZM';
					}
					my $gtypes = '';
					if (!$f->[13] || ($f->[13] =~  /,/) ) {
						$gtypes = $ORA2PG_SDO_GTYPE{0};
					} else {
						$gtypes = $f->[13];
					}
					$type = "geometry($gtypes$suffix";
					if ($f->[11]) {
						$type .= ",$f->[11]";
					}
					$type .= ")";
				}

				$type = $self->{'modify_type'}{"\L$table\E"}{"\L$f->[0]\E"} if (exists $self->{'modify_type'}{"\L$table\E"}{"\L$f->[0]\E"});
				if (!$self->{preserve_case}) {
					$fname = $self->quote_reserved_words($fname);
					$sql_output .= "\t\L$fname\E $type";
				} else {
					$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";
					}
				}
				if ($f->[4] ne "") {
					$f->[4] =~ s/^\s+//;
					$f->[4] =~ s/\s+$//;
					if ($self->{plsql_pgsql}) {
						$f->[4] = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $f->[4]);
					}
					if (($f->[4] ne '') && ($self->{type} ne 'FDW')) {
						if (($type eq 'boolean') && exists $self->{ora_boolean_values}{lc($f->[4])}) {
							$sql_output .= " DEFAULT '" . $self->{ora_boolean_values}{lc($f->[4])} . "'";
						} else {
							if (($f->[4] !~ /^'/) && ($f->[4] =~ /[^\d\.]/)) {
								if ($type =~ /CHAR|TEXT|ENUM/i) {
									$f->[4] = "'$f->[4]'";
								} elsif ($type =~ /DATE|TIME/i) {
									# do not use REPLACE_ZERO_DATE in default value, cause it can be NULL
									$f->[4] =~ s/^0000-00-00.*/1970-01-01 00:00:00/;
									$f->[4] = "'$f->[4]'" if ($f->[4] =~ /^\d+/);
								}
							}
							$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/,$//;
			if ( ($self->{type} ne 'FDW') && (!$self->{external_to_fdw} || (!grep(/^$table$/i, keys %{$self->{external_table}}) && !$self->{tables}{$table}{table_info}{connection})) ) {
				my $withoid = '';
				$withoid = 'WITH (OIDS)' if ($self->{with_oid});
				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}}) ) {
				$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}');\n";
			} elsif ($self->{is_mysql}) {
				my $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 $schem = "schema '$self->{schema}'," if ($self->{schema});
				$sql_output .= ") SERVER $self->{fdw_server} OPTIONS($schem table '$table');\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;
			my $foreign = '';
			$foreign = ' FOREIGN' if ($self->{type} eq 'FDW');
			$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 $f (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)};
				}
				if (!$self->{preserve_case}) {
					$sql_output .= "COMMENT ON COLUMN \L$tbname.$fname\E IS E'" . $self->{tables}{$table}{column_comments}{$f} .  "';\n";
				} else {
					$sql_output .= "COMMENT ON COLUMN $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");
			if (!$self->{preserve_case}) {
				$sql_output .= "ALTER $self->{tables}{$table}{table_info}{type} $tbname OWNER TO \L$owner\E;\n";
			} else {
				$sql_output .= "ALTER $self->{tables}{$table}{table_info}{type} $tbname OWNER TO \"$owner\";\n";
			}
		}
		if (exists $self->{tables}{$table}{alter_table}) {
			$obj_type =~ s/UNLOGGED //;
			foreach (@{$self->{tables}{$table}{alter_table}}) {
				$sql_output .= "\nALTER $obj_type $tbname $_;\n";
			}
		}
		if (exists $self->{tables}{$table}{alter_index}) {
			foreach (@{$self->{tables}{$table}{alter_index}}) {
				$sql_output .= "$_;\n";
			}
		}
		if ($self->{type} ne 'FDW') {
			# 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});
			if (!$self->{file_per_constraint}) {
				$sql_output .= $constraints;
				$constraints = '';
			}

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

	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}");
		$indices = "-- Nothing found of type indexes\n" if (!$indices);
		$indices =~ s/\n+/\n/gs;
		$self->dump($sql_header . $indices, $fhdl);
		$self->close_export_file($fhdl);
		$indices = '';
		if ($fts_indices) {
			$fts_indices =~ s/\n+/\n/gs;
			# FTS TRIGGERS are exported in a separated file to be able to parallelyze 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->dump($sql_header . $fts_indices, $fhdl);
			$self->close_export_file($fhdl);
			$fts_indices = '';
		}
	}

	# Dumping foreign key constraints
	foreach my $table (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_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}");
		$constraints = "-- Nothing found of type constraints\n" if (!$constraints);
		$self->dump($sql_header . $constraints, $fhdl);
		$self->close_export_file($fhdl);
		$constraints = '';
	}

	if (!$sql_output) {
		$sql_output = "-- Nothing found of type TABLE\n";
	}

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

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

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

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

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

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

	# Prefix partition name with tablename
	if ($part_name && $self->{prefix_partition}) {
		$tmptb = $self->get_replaced_tbname($table . '_' . $part_name);
	} else {
		$tmptb = $self->get_replaced_tbname($part_name || $table);
	}


	# Build the header of the query
	my @tt = ();
	my @stt = ();
	my @nn = ();
	my $col_list = '';

	# Extract column information following the Oracle position order
	my @fname = ();
	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"}}));
			}
		}
		if (!$self->{preserve_case}) {
			push(@fname, lc($fieldname));
		} else {
			push(@fname, $fieldname);
		}

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

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

		my $type = $self->_sql_type($f->[1], $f->[2], $f->[5], $f->[6]);
		$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 =~ s/"//g;
		if (!$self->{preserve_case}) {
			$colname = $self->quote_reserved_words($colname);
			$col_list .= "\L$colname\E,";
		} else {
			$col_list .= "\"$colname\",";
		}
	}
	$col_list =~ s/,$//;

	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 .= ") VALUES (";
	}

	my $sprep = '';
	if ($self->{pg_dsn}) {
		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);

	$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 COLUMN_NAME,COMMENTS,TABLE_NAME,OWNER FROM $self->{prefix}_COL_COMMENTS $condition";
	if ($self->{schema}) {
		$sql .= "WHERE OWNER='$self->{schema}' ";
	} else {
		$sql .= " WHERE OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
	}
	$sql .= "AND TABLE_NAME='$table' " if ($table);
	$sql .= $self->limit_to_objects('TABLE','TABLE_NAME') if (!$table);

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

=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 = @{$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 (keys %indexes) {

		# Cluster, bitmap join, reversed and IOT indexes will not be exported at all
		# Hash indexes will be exported as btree
		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/^"$c"$/"$self->{replaced_cols}{"\L$tbsaved\E"}{$c}"/i } @{$indexes{$idx}};
				map { s/^$c$/$self->{replaced_cols}{"\L$tbsaved\E"}{$c}/i } @{$indexes{$idx}};
			}
		}

		my @strings = ();
		my $i = 0;
		for (my $j = 0; $j <= $#{$indexes{$idx}}; $j++) {
			while ($indexes{$idx}->[$j] =~ s/'([^']+)'/%%string$i%%/) {
				push(@strings, $1);
				$i++;
			}
			if ($self->{plsql_pgsql}) {
				$indexes{$idx}->[$j] = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $indexes{$idx}->[$j]);
			}
		}

		# 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]);
						$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{$_} : $_) . ", ";
		}
		$columns =~ s/, $//;
		$columns =~ s/\s+/ /g;
		my $colscompare = $columns;
		$colscompare =~ s/"//gs;
		my $columnlist = '';
		my $skip_index_creation = 0;

		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 = @{$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 (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';
			}

			next if ( lc($columns) eq lc($pkcollist{$tbsaved}) );

			$columns = lc($columns) if (!$self->{preserve_case});
			for ($i = 0; $i <= $#strings; $i++) {
				$columns =~ s/\%\%string$i\%\%/'$strings[$i]'/;
			}
			my $schm = '';
			my $idxname = '';
			if ($idx =~ /^([^\.]+)\.(.*)$/) {
				$schm = $1;
				$idxname = $2;
			} else {
				$idxname = $idx;
			}
			$idxname = $self->quote_object_name($idxname);
			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; } @collist;
				$idxname = $self->quote_object_name($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);
			if ($self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_name} =~ /SPATIAL_INDEX/) {
				$str .= "CREATE INDEX$concurrently \L$idxname$self->{indexes_suffix}\E ON $table USING gist($columns)";
			} elsif ($self->{bitmap_as_gin} && $self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_name} eq 'BITMAP') {
				$str .= "CREATE INDEX$concurrently \L$idxname$self->{indexes_suffix}\E ON $table 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);
				$columns = join(" gin_trgm_ops, ", @cols);
				$columns .= " gin_trgm_ops";
				$str .= "CREATE INDEX$concurrently \L$idxname$self->{indexes_suffix}\E ON $table USING gin($columns)";
			} elsif ($self->{$objtyp}{$tbsaved}{idx_type}{$idx}{type_name} =~ /FULLTEXT|CONTEXT/) {
				# use Full text search, then create dedicated column and trigger before the index.
				map { s/"//g; } @{$indexes{$idx}};
				my $newcolname = $self->quote_object_name(join('_', @{$indexes{$idx}}));
				$fts_str .= "\n-- Append the FTS column to the table\n";
				$fts_str .= "\nALTER TABLE $table ADD COLUMN tsv_" . substr($newcolname,0,59) . " tsvector;\n";
				my $fctname = $self->quote_object_name($table.'_' . join('_', @{$indexes{$idx}}));
				$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';
				foreach my $col (@{$indexes{$idx}}) {
					$contruct_vector .= "\t\tsetweight(to_tsvector('pg_catalog.english', coalesce(new.$col,'')), '$weight') ||\n";
					$update_vector .= " setweight(to_tsvector('pg_catalog.english', coalesce($col,'')), '$weight') ||";
					$weight++;
				}
				$contruct_vector =~ s/\|\|$/;/s;
				$update_vector =~ s/\|\|$/;/s;

				$fts_str .= qq{
-- When the data migration is done without trigger, create tsvector data for all the existing records
UPDATE $table 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 $table
  FOR EACH ROW EXECUTE PROCEDURE $fctname();

} if (!$indexonly);
				if ($objtyp eq 'tables') {
					$str .= "CREATE$unique INDEX$concurrently \L$idxname$self->{indexes_suffix}\E ON $table USING gin(tsv_$newcolname)";
				} else {
					$fts_str .= "CREATE$unique INDEX$concurrently \L$idxname$self->{indexes_suffix}\E 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 \L$idxname$self->{indexes_suffix}\E ON $table ($columns)";
			} else {
				$str .= "CREATE$unique INDEX$concurrently \L$idxname$self->{indexes_suffix}\E 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}";
			}
			$str .= ";";
			push(@out, $str);
			push(@fts_out, $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);

		map { if ($_ !~ /\(.*\)/) { s/^/"/; s/$/"/; } } @{$indexes{$idx}};
		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"}{$c}"/i } @{$indexes{$idx}};
			}
		}
		map { s/"//gs } @{$indexes{$idx}};
		if (!$self->{preserve_case}) {
			map { $_ = $self->quote_reserved_words($_) } @{$indexes{$idx}};
		} else {
			map { if ($_ !~ /\(.*\)/) { s/^/"/; s/$/"/; } } @{$indexes{$idx}};
		}
		my $columns = join(',', @{$indexes{$idx}});
		my $colscompare = $columns;
		$colscompare =~ s/"//gs;
		my $columnlist = '';
		my $skip_index_creation = 0;
		foreach my $consname (keys %{$self->{tables}{$table}{unique_key}}) {
			next if ($consname ne $idx);
			my $constype =   $self->{tables}{$table}{unique_key}->{$consname}{type};
			if ($constype eq 'P') {
				$skip_index_creation = 1;
				last;
			}
			my @conscols = @{$self->{tables}{$table}{unique_key}->{$consname}{columns}};
			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\E"}{"\L$conscols[$i]\E"}) {
					$conscols[$i] = $self->{replaced_cols}{"\L$tbsaved\E"}{"\L$conscols[$i]\E"};
				}
			}
			$columnlist = join(',', @conscols);
			$columnlist =~ s/"//gs;
			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 { s/"//gs } @conscols;
		if (!$self->{preserve_case}) {
			map { $_ = $self->quote_reserved_words($_) } @conscols;
		} else {
			map { s/^/"/; s/$/"/; } @conscols;
		}
		my $columnlist = join(',', @conscols);
		if (!$self->{preserve_case}) {
			$columnlist = lc($columnlist);
		}
		if ($columnlist) {
			if ($self->{pkey_in_create}) {
				if (!$self->{keep_pkey_names} || ($constgen eq 'GENERATED NAME')) {
					$out .= "\tPRIMARY KEY ($columnlist)";
				} else {
					$out .= "\tCONSTRAINT \L$consname\E 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->{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 @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}{"$tbsaved"}{"\L$conscols[$i]\E"} && $self->{replaced_cols}{"$tbsaved"}{"\L$conscols[$i]\E"}) {
				$conscols[$i] = $self->{replaced_cols}{"$tbsaved"}{"\L$conscols[$i]\E"};
			}
		}
		map { s/"//gs } @conscols;
		if (!$self->{preserve_case}) {
			map { $_ = $self->quote_reserved_words($_) } @conscols;
		} else {
			map { s/^/"/; s/$/"/; } @conscols;
		}
		my $columnlist = join(',', @conscols);
		if (!$self->{preserve_case}) {
			$columnlist = lc($columnlist);
		}

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

	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};
		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}{"$tbsaved"} && $self->{replaced_cols}{"$tbsaved"}) {
				foreach my $c (keys %{$self->{replaced_cols}{"$tbsaved"}}) {
					$chkconstraint =~ s/"$c"/"$self->{replaced_cols}{"$tbsaved"}{$c}"/gsi;
					$chkconstraint =~ s/\b$c\b/$self->{replaced_cols}{"$tbsaved"}{$c}/gsi;
				}
			}
			if ($self->{plsql_pgsql}) {
				$chkconstraint = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $chkconstraint);
			}
			if (!$self->{preserve_case}) {
				foreach my $c (@$field_name) {
					# Force lower case
					my $ret = $self->quote_reserved_words($c);
					$chkconstraint =~ s/"$c"/\L$ret\E/igs;
				}
				$k = lc($k);
			}
			$out .= "ALTER TABLE $table ADD CONSTRAINT $k CHECK ($chkconstraint);\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 (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)) {
				push(@$state, @$h);
				last;
			}
		}
		foreach my $desttable (keys %{$self->{tables}{$tbsaved}{foreign_link}{$fkname}{remote}}) {
			push(@done, $fkname);
			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]))) {
				if (!$self->{preserve_case}) {
					$subsdesttable = lc($state->[6]) . '.' . $subsdesttable;
				} else {
					$subsdesttable = "\"$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"}{$c}"/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"}{$c}"/i } @rfkeys;
				}
			}
			if ($self->{preserve_case}) {
				map { s/["]+/"/g; } @rfkeys;
				map { s/["]+/"/g; } @lfkeys;
			} else {
				map { s/["]+//g; } @rfkeys;
				map { s/["]+//g; } @lfkeys;
			}
			if (!$self->{preserve_case}) {
				map { $_ = $self->quote_reserved_words($_) } @lfkeys;
				map { $_ = $self->quote_reserved_words($_) } @rfkeys;
			}

			if (!$self->{preserve_case}) {
					$fkname = lc($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] ) . ";\n";
			} else {
				$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] = lc($h->[0]) if (!$self->{preserve_case});
		$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() 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} \L$seqname\E RESTART WITH $nextvalue;";
		if ($self->{preserve_case}) {
			$seqname =~ s/\./"."/;
			$alter = "ALTER SEQUENCE $self->{pg_supports_ifexists} \"$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, $with_schema) = @_;

	# 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} || '';
			$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}) {
		$timeformat = 'YYYY-MM-DD HH24:MI:SS.FF';
	}
	my $timeformat_tz = $timeformat . ' TZH:TZM';
	# Lookup through columns information
	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 .= "$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->{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 $file_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);
		$file_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');
		$file_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 ($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) {
		$alias = "PARTITION($part_name)";
	} else {
		$alias = 'a';
	}
	$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})) = ?";
			}
		}
	}

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

	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/i;

	# Interval precision for year/month/day is not supported by PostgreSQL
	if ($type =~ /INTERVAL/i) {
		$type =~ s/(INTERVAL\s+YEAR)\s*\(\d+\)/$1/i;
		$type =~ s/(INTERVAL\s+YEAR\s+TO\s+MONTH)\s*\(\d+\)/$1/i;
		$type =~ s/(INTERVAL\s+DAY)\s*\(\d+\)/$1/i;
		# 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)/i;
			}
		}
	}

        # Overide the length
	if ( ($type eq 'NUMBER') && $precision ) {
		$len = $precision;
	} elsif ( ($type eq 'NUMBER') && ($len == 38) ) {
		$precision = $len;
	}

        if (exists $self->{data_type}{uc($type)}) {
		$type = uc($type); # Force uppercase
		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};
			}
		}
        }

        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);
	$condition .= "AND A.OWNER='$owner' " if ($owner);
	$condition .= $self->limit_to_objects('TABLE', 'A.TABLE_NAME') if (!$table);

	my $sth = '';
	if ($self->{db_version} !~ /Release 8/) {
		$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
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: " . $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
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: " . $self->{dbh}->errstr . "\n", 0, 1);
		}
	}
	$sth->execute or $self->logit("FATAL: " . $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) {

		if ($#{$row} == 9) {
			$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->[-2];
		if ($self->{export_schema} && !$self->{schema}) {
			$tmptable = "$row->[-1].$row->[-2]";
		}

		# 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: " . $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->[-2],$row->[0],$row->[-1]) 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);
				}
			}

			# Grad 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: " . $self->{dbh}->errstr . "\n", 0, 1);
				}
				$sth2->execute($row->[-2],$row->[0],$row->[-1]) 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->[-1] . "." . $row->[-2];
				my $squery = sprintf($spatial_gtype, $row->[0], $colname);
				my $sth2 = $self->{dbh}->prepare($squery);
				if (!$sth2) {
					$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
				}
				$sth2->execute or $self->logit("FATAL: " . $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}) {
			push(@{$data{$tmptable}{"$row->[0]"}}, (@$row, $pos, @geom_inf));
		} else {
			push(@{$data{"$row->[-2]"}{"$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);
	$condition .= "AND A.OWNER='$owner' " if ($owner);
	$condition .= $self->limit_to_objects('TABLE', 'A.TABLE_NAME') if (!$table);

	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: " . $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: " . $self->{dbh}->errstr . "\n", 0, 1);
		}
	}
	$sth->execute 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->[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;	
}


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

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

	my %result = ();
        my @accepted_constraint_types = ();
        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 $sql = "SELECT DISTINCT COLUMN_NAME,POSITION,CONSTRAINT_NAME,OWNER FROM $self->{prefix}_CONS_COLUMNS";
	if ($owner) {
		$sql .= " WHERE OWNER = '$owner' ";
	} else {
		$sql .= " WHERE OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	$sql .=  " ORDER BY POSITION";
	my $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 @cons_columns = ();
	while (my $r = $sth->fetch) {
		push(@cons_columns, [ @$r ]);
	}
	$sth->finish;

	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('UKEY', 'CONSTRAINT_NAME');

	if ($self->{db_version} !~ /Release 8/) {
		$sth = $self->{dbh}->prepare(<<END) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
SELECT CONSTRAINT_NAME,R_CONSTRAINT_NAME,SEARCH_CONDITION,DELETE_RULE,DEFERRABLE,DEFERRED,R_OWNER,CONSTRAINT_TYPE,GENERATED,TABLE_NAME,OWNER,INDEX_NAME
FROM $self->{prefix}_CONSTRAINTS
WHERE CONSTRAINT_TYPE IN $cons_types
AND STATUS='ENABLED'
$condition
END
	} else {
		$sth = $self->{dbh}->prepare(<<END) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
SELECT CONSTRAINT_NAME,R_CONSTRAINT_NAME,SEARCH_CONDITION,DELETE_RULE,DEFERRABLE,DEFERRED,R_OWNER,CONSTRAINT_TYPE,GENERATED,TABLE_NAME,OWNER,'' AS INDEX_NAME
FROM $self->{prefix}_CONSTRAINTS
WHERE CONSTRAINT_TYPE IN $cons_types
AND STATUS='ENABLED'
$condition
END
	}
	$sth->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

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

		my %constraint = (type => $row->[7], 'generated' => $row->[8], 'index_name' => $row->[11], columns => ());
		my @done = ();
		foreach my $r (@cons_columns) {
			# Skip constraints on system internal columns
			next if ($r->[0] =~ /^SYS_NC/i);
			next if (grep(/^$r->[0]$/, @done));
			if ( ($r->[2] eq $row->[0]) && ($row->[10] eq $r->[3]) ) {
				push(@{$constraint{'columns'}}, $r->[0]);
				push(@done, $r->[0]);
			}
		}
		if ($#{$constraint{'columns'}} >= 0) {
			if (!$self->{schema} && $self->{export_schema}) {
				$result{"$row->[10].$row->[9]"}{$row->[0]} = \%constraint;
			} else {
				$result{$row->[9]}{$row->[0]} = \%constraint;
			}
		}
	}
	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', 'CONSTRAINT_NAME');

	my $sth = $self->{dbh}->prepare(<<END) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
SELECT CONSTRAINT_NAME,R_CONSTRAINT_NAME,SEARCH_CONDITION,DELETE_RULE,DEFERRABLE,DEFERRED,R_OWNER,TABLE_NAME,OWNER
FROM $self->{prefix}_CONSTRAINTS
WHERE CONSTRAINT_TYPE='C' $condition
AND STATUS='ENABLED'
END

	$sth->execute 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]} = $row->[2];
	}

	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 $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 $condition2 = '';
	$condition2 .= "AND TABLE_NAME='$table' " if ($table);
	if ($owner) {
		$condition2 .= "OWNER = '$owner' ";
	} else {
		$condition2 .= "AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
	}
	$condition2 .= $self->limit_to_objects('TABLE','TABLE_NAME');
	#$condition2 =~ s/^AND //;

	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
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.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 = COLS.OWNER)
WHERE 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 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]";
		}
		push(@{$data{$local_table}}, [ ($row->[1],$row->[4],$row->[6],$row->[7],$row->[8],$row->[9],$row->[11],$row->[0],$row->[10]) ]);
		#            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 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 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'";
	$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 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 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 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 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 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) = @_;

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

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

	my $condition = '';
	$condition .= "AND A.TABLE_NAME='$table' " if ($table);
	$condition .= "AND A.INDEX_OWNER='$owner' AND B.OWNER='$owner' " if ($owner);
	$condition .= $self->limit_to_objects('TABLE|INDEX', "A.TABLE_NAME|A.INDEX_NAME") if (!$table);

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

	# Retrieve all indexes 
	my $sth = '';
	if ($self->{db_version} !~ /Release 8/) {
		$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 <> 'Y' $condition
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, 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 <> 'Y'
ORDER BY $self->{prefix}_IND_COLUMNS.COLUMN_POSITION
END
	}

	$sth->execute 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) {
		my $save_tb = $row->[-6];
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[-6] = "$row->[-5].$row->[-6]";
		}
		# 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->[-6]}{$row->[0]} = $row->[2];

		# Save original column name
		my $colname = $row->[1];
		# Enclose with double quote if required
		$row->[1] = $self->quote_reserved_words($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;
			if ($row->[-1] eq 'DESC') {
				$row->[1] .= " DESC";
			}
		}

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

		if ($self->{preserve_case}) {
			if (($row->[1] !~ /".*"/) && ($row->[1] !~ /\(.*\)/)) {
				$row->[1] =~ s/^/"/;
				$row->[1] =~ s/$/"/;
			}
		}
		# 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->[-6]}{$row->[0]}{type_name} = $row->[-3];
		if (($#{$row} > 6) && ($row->[7] eq 'Y')) {
			$idx_type{$row->[-6]}{$row->[0]}{type} = $row->[4] . ' JOIN';
		} else {
			$idx_type{$row->[-6]}{$row->[0]}{type} = $row->[4];
		}
		if ($row->[-3] =~ /SPATIAL_INDEX/) {
			$idx_type{$row->[-6]}{$row->[0]}{type} = 'SPATIAL INDEX';
			if ($row->[-2] =~ /layer_gtype=([^\s,]+)/i) {
				$idx_type{$row->[-5]}{$row->[0]}{type_constraint} = uc($1);
			}
			if ($row->[-2] =~ /sdo_indx_dims=(\d+)/i) {
				$idx_type{$row->[-6]}{$row->[0]}{type_dims} = $1;
			}
		}
		if ($row->[4] eq 'BITMAP') {
			$idx_type{$row->[-6]}{$row->[0]}{type} = $row->[4];
		}
		push(@{$data{$row->[-6]}{$row->[0]}}, $row->[1]);
		$index_tablespace{$row->[-6]}{$row->[0]} = $row->[-4];

	}

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


=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 or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

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

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

	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 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 '(.)'/) {
			$data{$row->[1]}{delimiter} = $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 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 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]}{username} = $row->[2];
		$data{$row->[1]}{host} = $row->[3];
	}

	return %data;
}

=head2 _get_job

This function implements an Oracle-native job information.

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

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

	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 = ();
	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 or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
	while (my $row = $sth->fetch) {
		if (!$self->{schema} && $self->{export_schema}) {
			$row->[0] = "$row->[0].$row->[3]";
		}
		$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.VIEW_NAME";


	# Compute view order, where depended view appear before using view
	my %view_order = ();
	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 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};
		@{$data{$row->[0]}{alias}} = $self->_alias_info ($row->[0]);
		if (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->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->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 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 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_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 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 procedures 
	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 or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);

	my %procedures = ();
	my @fct_done = ();
	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]$/, @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) {
			$procedures{"$row->[0]"}{text} .= $r->[0];
		}
		$procedures{"$row->[0]"}{owner} = $row->[1];
	}

	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 LIKE 'PACKAGE%'";
	$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 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]);
		my $sql = "SELECT TEXT FROM $self->{prefix}_SOURCE WHERE OWNER='$row->[1]' AND NAME='$row->[0]' AND (TYPE='PACKAGE' OR TYPE='PACKAGE BODY') ORDER BY TYPE, 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]}{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, $dbh, $name) = @_;

	# Retrieve all user defined types
	my $str = "SELECT DISTINCT OBJECT_NAME,OWNER 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}}) . "') ";
	}
	$str .= $self->limit_to_objects('TYPE', 'OBJECT_NAME') if (!$name);
	$str .= " ORDER BY OBJECT_NAME";

	my $sth = $dbh->prepare($str) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
	$sth->execute or $self->logit("FATAL: " . $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 = $dbh->prepare($sql) or $self->logit("FATAL: " . $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];
		}
		$tmp{name} = $row->[0];
		$tmp{owner} = $row->[1];
		push(@types, \%tmp);
	}

	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 = ();
	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 .= $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 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 ALL_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 .= $self->limit_to_objects('TABLE', 'A.TABLE_NAME');
        $sql .= " AND (A.IOT_TYPE IS NULL OR A.IOT_TYPE = 'IOT') ORDER BY A.OWNER, A.TABLE_NAME";

        $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);
	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 ($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 _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]);
		$row->[0] =  $self->normalize_query($row->[0]);
		$tmp_queries{$row->[0]}++;
		$self->logit(".",1);
	}
	$sth->finish;
	$self->logit("\n", 1);

	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')
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 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]);
		$self->logit(".",1);
	}
	$sth->finish;
	$self->logit("\n", 1);

	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 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];
		$self->logit(".",1);
	}
	$sth->finish;
	$self->logit("\n", 1);

	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 8/) {
		$highvalue = "'' AS HIGH_VALUE";
	}
	# 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')
	AND a.table_name = c.name
};
	$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_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 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 'MAXVALUE') || ($row->[3] eq 'DEFAULT')) {
			$default{$row->[0]} = $row->[2];
			next;
		}
		push(@{$parts{$row->[0]}{$row->[1]}{$row->[2]}}, { 'type' => $row->[5], 'value' => $row->[3], 'column' => $row->[7], 'colpos' => $row->[8], 'tablespace' => $row->[4], 'owner' => $row->[9]});
		$self->logit(".",1);
	}
	$sth->finish;
	$self->logit("\n", 1);

	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 8/) {
		$highvalue = "'' AS HIGH_VALUE";
	}
	# 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')
	AND a.table_name = c.name
};
	$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";
		}
	}
	$str .= "ORDER BY A.TABLE_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 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->[2];
			next;
		}

		push(@{$subparts{$row->[0]}{$row->[1]}{$row->[2]}}, { 'type' => $row->[5], 'value' => $row->[3], 'column' => $row->[7], 'colpos' => $row->[8], 'tablespace' => $row->[4], 'owner' => $row->[9]});
		$self->logit(".",1);
	}
	$sth->finish;
	$self->logit("\n", 1);

	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 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 8/) {
		$highvalue = "'' AS HIGH_VALUE";
	}
	# 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
};
	$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 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) = @_;

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

	my $highvalue = 'A.HIGH_VALUE';
	if ($self->{db_version} =~ /Release 8/) {
		$highvalue = "'' AS HIGH_VALUE";
	}
	# 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
};
	$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 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->[6].$row->[0]";
		}
		$parts{$row->[0]}++ if ($row->[2]);
	}
	$sth->finish;

	return %parts;
}


sub _get_custom_types
{
        my $str = uc(shift);

	my %all_types = %TYPE;
	$all_types{'DOUBLE'} = $all_types{'DOUBLE PRECISION'};
	delete $all_types{'DOUBLE PRECISION'};
	my @types_found = ();
	while ($str =~ s/(\w+)//s) {
		if (exists $all_types{$1}) {
			push(@types_found, $all_types{$1});
		}
	}
        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 < scalar(@$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] =~ /^ARRAY\(0x/) {
			my @type_col = ();
			my $is_nested = 0;
			for (my $i = 0;  $i <= $#{$row->[$idx]}; $i++) {
				if ($custom_types->{$data_type}[$i] eq '') {
					$custom_types->{$data_type}[$i] = $custom_types->{$data_type}[0];
					$is_nested = 1;
				}
				push(@type_col, $self->format_data_type($row->[$idx][$i], $custom_types->{$data_type}[$i], $action, $table, $idx, $colcond->[$idx]));
			}
			if (!$is_nested) {
				if ($action eq 'COPY') {
					map { s/^\\N$//; } @type_col; # \N for NULL is not allowed
					$row->[$idx] =  "(" . join(',', @type_col) . ")";
				} else {
					$row->[$idx] =  "ROW(" . join(',', @type_col) . ")";
				}
			} else {
				@type_col = '';
				for (my $i = 0;  $i <= $#{$row->[$idx]}; $i++) {
					push(@type_col, $row->[$idx][$i]);
				}
				if ($action eq 'COPY') {
					map { s/^\\N$//; } @type_col; # \N for NULL is not allowed
					map{ if (/,/) { s/^/""/; s/$/""/; }; } @type_col;
					$row->[$idx] =  '("{' . join(',', @type_col) . '}")';
				} else {
					map{ if (/,/) { s/^/""/; s/$/""/; }; } @type_col;
					$row->[$idx] =  "'(\"{" . join(',', @type_col) . "}\")'";
				}
			}
		} else {
			$row->[$idx] = $self->format_data_type($row->[$idx], $data_type, $action, $table, $src_data_types->[$idx], $idx, $colcond->[$idx], $sprep);
		}
	}
}

sub format_data_type
{
	my ($self, $col, $data_type, $action, $table, $src_type, $idx, $cond, $sprep) = @_;

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

	# 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 = "''";
			}
		} elsif ( ($src_type =~ /geometry/i) && ($self->{geometry_extract_type} eq 'WKB') ) {
			$col = "St_GeomFromWKB('\\x" . unpack('H*', $col) . "', $self->{spatial_srid}{$table}->[$idx])";
		} elsif ($cond->{isbytea}) {
			$col = $self->_escape_lob($col, $cond->{raw} ? 'RAW' : 'BLOB', $cond);
		} elsif ($cond->{istext}) {
			if ($cond->{clob}) {
				$col = $self->_escape_lob($col, 'CLOB', $cond);
			} elsif (!$sprep) {
				$col = $self->escape_insert($col);
			}
		} elsif ($cond->{isbit}) {
			$col = "B'$col'";
		} elsif ($cond->{isdate}) {
			if ($col =~ /^0000-00-00/) {
				$col = $self->{replace_zero_date} ?  "'$self->{replace_zero_date}'" : 'NULL';
			} elsif ($col =~ /^(\d+-\d+-\d+ \d+:\d+:\d+)\.$/) {
				$col = "'$1'";
			} else {
				$col = "'$col'";
			}
		} elsif ($data_type eq 'boolean') {
			if (exists $self->{ora_boolean_values}{lc($col)}) {
				$col = "'" . $self->{ora_boolean_values}{lc($col)} . "'";
			}
		} else {
			# covered now by the call to _numeric_format()
			# $col =~ s/,/\./;
			$col =~ s/\~/inf/;
			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}) {
			# covered now by the call to _numeric_format()
			$col =~ s/\~/inf/;
			$col = '\N' if ($col eq '');
		} elsif ($cond->{isbytea}) {
			$col = $self->_escape_lob($col, $cond->{raw} ? 'RAW' : 'BLOB', $cond);
		} elsif ($cond->{istext}) {
			$cond->{clob} ? $col = $self->_escape_lob($col, 'CLOB', $cond) : $col = $self->escape_copy($col);
		} 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)/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->{istext} = $data_types->[$idx] =~ /(char|text|xml)/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 output (gzip file, file or stdout).

=cut

sub dump
{
	my ($self, $data, $fh) = @_;

	if (!$self->{compress}) {
		if (defined $fh) {
			$fh->print($data);
		} elsif ($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 writing compressed data\n", 0, 1);
		} else {
			$fh->gzwrite($data) or $self->logit("FATAL: error writing compressed data\n", 0, 1);
		}
	} else {
		 $self->{fhout}->print($data);
	}

}


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

	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: $dirprefix${rname}_$self->{output}\n", 1);

	if ( ($self->{jobs} > 1) || ($self->{oracle_copies} > 1) ) {
		$self->{fhout}->close() if (defined $self->{fhout} && !$self->{file_per_table} && !$self->{pg_dsn});
		my $fh = $self->append_export_file($filename);
		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 ($self->{file_per_table} && $pname) {
			my $fh = $self->append_export_file($filename);
			$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\n", 0, 1);
			} else {
				$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;
				}
			}
		} 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')) {
			$AConfig{$var} = $val;
		} elsif ($var eq 'VIEW_AS_TABLE') {
			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 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;
			}
		}
	}
	$fh->close();
}

sub _extract_functions
{
	my ($self, $content) = @_;

	my @lines = split(/\n/s, $content);
	my @functions = ('');
	my $before = '';
	my $fcname =  '';
	for (my $i = 0; $i <= $#lines; $i++) { 
		if ($lines[$i] =~ /^(?:CREATE|CREATE OR REPLACE)?\s*(?:NONEDITABLE|EDITABLE)?\s*(FUNCTION|PROCEDURE)\s+([a-z0-9_\-\."]+)(.*)/i) {
			$fcname = $2;
			$fcname =~ s/^.*\.//;
			$fcname =~ s/"//g;
			if ($before) {
				push(@functions, "$before\n");
				$functions[-1] .= "FUNCTION $2 $3\n";
			} else {
				push(@functions, "FUNCTION $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);
	}
	#push(@functions, "$before\n") if ($before);

	map { s/END\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, $plsql, $owner) = @_;

	my $dirprefix = '';
	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
	my $content = "-- PostgreSQL does not recognize PACKAGES, using package_name_function_name() instead.\n";
	if ($self->{package_as_schema}) {
		$content = "-- PostgreSQL does not recognize PACKAGES, using SCHEMA instead.\n";
	}
	if ($self->{package_as_schema} && ($plsql =~ /PACKAGE\s+BODY\s*[^\s]+\s*(AS|IS)\s*/is)) {
		if (!$self->{preserve_case}) {
			$content .= "DROP SCHEMA $self->{pg_supports_ifexists} $pname CASCADE;\n";
			$content .= "CREATE SCHEMA $pname;\n";
		} else {
			$content .= "DROP SCHEMA $self->{pg_supports_ifexists} \"$pname\" CASCADE;\n";
			$content .= "CREATE SCHEMA \"$pname\";\n";
		}
		if ($self->{force_owner}) {
			$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
			if ($owner) {
				if (!$self->{preserve_case}) {
					$content .= "ALTER SCHEMA \L$pname\E OWNER TO \L$owner\E;\n";
				} else {
					$content .= "ALTER SCHEMA \"$pname\" OWNER TO \"$owner\";\n";
				}
			}
		}
	}


	# Convert type from the package header
	if ($plsql =~ s/PACKAGE\s+BODY\s*PACKAGE\s+([^\s]+)\s+(AS|IS)\s+TYPE\s+([^\s]+)\s+(AS|IS)\s+(.*;?)\s*PACKAGE BODY/PACKAGE BODY/is) {
		my $pname = $1;
		my $type = $3;
		my $params = $5;
		$params =~ s/(PROCEDURE|FUNCTION)\s+(.*?);//gis;
		$params =~ s/\s+END[^;]*;\s*$//is;
		$params =~ s/CREATE TYPE/TYPE/gis;
		while ($params =~ s/TYPE\s+([^\s\.]+\s+)/CREATE TYPE $pname.$1/is) {
			$self->{pkg_type}{$1} = "$pname.$1";
		}
		$params =~ s/\b$type\b/$pname.$type/gis;
		$self->logit("Dumping type $type from package $pname...\n", 1);
		$params = "CREATE TYPE $pname.$type AS $params\n";
		my @alltype = split(/CREATE /, $params);
		my $typnm = '';
		my $code = '';
		my $i = 1;
		foreach my $l (@alltype) {
			chomp($l);
			next if ($l =~ /^\s*$/);
			$code .= $l . "\n";
			if ($code =~ /^TYPE\s+([^\s\(]+)/is) {
				$typnm = $1;
			}
			next if (!$typnm);
			if ($code =~ /;/s) {
				push(@{$self->{types}}, { ('name' => $typnm, 'code' => $code, 'pos' => $i) });
				$typnm = '';
				$code = '';
				$i++;
			}
		}
		$i = 1;
		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});
			} else {
				$tpe->{code} = "CREATE OR REPLACE $tpe->{code}\n";
			}
			$content .= $tpe->{code} . "\n";
			$i++;
		}
	}

	# Convert the package body part
	if ($plsql =~ /PACKAGE\s+BODY\s*([^\s]+)\s*(AS|IS)\s*(.*)/is) {
		my $pname = $1;
		my $type = $2;
		my $ctt = $3;
		$pname =~ s/"//g;
		$pname =~ s/^.*\.//g;
		$self->logit("Dumping package $pname...\n", 1);
		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);
				}
			}
		}
		$self->{idxcomment} = 0;
		$ctt =~ s/END[^;]*;$//is;
		my %comments = $self->_remove_comments(\$ctt);
		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]);
			if (!exists $fct_detail{name}) {
				$functions[$i] = '';
				next;
			}
			$fct_detail{name} =~ s/^.*\.//;
			$fct_detail{name} =~ s/"//g;
			next if (!$fct_detail{name});
			if (!$self->{preserve_case}) {
				$fct_detail{name} = lc($fct_detail{name});
			}
			if (!exists $self->{package_functions}{$fct_detail{name}}) {
				my $res_name = $fct_detail{name};
				if ($self->{package_as_schema}) {
					$res_name = $pname . '.' . $res_name;
				} else {
					$res_name = $pname . '_' . $res_name;
				}
				$res_name =~ s/"_"/_/g;
				if (!$self->{preserve_case}) {
					$self->{package_functions}{"\L$fct_detail{name}\E"}{name} = lc($res_name);
				} else {
					$self->{package_functions}{"\L$fct_detail{name}\E"}{name} = $res_name;
				}
				$self->{package_functions}{"\L$fct_detail{name}\E"}{package} = $pname;
			}
		}

		$self->{pkgcost} = 0;
		foreach my $f (@functions) {
			next if (!$f);
			$content .= $self->_convert_function($owner, $f, $pname, \%comments);
		}
		$self->_restore_comments(\$content, \%comments);
		if ($self->{estimate_cost}) {
			$self->{total_pkgcost} += $self->{pkgcost} || 0;
		}
	}
	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, $comments) = @_;

	foreach my $k (keys %$comments) {
		$$content =~ s/$k[\n]?/$comments->{$k}\n/s;
	}

}

=head2 _remove_comments

This function is used to remove comments from SQL code
to allow easy parsing

=cut

sub _remove_comments
{
	my ($self, $content) = @_;

	my %comments = ();

	while ($$content =~ s/(\/\*(.*?)\*\/)/\%ORA2PG_COMMENT$self->{idxcomment}\%/s) {
		$comments{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $1;
		$self->{idxcomment}++;
	}

	while ($$content =~ s/(\'[^\'\n\r]+\b(PROCEDURE|FUNCTION)\s+[^\'\n\r]+\')/\%ORA2PG_COMMENT$self->{idxcomment}\%/is) {
		$comments{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $1;
		$self->{idxcomment}++;
	}
	my @lines = split(/\n/, $$content);
	for (my $j = 0; $j <= $#lines; $j++) {
		if (!$self->{is_mysql}) {
			if ($lines[$j] =~ s/(\s*\-\-.*)$/\%ORA2PG_COMMENT$self->{idxcomment}\%/) {
				$comments{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $1;
				chomp($comments{"\%ORA2PG_COMMENT$self->{idxcomment}\%"});
				$self->{idxcomment}++;
			}
		} else {
			# Mysql supports differents kinds of comment's starter
			if ( ($lines[$j] =~ s/(\s*COMMENT\s+'.*)$/\%ORA2PG_COMMENT$self->{idxcomment}\%/) ||
			($lines[$j] =~ s/(\s*\-\- .*)$/\%ORA2PG_COMMENT$self->{idxcomment}\%/) ||
			($lines[$j] =~ s/(\s*\# .*)$/\%ORA2PG_COMMENT$self->{idxcomment}\%/) ) {
				$comments{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} = $1;
				chomp($comments{"\%ORA2PG_COMMENT$self->{idxcomment}\%"});
				# Normalize start of comment
				$comments{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} =~ s/^(\s*)COMMENT/$1\-\- /;
				$comments{"\%ORA2PG_COMMENT$self->{idxcomment}\%"} =~ s/^(\s*)\#/$1\-\- /;
				$self->{idxcomment}++;
			}
		}
	}
	$$content = join("\n", @lines);

	return %comments;
}

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

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

	my %fct_detail = ();
	if (!$self->{is_mysql}) {
		%fct_detail = $self->_lookup_function($plsql);
	} else {
		%fct_detail = $self->_lookup_function($pname);
		$pname = '';
	}
	return if (!exists $fct_detail{name});

	$fct_detail{name} =~ s/^.*\.//;
	$fct_detail{name} =~ s/"//g;

	my $sep = '.';
	$sep = '_' if (!$self->{package_as_schema});
	my $fname = $fct_detail{name};
	if ($self->{preserve_case}) {
		$fname = "\"$fname\"";
		$fname = "\"$pname\"" . $sep . $fname if ($pname && !$self->{is_mysql});
	} else {
		$fname = lc($fname);
		$fname = $pname . $sep . $fname if ($pname && !$self->{is_mysql});
	}
	$fname =~ s/"_"/_/g;

	$fct_detail{args} =~ s/\s+IN\s+/ /ig; # Remove default IN keyword

	# Input parameters after one with a default value must also have defaults
	# and we need to sort the arguments so the ones with default values will be on the bottom
	$fct_detail{args} =~ s/^\(|\)\s*$//g;
	my @args_sorted;
	push(@args_sorted, grep {!/\sdefault\s/i} split ',', $fct_detail{args});
	push(@args_sorted, grep {/\sdefault\s/i} split ',', $fct_detail{args});
	$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);
	}

	if ($fct_detail{hasreturn}) {
		# Returns the right type
		$func_return = " RETURNS$fct_detail{setof} $fct_detail{func_ret_type} AS \$body\$\n";
	} else {
		my @nout = $fct_detail{args} =~ /\bOUT /igs;
		my @ninout = $fct_detail{args} =~ /\bINOUT /igs;
		# Return void when there's no out parameters
		if (($#nout < 0) && ($#ninout < 0)) {
			$func_return = " RETURNS VOID AS \$body\$\n";
		} else {
			$func_return = " AS \$body\$\n";
		}
	}

	my @at_ret_param = ();
	my @at_ret_type = ();
	my $at_suffix = '';
	if ($fct_detail{declare} =~ s/\s*PRAGMA\s+AUTONOMOUS_TRANSACTION\s*;//is) {
		$at_suffix = '_atx';
		# COMMIT is not allowed in PLPGSQL function
		$fct_detail{code} =~ s/\bCOMMIT\s*;//;
		my @tmp = split(',', $fct_detail{args});
		foreach my $p (@tmp) {
			if ($p =~ s/\s*OUT\s+//) {
				push(@at_ret_param, $p);
				push(@at_ret_type, $p);
			} elsif ($p =~ s/\s*INOUT\s+//) {
				push(@at_ret_param, $p);
				push(@at_ret_type, $p);
			}
		}
		map { s/^(.*?) //; } @at_ret_type;
		if ($#at_ret_param < 0) {
			push(@at_ret_param, 'ret ' . $fct_detail{func_ret_type});
			push(@at_ret_type, $fct_detail{func_ret_type});
		}
	}

	my $name = $fname;
	my $function = "\nCREATE OR REPLACE FUNCTION $fname$at_suffix $fct_detail{args}";
	if (!$pname || !$self->{package_as_schema}) {
		if ($self->{export_schema} && !$self->{schema}) {
			if (!$self->{preserve_case}) {
				$function = "\nCREATE OR REPLACE FUNCTION \L$owner\.$fname\E $fct_detail{args}";
				$name =  "\L$owner\.$fname\E";
				$self->logit("Parsing function \L$owner\.$fname\E...\n", 1);
			} else {
				$function = "\nCREATE OR REPLACE FUNCTION \"$owner\"\.$fname $fct_detail{args}";
				$name = "\"$owner\"\.$fname";
				$self->logit("Parsing function \"$owner\"\.$fname...\n", 1);
			}
		} elsif ($self->{export_schema} && $self->{schema}) {
			if (!$self->{preserve_case}) {
				$function = "\nCREATE OR REPLACE FUNCTION $self->{schema}\.$fname $fct_detail{args}";
				$name =  "$self->{schema}\.$fname";
				$self->logit("Parsing function $self->{schema}\.$fname...\n", 1);
			} else {
				$function = "\nCREATE OR REPLACE FUNCTION \"$self->{schema}\"\.$fname $fct_detail{args}";
				$name = "\"$self->{schema}\"\.$fname";
				$self->logit("Parsing function \"$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
--
};
		if (!$fct_detail{hasreturn}) {
			$at_wrapper .= "CREATE OR REPLACE FUNCTION $name $fct_detail{args} RETURNS VOID AS \$body\$";
		} else {
			$at_wrapper .= "CREATE OR REPLACE FUNCTION $name $fct_detail{args} $func_return";
		}
		map { s/(.*)/quote_nullable($1)/; }  @{$fct_detail{at_args}};
		my $params = join(" || ',' || ", @{$fct_detail{at_args}});
		$params = " '' " if (!$params);
		$at_wrapper .= qq{
DECLARE
	-- Change this to reflect the dblink connection string
	v_conn_str  text := 'port=5432 dbname=testdb host=localhost user=pguser password=pgpass';
	v_query     text;
};
		if (!$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);
};
		} 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 dblink(v_conn_str, v_query) AS p ($at_ret_param[0]);
	RETURN v_ret;
};
		}
		$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
--
};
		if (!$fct_detail{hasreturn}) {
			$at_wrapper .= "CREATE OR REPLACE FUNCTION $name $fct_detail{args} RETURNS VOID AS \$body\$";
		} else {
			$at_wrapper .= "CREATE OR REPLACE FUNCTION $name $fct_detail{args} $func_return";
		}
		map { s/(.*)/quote_nullable($1)/; }  @{$fct_detail{at_args}};
		my $params = join(" || ',' || ", @{$fct_detail{at_args}});
		$params = " '' " if (!$params);
		$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;

	$fct_detail{immutable} = ' IMMUTABLE' if ($fct_detail{immutable});
	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);
		$function .= "DECLARE\n$fct_detail{declare}\n" if ($fct_detail{declare});
		$function .= $fct_detail{code};
		$function .= "\n\$body\$\nLANGUAGE PLPGSQL\n";
		$revoke = "-- REVOKE ALL ON FUNCTION $name $fct_detail{args} FROM PUBLIC;";
		$revoke =~ s/[\n\r]+\s*/ /gs;
		$revoke .= "\n";
		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');
		}
		$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 FUNCTION $fname $fct_detail{args} OWNER TO";
			if (!$self->{preserve_case}) {
				$function .= " \L$owner\E;\n";
			} else {
				$function .= " \"$owner\";\n";
			}
		}
	}
	$function .= $revoke;
	$function = $at_wrapper . $function;

	if ($pname && $self->{file_per_function}) {
		$fname =~ s/^"*$pname"*\.//i;
		$fname =~ s/"//g; # Remove case sensitivity quoting
		$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-2016 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();

		my $fhdl = $self->open_export_file("$dirprefix\L$pname/$fname\E_$self->{output}", 1);
		$self->_restore_comments(\$function, $hrefcomments);
		$self->dump($sql_header . $function, $fhdl);
		$self->close_export_file($fhdl);
		$function = "\\i $dirprefix\L$pname/$fname\E_$self->{output}\n";
		return $function;
	}

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


	my @tbs = ();
	# Retrieve all tbs names used in view if possible
	if ($sqlstr =~ /\bFROM\b(.*)/is) {
		my $tmp = $1;
		$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;
		if ($tmp =~ /[\(\)]/) {
			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\$]+)["']*(,?)/\L$tb\E.\L$1\E$2/igs;
			# Escape table name
			$sqlstr =~ s/(^=\s?)["']*\b$regextb\b["']*/\L$tb\E/igs;
			# Escape AS names
			#$sqlstr =~ s/(\bAS\s*)["']*([A-Z_0-9]+)["']*/$1\L$2\E/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;
			# Escape AS names
			#$sqlstr =~ s/(\bAS\s*)["']*([A-Z_0-9]+)["']*/$1"$2"/igs;
			if ($tb =~ /(.*)\.(.*)/) {
				my $prefx = $1;
				my $sufx = $2;
				$sqlstr =~ s/"$regextb"/"$prefx"\."$sufx/g;
			}
		}
	}
	if ($self->{plsql_pgsql}) {
			$sqlstr = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $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) = @_;

	$level ||= 0;

	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 _convert_type

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_type
{
	my ($self, $plsql, $owner) = @_;

	my $unsupported = "-- Unsupported, please edit to match PostgreSQL syntax\n";
	my $content = '';
	my $type_name = '';
	if ($plsql =~ /TYPE\s+([^\s]+)\s+(IS|AS)\s*TABLE\s*OF\s+(.*)/is) {
		$type_name = $1;
		my $type_of = $3;
		if ($self->{export_schema} && !$self->{schema} && $owner) {
			$type_name = "$owner.$type_name";
		}
		$type_of =~ s/\s*NOT[\t\s]+NULL//s;
		$type_of =~ 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->{type_of_type}{'Nested Tables'}++;
			$content = "CREATE TYPE \L$type_name\E AS (\L$type_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 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 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 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});
		$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 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});
		$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;
		chomp($tbname);
		if ($self->{export_schema} && !$self->{schema} && $owner) {
			$type_name = "$owner.$type_name";
		}
		my $declar = Ora2Pg::PLSQL::replace_sql_type($tbname, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type});
		$content = qq{
CREATE TYPE \L$type_name\E AS ($type_name $declar\[$size\]);
};
		$self->{type_of_type}{Varrays}++;
	} else {
		$self->{type_of_type}{Unknown}++;
		$plsql =~ s/;$//s;
		$content = "${unsupported}CREATE OR REPLACE $plsql;"
	}

	if ($self->{force_owner}) {
		$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
		if ($owner) {
			if (!$self->{preserve_case}) {
				$content .= "ALTER TYPE $type_name OWNER TO \L$owner\E;\n";
			} else {
				$content .= "ALTER TYPE $type_name OWNER TO \"$owner\";\n";
			}
		}
	}

	return $content;
}

sub ask_for_data
{
	my ($self, $table, $cmd_head, $cmd_foot, $s_out, $nn, $tt, $sprep, $stt, $part_name) = @_;

	# Build SQL query to retrieve data from this table
	if (!$part_name) {
		$self->logit("Looking how to retrieve data from $table...\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);

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

	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(500000);
		}
		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 _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;
	$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);
		}
	}

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

		# 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
				my $custom_type = '';
				if (!exists $self->{data_type}{$data_type}) {
					$self->logit("Data type $stt->[$idx] is not native, searching on custom types.\n", 1);
					$custom_type = $self->_get_types($dbh, $stt->[$idx]);
					foreach my $tpe (sort {length($a->{name}) <=> length($b->{name}) } @{$custom_type}) {
						$self->logit("Looking inside custom type $tpe->{name} to extract values...\n", 1);
						push(@{$user_type{$data_type}}, &_get_custom_types($tpe->{code}));
					}
				}
			}
		}

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

		# 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
				my $custom_type = '';
				if (!exists $self->{data_type}{$data_type}) {
					$self->logit("Data type $stt->[$idx] is not native, searching on custom types.\n", 1);
					$custom_type = $self->_get_types($self->{dbh}, $stt->[$idx]);
					foreach my $tpe (sort {length($a->{name}) <=> length($b->{name}) } @{$custom_type}) {
						$self->logit("Looking inside custom type $tpe->{name} to extract values...\n", 1);
						push(@{$user_type{$data_type}}, &_get_custom_types($tpe->{code}));
					}
				}
			}
		}

		# 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}) {
			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}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
			} else {
				$sth = $self->{dbh}->prepare($query,{'ora_auto_lob' => 0, ora_exe_mode=>OCI_STMT_SCROLLABLE_READONLY, ora_check_sql => 1}) or $self->logit("FATAL: " . $self->{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 = $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);
	}

	# 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/, @$stt));

		if (!$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;

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


				# Retrieve LOB data from locator
				$self->{chunk_size} = 8192;
				# 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++) {
					if (($stt->[$j] =~ /LOB/) && $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"}) ) {
							while (1) {
								my $lobdata = $dbh->ora_lob_read($row[$j], $offset, $self->{chunk_size} );
								if ($dbh->errstr) {
									$self->logit("ERROR: " . $dbh->errstr . "\n", 0, 0);
									last;
								}
								last unless (defined $lobdata && length $lobdata);
								$offset += $self->{chunk_size};
								$lob_content .= $lobdata;
							}
						} else {
							while (1) {
								my $lobdata = $self->{dbh}->ora_lob_read($row[$j], $offset, $self->{chunk_size} );
								if ($self->{dbh}->errstr) {
									$self->logit("ERROR: " . $self->{dbh}->errstr . "\n", 0, 0);
									last;
								}
								last unless (defined $lobdata && length $lobdata);
								$offset += $self->{chunk_size};
								$lob_content .= $lobdata;
							}
						}
						if ($lob_content) {
							$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;
					}
				}
				push(@rows, [@row]);
				$total_record++;
				$self->{current_total_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 = ();
				}
			}
			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;
				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) {
			$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();

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

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

	open(OUTERROR, ">>$outfile") or $self->logit("FATAL: can not write to $outfile, $!\n", 0, 1);
	print OUTERROR "$s_out";
	foreach my $row (@$rows) {
		print OUTERROR join("\t", @$row), "\n";
	}
	print OUTERROR "\\.\n";
	close(OUTERROR);

}

sub log_error_insert
{
	my ($self, $table, $sql_out) = @_;

	my $outfile = '';
	if ($self->{output_dir} && !$noprefix) {
		$outfile = $self->{output_dir} . '/';
	}
	$outfile .= $table . '_error.log';

	open(OUTERROR, ">>$outfile") or $self->logit("FATAL: can not write to $outfile, $!\n", 0, 1);
	print OUTERROR "$sql_out\n";
	close(OUTERROR);

}


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

	# Open a connection to the postgreSQL database if required
	my $rname = $part_name || $table;

	if ($self->{pg_dsn}) {
		$0 = "ora2pg - sending data to PostgreSQL table $rname";
	} else {
		$0 = "ora2pg - sending data from table $rname to file";
	}

	# Connect to PostgreSQL if direct import is enabled
	my $dbhdest = undef;
	if ($self->{pg_dsn}) {
		$dbhdest = $self->_send_to_pgdb();
		$self->logit("Dumping data from table $rname into PostgreSQL...\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}) {
			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}) {
			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) {
		my $data_limit = $self->{data_limit};
		if (exists $self->{local_data_limit}{$table}) {
			$data_limit = $self->{local_data_limit}{$table};
		}
		$self->logit("DEBUG: Formatting bulk of $data_limit data 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/;$//;
			$self->logit("DEBUG: Sending COPY bulk output directly to PostgreSQL backend\n", 1);
			my $s = $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 {
			# 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->logit("DEBUG: Sending INSERT output directly to PostgreSQL backend\n", 1);
				unless($dbhdest->do($sql_out)) {
					if ($self->{log_on_error}) {
						$self->logit("WARNING (log error enabled): " . $dbhdest->errstr . "\n", 0, 0);
						$self->log_error_insert($table, $sql_out);
					} else {
						$self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
					}
				}
			} else {
				my $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') {
						$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
					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);
						}
					}
				}
				$ps->finish();
			}
		}
	} else {
		if ($part_name && $self->{prefix_partition})  {
			$part_name = $table . '_' . $part_name;
		}
		$self->data_dump($h_towrite . $sql_out . $e_towrite, $table, $part_name);
	}

	my $total_row = $self->{tables}{$table}{table_info}{num_rows};
	my $tt_record = @$rows;
	$dbhdest->disconnect() if ($dbhdest);

        # Set file temporary until the table export is done
        my $filename = $self->{output};
        if ($self->{file_per_table}) {
                $filename = "${rname}_$self->{output}";
        }
 
	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}) {
				$self->logit("\tOracle NLS_TIMESTAMP_FORMAT YYYY-MM-DD HH24:MI:SS.FF\n", 0);
			} else {
				$self->logit("\tOracle NLS_TIMESTAMP_FORMAT YYYY-MM-DD HH24:MI:SS\n", 0);
			}
			$self->logit("\tOracle NLS_DATE_FORMAT YYYY-MM-DD HH24:MI:SS\n", 0);
			$self->logit("\tPostgreSQL CLIENT_ENCODING $self->{client_encoding}\n", 0);
			$self->logit("\tPerl output encoding '$self->{binmode}'\n", 0);

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

		$self->logit("Reporting Oracle Content...\n", 1);
		my $uncovered_score = 'Ora2Pg::PLSQL::UNCOVERED_SCORE';
		if ($self->{is_mysql}) {
			$uncovered_score = 'Ora2Pg::PLSQL::UNCOVERED_MYSQL_SCORE';
		}
		# Get Oracle database version and size
		my $ver = $self->_get_version();
		my $size = $self->_get_database_size();
		# Get the list of all database objects
		my %objects = $self->_get_objects();
		# Determining how many non automatiques indexes will be exported
		my %all_indexes = ();
		$self->{skip_fkeys} = $self->{skip_indices} = $self->{skip_indexes} = $self->{skip_checks} = 0;
		$self->{view_as_table} = ();
		# Determining how many non automatiques indexes will be exported
		# Extract all tables informations
		$self->_tables();
		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 {length($a->{name}) <=> length($b->{name}) } @{$self->{types}}) {
				$self->_convert_type($tpe->{code}, $tpe->{owner});
			}
		}
		# Get definition of Database Link
		my %dblink = $self->_get_dblink();
		$objects{'DATABASE LINK'} = scalar keys %dblink;	
		# Get Jobs
		my %jobs = $self->_get_job();
		$objects{'JOB'} = scalar keys %jobs;
		# Get synonym inforamtion
		my %synonyms = $self->_synonyms();
		$objects{'SYNONYM'} = scalar keys %synonyms;	
		# 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 ($self->{is_mysql} && ($typ eq 'SYNONYM')); # Package are scanned with PACKAGE BODY not PACKAGE objects
			next if ($typ eq 'PACKAGE'); # Package are scanned with PACKAGE BODY not PACKAGE objects
			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')) {
				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});
				$report_info{'Objects'}{$typ}{'cost_value'} = 288 if (($typ eq 'TABLE') && ($report_info{'Objects'}{$typ}{'cost_value'} > 288));
				$report_info{'Objects'}{$typ}{'cost_value'} = 288 if (($typ eq 'INDEX') && ($report_info{'Objects'}{$typ}{'cost_value'} > 288));
				$report_info{'Objects'}{$typ}{'cost_value'} = 96 if (($typ eq 'TABLE PARTITION') && ($report_info{'Objects'}{$typ}{'cost_value'} > 96));
			}
			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.";
				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 and hash index(es) will be exported as b-tree index(es) if any. 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(es) will be exported as b-tree index(es) if any. 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 = 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}[10] <=> $self->{tables}{$t}{column_info}{$a}[10]} keys %{$self->{tables}{$t}{column_info}}) {
						$r = 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->_lookup_check_constraint($t, $self->{tables}{$t}{check_constraint},$self->{tables}{$t}{field_name}, 1);
					$total_check += ($#constraints + 1);
					if ($self->{estimate_cost} && ($#constraints >= 0)) {
						$report_info{'Objects'}{$typ}{'cost_value'} += (($#constraints + 1) * $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}{'detail'} .= "\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/;
			} 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'));
					$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') {
				my $packages = $self->_get_packages();
				my $total_size = 0;
				my $number_fct = 0;
				my $number_pkg = 0;
				foreach my $pkg (keys %{$packages}) {
					next if (!$packages->{$pkg}{text});
					$number_pkg++;
					$total_size += length($packages->{$pkg}{text});
					my @codes = split(/CREATE(?: OR REPLACE)?(?: NONEDITABLE| EDITABLE)? PACKAGE BODY/, $packages->{$pkg}{text});
					foreach my $txt (@codes) {
						my %infos = $self->_lookup_package("CREATE OR REPLACE PACKAGE BODY$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}{name});
								$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++;
						}
					}
				}
				if ($self->{estimate_cost}) {
					$report_info{'Objects'}{$typ}{'cost_value'} += ($number_fct*$Ora2Pg::PLSQL::OBJECT_SCORE{'FUNCTION'});
					$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'} .= "\L$partitions{$t} $t\E 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 'CLUSTER') {
				$report_info{'Objects'}{$typ}{'comment'} = "Clusters are not supported by PostgreSQL and will not be exported.";
			} elsif ($typ eq 'VIEW') {
				my %view_infos = $self->_get_views();
				foreach my $view (sort keys %view_infos) {
					if ($self->{estimate_cost}) {
						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $view_infos{$view}{text});
						next if ($cost <= ($cost_detail{SIZE}+$cost_detail{TEST}));
						$cost -= ($cost_detail{SIZE} + $cost_detail{TEST});
						$cost = sprintf("%.1f", $cost);
						delete $cost_detail{SIZE};
						delete $cost_detail{TEST};
						$report_info{'Objects'}{$typ}{'cost_value'} += $cost;
						$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) if (($d ne 'SIZE') && ($d ne 'TEST')); 
						}
					}
				}
				$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'};
		}
		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::plsql_to_plpgsql($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 = &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 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 = ();

			# 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.
		my %partitions = $self->_get_partitioned_table();

                # Look for external tables
                my %externals = ();
		if (!$self->{is_mysql}) {
			%externals = $self->_get_external_tables();
		}

		my @done = ();
		my $id = 0;
		# Set the table information for each class found
		my $i = 1;
		my $total_row_num = 0;
		foreach my $t (sort keys %tables_infos) {
			# 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"}) {
				$warning .= " - $partitions{$t} partitions";
			}

			# Search for reserved keywords
			my $ret = &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 (exists $partitions{$t}) {
				$kind = ' PARTITIONED';
			}
			if (exists $externals{$t}) {
				$kind = ' EXTERNAL';
			}
			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}[10] <=> $self->{tables}{$t}{column_info}{$b}[10];
						} 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]);
							$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]);
							$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,POSITION,SDO_DIM,SDO_GTYPE,SRID
					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]);
					$type = "$d->[1], $d->[2]" if (!$type);

					# Check if we need auto increment
					$warning = '';
					if ($d->[12] eq 'auto_increment') {
						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 {

						# Set the dimension, array is (srid, dims, gtype)
						my $suffix = '';
						if ($d->[12] == 3) {
							$suffix = 'Z';
						} elsif ($d->[12] == 4) {
							$suffix = 'ZM';
						}
						my $gtypes = '';
						if (!$d->[13] || ($d->[13] =~  /,/) ) {
							$gtypes = $ORA2PG_SDO_GTYPE{0};
						} else {
							$gtypes = $d->[13];
						}
						$type = "geometry($gtypes$suffix";
						if ($d->[11]) {
							$type .= ",$d->[11]";
						}
						$type .= ")";
						$type .= " - $d->[13]" if ($d->[13] =~  /,/);
						
					}
					my $ret = &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';
					}
					$self->logit(" => $type$warning$align\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*/, $self->{pg_schema})) . "')";
	} elsif ($self->{schema} && $self->{export_schema}) {
		return "AND $attrname = '$self->{schema}'";
	}

	my $cond = '';
	if ($visible) {
		$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 $tbmod;") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
			if (not $s->execute) {
				push(@errors, "Table $tbmod$orig does not exists in PostgreSQL database.") if ($s->state eq '42P01');
				next;
			}
			while ( my @row = $s->fetchrow()) {
				print "POSTGRES:$tbmod$orig:$row[0]\n";
				if ($row[0] != $tables_infos{$t}{num_rows}) {
					push(@errors, "Table $tbmod$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:$tbmod$orig:$row[0]\n";
				if ($row[0] != $numixd) {
					push(@errors, "Table $tbmod$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 (including primary keys)
	####
	print "\n";
	print "[TEST UNIQUE CONSTRAINTS COUNT]\n";
	my %unique_keys = $self->_unique_key('',$self->{schema});
	my $schema_cond = $self->get_schema_condition('pg_indexes.schemaname');
	my $sql = qq{
SELECT count(*)
FROM pg_class
  JOIN pg_index ON (pg_index.indexrelid=pg_class.relfilenode)
  JOIN pg_indexes ON (pg_class.relname=pg_indexes.indexname)
WHERE pg_class.relkind = 'i'
  AND pg_index.indisunique 
  AND pg_indexes.tablename=?
 $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:$tbmod$orig:$row[0]\n";
				if ($row[0] != $numixd) {
					push(@errors, "Table $tbmod$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";
	$schema_cond = $self->get_schema_condition('pg_indexes.schemaname');
	$sql = qq{
SELECT count(*)
FROM pg_class
  JOIN pg_index ON (pg_index.indexrelid=pg_class.relfilenode)
  JOIN pg_indexes ON (pg_class.relname=pg_indexes.indexname)
WHERE pg_class.relkind = 'i'
  AND pg_index.indisprimary
  AND pg_indexes.tablename=?
 $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:$tbmod$orig:$row[0]\n";
				if ($row[0] != $nbpk) {
					push(@errors, "Table $tbmod$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} !~ /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:$tbmod$orig:$row[0]\n";
					if ($row[0] != $nbcheck) {
						push(@errors, "Table $tbmod$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:$tbmod$orig:$row[0]\n";
				if ($row[0] != $nbnull) {
					push(@errors, "Table $tbmod$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 '') {
				$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:$tbmod$orig:$pgdef\n";
			if ($pgdef != $nbdefault) {
				push(@errors, "Table $tbmod$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:$tbmod$orig:$row[0]\n";
				if ($row[0] != $nbfk) {
					push(@errors, "Table $tbmod$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:$tbmod$orig:$row[0]\n";
				if ($row[0] != $nbtrg) {
					push(@errors, "Table $tbmod$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{$t}\n";
		my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
		if (exists $pg_part{$tbmod}) {
			print "POSTGRES:$tbmod$orig:$pg_part{$tbmod}\n";
			if ($pg_part{$tbmod} != $partitions{$t}) {
				push(@errors, "Table $tbmod$orig doesn't have the same number of partitions in source database ($partitions{$t}) and in PostgreSQL ($pg_part{$tbmod}).");
			}
		} else {
			push(@errors, "Table $tbmod$orig doesn't have the same number of partitions in source database ($partitions{$t}) 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 _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($self->{dbh});
		$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)');
	$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) {
			 push(@errors, "Function $f is missing in PostgreSQL database.") if (!exists $pg_function{$f});
		}
	}
	$self->show_test_errors('FUNCTION', @errors);
	@errors = ();
	print "\n";
}


=head2 _get_pkg_functions

This function retrieves the Oracle package list and the replacement
value to use when the schema replacement to package is not created

=cut

sub _get_pkg_functions
{
	my $self = shift;

	my $packages = $self->_get_packages();
	foreach my $pkg (keys %{$packages}) {
		next if (!$packages->{$pkg}{text});
		my @codes = split(/CREATE(?: OR REPLACE)?(?: NONEDITABLE| EDITABLE)? PACKAGE BODY/, $packages->{$pkg}{text});
		foreach my $txt (@codes) {
			my %infos = $self->_lookup_package("CREATE OR REPLACE PACKAGE BODY$txt");
			foreach my $f (sort keys %infos) {
				next if (!$f);
				my $int_name = $f;
				my $res_name = $f;
				$res_name =~ s/\./_/g;
				$res_name =~ s/"_"/_/g;
				if (!$self->{preserve_case}) {
					$self->{package_functions}{"\L$f\E"}{name} = lc($res_name);
				} else {
					$self->{package_functions}{"\L$f\E"}{name} = $res_name;
				}
				$self->{package_functions}{"\L$f\E"}{package} = $pkg;
			}
		}
	}
}

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

	$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

=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;
	while ( my @row = $sth->fetchrow()) {
		push(@{$infos{$row[1]}}, { ( name => $row[0], invalid => ($row[2] eq 'VALID') ? 0 : 1) });
	}
	$sth->finish();

	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->{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 _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 S.SEGMENT_NAME, ROUND(S.BYTES/1024/1024) SIZE_MB FROM ${prefix}_SEGMENTS A WHERE A.SEGMENT_TYPE LIKE 'TABLE%'";
	}
        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');
	}

	$sql .= " ORDER BY S.BYTES,S.SEGMENT_NAME DESC) WHERE ROWNUM <= $self->{top_max}";

        my $sth = $self->{dbh}->prepare( $sql ) or return undef;
        $sth->execute 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

=cut


sub _compile_schema
{
	my ($self, $dbh, $schema) = @_;

	my $qcomp = '';

	if ($schema || ($schema =~ /[a-z]/i)) {
		$qcomp = qq{begin
DBMS_UTILITY.compile_schema(schema => '$schema');
end;
};
	} elsif ($schema) {
		$qcomp = "EXEC DBMS_UTILITY.compile_schema(schema => sys_context('USERENV', 'SESSION_USER'));";
	}
	if ($qcomp) {
		my $sth = $dbh->do($qcomp) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
		$sth = undef;
	}

}


=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 $sth = $dbh->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS.FF'") 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}) {
		$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);
	} else {
		$sth = $dbh->do("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT='YYYY-MM-DD HH24:MI:SS.FF 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 _initial_command
{
	my ($self, $dbh) = @_;

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

	$dbh = $self->{dbh} if (!$dbh);

	$self->logit("DEBUG: executing initial command to Oracle\n", 1);

	my $sth = $dbh->do($self->{ora_initial_command}) 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 $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+)/) {

			$global_start_time = $1;

		# Store total number of tuples exported
		} elsif ($r =~ /^GLOBAL EXPORT ROW NUMBER: (\d+)/) {

			$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+)/) {

			$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+)/) {

			# 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+)/) {

			$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/) {

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

sub limit_to_objects
{
	my ($self, $obj_type, $column) = @_;

	my $str = '';
	$obj_type ||= $self->{type};
	$column ||= 'TABLE_NAME';

	my @cols = split(/\|/, $column);
	my @arr_type = split(/\|/, $obj_type);
	my @done = ();
	my $has_limitation = 0;
	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);

		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++) {
					$str .= "upper($colname) LIKE \U'$self->{limited}{$arr_type[$i]}->[$j]'\E";
					if ($j < $#{$self->{limited}{$arr_type[$i]}}) {
						$str .= " OR ";
					}
				}
			} else {
				for (my $j = 0; $j <= $#{$self->{limited}{$arr_type[$i]}}; $j++) {
					if ($self->{is_mysql}) {
						$str .= "upper($colname) RLIKE '\^$self->{limited}{$arr_type[$i]}->[$j]\$'" ;
					} else {
						$str .= "REGEXP_LIKE(upper($colname),'\^$self->{limited}{$arr_type[$i]}->[$j]\$')" ;
					}
					if ($j < $#{$self->{limited}{$arr_type[$i]}}) {
						$str .= " OR ";
					}
				}
			}
			$str .= ')';
			$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 \U'$self->{excluded}{$arr_type[$i]}->[$j]'\E" ;
					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 '\^$self->{excluded}{$arr_type[$i]}->[$j]\$'" ;
					} else {
						$str .= "NOT REGEXP_LIKE(upper($colname),'\^$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/)) {
			if ($self->{db_version} =~ /Release [89]/) {
				$str .= ' AND (';
				foreach my $t (@EXCLUDED_TABLES_8I) {
					$str .= " AND upper($colname) NOT LIKE \U'$t'\E";
				}
				$str .= ')';
			} else {
				$str .= ' AND ( ';
				for (my $j = 0; $j <= $#EXCLUDED_TABLES; $j++) {
					if ($self->{is_mysql}) {
						$str .= " upper($colname) NOT RLIKE '\^$EXCLUDED_TABLES[$j]\$'" ;
					} else {
						$str .= " NOT REGEXP_LIKE(upper($colname),'\^$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};
		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"}{$c}"/gsi;
					$chkconstraint =~ s/\b$c\b/$self->{replaced_cols}{"\L$tbsaved\E"}{$c}/gsi;
				}
			}
			if ($self->{plsql_pgsql}) {
				$chkconstraint = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $chkconstraint);
			}
			next if ($nonotnull && ($chkconstraint =~ /IS NOT NULL/));
			if (!$self->{preserve_case}) {
				foreach my $c (@$field_name) {
					# Force lower case
					my $ret = $self->quote_reserved_words($c);
					$chkconstraint =~ s/"$c"/$ret/igs;
					$chkconstraint =~ s/\b$c\b/$ret/gsi;
				}
				$k = lc($k);
			}
			push(@chk_constr,  "ALTER TABLE $table ADD CONSTRAINT $k CHECK ($chkconstraint);\n");
		}
	}

	return @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 =~ /PACKAGE\s+BODY\s*([^\s]+)\s*(AS|IS)\s*(.*)/is) {
		my $pname = $1;
		my $type = $2;
		$content = $3;
		$pname =~ s/"//g;
		$self->logit("Looking at package $pname...\n", 1);
		$self->{idxcomment} = 0;
		$content =~ s/END[^;]*;$//is;
		my %comments = $self->_remove_comments(\$content);
		my @functions = $self->_extract_functions($content);
		foreach my $f (@functions) {
			next if (!$f);
			my %fct_detail = $self->_lookup_function($f);
			next if (!exists $fct_detail{name});
			$fct_detail{name} =~ s/^.*\.//;
			$fct_detail{name} =~ s/"//g;
			$infos{"$pname.$fct_detail{name}"}{name} = $f if ($fct_detail{name});
			$infos{"$pname.$fct_detail{name}"}{type} = $fct_detail{type} if ($fct_detail{type});
		}
	}

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

	if ($self->{is_mysql}) {
		if ($self->{type} eq 'FUNCTION') {
			return Ora2Pg::MySQL::_lookup_function($self, $plsql);
		} else {
			return Ora2Pg::MySQL::_lookup_procedure($self, $plsql);
		}
	}

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

	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;

		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);
		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;
		}
		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/IN\s+OUT/INOUT/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});
		$fct_detail{declare} = Ora2Pg::PLSQL::replace_sql_type($fct_detail{declare}, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type});

		# Replace PL/SQL code into PL/PGSQL similar code
		$fct_detail{declare} = Ora2Pg::PLSQL::plsql_to_plpgsql($self, $fct_detail{declare});
		if ($fct_detail{code}) {
			$fct_detail{code} = Ora2Pg::PLSQL::plsql_to_plpgsql($self, "BEGIN".$fct_detail{code});
		}
		# 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
		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}};
	} else {
		delete $fct_detail{func_ret_type};
		delete $fct_detail{declare};
		$fct_detail{code} = $plsql;
	}

	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}) {
		if (!$self->{preserve_case}) {
			$local_path = ', ' . lc($self->{postgis_schema});
		} else {
			$local_path = ', "' . $self->{postgis_schema} . '"';
		}
	}
	if ($self->{data_type}{BFILE} eq 'efile') {
			$local_path .= ', external_file';
	}
	
	my $search_path = '';
	if (!$self->{schema} && $self->{export_schema} && $owner) {
		if (!$self->{preserve_case}) {
			$search_path = "SET search_path = \L$owner\E$local_path;";
		} else {
			$search_path = "SET search_path = \"$owner\"$local_path;";
		}
	} elsif ($self->{export_schema} && !$owner) {
		if ($self->{pg_schema}) {
			if (!$self->{preserve_case}) {
				$search_path = "SET search_path = \L$self->{pg_schema}\E$local_path;";
			} else {
				$search_path = "SET search_path = \"$self->{pg_schema}\"$local_path;";
			}
		} elsif ($self->{schema}) {
			if (!$self->{preserve_case}) {
				$search_path = "SET search_path = \L$self->{schema}\E$local_path, pg_catalog;";
			} else {
				$search_path = "SET search_path = \"$self->{schema}\"$local_path, pg_catalog;";
			}
		}
	}

	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
#CONTEXT
#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->logit("-------------------------------------------------------------------------------\n", 0);
		$self->logit("Ora2Pg v$VERSION - Database Migration Report\n", 0);
		$self->logit("-------------------------------------------------------------------------------\n", 0);
		$self->logit("Version\t$report_info{'Version'}\n", 0);
		$self->logit("Schema\t$report_info{'Schema'}\n", 0);
		$self->logit("Size\t$report_info{'Size'}\n\n", 0);
		$self->logit("-------------------------------------------------------------------------------\n", 0);
		$self->logit("Object\tNumber\tInvalid$cost_header\tComments\tDetails\n", 0);
		$self->logit("-------------------------------------------------------------------------------\n", 0);
		foreach my $typ (sort keys %{ $report_info{'Objects'} } ) {
			$report_info{'Objects'}{$typ}{'detail'} =~ s/\n/\. /gs;
			if ($self->{estimate_cost}) {
				$self->logit("$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", 0);
			} else {
				$self->logit("$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", 0);
			}
		}
		$self->logit("-------------------------------------------------------------------------------\n", 0);
		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->logit("Total\t$report_info{'total_object_number'}\t$report_info{'total_object_invalid'}\t$report_info{'total_cost_value'}\t$comment\n", 0);
		} else {
			$self->logit("Total\t$report_info{'total_object_number'}\t$report_info{'total_object_invalid'}\n", 0);
		}
		$self->logit("-------------------------------------------------------------------------------\n", 0);
		if ($self->{estimate_cost}) {
			$self->logit("Migration level : $difficulty\n", 0);
			$self->logit("-------------------------------------------------------------------------------\n", 0);
			$self->logit($lbl_mig_type, 0);
			$self->logit("-------------------------------------------------------------------------------\n", 0);
			if (scalar keys %{ $report_info{'full_function_details'} }) {
				$self->logit("\nDetails of cost assessment per function\n", 0);
				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->logit("Function $fct total estimated cost: $report_info{'full_function_details'}{$fct}{count}\n", 0);
					$self->logit($report_info{'full_function_details'}{$fct}{info}, 0);
				}
				$self->logit("-------------------------------------------------------------------------------\n", 0);
			}
			if (scalar keys %{ $report_info{'full_trigger_details'} }) {
				$self->logit("\nDetails of cost assessment per trigger\n", 0);
				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->logit("Trigger $fct total estimated cost: $report_info{'full_trigger_details'}{$fct}{count}\n", 0);
					$self->logit($report_info{'full_trigger_details'}{$fct}{info}, 0);
				}
				$self->logit("-------------------------------------------------------------------------------\n", 0);
			}
			if (scalar keys %{ $report_info{'full_view_details'} }) {
				$self->logit("\nDetails of cost assessment per view\n", 0);
				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->logit("View $fct total estimated cost: $report_info{'full_view_details'}{$fct}{count}\n", 0);
					$self->logit($report_info{'full_view_details'}{$fct}{info}, 0);
				}
				$self->logit("-------------------------------------------------------------------------------\n", 0);
			}
		}
	} elsif ($self->{dump_as_csv}) {
		$self->logit("-------------------------------------------------------------------------------\n", 0);
		$self->logit("Ora2Pg v$VERSION - Database Migration Report\n", 0);
		$self->logit("-------------------------------------------------------------------------------\n", 0);
		$self->logit("Version\t$report_info{'Version'}\n", 0);
		$self->logit("Schema\t$report_info{'Schema'}\n", 0);
		$self->logit("Size\t$report_info{'Size'}\n\n", 0);
		$self->logit("-------------------------------------------------------------------------------\n\n", 0);
		$self->logit("Object;Number;Invalid;Estimated cost;Comments\n", 0);
		foreach my $typ (sort keys %{ $report_info{'Objects'} } ) {
			$report_info{'Objects'}{$typ}{'detail'} =~ s/\n/\. /gs;
			$self->logit("$typ;$report_info{'Objects'}{$typ}{'number'};$report_info{'Objects'}{$typ}{'invalid'};$report_info{'Objects'}{$typ}{'cost_value'};$report_info{'Objects'}{$typ}{'comment'}\n", 0);
		}
		my $human_cost = $self->_get_human_cost($report_info{'total_cost_value'});
		$difficulty = '' if (!$self->{estimate_cost});
		$self->logit("\n", 0);
		$self->logit("Total Number;Total Invalid;Total Estimated cost;Human days cost;Migration level\n", 0);
		$self->logit("$report_info{'total_object_number'};$report_info{'total_object_invalid'};$report_info{'total_cost_value'};$human_cost;$difficulty\n", 0);
	} 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->logit('"' . join('";"', @header) . '"' . "\n");
		}
		$self->logit('"' . 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->logit($html_header, 0);
		foreach my $typ (sort keys %{ $report_info{'Objects'} } ) {
			$report_info{'Objects'}{$typ}{'detail'} =~ s/\n/<br>/gs;
			if ($self->{estimate_cost}) {
				$self->logit("<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", 0);
			} else {
				$self->logit("<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", 0);
			}
		}
		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->logit("<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", 0);
		} else {
			$self->logit("<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", 0);
		}
		$self->logit("</table>\n</div>\n", 0);
		if ($self->{estimate_cost}) {
			$self->logit("<h2>Migration level: $difficulty</h2>\n", 0);
			$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->logit($lbl_mig_type, 0);
			if (scalar keys %{ $report_info{'full_function_details'} }) {
				$self->logit("<h2>Details of cost assessment per function</h2>\n", 0);
				$self->logit("<ul>\n", 0);
				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->logit("<li>Function $fct total estimated cost: $report_info{'full_function_details'}{$fct}{count}</li>\n", 0);
					$self->logit("<ul>\n", 0);
					$report_info{'full_function_details'}{$fct}{info} =~ s/\t/<li>/gs;
					$report_info{'full_function_details'}{$fct}{info} =~ s/\n/<\/li>\n/gs;
					$self->logit($report_info{'full_function_details'}{$fct}{info}, 0);
					$self->logit("</ul>\n", 0);
				}
				$self->logit("</ul>\n", 0);
			}
			if (scalar keys %{ $report_info{'full_trigger_details'} }) {
				$self->logit("<h2>Details of cost assessment per trigger</h2>\n", 0);
				$self->logit("<ul>\n", 0);
				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->logit("<li>Trigger $fct total estimated cost: $report_info{'full_trigger_details'}{$fct}{count}</li>\n", 0);
					$self->logit("<ul>\n", 0);
					$report_info{'full_trigger_details'}{$fct}{info} =~ s/\t/<li>/gs;
					$report_info{'full_trigger_details'}{$fct}{info} =~ s/\n/<\/li>\n/gs;
					$self->logit($report_info{'full_trigger_details'}{$fct}{info}, 0);
					$self->logit("</ul>\n", 0);
				}
				$self->logit("</ul>\n", 0);
			}
			if (scalar keys %{ $report_info{'full_view_details'} }) {
				$self->logit("<h2>Details of cost assessment per view</h2>\n", 0);
				$self->logit("<ul>\n", 0);
				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->logit("<li>View $fct total estimated cost: $report_info{'full_view_details'}{$fct}{count}</li>\n", 0);
					$self->logit("<ul>\n", 0);
					$report_info{'full_view_details'}{$fct}{info} =~ s/\t/<li>/gs;
					$report_info{'full_view_details'}{$fct}{info} =~ s/\n/<\/li>\n/gs;
					$self->logit($report_info{'full_view_details'}{$fct}{info}, 0);
					$self->logit("</ul>\n", 0);
				}
				$self->logit("</ul>\n", 0);
			}
		}
		my $html_footer = qq{
<div id="footer">
Generated by <a href="http://ora2pg.darold.net/">Ora2Pg v$VERSION</a>
</div>
</body>
</html>
};
		$self->logit($html_footer, 0);
	}

}

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);
	$fh->close();

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

	if ($self->{type} eq 'COPY') {
		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');
			$col = '\\\\x' . $col;
		} elsif (($generic_type eq 'CLOB') || $cond->{istext}) {
			$col = $self->escape_copy($col);
		}
	} 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);
		}
	}

	return $col;
}

sub escape_copy
{
	my ($self, $col) = @_;
	if ($self->{has_utf8_fct}) {
		utf8::encode($col) if (!utf8::valid($col));
	}
	my $replacements = {
		"\0" => "",
		"\\" => "\\\\",
		"\r" => "\\r",
		"\n" => "\\n",
		"\t" => "\\t",
	};
#	$col =~ s/\0//gs;
#	$col =~ s/\\/\\\\/gs;
#	$col =~ s/\r/\\r/gs;
#	$col =~ s/\n/\\n/gs;
#	$col =~ s/\t/\\t/gs;
	$col =~ s/(\0|\\|\r|\n|\t)/$replacements->{$1}/egs;
	if (!$self->{noescape}) {
		$col =~ s/\f/\\f/gs;
		$col =~ s/([\1-\10\13-\14\16-\37])/sprintf("\\%03o", ord($1))/egs;
#		$col =~ s/([\1-\10])/sprintf("\\%03o", ord($1))/egs;
#		$col =~ s/([\13-\14])/sprintf("\\%03o", ord($1))/egs;
#		$col =~ s/([\16-\37])/sprintf("\\%03o", ord($1))/egs;
	}
	return $col;
}

sub escape_insert
{
	my ($self, $col) = @_;

	if (!$self->{standard_conforming_strings}) {
		$col =~ s/'/''/gs; # double single quote
		$col =~ s/\\/\\\\/gs;
		$col =~ s/\0//gs;
		$col = "'$col'";
	} else {
		$col =~ s/\0//gs;
		$col =~ s/\\/\\\\/gs;
		$col =~ s/'/\\'/gs; # escape single quote
		$col =~ s/\r/\\r/gs;
		$col =~ s/\n/\\n/gs;
		$col = "E'$col'";
	}
	return $col;
}

1;

__END__


=head1 AUTHOR

Gilles Darold <gilles _AT_ darold _DOT_ net>


=head1 COPYRIGHT

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

