New Upstream Release - ora2pg

Ready changes

Summary

Merged new upstream version: 24.0 (was: 23.2).

Resulting package

Built on 2023-07-31T18:58 (took 5m59s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases ora2pg

Lintian Result

Diff

diff --git a/Makefile.PL b/Makefile.PL
index f8e9332..e28d489 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -229,6 +229,7 @@ FORCE_PLSQL_ENCODING	0
 #       TEST_COUNT	perform only a row count between Oracle and PostgreSQL tables.
 #       TEST_VIEW	perform a count on both side of number of rows returned by views
 #       TEST_DATA	perform data validation check on rows at both sides.
+#	SEQUENCE_VALUES export DDL to set the last values of sequences
 
 TYPE		TABLE
 
@@ -630,16 +631,12 @@ INDEXES_RENAMING	0
 # columns of type varchar2(N) where N >= 128.
 USE_INDEX_OPCLASS	0
 
-# Enable this directive if you want that your partition table name will be
-# exported using the parent table name. Disabled by default. If you have
-# multiple partitioned table, when exported to PostgreSQL some partitions
-# could have the same name but different parent tables. This is not allowed,
-# table name must be unique. 
-PREFIX_PARTITION	0
-
-# Disable this directive if your subpartitions are dedicated to your partition 
-# (in case of your partition_name is a part of your subpartition_name)
-PREFIX_SUB_PARTITION	1
+# Enable this directive if you want that your partition tables will be
+# renamed. Disabled by default. If you have multiple partitioned table,
+# when exported to PostgreSQL some partitions could have the same name
+# but different parent tables. This is not allowed, table name must be
+# unique, in this case enable this directive. 
+RENAME_PARTITION	0
 
 # If you don't want to reproduce the partitioning like in Oracle and want to
 # export all partitionned Oracle data into the main single table in PostgreSQL
@@ -767,6 +764,9 @@ CREATE_OR_REPLACE	1
 # directive can be used multiple time.
 #PG_INITIAL_COMMAND
 
+# Add an ON CONFLICT DO NOTHING to all INSERT statements generated for this
+# type of data export.
+INSERT_ON_CONFLICT	0
 
 
 #------------------------------------------------------------------------------
@@ -783,7 +783,7 @@ CREATE_OR_REPLACE	1
 # 	DATA_TYPE	NUMBER(*\\,0):bigint
 # Here is the default replacement for all Oracle's types. You don't have to
 # recopy all type conversion but just the one you want to rewrite.
-#DATA_TYPE	VARCHAR2:varchar,NVARCHAR2:varchar,DATE:timestamp,LONG:text,LONG RAW:bytea,CLOB:text,NCLOB:text,BLOB:bytea,BFILE:bytea,RAW(16):uuid,RAW(32):uuid,RAW:bytea,UROWID:oid,ROWID:oid,FLOAT:double precision,DEC:decimal,DECIMAL:decimal,DOUBLE PRECISION:double precision,INT:integer,INTEGER:integer,REAL:real,SMALLINT:smallint,BINARY_FLOAT:double precision,BINARY_DOUBLE:double precision,TIMESTAMP:timestamp,XMLTYPE:xml,BINARY_INTEGER:integer,PLS_INTEGER:integer,TIMESTAMP WITH TIME ZONE:timestamp with time zone,TIMESTAMP WITH LOCAL TIME ZONE:timestamp with time zone
+#DATA_TYPE	VARCHAR2:varchar,NVARCHAR2:varchar,NVARCHAR:varchar,NCHAR:char,DATE:timestamp(0),LONG:text,LONG RAW:bytea,CLOB:text,NCLOB:text,BLOB:bytea,BFILE:bytea,RAW(16):uuid,RAW(32):uuid,RAW:bytea,UROWID:oid,ROWID:oid,FLOAT:double precision,DEC:decimal,DECIMAL:decimal,DOUBLE PRECISION:double precision,INT:integer,INTEGER:integer,REAL:real,SMALLINT:smallint,BINARY_FLOAT:double precision,BINARY_DOUBLE:double precision,TIMESTAMP:timestamp,XMLTYPE:xml,BINARY_INTEGER:integer,PLS_INTEGER:integer,TIMESTAMP WITH TIME ZONE:timestamp with time zone,TIMESTAMP WITH LOCAL TIME ZONE:timestamp with time zone
 
 # If set to 1 replace portable numeric type into PostgreSQL internal type.
 # Oracle data type NUMBER(p,s) is approximatively converted to real and
@@ -930,6 +930,10 @@ DATA_LIMIT	$DATA_LIMIT_DEFAULT
 # ressources, setting it to a too high value can produce OOM.
 #BLOB_LIMIT	500
 
+# Apply same behavior on CLOB than BLOB with BLOB_LIMIT settings. This is
+# especially useful if you have large CLOB data.
+CLOB_AS_BLOB	1
+
 # By default all data that are not of type date or time are escaped. If you
 # experience any problem with that you can set it to 1 to disable it. This
 # directive is only used during a COPY export type.
@@ -1108,7 +1112,8 @@ JOBS		1
 # Multiprocess support. This directive should defined the number of parallel
 # connection to Oracle when extracting data. The limit is the number of cores
 # on your machine. This is useful if Oracle is the bottleneck. Take care that
-# this directive can only be used if there is a column defined in DEFINED_PK.
+# this directive can only be used if there is a primary / unique key defined
+# on a numeric column or that a column is defined in DEFINED_PK.
 ORACLE_COPIES	1
 
 # Multiprocess support. This directive should defined the number of tables
@@ -1162,7 +1167,7 @@ SYNCHRONOUS_COMMIT	0
 # If the above configuration directive is not enough to validate your PL/SQL code
 # enable this configuration directive to allow export of all PL/SQL code even if
 # it is marked as invalid. The 'VALID' or 'INVALID' status applies to functions,
-# procedures, packages and user defined types.
+# procedures, packages and user defined types. It also concern disabled triggers.
 EXPORT_INVALID	0
 
 # Enable PLSQL to PLPSQL conversion. This is a work in progress, feel
@@ -1356,6 +1361,20 @@ DEFAULT_SRID		4326
 # before use.
 GEOMETRY_EXTRACT_TYPE	INTERNAL
 
+# Oracle function to use to extract the srid from ST_Geometry meta information
+ST_SRID_FUNCTION	ST_SRID
+
+# Oracle function to use to extract the dimension from ST_Geometry meta information
+ST_DIMENSION_FUNCTION	ST_DIMENSION
+
+# Oracle function to used to convert an ST_Geometry value into WKB format
+ST_ASBINARY_FUNCTION	ST_ASBINARY
+
+# Oracle function to used to convert an ST_Geometry value into WKT format
+ST_ASTEXT_FUNCTION	ST_ASTEXT
+
+# Oracle function to use to extract the geometry type from a ST_Geometry column
+ST_GEOMETRYTYPE_FUNCTION	ST_GEOMETRYTYPE
 
 #------------------------------------------------------------------------------
 # FDW SECTION (Control Foreign Data Wrapper export)
@@ -1394,6 +1413,21 @@ MYSQL_PIPES_AS_CONCAT		0
 # with format; DDHH24MMSS::bigint, this depend of your apps usage.
 MYSQL_INTERNAL_EXTRACT_FORMAT	0
 
+#------------------------------------------------------------------------------
+# SQL Server SECTION (Control MSSQL export behavior)
+#------------------------------------------------------------------------------
+
+# PostgreSQL has no equivalent to rowversion datatype and feature, if you want
+# to remove these useless columns, enable this directive. Columns of datatype
+# 'rowversion' or 'timestamp' will not be exported.
+DROP_ROWVERSION		0
+
+# Emulate the same behavior of MSSQL with case insensitive search. If the value
+# is citext it will use the citext data type instead of char/varchar/text in
+# tables DDL (Ora2Pg will add a CHECK constraint for columns with a precision).
+# Instead of citext you can also set a collation name that will be used in the
+# columns definitions. To disable case insensitive search set it to: none.
+CASE_INSENSITIVE_SEARCH	citext
 };
 close(OUTCFG);
 
@@ -1414,7 +1448,7 @@ if ($^O !~ /MSWin32|dos/i) {
 WriteMakefile(
     'NAME'         => 'Ora2Pg',
     'VERSION_FROM' => 'lib/Ora2Pg.pm',
-    'LICENSE'      => 'GPLv3',
+    'LICENSE'      => 'gpl_3',
     'dist'         => {
 			'COMPRESS'=>'gzip -9f', 'SUFFIX' => 'gz',
 			'ZIP'=>'/usr/bin/zip','ZIPFLAGS'=>'-rl'
diff --git a/README b/README
index ebe1ca5..85f8c92 100644
--- a/README
+++ b/README
@@ -56,9 +56,9 @@ FEATURES
             - Export DBLINK as Oracle FDW.
             - Export SYNONYMS as views.
             - Export DIRECTORY as external table or directory for external_file extension.
-            - Full MySQL export just like Oracle database.
             - Dispatch a list of SQL orders over multiple PostgreSQL connections
             - Perform a diff between Oracle and PostgreSQL database for test purpose.
+            - MySQL/MariaDB and Microsoft SQL Server migration.
 
     Ora2Pg does its best to automatically convert your Oracle database to
     PostgreSQL but there's still manual works to do. The Oracle specific
@@ -101,19 +101,19 @@ INSTALLATION
     You also need a modern Perl distribution (perl 5.10 and more). To
     connect to a database and proceed to his migration you need the DBI Perl
     module > 1.614. To migrate an Oracle database you need the DBD::Oracle
-    Perl modules to be installed. To migrate a MySQL database you need the
-    DBD::MySQL Perl modules. These modules are used to connect to the
-    database but they are not mandatory if you want to migrate DDL input
-    files.
+    Perl modules to be installed.
 
     To install DBD::Oracle and have it working you need to have the Oracle
     client libraries installed and the ORACLE_HOME environment variable must
     be defined.
 
     If you plan to export a MySQL database you need to install the Perl
-    module DBD::mysql which requires that the mysql client libraries are
+    module DBD::MySQL which requires that the mysql client libraries are
     installed.
 
+    If you plan to export a SQL Server database you need to install the Perl
+    module DBD::ODBC which requires that the unixODBC package is installed.
+
     On some Perl distribution you may need to install the Time::HiRes Perl
     module.
 
@@ -122,6 +122,7 @@ INSTALLATION
 
             perl -MCPAN -e 'install DBD::Oracle'
             perl -MCPAN -e 'install DBD::MySQL'
+            perl -MCPAN -e 'install DBD::ODBC'
             perl -MCPAN -e 'install Time::HiRes'
 
     otherwise use the packages provided by your distribution.
@@ -145,6 +146,29 @@ INSTALLATION
 
     otherwise use the packages provided by your distribution.
 
+  Instruction for SQL Server
+    For SQL Server you need to install the unixodbc package and the Perl
+    DBD::ODBC driver:
+
+            sudo apt install unixodbc
+            sudo apt install libdbd-odbc-perl
+
+    then install the Microsoft ODBC Driver for SQL Server. Follow the
+    instructions relative to your operating system from here:
+
+            https://docs.microsoft.com/fr-fr/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server?view=sql-server-ver16
+
+    Once it is done set the following in the /etc/odbcinst.ini file by
+    adjusting the SQL Server ODBC driver version:
+
+            [msodbcsql18]
+            Description=Microsoft ODBC Driver 18 for SQL Server
+            Driver=/opt/microsoft/msodbcsql18/lib64/libmsodbcsql-18.0.so.1.1
+            UsageCount=1
+
+    See ORACLE_DSN to know how to use the driver to connect to your MSSQL
+    database.
+
   Installing Ora2Pg
     Like any other Perl Module Ora2Pg can be installed with the following
     commands:
@@ -302,6 +326,7 @@ CONFIGURATION
         -L | --limit num  : Number of tuples extracted from Oracle and stored in
                             memory before writing, default: 10000.
         -m | --mysql      : Export a MySQL database instead of an Oracle schema.
+        -M | --mssql      : Export a Microsoft SQL Server database.
         -n | --namespace schema : Set the Oracle schema to extract from.
         -N | --pg_schema schema : Set PostgreSQL's search_path.
         -o | --out file   : Set the path to the output file where SQL will
@@ -493,6 +518,10 @@ CONFIGURATION
 
         the 'sid' part is replaced by 'database'.
 
+        For MS SQL Server it will look like this:
+
+                dbi:ODBC:driver=msodbcsql18;server=mydb.database.windows.net;database=testdb
+
     ORACLE_USER et ORACLE_PWD
         These two directives are used to define the user and password for
         the Oracle database connection. Note that if you can it is better to
@@ -507,6 +536,11 @@ CONFIGURATION
         To connect to a local ORACLE instance with connections "as sysdba"
         you have to set ORACLE_USER to "/" and an empty password.
 
+        To make a connection using an Oracle Secure External Password Store
+        (SEPS), first configure the Oracle Wallet and then set both the
+        ORACLE_USER and ORACLE_PWD directives to the special value of
+        "__SEPS__" (without the quotes but with the double underscore).
+
     USER_GRANTS
         Set this directive to 1 if you connect the Oracle database as simple
         user and do not have enough grants to extract things from the
@@ -550,7 +584,7 @@ CONFIGURATION
     If your Oracle Client config file already includes the encryption
     method, then DBD:Oracle uses those settings to encrypt the connection
     while you extract the data. For example if you have configured the
-    Oracle Client config file (sqlnet.or or .sqlnet) with the following
+    Oracle Client config file (sqlnet.ora or .sqlnet) with the following
     information:
 
             # Configure encryption of connections to Oracle
@@ -638,7 +672,7 @@ CONFIGURATION
         This will ask to Oracle to validate the PL/SQL that could have been
         invalidate after a export/import for example. The 'VALID' or
         'INVALID' status applies to functions, procedures, packages and user
-        defined types.
+        defined types. It also concern disabled triggers.
 
     EXPORT_INVALID
         If the above configuration directive is not enough to validate your
@@ -773,6 +807,7 @@ CONFIGURATION
                 - TEST_COUNT: perform a row count diff between Oracle and PostgreSQL table.
                 - TEST_VIEW: perform a count on both side of number of rows returned by views.
                 - TEST_DATA: perform data validation check on rows at both sides.
+                - SEQUENCE_VALUES: export DDL to set the last values of sequences
 
         Only one type of export can be perform at the same time so the TYPE
         directive must be unique. If you have more than one only the last
@@ -920,7 +955,8 @@ CONFIGURATION
 
         where COLUMN is a technical key like a primary or unique key where
         split will be based and the current core used by the query
-        (CUR_PROC).
+        (CUR_PROC). You can also force the column name to use using the
+        DEFINED_PK configuration directive.
 
         Doesn't work under Windows Operating System, it is simply disabled.
 
@@ -1491,18 +1527,17 @@ CONFIGURATION
         example, set it to 128 to create these kind of indexes on columns of
         type varchar2(N) where N >= 128.
 
-    PREFIX_PARTITION
-        Enable this directive if you want that your partition table name
-        will be exported using the parent table name. Disabled by default.
-        If you have multiple partitioned table, when exported to PostgreSQL
-        some partitions could have the same name but different parent
-        tables. This is not allowed, table name must be unique.
-
-    PREFIX_SUB_PARTITION
-        Enable this directive if you want that your subpartition table name
-        will be exported using the parent partition name. Enabled by
-        default. If the partition names are a part of the subpartition
-        names, you should enable this directive.
+    RENAME_PARTITION
+        Enable this directive if you want that your partition tables will be
+        renamed. Disabled by default. If you have multiple partitioned
+        table, when exported to PostgreSQL some partitions could have the
+        same name but different parent tables. This is not allowed, table
+        name must be unique, in this case enable this directive. A partition
+        will be renamed following the rule: "tablename"_part"pos" where
+        "pos" is the partition number. For subpartition this is:
+        "tablename"_part"pos"_subpart"pos" If this is partition/subpartition
+        default: "tablename"_part_default
+        "tablename"_part"pos"_subpart_default
 
     DISABLE_PARTITION
         If you don't want to reproduce the partitioning like in Oracle and
@@ -1598,6 +1633,31 @@ CONFIGURATION
         Use this directive to add a specific schema to the search path to
         look for PostGis functions.
 
+    ST_SRID_FUNCTION
+        Oracle function to use to extract the srid from ST_Geometry meta
+        information. Default: ST_SRID, for example it should be set to
+        sde.st_srid for ArcSDE.
+
+    ST_DIMENSION_FUNCTION
+        Oracle function to use to extract the dimension from ST_Geometry
+        meta information. Default: ST_DIMENSION, for example it should be
+        set to sde.st_dimention for ArcSDE.
+
+    ST_GEOMETRYTYPE_FUNCTION
+        Oracle function to use to extract the geometry type from a
+        ST_Geometry column Default: ST_GEOMETRYTYPE, for example it should
+        be set to sde.st_geometrytype for ArcSDE.
+
+    ST_ASBINARY_FUNCTION
+        Oracle function to used to convert an ST_Geometry value into WKB
+        format. Default: ST_ASBINARY, for example it should be set to
+        sde.st_asbinary for ArcSDE.
+
+    ST_ASTEXT_FUNCTION
+        Oracle function to used to convert an ST_Geometry value into WKT
+        format. Default: ST_ASTEXT, for example it should be set to
+        sde.st_astext for ArcSDE.
+
   PostgreSQL Import
     By default conversion to PostgreSQL format is written to file
     'output.sql'. The command:
@@ -1623,6 +1683,10 @@ CONFIGURATION
         BLOB_LIMIT. Exporting BLOB use lot of resources, setting it to a too
         high value can produce OOM.
 
+    CLOB_AS_BLOB
+        Apply same behavior on CLOB than BLOB with BLOB_LIMIT settings. This
+        is especially useful if you have large CLOB data. Default: enabled
+
     OUTPUT
         The Ora2Pg output filename can be changed with this directive.
         Default value is output.sql. if you set the file name with extension
@@ -1855,6 +1919,11 @@ CONFIGURATION
         just after the connection. For example to set some session
         parameters. This directive can be used multiple times.
 
+    INSERT_ON_CONFLICT
+        When enabled this instruct Ora2Pg to add an ON CONFLICT DO NOTHING
+        clause to all INSERT statements generated for this type of data
+        export.
+
   Column type control
     PG_NUMERIC_TYPE
         If set to 1 replace portable numeric type into PostgreSQL internal
@@ -1885,7 +1954,7 @@ CONFIGURATION
         "Oracle datatype:Postgresql datatype". Here are the default list
         used:
 
-                DATA_TYPE       VARCHAR2:varchar,NVARCHAR2:varchar,DATE:timestamp,LONG:text,LONG RAW:bytea,CLOB:text,NCLOB:text,BLOB:bytea,BFILE:bytea,RAW(16):uuid,RAW(32):uuid,RAW:bytea,UROWID:oid,ROWID:oid,FLOAT:double precision,DEC:decimal,DECIMAL:decimal,DOUBLE PRECISION:double precision,INT:integer,INTEGER:integer,REAL:real,SMALLINT:smallint,BINARY_FLOAT:double precision,BINARY_DOUBLE:double precision,TIMESTAMP:timestamp,XMLTYPE:xml,BINARY_INTEGER:integer,PLS_INTEGER:integer,TIMESTAMP WITH TIME ZONE:timestamp with time zone,TIMESTAMP WITH LOCAL TIME ZONE:timestamp with time zone
+                DATA_TYPE       VARCHAR2:varchar,NVARCHAR2:varchar,NVARCHAR:varchar,NCHAR:char,DATE:timestamp(0),LONG:text,LONG RAW:bytea,CLOB:text,NCLOB:text,BLOB:bytea,BFILE:bytea,RAW(16):uuid,RAW(32):uuid,RAW:bytea,UROWID:oid,ROWID:oid,FLOAT:double precision,DEC:decimal,DECIMAL:decimal,DOUBLE PRECISION:double precision,INT:integer,INTEGER:integer,REAL:real,SMALLINT:smallint,BINARY_FLOAT:double precision,BINARY_DOUBLE:double precision,TIMESTAMP:timestamp,XMLTYPE:xml,BINARY_INTEGER:integer,PLS_INTEGER:integer,TIMESTAMP WITH TIME ZONE:timestamp with time zone,TIMESTAMP WITH LOCAL TIME ZONE:timestamp with time zone
 
         The directive and the list definition must be a single line.
 
@@ -2450,6 +2519,21 @@ CONFIGURATION
         will be replaced with format; DDHH24MMSS::bigint, this depend of
         your apps usage.
 
+  Control SQL Server export behavior
+    DROP_ROWVERSION
+        PostgreSQL has no equivalent to rowversion datatype and feature, if
+        you want to remove these useless columns, enable this directive.
+        Columns of datatype 'rowversion' or 'timestamp' will not be
+        exported.
+
+    CASE_INSENSITIVE_SEARCH
+        Emulate the same behavior of MSSQL with case insensitive search. If
+        the value is citext it will use the citext data type instead of
+        char/varchar/text in tables DDL (Ora2Pg will add a CHECK constraint
+        for columns with a precision). Instead of citext you can also set a
+        collation name that will be used in the columns definitions. To
+        disable case insensitive search set it to: none.
+
   Special options to handle character encoding
     NLS_LANG and NLS_NCHAR
         By default Ora2Pg will set NLS_LANG to AMERICAN_AMERICA.AL32UTF8 and
@@ -2744,9 +2828,10 @@ CONFIGURATION
 
             ora2pg -c ora2pg.conf -t KETTLE -J 4 -j 12 -a EMPLOYEES -o load_mydata.sh
 
-    This is only possible if you have defined the technical key to used to
-    split the query between cores in the DEFINED_PKEY configuration
-    directive. For example:
+    This is only possible if there is a unique key defined on a numeric
+    column or that you have defined the technical key to used to split the
+    query between cores in the DEFINED_PKEY configuration directive. For
+    example:
 
             DEFINED_PK      EMPLOYEES:employee_id
 
@@ -2960,6 +3045,7 @@ CONFIGURATION
             "type","schema/database","dsn","user","password"
             "MYSQL","sakila","dbi:mysql:host=192.168.1.10;database=sakila;port=3306","root","secret"
             "ORACLE","HR","dbi:Oracle:host=192.168.1.10;sid=XE;port=1521","system","manager"
+            "MSSQL","HR","dbi:ODBC:driver=msodbcsql18;server=srv.database.windows.net;database=testdb","system","manager"
 
        The CSV field separator must be a comma.
 
@@ -2970,6 +3056,7 @@ CONFIGURATION
        For example:
 
             "ORACLE","","dbi:Oracle:host=192.168.1.10;sid=XE;port=1521","system","manager"
+            "MSSQL","","dbi:ODBC:driver=msodbcsql18;server=srv.database.windows.net;database=testdb","usrname","passwd"
 
        will generate a report for all schema in the XE instance. Note that in this
        case the SCHEMA directive in ora2pg.conf must not be set.
@@ -3364,7 +3451,7 @@ SUPPORT
     applied.
 
 LICENSE
-    Copyright (c) 2000-2022 Gilles Darold - All rights reserved.
+    Copyright (c) 2000-2023 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
diff --git a/changelog b/changelog
index b281048..a0569f1 100644
--- a/changelog
+++ b/changelog
@@ -1,3 +1,222 @@
+2023 07 05 - v24.0
+
+This major release adds support to migration of SQL Server database to
+PostgreSQL. It also fixes several issues reported since past height months
+and adds some new features and improvements.
+
+  * Enable the use of ALLOW/EXCLUDE directive with SHOW_* reports and throw
+    a fatal error if global filters in ALLOW/EXCLUDE are set.
+  * Add replacement of DBMS_LOCK.SLEEP with pg_sleep
+  * Split estimate cost details per function/procedure/and package function.
+  * Add cmin, cmax, ctid to reserved keywords list.
+  * Add cost for presence of ADD CONSTRAINT in PLSQL code. It needs constraint
+    name stability.
+  * Add CLOB_AS_BLOB configuration directive to treat CLOB as BLOB when
+    exporting data. When enabled Ora2Pg will apply same behavior on CLOB
+    than BLOB with BLOB_LIMIT setting. This could be useful if you have
+    large CLOB data. Enabled by default. Thanks to Omar Mebarki for the patch.
+  * Allow COPY and TABLE type to use the NULLIF construct. Thanks to Luke Davies
+    for the patch.
+  * Add new SEQUENCE_VALUES export type to export DDL to set the last values
+    of sequences from current Oracle database last values like the following
+    statements: ALTER SEQUENCE departments_seq START WITH 290;
+    Thanks to sergey grinko for the feature request.
+  * Add replacement of Oracle variable : varname into PG :'varname'.
+  * Add SQL Server migration to Ora2Pg. Most of the SQL Server objects are
+    supported as well as data export. Translation of the TSQL stored
+    procedures to plpgsql is complicated because of the lack of statement
+    separator in TSQL but as usual Ora2Pg is doing is best to do as much
+    work as possible. Migration assessment is also possible with SQL Server
+    database. There is some dedicated configuration directives added to
+    ora2Pg.conf.
+  * Add support to MySQL PARTITION BY KEY() with a translation to HASH
+    partitioned table using the PK/UK definition of the table or the
+    columns specified in the KEY() clause. Thanks to Shubham Dabriwala
+    for the report.
+  * Make EXPORT_INVALID configuration directive works with TRIGGER export.
+    Until now disabled triggers were not exported, setting EXPORT_INVALID
+    to 1 will force the export of disabled triggers. Thanks to chetank-yb
+    for the report.
+  * Add support of MySQL generated default value on update. For example:
+      CREATE TABLE t1 (
+        dt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+      );
+    Ora2Pg will translate this syntax into a trigger on the table to force
+    the value of the column on an update event.
+    Thanks to heysky for the report.
+  * Add translation of ST_GEOMETRY data type to PostGis geometry datatype.
+  * Replace ROWNUM in target list with a "row_number() over ()" clause. Thanks
+    to Rui Pereira for the report.
+
+New configuration directives:
+
+  * Add configuration directive ST_GEOMETRYTYPE_FUNCTION to be able to set the
+    function to use to extract the geometry type from a ST_Geometry column.
+    Default: ST_GeometryType, example it should be set to sde.ST_GeometryType
+    for ArcSDE. Thanks to Albert88373 for the report.
+  * Add four new configuration directive to be able to change or prefix the
+    functions used to extract information from ST_Geometry object and values.
+    - ST_SRID_FUNCTION: Oracle function to use to extract the srid from
+      ST_Geometry meta information. Default: ST_SRID, for example it should be
+      set to sde.st_srid for ArcSDE.
+    - ST_DIMENSION_FUNCTION: Oracle function to use to extract the dimension
+      from ST_Geometry meta information. Default: ST_DIMENSION, for example it
+      should be set to sde.st_dimention for ArcSDE.
+    - ST_ASBINARY_FUNCTION: Oracle function to used to convert an ST_Geometry
+      value into WKB format. Default: ST_ASBINARY, for example it should be set
+      to sde.st_asbinary for ArcSDE.
+    - ST_ASTEXT_FUNCTION: Oracle function to used to convert an ST_Geometry
+      value into WKT format. Default: ST_ASTEXT, for example it should be set
+      to sde.st_astext for ArcSDE.
+    Thanks to Albert88373 for the report.
+  * Add INSERT_ON_CONFLICT configuration directive. When enabled this instruct
+    Ora2Pg to add an ON CONFLICT DO NOTHING clause to all INSERT statements
+    generated for this type of data export. Thanks to Clemens Rieder for the
+    feature request.
+
+Backward compatibility:
+
+  * Change the behavior of CASE_INSENSITIVE_SEARCH to allow the use of a
+    collation instead of the citext extension. To disable the feature the
+    value none can be used. If the migration is not MSSQL this feature is
+    disabled.
+  * Remove PREFIX_PARTITION configuration directive, it is now replaced by
+    the RENAME_PARTITION directive. Previous behavior was to construct the
+    partition name from the table name, the partition name and the sub
+    partition name if any. The problem is that we often reach the max length
+    for an object name and this leads to duplicate partition name. Now, when
+    RENAME_PARTITION is enabled the partition tables will be renamed
+    following rules:
+        <tablename>_part<pos>
+    where "pos" is the partition number. For subpartition this is:
+       <tablename>_part<pos>_subpart<pos>
+    If this is partition/subpartition default:
+        <tablename>_part_default
+        <tablename>_part<pos>_subpart_default
+    This change will break backward comaptibilty, if PREFIX_PARTITION is
+    still set, it will simply enable RENAME_PARTITION.
+  * Set START value to MINVALUE when a sequence is cycled and that the START
+    value is upper that MAXVALUE. Thanks to Shane Borden for the report.
+
+Here is the full list of changes and acknowledgements:
+
+  - Fix MODIFY_STRUCT that was not working with MySQL. Thanks to Code-UV for
+    the report.
+  - Fix license string in Makefile.PL. Thanks to RodRaen for the report.
+  - Do not remove non alphanumeric character in index name. Thanks to gwidt
+    for the report.
+  - Reorder trigger event when the update of column is not the last one. Thanks
+    to tayalarun1 for the report.
+  - Fix export of MySQL function containing special characters and white spaces
+    in names. Thanks to Shubham Dabriwala for the report.
+  - Fix grant export for partitions. Thanks to elexus for the report.
+  - Add some other transformation for sqlplus/psql scripts.
+  - Remove comma as possible separator for values in DEFINED_PK, it was
+    preventing the use of a function with multiple parameters.
+  - Fix export of geometry tables when PG_SCHEMA is set.
+  - Add rewriting of some sqlplus settings to psql settings.
+  - Fix TABLESPACE export for partitioned tables. Thanks to elexus for the
+    report.
+  - Fix for Issue #1637. Thanks to Simon Pane for the patch.
+  - Fix typo in --init_project directories tree generation for sequences
+    values.
+  - Fix alias in view target list for function call without alias defined in
+    MySQL export. Thanks to Shubham Dabriwala for the report.
+  - Fix Mysql procedure export when a datatype with precision is used in
+    parameter list. Thanks to Shubham Dabriwala for the report.
+  - Fix collation on string default values. Thanks to Shubham Dabriwalafor
+    the report.
+  - Exclude recycle bin object from ALL_TAB_COLUMNS lookup. Thanks to Dave
+    Betterton for the report.
+  - Fix data types translation (TINYINT|SMALLINT|MEDIUMINT|INTEGER|BIGINT|INT)
+    for MySQL table export. Thanks to Shubham Dabriwala for the report.
+  - Do not export synonym destination table with table_owner when EXPORT_SCHEMA
+    is disabled. Thanks to Priyanshi Gupta for the report.
+  - Fully qualify calls to get_sequence_last_values() when PG_SCHEMA is set.
+    Thanks to Marius Hope for the report.
+  - Fix regression on exporting view as table when VIEW_AS_TABLE contains
+    regexp. Thanks to Neil Bower for the report.
+  - Fix missing execution of initial command statements at start of TEST_DATA
+    action and on both side, those applying to source and destination. Thanks
+    to Petter Jacobsen for the report.
+  - Fix script to get sequence last value with TEST action. Thanks to franxav06
+    for the patch.
+  - Prepend PERFORM before call to DBMS_OUTPUT.* when USE_ORAFCE is enabled.
+  - Disable USE_ORAFCE when export type is SHOW_REPORT.
+  - Extending the enhancement in Pull Request #1621 to the Oracle_FDW user
+    mapping. Thanks to Simon Pane for the patch.
+  - Changed prefix string to "DIFF:" in test report. Thanks to Simon Pane for
+    the patch.
+  - Fix cases where %ROWCOUNT was not correctly replaced. Thanks to Rui Pereira
+    for the report.
+  - Fix parsing of ORACLE_DSN when creating foreign server in COPY mode. Thanks
+    to Luke Davies for the report.
+  - Fix for Issue #1622, #1627. Thanks to Simon Pane for the patch.
+  - Fix index creation with DESC order in COPY action when DROP_INDEXES is
+    enabled. Thanks to Luke Davies for the report.
+  - Fix for Issue #1610, #1612, #1617 and #1381. Thanks to Simon Pane for the
+    patch.
+  - Fix typo in sqlnet.ora name (was sqlnet.or). Thanks to Martin Nash for the
+    patch.
+  - Fix data export, REPLACE_QUERY was not applied. Thanks to Bachev Constantin
+    for the report.
+  - Fix call to replace_sys_context().
+  - Fix timestamp(n) data type translation.
+  - Remove use of column GENERATION_EXPRESSION for MySQL version < 5.7.0. Thanks
+    to Hans Choi for the report.
+  - Fix conversion of DATE datatype to timestamp(0) instead of timestamp. Thanks
+    to Akhil Reddy for the report.
+  - Add NVARCHAR/NCHAR defaut convertion data types to DATA_TYPE configuration
+    directive in ora2pg.conf comments. Thanks to Akhil Reddy for the report.
+  - Rename method _get_partitions_type function into _get_partitions_list.
+  - Fix synonym export when no schema information is available.
+  - Fix support of REFERENCING clause in triggers.
+  - Fix partition output file renaming with new RENAME_PARTITION directive.
+    Thanks to Rahul Barigidad for the report.
+  - Fix export of the ROWNUM clause when there is a variable.
+  - Fix sprintf placeholders in geometry queries.
+  - Fix some others issues with row count report.
+  - Fix row count with destination schema and when the PostgreSQL table
+    doesn't exist.
+    Thanks to bizen-ya for the report.
+  - Fix tests comparison with the different settings of EXPORT_SCHEMA,
+    SCHEMA and PG_SCHEMA. Thanks to Marius Hope and bizen-ya for the
+    report.
+  - Fix St_AsText() call for MySQL data extraction.
+  - Add column count comparison for MySQL
+  - Export multi column partition by list as an expression with concat
+    operator. Multi column partition by list is not supported by PostgreSQL.
+  - Fix creation of non existant indexes on partition. Thanks to Shubham
+    Dabriwala for the report.
+  - Fix MySQL function export when there is no BEGIN clause. Thanks to
+    Shubham Dabriwala for the report.
+  - Fix MySQL export of unsigned numeric. Thanks to Shubham Dabriwala for
+    the report.
+  - Fix MySQL output with wrong synthax for JOIN without ON clause. Thanks
+    to Shubham Dabriwala for the report.
+  - Fix virtual column export. Thanks to Rafal Hollins for the report.
+  - Fix index creation on partition with no columns for MySQL export.
+    Thanks to Shubham Dabriwala for the report.
+  - Fix export of MySQL auto_increment when PG_INTEGER_TYPE is disabled.
+    Thanks to Shubham Dabriwala for the report.
+  - Fix MySQL subpartition export. Thanks to Sanyam Singhal for the report.
+  - Move any INTO clause in CONNECT BY query to the final SELECT on the
+    resulting CTE. Thanks to taptarap for the report.
+  - Fix translation of MySQL curtime() function in default values. Thanks
+    to Shubham Dabriwala for the report.
+  - Fix possible "Nested quantifiers in regex" error when exporting package
+    with package name containing regex special characters. Thanks to durandm70
+    for the report.
+  - Fix documentation about use of unique key for ORACLE_COPY.
+  - Fix extra comma at end of a CHECK contraint. Thanks to Shubham Dabriwala
+    for the report.
+  - Always add DROP TYPE statements with package export even if DROP_IF_EXISTS
+    is not enabled. Thanks to Rui Pereira for the report.
+  - Fix default value of simple dot in MySQL export. Thanks to Shubham
+    Dabriwala for the report.
+  - Fix regression in data type translation after fix on unsigned numeric type.
+    Thanks to Shubham Dabriwala for the report.
+
 2022 10 08 - v23.2
 
 This release fix several issues reported since past height months and
diff --git a/debian/changelog b/debian/changelog
index d23e395..7c7974c 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+ora2pg (24.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Mon, 31 Jul 2023 18:52:54 -0000
+
 ora2pg (23.2-1) unstable; urgency=medium
 
   * New upstream version 23.2.
diff --git a/debian/patches/01_Ora2Pg.pod.diff b/debian/patches/01_Ora2Pg.pod.diff
index 6c70c2b..c73cc29 100644
--- a/debian/patches/01_Ora2Pg.pod.diff
+++ b/debian/patches/01_Ora2Pg.pod.diff
@@ -2,9 +2,11 @@ Description: Fix wrong paths
 Forwarded: no
 Author: Julián Moreno Patiño <darkjunix@gmail.com
 Last-Update: 2011-01-25
---- a/doc/Ora2Pg.pod
-+++ b/doc/Ora2Pg.pod
-@@ -138,7 +138,7 @@ Like any other Perl Module Ora2Pg can be
+Index: ora2pg.git/doc/Ora2Pg.pod
+===================================================================
+--- ora2pg.git.orig/doc/Ora2Pg.pod
++++ ora2pg.git/doc/Ora2Pg.pod
+@@ -177,7 +177,7 @@ Like any other Perl Module Ora2Pg can be
  	make && make install
  
  This will install Ora2Pg.pm into your site Perl repository, ora2pg into
@@ -13,7 +15,7 @@ Last-Update: 2011-01-25
  
  On Windows(tm) OSes you may use instead:
  
-@@ -247,7 +247,7 @@ Instant Client installation:
+@@ -286,7 +286,7 @@ Instant Client installation:
  By default Ora2Pg will look for /etc/ora2pg/ora2pg.conf configuration file, if
  the file exist you can simply execute:
  
@@ -22,7 +24,7 @@ Last-Update: 2011-01-25
  
  or under Windows(tm) run ora2pg.bat file, located in your perl bin directory.
  Windows(tm) users may also find a template configuration file in C:\ora2pg
-@@ -255,7 +255,7 @@ Windows(tm) users may also find a templa
+@@ -294,7 +294,7 @@ Windows(tm) users may also find a templa
  If you want to call another configuration file, just give the path as command
  line argument:
  
diff --git a/debian/patches/02_remove_unnecessary_files.diff b/debian/patches/02_remove_unnecessary_files.diff
index a82f24d..623a1a8 100644
--- a/debian/patches/02_remove_unnecessary_files.diff
+++ b/debian/patches/02_remove_unnecessary_files.diff
@@ -2,8 +2,10 @@ Description: Modify Makefile.PL to match Debian policy
 Forwarded: yes
 Author: Julián Moreno Patiño <darkjunix@gmail.com
 Last-Update: 2016-12-01
---- a/Makefile.PL
-+++ b/Makefile.PL
+Index: ora2pg.git/Makefile.PL
+===================================================================
+--- ora2pg.git.orig/Makefile.PL
++++ ora2pg.git/Makefile.PL
 @@ -14,7 +14,7 @@ while ($_ = shift) {
  my $CONFDIR = $ENV{CONFDIR} || '/etc/ora2pg';
  my $RPM_CONFDIR = $CONFDIR;
@@ -13,7 +15,7 @@ Last-Update: 2016-12-01
  my $DATA_LIMIT_DEFAULT = 10000;
  if ($^O =~ /MSWin32|dos/i) {
  	$DATA_LIMIT_DEFAULT = 2000;
-@@ -1397,20 +1397,6 @@ MYSQL_INTERNAL_EXTRACT_FORMAT	0
+@@ -1431,20 +1431,6 @@ CASE_INSENSITIVE_SEARCH	citext
  };
  close(OUTCFG);
  
@@ -34,7 +36,7 @@ Last-Update: 2016-12-01
  WriteMakefile(
      'NAME'         => 'Ora2Pg',
      'VERSION_FROM' => 'lib/Ora2Pg.pm',
-@@ -1422,7 +1408,7 @@ WriteMakefile(
+@@ -1456,7 +1442,7 @@ WriteMakefile(
      'AUTHOR'       => 'Gilles Darold (gilles _AT_ darold _DOT_ net)',
      'ABSTRACT'     => 'Oracle to PostgreSQL migration toolkit',
      'EXE_FILES'    => [ qw(scripts/ora2pg scripts/ora2pg_scanner) ],
@@ -43,7 +45,7 @@ Last-Update: 2016-12-01
      'DESTDIR'      => $PREFIX,
      'INSTALLDIRS'  => $ENV{INSTALLDIRS},
      'clean'        => {FILES => "$DEST_CONF_FILE lib/blib/"},
-@@ -1456,7 +1442,6 @@ install_all :
+@@ -1490,7 +1476,6 @@ install_all :
  	\@\$(CP) -f $DEST_CONF_FILE $CONFDIR/$DEST_CONF_FILE
  	\@\$(MKPATH) $DOCDIR
  	\@\$(CP) -f README $DOCDIR/README
diff --git a/doc/Ora2Pg.pod b/doc/Ora2Pg.pod
index 337013a..45401bf 100644
--- a/doc/Ora2Pg.pod
+++ b/doc/Ora2Pg.pod
@@ -59,9 +59,9 @@ Features included:
 	- Export DBLINK as Oracle FDW.
 	- Export SYNONYMS as views.
 	- Export DIRECTORY as external table or directory for external_file extension.
-	- Full MySQL export just like Oracle database.
 	- Dispatch a list of SQL orders over multiple PostgreSQL connections
 	- Perform a diff between Oracle and PostgreSQL database for test purpose.
+	- MySQL/MariaDB and Microsoft SQL Server migration.
 
 Ora2Pg does its best to automatically convert your Oracle database to PostgreSQL
 but there's still manual works to do. The Oracle specific PL/SQL code generated
@@ -101,15 +101,16 @@ and install them where you want, for example: /opt/oracle/instantclient_12_2/
 You also need a modern Perl distribution (perl 5.10 and more). To connect to a
 database and proceed to his migration you need the DBI Perl module > 1.614.
 To migrate an Oracle database you need the DBD::Oracle Perl modules to be
-installed. To migrate a MySQL database you need the DBD::MySQL Perl modules.
-These modules are used to connect to the database but they are not mandatory
-if you want to migrate DDL input files.
+installed.
 
 To install DBD::Oracle and have it working you need to have the Oracle client
 libraries installed and the ORACLE_HOME environment variable must be defined.
 
 If you plan to export a MySQL database you need to install the Perl module
-DBD::mysql which requires that the mysql client libraries are installed.
+DBD::MySQL which requires that the mysql client libraries are installed.
+
+If you plan to export a SQL Server database you need to install the Perl module
+DBD::ODBC which requires that the unixODBC package is installed.
 
 On some Perl distribution you may need to install the Time::HiRes Perl module. 
 
@@ -118,6 +119,7 @@ using CPAN:
 
 	perl -MCPAN -e 'install DBD::Oracle'
 	perl -MCPAN -e 'install DBD::MySQL'
+	perl -MCPAN -e 'install DBD::ODBC'
 	perl -MCPAN -e 'install Time::HiRes'
 
 otherwise use the packages provided by your distribution.
@@ -142,6 +144,29 @@ using CPAN:
 
 otherwise use the packages provided by your distribution.
 
+=head2 Instruction for SQL Server
+
+For SQL Server you need to install the unixodbc package and the Perl
+DBD::ODBC driver:
+
+	sudo apt install unixodbc
+	sudo apt install libdbd-odbc-perl
+
+then install the Microsoft ODBC Driver for SQL Server. Follow the instructions
+relative to your operating system from here:
+
+	https://docs.microsoft.com/fr-fr/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server?view=sql-server-ver16
+
+Once it is done set the following in the /etc/odbcinst.ini file by adjusting
+the SQL Server ODBC driver version:
+
+	[msodbcsql18]
+	Description=Microsoft ODBC Driver 18 for SQL Server
+	Driver=/opt/microsoft/msodbcsql18/lib64/libmsodbcsql-18.0.so.1.1
+	UsageCount=1
+
+See ORACLE_DSN to know how to use the driver to connect to your MSSQL database.
+
 =head2 Installing Ora2Pg
 
 Like any other Perl Module Ora2Pg can be installed with the following commands:
@@ -299,6 +324,7 @@ Usage: ora2pg [-dhpqv --estimate_cost --dump_as_html] [--option value]
     -L | --limit num  : Number of tuples extracted from Oracle and stored in
                         memory before writing, default: 10000.
     -m | --mysql      : Export a MySQL database instead of an Oracle schema.
+    -M | --mssql      : Export a Microsoft SQL Server database.
     -n | --namespace schema : Set the Oracle schema to extract from.
     -N | --pg_schema schema : Set PostgreSQL's search_path.
     -o | --out file   : Set the path to the output file where SQL will
@@ -366,7 +392,7 @@ Usage: ora2pg [-dhpqv --estimate_cost --dump_as_html] [--option value]
                         can be changed using -C | --cdc_file.
    --lo_import        : use psql \lo_import command to import BLOB as large
                         object. Can be use to import data with COPY and import
-                        large object manually in a second pass. It is recquired
+			large object manually in a second pass. It is recquired
                         for BLOB > 1GB. See documentation for more explanation.
    --mview_as_table str: Comma separated list of materialized views to export
                         as regular table.
@@ -495,6 +521,10 @@ For MySQL the DSN will lool like this:
 
 the 'sid' part is replaced by 'database'.
 
+For MS SQL Server it will look like this:
+
+	dbi:ODBC:driver=msodbcsql18;server=mydb.database.windows.net;database=testdb
+
 =item ORACLE_USER et ORACLE_PWD
 
 These two directives are used to define the user and password for the Oracle
@@ -509,6 +539,12 @@ ORACLE_USER is not set it will be asked interactively too.
 To connect to a local ORACLE instance with connections "as sysdba" you have to
 set ORACLE_USER to "/" and an empty password.
 
+To make a connection using an Oracle Secure External Password Store (SEPS), 
+first configure the Oracle Wallet and then set both the ORACLE_USER and 
+ORACLE_PWD directives to the special value of "__SEPS__" (without the quotes 
+but with the double underscore).
+
+
 =item USER_GRANTS
 
 Set this directive to 1 if you connect the Oracle database as simple user and
@@ -558,7 +594,7 @@ to set some session parameters. This directive can be used multiple times.
 If your Oracle Client config file already includes the encryption method,
 then DBD:Oracle uses those settings to encrypt the connection while you
 extract the data. For example if you have configured the Oracle Client
-config file (sqlnet.or or .sqlnet) with the following information:
+config file (sqlnet.ora or .sqlnet) with the following information:
 
 	# Configure encryption of connections to Oracle
 	SQLNET.ENCRYPTION_CLIENT = required
@@ -653,7 +689,8 @@ specific schema, set COMPILE_SCHEMA to the schema name you want to recompile.
 
 This will ask to Oracle to validate the PL/SQL that could have been invalidate
 after a export/import for example. The 'VALID' or 'INVALID' status applies to
-functions, procedures, packages and user defined types.
+functions, procedures, packages and user defined types. It also concern disabled
+triggers.
 
 =item EXPORT_INVALID
 
@@ -794,6 +831,7 @@ Here are the different values of the TYPE directive, default is TABLE:
 	- TEST_COUNT: perform a row count diff between Oracle and PostgreSQL table.
 	- TEST_VIEW: perform a count on both side of number of rows returned by views.
 	- TEST_DATA: perform data validation check on rows at both sides.
+	- SEQUENCE_VALUES: export DDL to set the last values of sequences
 
 
 Only one type of export can be perform at the same time so the TYPE directive
@@ -938,7 +976,8 @@ of cores given as value to ORACLE_COPIES as follow:
 	SELECT * FROM MYTABLE WHERE ABS(MOD(COLUMN, ORACLE_COPIES)) = CUR_PROC
 
 where COLUMN is a technical key like a primary or unique key where split
-will be based and the current core used by the query (CUR_PROC).
+will be based and the current core used by the query (CUR_PROC). You can
+also force the column name to use using the DEFINED_PK configuration directive.
 
 Doesn't work under Windows Operating System, it is simply disabled.
 
@@ -1528,21 +1567,19 @@ change indexes on columns where the character limit is greater or equal than
 this value. For example, set it to 128 to create these kind of indexes on
 columns of type varchar2(N) where N >= 128.
 
-=item PREFIX_PARTITION
-
-Enable this directive if you want that your partition table name will be
-exported using the parent table name. Disabled by default. If you have
-multiple partitioned table, when exported to PostgreSQL some partitions
-could have the same name but different parent tables. This is not allowed,
-table name must be unique.
-
-
-=item PREFIX_SUB_PARTITION
-
-Enable this directive if you want that your subpartition table name will be
-exported using the parent partition name. Enabled by default. If the partition 
-names are a part of the subpartition names, you should enable this directive.
+=item RENAME_PARTITION
 
+Enable this directive if you want that your partition tables will be renamed.
+Disabled by default. If you have multiple partitioned table, when exported to
+PostgreSQL some partitions could have the same name but different parent tables.
+This is not allowed, table name must be unique, in this case enable this
+directive. A partition will be renamed following the rule:
+    "tablename"_part"pos"
+where "pos" is the partition number. For subpartition this is:
+    "tablename"_part"pos"_subpart"pos"
+If this is partition/subpartition default:
+    "tablename"_part_default
+    "tablename"_part"pos"_subpart_default
 
 =item DISABLE_PARTITION
 
@@ -1641,6 +1678,35 @@ before use. Default spatial object extraction type is INTERNAL.
 Use this directive to add a specific schema to the search path to look
 for PostGis functions.
 
+=item ST_SRID_FUNCTION
+
+Oracle function to use to extract the srid from ST_Geometry meta information.
+Default: ST_SRID, for example it should be set to sde.st_srid for ArcSDE.
+
+=item ST_DIMENSION_FUNCTION
+
+Oracle function to use to extract the dimension from ST_Geometry meta
+information. Default: ST_DIMENSION, for example it should be set to
+sde.st_dimention for ArcSDE.
+
+=item ST_GEOMETRYTYPE_FUNCTION
+
+Oracle function to use to extract the geometry type from a ST_Geometry column
+Default: ST_GEOMETRYTYPE, for example it should be set to sde.st_geometrytype
+for ArcSDE.
+
+=item ST_ASBINARY_FUNCTION
+
+Oracle function to used to convert an ST_Geometry value into WKB format.
+Default: ST_ASBINARY, for example it should be set to sde.st_asbinary for
+ArcSDE.
+
+=item ST_ASTEXT_FUNCTION
+
+Oracle function to used to convert an ST_Geometry value into WKT format.
+Default: ST_ASTEXT, for example it should be set to sde.st_astext for
+ArcSDE.
+
 =back
 
 =head2 PostgreSQL Import
@@ -1671,6 +1737,11 @@ value of this directive by dividing it by 10 until his value is below 1000.
 You can control this value by setting BLOB_LIMIT. Exporting BLOB use lot of
 resources, setting it to a too high value can produce OOM.
 
+=item CLOB_AS_BLOB
+
+Apply same behavior on CLOB than BLOB with BLOB_LIMIT settings. This is
+especially useful if you have large CLOB data. Default: enabled
+
 =item OUTPUT
 
 The Ora2Pg output filename can be changed with this directive. Default value is
@@ -1911,6 +1982,11 @@ This directive can be used to send an initial command to PostgreSQL, just after
 the connection. For example to set some session parameters. This directive can
 be used multiple times.
 
+=item INSERT_ON_CONFLICT
+
+When enabled this instruct Ora2Pg to add an ON CONFLICT DO NOTHING clause to all
+INSERT statements generated for this type of data export.
+
 =back
 
 =head2 Column type control
@@ -1947,7 +2023,7 @@ PostgreSQL types to redefine data type translation used in Ora2pg. The syntax
 is a comma-separated list of "Oracle datatype:Postgresql datatype". Here are
 the default list used:
 
-	DATA_TYPE	VARCHAR2:varchar,NVARCHAR2:varchar,DATE:timestamp,LONG:text,LONG RAW:bytea,CLOB:text,NCLOB:text,BLOB:bytea,BFILE:bytea,RAW(16):uuid,RAW(32):uuid,RAW:bytea,UROWID:oid,ROWID:oid,FLOAT:double precision,DEC:decimal,DECIMAL:decimal,DOUBLE PRECISION:double precision,INT:integer,INTEGER:integer,REAL:real,SMALLINT:smallint,BINARY_FLOAT:double precision,BINARY_DOUBLE:double precision,TIMESTAMP:timestamp,XMLTYPE:xml,BINARY_INTEGER:integer,PLS_INTEGER:integer,TIMESTAMP WITH TIME ZONE:timestamp with time zone,TIMESTAMP WITH LOCAL TIME ZONE:timestamp with time zone
+	DATA_TYPE	VARCHAR2:varchar,NVARCHAR2:varchar,NVARCHAR:varchar,NCHAR:char,DATE:timestamp(0),LONG:text,LONG RAW:bytea,CLOB:text,NCLOB:text,BLOB:bytea,BFILE:bytea,RAW(16):uuid,RAW(32):uuid,RAW:bytea,UROWID:oid,ROWID:oid,FLOAT:double precision,DEC:decimal,DECIMAL:decimal,DOUBLE PRECISION:double precision,INT:integer,INTEGER:integer,REAL:real,SMALLINT:smallint,BINARY_FLOAT:double precision,BINARY_DOUBLE:double precision,TIMESTAMP:timestamp,XMLTYPE:xml,BINARY_INTEGER:integer,PLS_INTEGER:integer,TIMESTAMP WITH TIME ZONE:timestamp with time zone,TIMESTAMP WITH LOCAL TIME ZONE:timestamp with time zone
 
 The directive and the list definition must be a single line.
 
@@ -2530,6 +2606,26 @@ with format; DDHH24MMSS::bigint, this depend of your apps usage.
 
 =back
 
+=head2 Control SQL Server export behavior
+
+=over 4
+
+=item DROP_ROWVERSION
+
+PostgreSQL has no equivalent to rowversion datatype and feature, if you want
+to remove these useless columns, enable this directive. Columns of datatype
+'rowversion' or 'timestamp' will not be exported.
+
+=item CASE_INSENSITIVE_SEARCH
+
+Emulate the same behavior of MSSQL with case insensitive search. If the value
+is citext it will use the citext data type instead of char/varchar/text in
+tables DDL (Ora2Pg will add a CHECK constraint for columns with a precision).
+Instead of citext you can also set a collation name that will be used in the
+columns definitions. To disable case insensitive search set it to: none.
+
+=back
+
 =head2 Special options to handle character encoding
 
 =over 4
@@ -2842,8 +2938,9 @@ to extract data from the Oracle with the -J command line option as follow:
 
 	ora2pg -c ora2pg.conf -t KETTLE -J 4 -j 12 -a EMPLOYEES -o load_mydata.sh
 
-This is only possible if you have defined the technical key to used to split
-the query between cores in the DEFINED_PKEY configuration directive. For example:
+This is only possible if there is a unique key defined on a numeric column or
+that you have defined the technical key to used to split the query between cores
+in the DEFINED_PKEY configuration directive. For example:
 
 	DEFINED_PK      EMPLOYEES:employee_id
 
@@ -3051,6 +3148,7 @@ Usage: ora2pg_scanner -l CSVFILE [-o OUTDIR]
 	"type","schema/database","dsn","user","password"
 	"MYSQL","sakila","dbi:mysql:host=192.168.1.10;database=sakila;port=3306","root","secret"
 	"ORACLE","HR","dbi:Oracle:host=192.168.1.10;sid=XE;port=1521","system","manager"
+	"MSSQL","HR","dbi:ODBC:driver=msodbcsql18;server=srv.database.windows.net;database=testdb","system","manager"
 
    The CSV field separator must be a comma.
 
@@ -3061,6 +3159,7 @@ Usage: ora2pg_scanner -l CSVFILE [-o OUTDIR]
    For example:
 
 	"ORACLE","","dbi:Oracle:host=192.168.1.10;sid=XE;port=1521","system","manager"
+	"MSSQL","","dbi:ODBC:driver=msodbcsql18;server=srv.database.windows.net;database=testdb","usrname","passwd"
 
    will generate a report for all schema in the XE instance. Note that in this
    case the SCHEMA directive in ora2pg.conf must not be set.
@@ -3450,7 +3549,7 @@ your ideas, features request or patches and there will be applied.
 
 =head1 LICENSE
 
-Copyright (c) 2000-2022 Gilles Darold - All rights reserved.
+Copyright (c) 2000-2023 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
diff --git a/doc/ora2pg.3 b/doc/ora2pg.3
index 1faf99f..89b61b3 100644
--- a/doc/ora2pg.3
+++ b/doc/ora2pg.3
@@ -133,7 +133,7 @@
 .\" ========================================================================
 .\"
 .IX Title "ORA2PG 1"
-.TH ORA2PG 1 "2022-09-29" "perl v5.34.0" "User Contributed Perl Documentation"
+.TH ORA2PG 1 "2023-06-28" "perl v5.34.0" "User Contributed Perl Documentation"
 .\" For nroff, turn off justification.  Always turn off hyphenation; it makes
 .\" way too many mistakes in technical documents.
 .if n .ad l
@@ -195,9 +195,9 @@ Features included:
 \&        \- Export DBLINK as Oracle FDW.
 \&        \- Export SYNONYMS as views.
 \&        \- Export DIRECTORY as external table or directory for external_file extension.
-\&        \- Full MySQL export just like Oracle database.
 \&        \- Dispatch a list of SQL orders over multiple PostgreSQL connections
 \&        \- Perform a diff between Oracle and PostgreSQL database for test purpose.
+\&        \- MySQL/MariaDB and Microsoft SQL Server migration.
 .Ve
 .PP
 Ora2Pg does its best to automatically convert your Oracle database to PostgreSQL
@@ -238,24 +238,26 @@ and install them where you want, for example: /opt/oracle/instantclient_12_2/
 You also need a modern Perl distribution (perl 5.10 and more). To connect to a
 database and proceed to his migration you need the \s-1DBI\s0 Perl module > 1.614.
 To migrate an Oracle database you need the DBD::Oracle Perl modules to be
-installed. To migrate a MySQL database you need the DBD::MySQL Perl modules.
-These modules are used to connect to the database but they are not mandatory
-if you want to migrate \s-1DDL\s0 input files.
+installed.
 .PP
 To install DBD::Oracle and have it working you need to have the Oracle client
 libraries installed and the \s-1ORACLE_HOME\s0 environment variable must be defined.
 .PP
 If you plan to export a MySQL database you need to install the Perl module
-DBD::mysql which requires that the mysql client libraries are installed.
+DBD::MySQL which requires that the mysql client libraries are installed.
+.PP
+If you plan to export a \s-1SQL\s0 Server database you need to install the Perl module
+\&\s-1DBD::ODBC\s0 which requires that the unixODBC package is installed.
 .PP
 On some Perl distribution you may need to install the Time::HiRes Perl module.
 .PP
 If your distribution doesn't include these Perl modules you can install them
 using \s-1CPAN:\s0
 .PP
-.Vb 3
+.Vb 4
 \&        perl \-MCPAN \-e \*(Aqinstall DBD::Oracle\*(Aq
 \&        perl \-MCPAN \-e \*(Aqinstall DBD::MySQL\*(Aq
+\&        perl \-MCPAN \-e \*(Aqinstall DBD::ODBC\*(Aq
 \&        perl \-MCPAN \-e \*(Aqinstall Time::HiRes\*(Aq
 .Ve
 .PP
@@ -281,6 +283,34 @@ using \s-1CPAN:\s0
 .Ve
 .PP
 otherwise use the packages provided by your distribution.
+.SS "Instruction for \s-1SQL\s0 Server"
+.IX Subsection "Instruction for SQL Server"
+For \s-1SQL\s0 Server you need to install the unixodbc package and the Perl
+\&\s-1DBD::ODBC\s0 driver:
+.PP
+.Vb 2
+\&        sudo apt install unixodbc
+\&        sudo apt install libdbd\-odbc\-perl
+.Ve
+.PP
+then install the Microsoft \s-1ODBC\s0 Driver for \s-1SQL\s0 Server. Follow the instructions
+relative to your operating system from here:
+.PP
+.Vb 1
+\&        https://docs.microsoft.com/fr\-fr/sql/connect/odbc/linux\-mac/installing\-the\-microsoft\-odbc\-driver\-for\-sql\-server?view=sql\-server\-ver16
+.Ve
+.PP
+Once it is done set the following in the /etc/odbcinst.ini file by adjusting
+the \s-1SQL\s0 Server \s-1ODBC\s0 driver version:
+.PP
+.Vb 4
+\&        [msodbcsql18]
+\&        Description=Microsoft ODBC Driver 18 for SQL Server
+\&        Driver=/opt/microsoft/msodbcsql18/lib64/libmsodbcsql\-18.0.so.1.1
+\&        UsageCount=1
+.Ve
+.PP
+See \s-1ORACLE_DSN\s0 to know how to use the driver to connect to your \s-1MSSQL\s0 database.
 .SS "Installing Ora2Pg"
 .IX Subsection "Installing Ora2Pg"
 Like any other Perl Module Ora2Pg can be installed with the following commands:
@@ -455,6 +485,7 @@ Usage: ora2pg [\-dhpqv \-\-estimate_cost \-\-dump_as_html] [\-\-option value]
 \&    \-L | \-\-limit num  : Number of tuples extracted from Oracle and stored in
 \&                        memory before writing, default: 10000.
 \&    \-m | \-\-mysql      : Export a MySQL database instead of an Oracle schema.
+\&    \-M | \-\-mssql      : Export a Microsoft SQL Server database.
 \&    \-n | \-\-namespace schema : Set the Oracle schema to extract from.
 \&    \-N | \-\-pg_schema schema : Set PostgreSQL\*(Aqs search_path.
 \&    \-o | \-\-out file   : Set the path to the output file where SQL will
@@ -656,6 +687,12 @@ For MySQL the \s-1DSN\s0 will lool like this:
 .Ve
 .Sp
 the 'sid' part is replaced by 'database'.
+.Sp
+For \s-1MS SQL\s0 Server it will look like this:
+.Sp
+.Vb 1
+\&        dbi:ODBC:driver=msodbcsql18;server=mydb.database.windows.net;database=testdb
+.Ve
 .IP "\s-1ORACLE_USER\s0 et \s-1ORACLE_PWD\s0" 4
 .IX Item "ORACLE_USER et ORACLE_PWD"
 These two directives are used to define the user and password for the Oracle
@@ -669,6 +706,11 @@ Term::ReadKey Perl module, Ora2Pg will ask for the password interactively. If
 .Sp
 To connect to a local \s-1ORACLE\s0 instance with connections \*(L"as sysdba\*(R" you have to
 set \s-1ORACLE_USER\s0 to \*(L"/\*(R" and an empty password.
+.Sp
+To make a connection using an Oracle Secure External Password Store (\s-1SEPS\s0), 
+first configure the Oracle Wallet and then set both the \s-1ORACLE_USER\s0 and 
+\&\s-1ORACLE_PWD\s0 directives to the special value of \*(L"_\|_SEPS_\|_\*(R" (without the quotes 
+but with the double underscore).
 .IP "\s-1USER_GRANTS\s0" 4
 .IX Item "USER_GRANTS"
 Set this directive to 1 if you connect the Oracle database as simple user and
@@ -713,7 +755,7 @@ to set some session parameters. This directive can be used multiple times.
 If your Oracle Client config file already includes the encryption method,
 then DBD:Oracle uses those settings to encrypt the connection while you
 extract the data. For example if you have configured the Oracle Client
-config file (sqlnet.or or .sqlnet) with the following information:
+config file (sqlnet.ora or .sqlnet) with the following information:
 .PP
 .Vb 4
 \&        # Configure encryption of connections to Oracle
@@ -800,7 +842,8 @@ specific schema, set \s-1COMPILE_SCHEMA\s0 to the schema name you want to recomp
 .Sp
 This will ask to Oracle to validate the \s-1PL/SQL\s0 that could have been invalidate
 after a export/import for example. The '\s-1VALID\s0' or '\s-1INVALID\s0' status applies to
-functions, procedures, packages and user defined types.
+functions, procedures, packages and user defined types. It also concern disabled
+triggers.
 .IP "\s-1EXPORT_INVALID\s0" 4
 .IX Item "EXPORT_INVALID"
 If the above configuration directive is not enough to validate your \s-1PL/SQL\s0 code
@@ -935,6 +978,7 @@ Here are the different values of the \s-1TYPE\s0 directive, default is \s-1TABLE
 \&        \- TEST_COUNT: perform a row count diff between Oracle and PostgreSQL table.
 \&        \- TEST_VIEW: perform a count on both side of number of rows returned by views.
 \&        \- TEST_DATA: perform data validation check on rows at both sides.
+\&        \- SEQUENCE_VALUES: export DDL to set the last values of sequences
 .Ve
 .Sp
 Only one type of export can be perform at the same time so the \s-1TYPE\s0 directive
@@ -1083,7 +1127,8 @@ of cores given as value to \s-1ORACLE_COPIES\s0 as follow:
 .Ve
 .Sp
 where \s-1COLUMN\s0 is a technical key like a primary or unique key where split
-will be based and the current core used by the query (\s-1CUR_PROC\s0).
+will be based and the current core used by the query (\s-1CUR_PROC\s0). You can
+also force the column name to use using the \s-1DEFINED_PK\s0 configuration directive.
 .Sp
 Doesn't work under Windows Operating System, it is simply disabled.
 .IP "\s-1DEFINED_PK\s0" 4
@@ -1695,18 +1740,19 @@ using those operators. If you set it to a value greater than 1 it will only
 change indexes on columns where the character limit is greater or equal than
 this value. For example, set it to 128 to create these kind of indexes on
 columns of type varchar2(N) where N >= 128.
-.IP "\s-1PREFIX_PARTITION\s0" 4
-.IX Item "PREFIX_PARTITION"
-Enable this directive if you want that your partition table name will be
-exported using the parent table name. Disabled by default. If you have
-multiple partitioned table, when exported to PostgreSQL some partitions
-could have the same name but different parent tables. This is not allowed,
-table name must be unique.
-.IP "\s-1PREFIX_SUB_PARTITION\s0" 4
-.IX Item "PREFIX_SUB_PARTITION"
-Enable this directive if you want that your subpartition table name will be
-exported using the parent partition name. Enabled by default. If the partition 
-names are a part of the subpartition names, you should enable this directive.
+.IP "\s-1RENAME_PARTITION\s0" 4
+.IX Item "RENAME_PARTITION"
+Enable this directive if you want that your partition tables will be renamed.
+Disabled by default. If you have multiple partitioned table, when exported to
+PostgreSQL some partitions could have the same name but different parent tables.
+This is not allowed, table name must be unique, in this case enable this
+directive. A partition will be renamed following the rule:
+    \*(L"tablename\*(R"_part\*(L"pos\*(R"
+where \*(L"pos\*(R" is the partition number. For subpartition this is:
+    \*(L"tablename\*(R"_part\*(L"pos\*(R"_subpart\*(L"pos\*(R"
+If this is partition/subpartition default:
+    \*(L"tablename\*(R"_part_default
+    \*(L"tablename\*(R"_part\*(L"pos\*(R"_subpart_default
 .IP "\s-1DISABLE_PARTITION\s0" 4
 .IX Item "DISABLE_PARTITION"
 If you don't want to reproduce the partitioning like in Oracle and want to
@@ -1800,6 +1846,30 @@ before use. Default spatial object extraction type is \s-1INTERNAL.\s0
 .IX Item "POSTGIS_SCHEMA"
 Use this directive to add a specific schema to the search path to look
 for PostGis functions.
+.IP "\s-1ST_SRID_FUNCTION\s0" 4
+.IX Item "ST_SRID_FUNCTION"
+Oracle function to use to extract the srid from ST_Geometry meta information.
+Default: \s-1ST_SRID,\s0 for example it should be set to sde.st_srid for ArcSDE.
+.IP "\s-1ST_DIMENSION_FUNCTION\s0" 4
+.IX Item "ST_DIMENSION_FUNCTION"
+Oracle function to use to extract the dimension from ST_Geometry meta
+information. Default: \s-1ST_DIMENSION,\s0 for example it should be set to
+sde.st_dimention for ArcSDE.
+.IP "\s-1ST_GEOMETRYTYPE_FUNCTION\s0" 4
+.IX Item "ST_GEOMETRYTYPE_FUNCTION"
+Oracle function to use to extract the geometry type from a ST_Geometry column
+Default: \s-1ST_GEOMETRYTYPE,\s0 for example it should be set to sde.st_geometrytype
+for ArcSDE.
+.IP "\s-1ST_ASBINARY_FUNCTION\s0" 4
+.IX Item "ST_ASBINARY_FUNCTION"
+Oracle function to used to convert an ST_Geometry value into \s-1WKB\s0 format.
+Default: \s-1ST_ASBINARY,\s0 for example it should be set to sde.st_asbinary for
+ArcSDE.
+.IP "\s-1ST_ASTEXT_FUNCTION\s0" 4
+.IX Item "ST_ASTEXT_FUNCTION"
+Oracle function to used to convert an ST_Geometry value into \s-1WKT\s0 format.
+Default: \s-1ST_ASTEXT,\s0 for example it should be set to sde.st_astext for
+ArcSDE.
 .SS "PostgreSQL Import"
 .IX Subsection "PostgreSQL Import"
 By default conversion to PostgreSQL format is written to file 'output.sql'.
@@ -1825,6 +1895,10 @@ When Ora2Pg detect a table with some \s-1BLOB\s0 it will automatically reduce th
 value of this directive by dividing it by 10 until his value is below 1000.
 You can control this value by setting \s-1BLOB_LIMIT.\s0 Exporting \s-1BLOB\s0 use lot of
 resources, setting it to a too high value can produce \s-1OOM.\s0
+.IP "\s-1CLOB_AS_BLOB\s0" 4
+.IX Item "CLOB_AS_BLOB"
+Apply same behavior on \s-1CLOB\s0 than \s-1BLOB\s0 with \s-1BLOB_LIMIT\s0 settings. This is
+especially useful if you have large \s-1CLOB\s0 data. Default: enabled
 .IP "\s-1OUTPUT\s0" 4
 .IX Item "OUTPUT"
 The Ora2Pg output filename can be changed with this directive. Default value is
@@ -2047,6 +2121,10 @@ set this directive to 1, ora2pg will not try to change the setting.
 This directive can be used to send an initial command to PostgreSQL, just after
 the connection. For example to set some session parameters. This directive can
 be used multiple times.
+.IP "\s-1INSERT_ON_CONFLICT\s0" 4
+.IX Item "INSERT_ON_CONFLICT"
+When enabled this instruct Ora2Pg to add an \s-1ON CONFLICT DO NOTHING\s0 clause to all
+\&\s-1INSERT\s0 statements generated for this type of data export.
 .SS "Column type control"
 .IX Subsection "Column type control"
 .IP "\s-1PG_NUMERIC_TYPE\s0" 4
@@ -2077,7 +2155,7 @@ is a comma-separated list of \*(L"Oracle datatype:Postgresql datatype\*(R". Here
 the default list used:
 .Sp
 .Vb 1
-\&        DATA_TYPE       VARCHAR2:varchar,NVARCHAR2:varchar,DATE:timestamp,LONG:text,LONG RAW:bytea,CLOB:text,NCLOB:text,BLOB:bytea,BFILE:bytea,RAW(16):uuid,RAW(32):uuid,RAW:bytea,UROWID:oid,ROWID:oid,FLOAT:double precision,DEC:decimal,DECIMAL:decimal,DOUBLE PRECISION:double precision,INT:integer,INTEGER:integer,REAL:real,SMALLINT:smallint,BINARY_FLOAT:double precision,BINARY_DOUBLE:double precision,TIMESTAMP:timestamp,XMLTYPE:xml,BINARY_INTEGER:integer,PLS_INTEGER:integer,TIMESTAMP WITH TIME ZONE:timestamp with time zone,TIMESTAMP WITH LOCAL TIME ZONE:timestamp with time zone
+\&        DATA_TYPE       VARCHAR2:varchar,NVARCHAR2:varchar,NVARCHAR:varchar,NCHAR:char,DATE:timestamp(0),LONG:text,LONG RAW:bytea,CLOB:text,NCLOB:text,BLOB:bytea,BFILE:bytea,RAW(16):uuid,RAW(32):uuid,RAW:bytea,UROWID:oid,ROWID:oid,FLOAT:double precision,DEC:decimal,DECIMAL:decimal,DOUBLE PRECISION:double precision,INT:integer,INTEGER:integer,REAL:real,SMALLINT:smallint,BINARY_FLOAT:double precision,BINARY_DOUBLE:double precision,TIMESTAMP:timestamp,XMLTYPE:xml,BINARY_INTEGER:integer,PLS_INTEGER:integer,TIMESTAMP WITH TIME ZONE:timestamp with time zone,TIMESTAMP WITH LOCAL TIME ZONE:timestamp with time zone
 .Ve
 .Sp
 The directive and the list definition must be a single line.
@@ -2629,6 +2707,20 @@ Use it only if Ora2Pg fail on auto detecting this behavior.
 Enable this directive if you want \s-1\fBEXTRACT\s0()\fR replacement to use the internal
 format returned as an integer, for example \s-1DD HH24:MM:SS\s0 will be replaced
 with format; DDHH24MMSS::bigint, this depend of your apps usage.
+.SS "Control \s-1SQL\s0 Server export behavior"
+.IX Subsection "Control SQL Server export behavior"
+.IP "\s-1DROP_ROWVERSION\s0" 4
+.IX Item "DROP_ROWVERSION"
+PostgreSQL has no equivalent to rowversion datatype and feature, if you want
+to remove these useless columns, enable this directive. Columns of datatype
+\&'rowversion' or 'timestamp' will not be exported.
+.IP "\s-1CASE_INSENSITIVE_SEARCH\s0" 4
+.IX Item "CASE_INSENSITIVE_SEARCH"
+Emulate the same behavior of \s-1MSSQL\s0 with case insensitive search. If the value
+is citext it will use the citext data type instead of char/varchar/text in
+tables \s-1DDL\s0 (Ora2Pg will add a \s-1CHECK\s0 constraint for columns with a precision).
+Instead of citext you can also set a collation name that will be used in the
+columns definitions. To disable case insensitive search set it to: none.
 .SS "Special options to handle character encoding"
 .IX Subsection "Special options to handle character encoding"
 .IP "\s-1NLS_LANG\s0 and \s-1NLS_NCHAR\s0" 4
@@ -2942,8 +3034,9 @@ to extract data from the Oracle with the \-J command line option as follow:
 \&        ora2pg \-c ora2pg.conf \-t KETTLE \-J 4 \-j 12 \-a EMPLOYEES \-o load_mydata.sh
 .Ve
 .PP
-This is only possible if you have defined the technical key to used to split
-the query between cores in the \s-1DEFINED_PKEY\s0 configuration directive. For example:
+This is only possible if there is a unique key defined on a numeric column or
+that you have defined the technical key to used to split the query between cores
+in the \s-1DEFINED_PKEY\s0 configuration directive. For example:
 .PP
 .Vb 1
 \&        DEFINED_PK      EMPLOYEES:employee_id
@@ -3168,6 +3261,7 @@ Usage: ora2pg_scanner \-l \s-1CSVFILE\s0 [\-o \s-1OUTDIR\s0]
 \&        "type","schema/database","dsn","user","password"
 \&        "MYSQL","sakila","dbi:mysql:host=192.168.1.10;database=sakila;port=3306","root","secret"
 \&        "ORACLE","HR","dbi:Oracle:host=192.168.1.10;sid=XE;port=1521","system","manager"
+\&        "MSSQL","HR","dbi:ODBC:driver=msodbcsql18;server=srv.database.windows.net;database=testdb","system","manager"
 \&
 \&   The CSV field separator must be a comma.
 \&
@@ -3178,6 +3272,7 @@ Usage: ora2pg_scanner \-l \s-1CSVFILE\s0 [\-o \s-1OUTDIR\s0]
 \&   For example:
 \&
 \&        "ORACLE","","dbi:Oracle:host=192.168.1.10;sid=XE;port=1521","system","manager"
+\&        "MSSQL","","dbi:ODBC:driver=msodbcsql18;server=srv.database.windows.net;database=testdb","usrname","passwd"
 \&
 \&   will generate a report for all schema in the XE instance. Note that in this
 \&   case the SCHEMA directive in ora2pg.conf must not be set.
@@ -3580,7 +3675,7 @@ Any contribution to build a better tool is welcome, you just have to send me
 your ideas, features request or patches and there will be applied.
 .SH "LICENSE"
 .IX Header "LICENSE"
-Copyright (c) 2000\-2022 Gilles Darold \- All rights reserved.
+Copyright (c) 2000\-2023 Gilles Darold \- All rights reserved.
 .PP
 .Vb 4
 \&        This program is free software: you can redistribute it and/or modify
diff --git a/lib/Ora2Pg.pm b/lib/Ora2Pg.pm
index f8f9c55..058d311 100644
--- a/lib/Ora2Pg.pm
+++ b/lib/Ora2Pg.pm
@@ -4,7 +4,7 @@ package Ora2Pg;
 # Name     : Ora2Pg.pm
 # Language : Perl
 # Authors  : Gilles Darold, gilles _AT_ darold _DOT_ net
-# Copyright: Copyright (c) 2000-2022 : Gilles Darold - All rights reserved -
+# Copyright: Copyright (c) 2000-2023 : Gilles Darold - All rights reserved -
 # Function : Main module used to export Oracle database schema to PostgreSQL
 # Usage    : See documentation in this file with perldoc.
 #------------------------------------------------------------------------------
@@ -43,7 +43,7 @@ use Encode;
 #set locale to LC_NUMERIC C
 setlocale(LC_NUMERIC,"C");
 
-$VERSION = '23.2';
+$VERSION = '24.0';
 $PSQL = $ENV{PLSQL} || 'psql';
 
 $| = 1;
@@ -246,8 +246,8 @@ our %INDEX_TYPE = (
 # Reserved keywords in PostgreSQL
 our @KEYWORDS = qw(
 	ALL ANALYSE ANALYZE AND ANY ARRAY AS ASC ASYMMETRIC AUTHORIZATION BINARY
-	BOTH CASE CAST CHECK COLLATE COLLATION COLUMN CONCURRENTLY CONSTRAINT CREATE
-	CROSS CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA CURRENT_TIME
+	BOTH CASE CAST CHECK CMAX CMIN COLLATE COLLATION COLUMN CONCURRENTLY CONSTRAINT CREATE
+	CROSS CTID CURRENT_CATALOG CURRENT_DATE CURRENT_ROLE CURRENT_SCHEMA CURRENT_TIME
 	CURRENT_TIMESTAMP CURRENT_USER DEFAULT DEFERRABLE DESC DISTINCT DO ELSE END
 	EXCEPT FALSE FETCH FOR FOREIGN FREEZE FROM FULL GRANT GROUP HAVING ILIKE IN
 	INITIALLY INNER INTERSECT INTO IS ISNULL JOIN KEY LATERAL LEADING LEFT LIKE LIMIT
@@ -861,6 +861,11 @@ sub _db_connection
 		use Ora2Pg::MySQL;
 		return Ora2Pg::MySQL::_db_connection($self);
 	}
+	elsif ($self->{is_mssql})
+	{
+		use Ora2Pg::MSSQL;
+		return Ora2Pg::MSSQL::_db_connection($self);
+	}
 	else
 	{
 		use Ora2Pg::Oracle;
@@ -932,9 +937,8 @@ sub _init
 	$self->{function_metadata} = ();
 	$self->{grant_object} = '';
 
-	# Used to precise if we need to prefix partition tablename with main tablename
-	$self->{prefix_partition} = 0;
-	$self->{prefix_part_subpartition} = 1;
+	# Used to precise if we need to rename the partitions
+	$self->{rename_partition} = 0;
 
 	# Use to preserve the data export type with geometry objects
 	$self->{local_type} = '';
@@ -948,6 +952,11 @@ sub _init
 	$self->{mysql_internal_extract_format} = 0;
 	$self->{mysql_pipes_as_concat} = 0;
 
+	# Initialize some variable related to export of mssql database
+	$self->{is_mssql} = 0;
+	$self->{drop_rowversion} = 0;
+	$self->{case_insensitive_search} = 'citext';
+
 	# List of users for audit trail
 	$self->{audit_user} = '';
 
@@ -1008,10 +1017,6 @@ sub _init
 		}
 	}
 
-	# Set default system user/schema to not export. Most of them are extracted from this doc:
-	# http://docs.oracle.com/cd/E11882_01/server.112/e10575/tdpsg_user_accounts.htm#TDPSG20030
-	push(@{$self->{sysusers}},'SYSTEM','CTXSYS','DBSNMP','EXFSYS','LBACSYS','MDSYS','MGMT_VIEW','OLAPSYS','ORDDATA','OWBSYS','ORDPLUGINS','ORDSYS','OUTLN','SI_INFORMTN_SCHEMA','SYS','SYSMAN','WK_TEST','WKSYS','WKPROXY','WMSYS','XDB','APEX_PUBLIC_USER','DIP','FLOWS_020100','FLOWS_030000','FLOWS_040100','FLOWS_010600','FLOWS_FILES','MDDATA','ORACLE_OCM','SPATIAL_CSW_ADMIN_USR','SPATIAL_WFS_ADMIN_USR','XS$NULL','PERFSTAT','SQLTXPLAIN','DMSYS','TSMSYS','WKSYS','APEX_040000','APEX_040200','DVSYS','OJVMSYS','GSMADMIN_INTERNAL','APPQOSSYS','DVSYS','DVF','AUDSYS','APEX_030200','MGMT_VIEW','ODM','ODM_MTR','TRACESRV','MTMSYS','OWBSYS_AUDIT','WEBSYS','WK_PROXY','OSE$HTTP$ADMIN','AURORA$JIS$UTILITY$','AURORA$ORB$UNAUTHENTICATED','DBMS_PRIVILEGE_CAPTURE','CSMIG', 'MGDSYS', 'SDE','DBSFWUSER');
-
 	# Set default tablespace to exclude when using USE_TABLESPACE
 	push(@{$self->{default_tablespaces}}, 'TEMP', 'USERS','SYSTEM');
 
@@ -1148,6 +1153,9 @@ sub _init
 	# Defined if we must add a drop if exists statement before creating an object
 	$self->{drop_if_exists} ||= 0;
 
+	# Disable ON CONFLICT clause by default
+	$self->{insert_on_conflict} ||= 0;
+
 	# Overwrite configuration with all given parameters
 	# and try to preserve backward compatibility
 	foreach my $k (keys %options)
@@ -1198,6 +1206,8 @@ sub _init
 			$self->{oracle_pwd} = $options{password};
 		} elsif (($k eq 'is_mysql') && $options{is_mysql}) {
 			$self->{is_mysql} = $options{is_mysql};
+		} elsif (($k eq 'is_mssql') && $options{is_mssql}) {
+			$self->{is_mssql} = $options{is_mssql};
 		}
 		elsif ($k eq 'where')
 		{
@@ -1219,6 +1229,11 @@ sub _init
 		}
 	}
 
+	# Do not allow global allow/exclude with SHOW_* reports
+	if ($self->{type} =~ /SHOW_/ && ($#{$self->{limited}{ALL}} >= 0 || $#{$self->{excluded}{ALL}} >= 0)) {
+		$self->logit("FATAL: you can not use global filters in ALLOW/EXCLUDE directive with SHOW_* reports\n", 0, 1);
+	}
+
 	# Global regex will be applied to the export type only
 	foreach my $i (@{$self->{limited}{ALL}})
 	{
@@ -1273,11 +1288,36 @@ sub _init
 	unlink($dirprefix . 'temp_pass2_file.dat');
 	unlink($dirprefix . 'temp_cost_file.dat');
 
+	# Autodetexct if we are exporting a MySQL database
+	if ($self->{oracle_dsn} =~ /dbi:mysql/i) {
+		$self->{is_mysql} = 1;
+	} elsif ($self->{oracle_dsn} =~ /dbi:ODBC:driver=msodbcsql/i) {
+		$self->{is_mssql} = 1;
+	}
+
 	# Preload our dedicated function per DBMS
 	if ($self->{is_mysql}) {
+		@{$self->{sysusers}} = ();
 		import Ora2Pg::MySQL;
+		$self->{sgbd_name} = 'MySQL';
+	} elsif ($self->{is_mssql}) {
+		push(@{$self->{sysusers}}, 'sys');
+		import Ora2Pg::MSSQL;
+		$self->{sgbd_name} = 'MSSQL';
 	} else {
 		import Ora2Pg::Oracle;
+		$self->{sgbd_name} = 'Oracle';
+	}
+
+	# Export json configuration test
+	$self->{json_test} ||= 0;
+	# Show dependencies between stored procedures
+	$self->{print_dependencies} ||= 0;
+
+	# Set default system user/schema to not export. Most of them are extracted from this doc:
+	# http://docs.oracle.com/cd/E11882_01/server.112/e10575/tdpsg_user_accounts.htm#TDPSG20030
+	if (!$self->{is_mysql} && !$self->{is_mssql}) {
+		push(@{$self->{sysusers}},'SYSTEM','CTXSYS','DBSNMP','EXFSYS','LBACSYS','MDSYS','MGMT_VIEW','OLAPSYS','ORDDATA','OWBSYS','ORDPLUGINS','ORDSYS','OUTLN','SI_INFORMTN_SCHEMA','SYS','SYSMAN','WK_TEST','WKSYS','WKPROXY','WMSYS','XDB','APEX_PUBLIC_USER','DIP','FLOWS_020100','FLOWS_030000','FLOWS_040100','FLOWS_010600','FLOWS_FILES','MDDATA','ORACLE_OCM','SPATIAL_CSW_ADMIN_USR','SPATIAL_WFS_ADMIN_USR','XS$NULL','PERFSTAT','SQLTXPLAIN','DMSYS','TSMSYS','WKSYS','APEX_040000','APEX_040200','DVSYS','OJVMSYS','GSMADMIN_INTERNAL','APPQOSSYS','DVSYS','DVF','AUDSYS','APEX_030200','MGMT_VIEW','ODM','ODM_MTR','TRACESRV','MTMSYS','OWBSYS_AUDIT','WEBSYS','WK_PROXY','OSE$HTTP$ADMIN','AURORA$JIS$UTILITY$','AURORA$ORB$UNAUTHENTICATED','DBMS_PRIVILEGE_CAPTURE','CSMIG', 'MGDSYS', 'SDE','DBSFWUSER');
 	}
 
 	# Log file handle
@@ -1295,7 +1335,13 @@ sub _init
 	if (not defined $self->{default_srid}) {
 		$self->{default_srid} = 4326;
 	}
-	
+	# Default function to use for ST_Geometry
+	$self->{st_srid_function} ||= 'ST_SRID';
+	$self->{st_dimension_function} ||= 'ST_DIMENSION';
+	$self->{st_geometrytype_function} ||=  'ST_GeometryType';
+	$self->{st_asbinary_function} ||= 'ST_AsBinary';
+	$self->{st_astext_function} ||= 'ST_AsText';
+
 	# Force Ora2Pg to extract spatial object in binary format
 	$self->{geometry_extract_type} = uc($self->{geometry_extract_type});
 	if (!$self->{geometry_extract_type} || !grep(/^$self->{geometry_extract_type}$/, 'WKT','WKB','INTERNAL')) {
@@ -1309,6 +1355,14 @@ sub _init
 
 	# Disable the use of orafce library by default
 	$self->{use_orafce} ||= 0;
+	if ($self->{type} eq 'SHOW_REPORT') {
+		$self->{use_orafce} = 0;
+	}
+
+	# Disable the use of mssqlfce library by default
+	$self->{use_mssqlfce} ||= 0;
+	$self->{local_schemas} = ();
+	$self->{local_schemas_regex} = '';
 
 	# Do not apply any default table filtering to improve performances by not applying regexp
 	$self->{no_excluded_table} ||= 0;
@@ -1377,28 +1431,30 @@ sub _init
 		$self->{has_utf8_fct} = 0;
 	}
 
-	# Autodetexct if we are exporting a MySQL database
-	if ($self->{oracle_dsn} =~ /dbi:mysql/i) {
-		$self->{is_mysql} = 1;
-	}
-
-	if ($self->{is_mysql}) {
-		# MySQL do not supports this syntax fallback to read committed
+	if ($self->{is_mysql} or $self->{is_mssql}) {
+		# MySQL and MSQL do not supports this syntax fallback to read committed
 		$self->{transaction} =~ s/(READ ONLY|READ WRITE)/ISOLATION LEVEL READ COMMITTED/;
 	}
 
 	# Set Oracle, Perl and PostgreSQL encoding that will be used
 	$self->_init_environment();
 
+	# Backward compatibility
+	$self->{rename_partition} = 1 if (!$self->{rename_partition} && $self->{prefix_partition});
+
 	# Multiple Oracle connection
 	$self->{oracle_copies} ||= 0;
 	$self->{ora_conn_count} = 0;
 	$self->{data_limit} ||= 10000;
 	$self->{blob_limit} ||= 0;
+	$self->{clob_as_blob} ||= 0;
 	$self->{disable_partition} ||= 0;
 	$self->{parallel_tables} ||= 0;
 	$self->{use_lob_locator} ||= 0;
 
+        $self->{disable_partition} = 1 if ($self->{is_mssql} and
+                                ($self->{type} eq 'COPY' or $self->{type} eq 'INSERT'));
+
 	# Transformation and output during data export
 	$self->{oracle_speed} ||= 0;
 	$self->{ora2pg_speed} ||= 0;
@@ -1421,6 +1477,8 @@ sub _init
 		# Set default type conversion
 		if ($self->{is_mysql}) {
 			%{$self->{data_type}} = %Ora2Pg::MySQL::SQL_TYPE;
+		} elsif ($self->{is_mssql}) {
+			%{$self->{data_type}} = %Ora2Pg::MSSQL::SQL_TYPE;
 		} else {
 			%{$self->{data_type}} = %Ora2Pg::Oracle::SQL_TYPE;
 		}
@@ -1443,6 +1501,8 @@ sub _init
 		# Set default type conversion
 		if ($self->{is_mysql}) {
 			%{$self->{data_type}} = %Ora2Pg::MySQL::SQL_TYPE;
+		} elsif ($self->{is_mssql}) {
+			%{$self->{data_type}} = %Ora2Pg::MSSQL::SQL_TYPE;
 		} else {
 			%{$self->{data_type}} = %Ora2Pg::Oracle::SQL_TYPE;
 		}
@@ -1474,7 +1534,7 @@ sub _init
 	# when USE_TABLESPACE is enabled
 	if ($self->{use_tablespace} && !$self->{keep_pkey_names})
 	{
-	    print STDERR "WARNING: Enforcing KEEP_PKEY_NAMES to 1 as USE_TABLESPACE is enabled.\n";
+	    print STDERR "WARNING: Enforcing KEEP_PKEY_NAMES to 1 because USE_TABLESPACE is enabled.\n";
 	    $self->{keep_pkey_names} = 1;
 	}
 
@@ -1542,10 +1602,12 @@ sub _init
 	if ($self->{longtrunkok} && not defined $self->{longtruncok}) {
 		$self->{longtruncok} = $self->{longtrunkok};
 	}
+	$self->{use_lob_locator} = 0 if ($self->{is_mssql});
 	$self->{longtruncok} = 0 if (not defined $self->{longtruncok});
 	# With lob locators LONGREADLEN must at least be 1MB
 	if (!$self->{longreadlen} || $self->{use_lob_locator}) {
 		$self->{longreadlen} = (1023*1024);
+		$self->{longtruncok} = 1;
 	}
 
 	# Backward compatibility with PG_NUMERIC_TYPE alone
@@ -1677,6 +1739,8 @@ sub _init
 		# Connect the database
 		if ($self->{oracle_dsn} =~ /dbi:mysql/i) {
 			$self->{is_mysql} = 1;
+		} elsif ($self->{oracle_dsn} =~ /dbi:ODBC:driver=msodbcsql/i) {
+			$self->{is_mssql} = 1;
 		}
 		$self->{dbh} = $self->_db_connection();
 
@@ -1684,11 +1748,11 @@ sub _init
 		$self->{db_version} = $self->_get_version();
 
 		# Compile again all objects in the schema
-		if (!$self->{is_mysql} && $self->{compile_schema}) {
+		if (!$self->{is_mysql} && !$self->{is_mssql} && $self->{compile_schema}) {
 			$self->_compile_schema(uc($self->{compile_schema}));
 		}
 
-		if (!grep(/^$self->{type}$/, 'COPY', 'INSERT', 'SEQUENCE', 'GRANT', 'TABLESPACE', 'QUERY', 'SYNONYM', 'FDW', 'KETTLE', 'DBLINK', 'DIRECTORY') && $self->{type} !~ /SHOW_/)
+		if (!grep(/^$self->{type}$/, 'COPY', 'INSERT', 'SEQUENCE', 'SEQUENCE_VALUES', 'GRANT', 'TABLESPACE', 'QUERY', 'SYNONYM', 'FDW', 'KETTLE', 'DBLINK', 'DIRECTORY') && $self->{type} !~ /SHOW_/)
 		{
 			if ($self->{plsql_pgsql} && !$self->{no_function_metadata})
 			{
@@ -1702,7 +1766,7 @@ sub _init
 						if ($self->{type} eq 'VIEW')
 						{
 							# Limit to package lookup with VIEW export type
-							$self->_get_package_function_list($o) if (!$self->{is_mysql});
+							$self->_get_package_function_list($o) if (!$self->{is_mysql} && !$self->{is_mssql});
 						}
 						else
 						{
@@ -1714,7 +1778,7 @@ sub _init
 				if ($self->{type} eq 'VIEW')
 				{
 					# Limit to package lookup with WIEW export type
-					$self->_get_package_function_list() if (!$self->{is_mysql});
+					$self->_get_package_function_list() if (!$self->{is_mysql} && !$self->{is_mssql});
 				}
 				else
 				{
@@ -1730,7 +1794,7 @@ sub _init
 	{
 		$self->{plsql_pgsql} = 1;
 
-		if (grep(/^$self->{type}$/, 'TABLE', 'SEQUENCE', 'GRANT', 'TABLESPACE', 'VIEW', 'TRIGGER', 'QUERY', 'FUNCTION','PROCEDURE','PACKAGE','TYPE','SYNONYM', 'DIRECTORY', 'DBLINK','LOAD'))
+		if (grep(/^$self->{type}$/, 'TABLE', 'SEQUENCE', 'SEQUENCE_VALUES', 'GRANT', 'TABLESPACE', 'VIEW', 'TRIGGER', 'QUERY', 'FUNCTION','PROCEDURE','PACKAGE','TYPE','SYNONYM', 'DIRECTORY', 'DBLINK','LOAD'))
 		{
 			if ($self->{type} eq 'LOAD')
 			{
@@ -1758,6 +1822,11 @@ sub _init
 		for my $t (keys %{$self->{'exclude_columns'}}) {
 			$self->exclude_columns($t, @{$self->{'exclude_columns'}{$t}});
 		}
+		# Look for custom data type
+		if ($self->{is_mssql}) {
+			$self->logit("Looking for user defined data type of type FROM => DOMAIN...\n", 1);
+			$self->_get_types();
+		}
 	}
 
 	if ($self->{oracle_fdw_data_export} && scalar keys %{$self->{'modify_struct'}} > 0) {
@@ -1796,7 +1865,7 @@ sub _init
 			$self->_synonyms();
 		} elsif ($self->{type} eq 'GRANT') {
 			$self->_grants();
-		} elsif ($self->{type} eq 'SEQUENCE') {
+		} elsif ($self->{type} eq 'SEQUENCE' || $self->{type} eq 'SEQUENCE_VALUES') {
 			$self->_sequences();
 		} elsif ($self->{type} eq 'TRIGGER') {
 			$self->_triggers();
@@ -1809,6 +1878,13 @@ sub _init
 		} elsif ($self->{type} eq 'TYPE') {
 			$self->_types();
 		} elsif ($self->{type} eq 'TABLESPACE') {
+			# Partitionned table do not accept NOT VALID constraint
+			if ($self->{pg_supports_partition})
+			{
+				# Get the list of partition
+				($self->{partitions}, $self->{partitions_default}) = $self->_get_partitions();
+				($self->{subpartitions}, $self->{subpartitions_default}) = $self->_get_subpartitions();
+			}
 			$self->_tablespaces();
 		} elsif ($self->{type} eq 'PARTITION') {
 			$self->_partitions();
@@ -1839,6 +1915,7 @@ sub _init
 			foreach my $o ('VIEW', 'MVIEW', 'SEQUENCE', 'TYPE', 'FDW')
 			{
 				next if ($self->{is_mysql} && grep(/^$o$/, 'MVIEW','TYPE','FDW'));
+				next if ($self->{is_mssql} && grep(/^$o$/, 'FDW'));
 				$self->_count_object($o);
 			}
 			# count function/procedure/package function
@@ -1901,7 +1978,7 @@ sub _init
 		}
 		else
 		{
-			warn "type option must be (TABLE, VIEW, GRANT, SEQUENCE, TRIGGER, PACKAGE, FUNCTION, PROCEDURE, PARTITION, TYPE, INSERT, COPY, TABLESPACE, SHOW_REPORT, SHOW_VERSION, SHOW_SCHEMA, SHOW_TABLE, SHOW_COLUMN, SHOW_ENCODING, FDW, MVIEW, QUERY, KETTLE, DBLINK, SYNONYM, DIRECTORY, LOAD, TEST, TEST_COUNT, TEST_VIEW, TEST_DATA), unknown $self->{type}\n";
+			warn "type option must be (TABLE, VIEW, GRANT, SEQUENCE, SEQUENCE_VALUES, TRIGGER, PACKAGE, FUNCTION, PROCEDURE, PARTITION, TYPE, INSERT, COPY, TABLESPACE, SHOW_REPORT, SHOW_VERSION, SHOW_SCHEMA, SHOW_TABLE, SHOW_COLUMN, SHOW_ENCODING, FDW, MVIEW, QUERY, KETTLE, DBLINK, SYNONYM, DIRECTORY, LOAD, TEST, TEST_COUNT, TEST_VIEW, TEST_DATA), unknown $self->{type}\n";
 		}
 		$self->replace_tables(%{$self->{'replace_tables'}});
 		$self->replace_cols(%{$self->{'replace_cols'}});
@@ -1946,18 +2023,25 @@ 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 {
+	if (!$self->{nls_lang})
+	{
+		if ($self->{is_mysql}) {
 			$self->{nls_lang} = 'utf8';
+		} elsif ($self->{is_mssql}) {
+			$self->{nls_lang} = 'iso_1';
+			$self->{client_encoding} = 'LATIN1' if (!$self->{client_encoding});
+		} else {
+			$self->{nls_lang} = 'AMERICAN_AMERICA.AL32UTF8';
 		}
 	}
-	if (!$self->{nls_nchar}) {
-		if (!$self->{is_mysql}) {
-			$self->{nls_nchar} = 'AL32UTF8';
-		} else {
+	if (!$self->{nls_nchar})
+	{
+		if ($self->{is_mysql}) {
 			$self->{nls_nchar} = 'utf8_general_ci';
+		} elsif ($self->{is_mssql}) {
+			$self->{nls_nchar} = 'SQL_Latin1_General_CP1_CI_AS';
+		} else {
+			$self->{nls_nchar} = 'AL32UTF8';
 		}
 	}
 	$ENV{NLS_LANG} = $self->{nls_lang};
@@ -1970,10 +2054,9 @@ sub _init_environment
 	$self->set_binmode();
 
 	# Set default PostgreSQL client encoding to UTF8
-	if (!$self->{client_encoding} || ($self->{nls_lang} =~ /UTF8/) ) {
+	if (!$self->{client_encoding} || $self->{nls_lang} =~ /UTF8/i) {
 		$self->{client_encoding} = 'UTF8';
 	}
-
 }
 
 sub set_binmode
@@ -2087,7 +2170,7 @@ sub _send_to_pgdb
 	$ENV{PGAPPNAME} = 'ora2pg ' || $VERSION;
 
 	# Connect the destination database
-	my $dbhdest = DBI->connect($self->{pg_dsn}, $self->{pg_user}, $self->{pg_pwd}, {AutoInactiveDestroy => 1});
+	my $dbhdest = DBI->connect($self->{pg_dsn}, $self->{pg_user}, $self->{pg_pwd}, {AutoInactiveDestroy => 1, PrintError => 0});
 
 	# Check for connection failure
 	if (!$dbhdest) {
@@ -2130,7 +2213,6 @@ sub _sequences
 
 	$self->logit("Retrieving sequences information...\n", 1);
 	$self->{sequences} = $self->_get_sequences();
-
 }
 
 
@@ -2160,10 +2242,130 @@ sub _functions
 	my $self = shift;
 
 	$self->logit("Retrieving functions information...\n", 1);
+
 	$self->{functions} = $self->_get_functions();
 
 }
 
+sub start_function_json_config
+{
+	my ($self, $type) = @_;
+
+	return if (!$self->{json_test});
+
+	my $dirprefix = '';
+	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
+
+	unlink("${dirprefix}$type.json");
+
+	$self->{oracle_dsn} =~ /host=([^;]+)/;
+	my $ora_host = $1 || 'localhost';
+	$self->{oracle_dsn} =~ /port=(\d+)/;
+	my $ora_port = $1 || ((!$self->{is_mysql}) ? 1521 : 3306);
+	my $sid = '';
+	if (!$self->{is_mysql}) {
+		$self->{oracle_dsn} =~ /(service_name|sid)=([^;]+)/;
+		$sid = $2 || '';
+	} else {
+		$self->{oracle_dsn} =~ /(database)=([^;]+)/;
+		$sid = $2 || '';
+	}
+
+	my $pg_host = 'localhost';
+	if ($self->{pg_dsn} =~ /host=([^;]+)/) {
+		$pg_host = $1;
+	}
+	my $pg_port = 5432;
+	if ($self->{pg_dsn} =~ /port=(\d+)/) {
+		$pg_port = $1;
+	}
+	my $pg_db = '';
+	if ($self->{pg_dsn} =~ /dbname=([^;]+)/) {
+		$pg_db = $1;
+	}
+
+	my $tfh = $self->append_export_file($dirprefix . "$type.json", 1);
+	flock($tfh, 2) || die "FATAL: can't lock file ${dirprefix}$type.json\n";
+	$tfh->print(qq/{
+  "oraConfig": {
+    "dsn": "$self->{oracle_dsn}",
+    "host": "$ora_host",
+    "port": $ora_port,
+    "user": "$self->{oracle_user}",
+    "password": "$self->{oracle_pwd}",
+    "service_name": "$sid",
+    "schema": "$self->{schema}"
+  },
+  "pgConfig": {
+    "dsn": "$self->{pg_dsn}",
+    "host": "$pg_host",
+    "port": $pg_port,
+    "user": "$self->{pg_user}",
+    "password": "$self->{pg_pwd}",
+    "dbname": "$pg_db",
+    "schema": "$self->{pg_schema}"
+  },
+  "procfuncConfig": [
+/);
+	$self->close_export_file($tfh, 1);
+}
+
+sub end_function_json_config
+{
+	my ($self, $type) = @_;
+
+	return if (!$self->{json_test});
+
+	my $dirprefix = '';
+	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
+
+	my $tfh = $self->append_export_file($dirprefix . "$type.json", 1);
+	flock($tfh, 2) || die "FATAL: can't lock file ${dirprefix}$type.json\n";
+	# Add an empty json entry at end
+	$tfh->print(qq/    {
+      "routine_type": "",
+      "ora": {
+        "routine_name": "",
+        "return_type": "",
+        "args_list": [
+          {
+            "0": [
+              {
+                "name": "",
+                "mode": "",
+                "type": "",
+                "default": "",
+                "value": ""
+              }
+              ]
+          } ]
+      },
+      "pg": {
+        "routine_name": "",
+        "return_type": "",
+        "args_list": [
+          {
+            "0": [
+              {
+                "name": "",
+                "mode": "",
+                "type": "",
+                "default": "",
+                "value": ""
+              }
+              ]
+          } ]
+      }
+    }
+/);
+
+	# terminate the json document
+	$tfh->print(qq/  ]
+}
+/);
+	$self->close_export_file($tfh, 1);
+}
+
 =head2 _procedures
 
 This function is used to retrieve all procedures information.
@@ -2191,6 +2393,9 @@ sub _packages
 {
 	my ($self) = @_;
 
+	if ($self->{is_mysql} or $self->{is_mssql}) {
+		$self->logit("Action type PACKAGES is not available for $self->{sgbd_name}.\n", 0, 1);
+	}
 	$self->logit("Retrieving packages information...\n", 1);
 	$self->{packages} = $self->_get_packages();
 
@@ -2257,6 +2462,12 @@ sub _tables
 {
 	my ($self, $nodetail) = @_;
 
+	if ($self->{is_mssql} && $self->{type} eq 'TABLE')
+	{
+		$self->logit("Retrieving table partitioning information...\n", 1);
+		%{ $self->{partitions_list} } = $self->_get_partitioned_table();
+	}
+
 	# Get all tables information specified by the DBI method table_info
 	$self->logit("Retrieving table information...\n", 1);
 
@@ -2387,7 +2598,7 @@ sub _tables
 		}
 		$count_table++;
 
-		if (grep(/^$t$/, @done)) {
+		if (grep(/^\Q$t\E$/, @done)) {
 			$self->logit("Duplicate entry found: $t\n", 1);
 		} else {
 			push(@done, $t);
@@ -2435,24 +2646,34 @@ sub _tables
 		if ($self->{type} ne 'SHOW_REPORT')
 		{
 			my $tmp_tbname = $t;
-			if (!$self->{is_mysql})
+			if ($self->{is_mysql})
 			{
-				if ( $t !~ /\./ ) {
-					$tmp_tbname = "\"$tables_infos{$t}{owner}\".\"$t\"";
+				if ( $t !~ /\./ && $tables_infos{$t}{owner}) {
+					$tmp_tbname = "\`$tables_infos{$t}{owner}\`.\`$t\`";
 				} else {
 					# in case we already have the schema name, add doublequote
-					$tmp_tbname =~ s/\./"."/;
-					$tmp_tbname = "\"$tmp_tbname\"";
+					$tmp_tbname =~ s/\./\`.\`/;
+					$tmp_tbname = "\`$tmp_tbname\`";
 				}
 			}
-			else
+			elsif ($self->{is_mssql})
 			{
 				if ( $t !~ /\./ && $tables_infos{$t}{owner}) {
-					$tmp_tbname = "\`$tables_infos{$t}{owner}\`.\`$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\`";
+					$tmp_tbname =~ s/\./\].\[/;
+					$tmp_tbname = "[$tmp_tbname]";
+				}
+			}
+			else
+			{
+				if ( $t !~ /\./ ) {
+					$tmp_tbname = "\"$tables_infos{$t}{owner}\".\"$t\"";
+				} else {
+					# in case we already have the schema name, add doublequote
+					$tmp_tbname =~ s/\./"."/;
+					$tmp_tbname = "\"$tmp_tbname\"";
 				}
 			}
 			my $query = "SELECT * FROM $tmp_tbname WHERE 1=0";
@@ -2486,12 +2707,20 @@ sub _tables
 	if ($#{$self->{view_as_table}} >= 0)
 	{
 		my %view_infos = $self->_get_views();
+		my @exanped_views = ();
+		foreach my $view (sort keys %view_infos)
+		{
+			foreach my $pattern (@{$self->{view_as_table}}) {
+				push(@exanped_views, $view) if ($view =~ /^$pattern$/i);
+			}
+		}
+
 		# Retrieve comment of each columns
 		my %columns_comments = $self->_column_comments();
 		foreach my $view (keys %columns_comments)
 		{
 			next if (!exists $view_infos{$view});
-			next if (!grep($view =~ /^$_$/i, @{$self->{view_as_table}}));
+			next if (!grep(/^$view$/i, @exanped_views));
 			foreach my $c (keys %{$columns_comments{$view}}) {
 				$self->{tables}{$view}{column_comments}{$c} = $columns_comments{$view}{$c};
 			}
@@ -2501,7 +2730,7 @@ sub _tables
 		{
 			# Set the table information for each class found
 			# Jump to desired extraction
-			next if (!grep($view =~ /^$_$/i, @{$self->{view_as_table}}));
+			next if (!grep(/^$view$/i, @exanped_views));
 			$self->logit("Scanning view $view to export as table...\n", 0);
 			$self->{tables}{$view}{type} = 'view';
 			$self->{tables}{$view}{text} = $view_infos{$view}{text};
@@ -2535,7 +2764,7 @@ sub _tables
 			}
 			$self->{tables}{$view}{field_name} = $sth->{NAME};
 			$self->{tables}{$view}{field_type} = $sth->{TYPE};
-			my %columns_infos = $self->_column_info($view, $self->{schema}, 'VIEW');
+			my %columns_infos = $self->_column_info($view, $self->{schema}, 'VIEW', @exanped_views);
 			foreach my $tb (keys %columns_infos)
 			{
 				next if ($tb ne $view);
@@ -2552,11 +2781,19 @@ sub _tables
 	if ($#{$self->{mview_as_table}} >= 0)
 	{
 		my %view_infos = $self->_get_materialized_views();
+		my @exanped_views = ();
+		foreach my $view (sort keys %view_infos)
+		{
+			foreach my $pattern (@{$self->{mview_as_table}}) {
+				push(@exanped_views, $view) if ($view =~ /^$pattern$/i);
+			}
+		}
+
 		foreach my $view (sort keys %view_infos)
 		{
 			# Set the table information for each class found
 			# Jump to desired extraction
-			next if (!grep($view =~ /^$_$/i, @{$self->{mview_as_table}}));
+			next if (!grep(/^$view$/i, @exanped_views));
 			$self->logit("Scanning materialized view $view to export as table...\n", 0);
 			if (exists $self->{tables}{$view})
 			{
@@ -2592,7 +2829,7 @@ sub _tables
 			}
 			$self->{tables}{$view}{field_name} = $sth->{NAME};
 			$self->{tables}{$view}{field_type} = $sth->{TYPE};
-			my %columns_infos = $self->_column_info($view, $self->{schema}, 'MVIEW');
+			my %columns_infos = $self->_column_info($view, $self->{schema}, 'MVIEW', @exanped_views);
 			foreach my $tb (keys %columns_infos)
 			{
 				next if ($tb ne $view);
@@ -2608,7 +2845,7 @@ sub _tables
 		%{$self->{external_table}} = $self->_get_external_tables();
 	}
 
-	if ($self->{type} eq 'TABLE')
+	if (!$self->{is_mssql} && $self->{type} eq 'TABLE')
 	{
 		$self->logit("Retrieving table partitioning information...\n", 1);
 		%{ $self->{partitions_list} } = $self->_get_partitioned_table();
@@ -3439,8 +3676,10 @@ sub read_trigger_from_file
 
 		next if (!$t_name || ! $tb_name);
 
-		# Remove referencing clause, not supported by PostgreSQL
-		$trigger =~ s/REFERENCING\s+(.*?)(FOR\s+EACH\s+)/$2/is;
+		# Remove referencing clause, not supported by PostgreSQL < 10
+		if ($self->{pg_version} < 10) {
+			$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;
@@ -3717,17 +3956,19 @@ sub _views
 	}
 
 	my $i = 1;
-	foreach my $view (sort keys %view_infos) {
+	foreach my $view (sort keys %view_infos)
+	{
 		$self->logit("[$i] Scanning $view...\n", 1);
 		$self->{views}{$view}{text} = $view_infos{$view}{text};
 		$self->{views}{$view}{owner} = $view_infos{$view}{owner};
+		$self->{views}{$view}{check_option} = $view_infos{$view}{check_option};
+		$self->{views}{$view}{updatable} = $view_infos{$view}{updatable};
 		$self->{views}{$view}{iter} = $view_infos{$view}{iter} if (exists $view_infos{$view}{iter});
 		$self->{views}{$view}{comment} = $view_infos{$view}{comment};
                 # Retrieve also aliases from views
                 $self->{views}{$view}{alias} = $view_infos{$view}{alias};
 		$i++;
 	}
-
 }
 
 =head2 _materialized_views
@@ -3887,7 +4128,8 @@ sub get_replaced_tbname
 {
 	my ($self, $tmptb) = @_;
 
-	if (exists $self->{replaced_tables}{"\L$tmptb\E"} && $self->{replaced_tables}{"\L$tmptb\E"}) {
+	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)};
 	}
@@ -4018,7 +4260,7 @@ sub _export_table_data
 		{
 			my $part_name = $self->{partitions}{$table}{$pos}{name};
 			my $tbpart_name = $part_name;
-			$tbpart_name = $table . '_' . $part_name if ($self->{prefix_partition});
+			$tbpart_name = $table . '_part' . $pos if ($self->{rename_partition});
 			next if ($self->{allow_partition} && !grep($_ =~ /^$tbpart_name$/i, @{$self->{allow_partition}}));
 
 			if (exists $self->{subpartitions}{$table}{$part_name})
@@ -4029,7 +4271,7 @@ sub _export_table_data
 					next if ($self->{allow_partition} && !grep($_ =~ /^$subpart$/i, @{$self->{allow_partition}}));
 					my $sub_tb_name = $subpart;
 					$sub_tb_name =~ s/^[^\.]+\.//; # remove schema part if any
-					$sub_tb_name = "${table}_$sub_tb_name" if ($self->{prefix_partition});
+					$sub_tb_name = $table . '_part' . $pos . '_subpart' . $p if ($self->{rename_partition});
 					if ($self->{file_per_table} && !$self->{pg_dsn}) {
 						# Do not dump data again if the file already exists
 						next if ($self->file_exists("$dirprefix${sub_tb_name}_$self->{output}"));
@@ -4041,30 +4283,38 @@ sub _export_table_data
 					}
 
 					$self->logit("Dumping sub partition table $table ($subpart)...\n", 1);
-					$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $subpart, 1);
+					$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $subpart, 1, $tbpart_name, $sub_tb_name);
 					# Rename temporary filename into final name
-					$self->rename_dump_partfile($dirprefix, $sub_tb_name);
+					$self->rename_dump_partfile($dirprefix, $subpart, $table);
 				}
 				# Now load content of the default subpartition table
 				if ($self->{subpartitions_default}{$table}{$part_name})
 				{
-					if (!$self->{allow_partition} || grep($_ =~ /^$self->{subpartitions_default}{$table}{$part_name}$/i, @{$self->{allow_partition}}))
+					if (!$self->{allow_partition} || grep($_ =~ /^$self->{subpartitions_default}{$table}{$part_name}{name}$/i, @{$self->{allow_partition}}))
 					{
 						if ($self->{file_per_table} && !$self->{pg_dsn})
 						{
 							# Do not dump data again if the file already exists
-							if (!$self->file_exists("$dirprefix$self->{subpartitions_default}{$table}{$part_name}_$self->{output}"))
+							if (!$self->file_exists("$dirprefix$self->{subpartitions_default}{$table}{$part_name}{name}_$self->{output}"))
 							{
-								$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{subpartitions_default}{$table}{$part_name}, 1);
+								if ($self->{rename_partition}) {
+									$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{subpartitions_default}{$table}{$part_name}{name}, 1, $tbpart_name, $tbpart_name . '_subpart_default');
+								} else {
+									$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{subpartitions_default}{$table}{$part_name}{name}, 1, $part_name, $self->{subpartitions_default}{$table}{$part_name}{name});
+								}
 							}
 						}
 						else
 						{
-							$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{subpartitions_default}{$table}{$part_name}, 1);
+							if ($self->{rename_partition}) {
+								$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{subpartitions_default}{$table}{$part_name}{name}, 1, $tbpart_name, $tbpart_name . '_subpart_default');
+							} else {
+								$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{subpartitions_default}{$table}{$part_name}{name}, 1, $part_name, $self->{subpartitions_default}{$table}{$part_name}{name});
+							}
 						}
 					}
 					# Rename temporary filename into final name
-					$self->rename_dump_partfile($dirprefix, $self->{subpartitions_default}{$table}{$part_name}, $table);
+					$self->rename_dump_partfile($dirprefix, $self->{subpartitions_default}{$table}{$part_name}{name}, $table);
 				}
 			}
 			else
@@ -4076,31 +4326,31 @@ sub _export_table_data
 				}
 
 				$self->logit("Dumping partition table $table ($part_name)...\n", 1);
-				$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $part_name);
+				$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $part_name, 0, $tbpart_name, $sub_tb_name);
 				# Rename temporary filename into final name
 				$self->rename_dump_partfile($dirprefix, $part_name, $table);
 			}
 		}
 
 		# Now load content of the default partition table
-		if ($self->{partitions_default}{$table})
+		if (exists $self->{partitions_default}{$table})
 		{
-			if (!$self->{allow_partition} || grep($_ =~ /^$self->{partitions_default}{$table}$/i, @{$self->{allow_partition}}))
+			if (!$self->{allow_partition} || grep($_ =~ /^$self->{partitions_default}{$table}{name}$/i, @{$self->{allow_partition}}))
 			{
 				if ($self->{file_per_table} && !$self->{pg_dsn})
 				{
 					# Do not dump data again if the file already exists
-					if (!$self->file_exists("$dirprefix$self->{partitions_default}{$table}_$self->{output}"))
+					if (!$self->file_exists("$dirprefix$self->{partitions_default}{$table}{name}_$self->{output}"))
 					{
-						$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{partitions_default}{$table});
+						$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{partitions_default}{$table}{name}, 0, $tbpart_name, $sub_tb_name);
 					}
 				}
 				else
 				{
-					$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{partitions_default}{$table});
+					$total_record = $self->_dump_table($dirprefix, $sql_header, $table, $self->{partitions_default}{$table}{name}, 0, $tbpart_name, $sub_tb_name);
 				}
 				# Rename temporary filename into final name
-				$self->rename_dump_partfile($dirprefix, $self->{partitions_default}{$table}, $table);
+				$self->rename_dump_partfile($dirprefix, $self->{partitions_default}{$table}{name}, $table);
 			}
 		}
 	}
@@ -4202,11 +4452,12 @@ sub rename_dump_partfile
 {
 	my ($self, $dirprefix, $partname, $tbl) = @_;
 
-	my $filename = "${dirprefix}tmp_${partname}_$self->{output}";
-	my $filedest = "${dirprefix}${partname}_$self->{output}";
-	if ($tbl && $self->{prefix_partition}) {
-		$filename = "${dirprefix}tmp_${tbl}_${partname}_$self->{output}";
-		$filedest = "${dirprefix}${tbl}_${partname}_$self->{output}";
+        my $filename = "${dirprefix}tmp_${tbl}_${partname}_$self->{output}";
+        my $filedest = "${dirprefix}${tbl}_${partname}_$self->{output}";
+        if (!$tbl)
+        {
+                $filename = "${dirprefix}tmp_${partname}_$self->{output}";
+                $filedest = "${dirprefix}${partname}_$self->{output}";
 	}
 	if (-e $filename) {
 		$self->logit("Renaming temporary file $filename into $filedest\n", 1);
@@ -4232,7 +4483,8 @@ sub translate_function
 	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
 
 	# Clear memory in multiprocess mode
-	if ($self->{jobs} > 1) {
+	if ($self->{jobs} > 1)
+	{
 		$self->{functions} = (); 
 		$self->{procedures} = (); 
 	}
@@ -4252,12 +4504,14 @@ sub translate_function
 		}
 		$fct_count++;
 		$self->logit("Dumping function $fct...\n", 1);
-		if ($self->{file_per_function}) {
+		if ($self->{file_per_function})
+		{
 			my $f = "$dirprefix${fct}_$self->{output}";
 			$f =~ s/\.(?:gz|bz2)$//i;
 			$self->dump("\\i$self->{psql_relative_path} $f\n");
 			$self->save_filetoupdate_list("ORA2PG_$self->{type}", lc($fct), "$dirprefix${fct}_$self->{output}");
-		} else {
+		}
+		else {
 			$self->save_filetoupdate_list("ORA2PG_$self->{type}", lc($fct), "$dirprefix$self->{output}");
 		}
 
@@ -4285,7 +4539,7 @@ sub translate_function
 				$sql_output .= $sql_f . "\n\n";
 				if ($self->{estimate_cost})
 				{
-					my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $sql_f);
+					my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $sql_f, 'FUNCTION');
 					$cost += $Ora2Pg::PLSQL::OBJECT_SCORE{'FUNCTION'};
 					$lcost += $cost;
 					$self->logit("Function ${fct} estimated cost: $cost\n", 1);
@@ -4321,7 +4575,7 @@ sub translate_function
 		}
 
 		my $sql_header = "-- Generated by Ora2Pg, the Oracle database Schema converter, version $VERSION\n";
-		$sql_header .= "-- Copyright 2000-2022 Gilles DAROLD. All rights reserved.\n";
+		$sql_header .= "-- Copyright 2000-2023 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";
@@ -4353,7 +4607,7 @@ sub _replace_declare_var
 
 	if ($$code =~ s/\b(DECLARE\s+(?:.*?)\s+BEGIN)/\%DECLARE\%/is) {
 		my $declare = $1;
-		# Collect user defined function
+		# Collect user defined exception
 		while ($declare =~ s/\b([^\s]+)\s+EXCEPTION\s*;//i) {
 			my $e = lc($1);
 			if (!exists $Ora2Pg::PLSQL::EXCEPTION_MAP{"\U$e\L"} && !grep(/^$e$/, values %Ora2Pg::PLSQL::EXCEPTION_MAP) && !exists $self->{custom_exception}{$e}) {
@@ -4406,7 +4660,7 @@ sub _set_file_header
 	return '' if ($self->{no_header});
 
 	my $sql_header = "-- Generated by Ora2Pg, the Oracle database Schema converter, version $VERSION\n";
-	$sql_header .= "-- Copyright 2000-2022 Gilles DAROLD. All rights reserved.\n";
+	$sql_header .= "-- Copyright 2000-2023 Gilles DAROLD. All rights reserved.\n";
 	$sql_header .= "-- DATASOURCE: $self->{oracle_dsn}\n\n";
 	if ($self->{client_encoding})
 	{
@@ -4749,12 +5003,19 @@ LANGUAGE plpgsql ;
 		{
 			$sql_output .= "DROP MATERIALIZED VIEW $self->{pg_supports_ifexists} $view;\n" if ($self->{drop_if_exists});
 			$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});
+			if (!$self->{is_mysql} && !$self->{is_mssql})
+			{
+				$sql_output .= "BUILD $self->{materialized_views}{$view}{build_mode}\n";
+				$sql_output .= "REFRESH $self->{materialized_views}{$view}{refresh_method} ON $self->{materialized_views}{$view}{refresh_mode}\n";
+				$sql_output .= "ENABLE QUERY REWRITE" if ($self->{materialized_views}{$view}{rewritable});
+				$sql_output .= "AS ";
+			}
+			$sql_output .= "$self->{materialized_views}{$view}{text}";
+			if (!$self->{is_mysql} && !$self->{is_mssql})
+			{
+				$sql_output .= " USING INDEX" if ($self->{materialized_views}{$view}{no_index});
+				$sql_output .= " USING NO INDEX" if (!$self->{materialized_views}{$view}{no_index});
+			}
 			$sql_output .= ";\n\n";
 
 			# Set the index definition
@@ -4773,6 +5034,9 @@ LANGUAGE plpgsql ;
 			{
 				$sql_output .= "DROP VIEW $self->{pg_supports_ifexists} \L$view\E_mview;\n" if ($self->{drop_if_exists});
 				$sql_output .= "CREATE VIEW \L$view\E_mview AS\n";
+				if ($self->{is_mssql}) {
+					$self->{materialized_views}{$view}{text} =~ s/^(.*?)\s+AS\s+//is;
+				}
 				$sql_output .= $self->{materialized_views}{$view}{text};
 				$sql_output .= ";\n\n";
 				$sql_output .= "SELECT create_materialized_view('\L$view\E','\L$view\E_mview', change with the name of the colum to used for the index);\n\n\n";
@@ -4789,6 +5053,9 @@ LANGUAGE plpgsql ;
 			{
 				$sql_output .= "DROP MATERIALIZED VIEW $self->{pg_supports_ifexists} \L$view\E;\n" if ($self->{drop_if_exists});
 				$sql_output .= "CREATE MATERIALIZED VIEW \L$view\E AS\n";
+				if ($self->{is_mssql}) {
+					$self->{materialized_views}{$view}{text} =~ s/^(.*?)\s+AS\s+//is;
+				}
 				$sql_output .= $self->{materialized_views}{$view}{text};
 				if ($self->{materialized_views}{$view}{build_mode} eq 'DEFERRED') {
 					$sql_output .= " WITH NO DATA";
@@ -4858,6 +5125,7 @@ sub export_grant
 	foreach my $table (sort {"$self->{grants}{$a}{type}.$a" cmp "$self->{grants}{$b}{type}.$b" } keys %{$self->{grants}}) {
 		my $realtable = lc($table);
 		my $obj = $self->{grants}{$table}{type} || 'TABLE';
+		$obj =~ s/ (PARTITION|SUBPARTITION)//i;
 		if ($self->{export_schema} && $self->{schema}) {
 			$realtable = $self->quote_object_name("$self->{schema}.$table");
 		} elsif ($self->{preserve_case}) {
@@ -4994,16 +5262,26 @@ sub export_sequence
 		$cycle = ' CYCLE' if ($self->{sequences}{$seq}->[6] eq 'Y');
 		$sql_output .= "DROP SEQUENCE $self->{pg_supports_ifexists} " . $self->quote_object_name($seq) . ";\n" if ($self->{drop_if_exists});
 		$sql_output .= "CREATE SEQUENCE " . $self->quote_object_name($seq) . " INCREMENT $self->{sequences}{$seq}->[3]";
-		if ($self->{sequences}{$seq}->[1] eq '' || $self->{sequences}{$seq}->[1] < (-2**63-1)) {
+		if ($self->{sequences}{$seq}->[1] eq '' || $self->{sequences}{$seq}->[1] <= (-2**63/2)) {
+			$sql_output .= " NO MINVALUE";
+		# MSSQL has 32 bit sequences
+		} elsif ($self->{sequences}{$seq}->[1] eq '' || $self->{sequences}{$seq}->[1] <= (-2**32/2)) {
 			$sql_output .= " NO MINVALUE";
 		} else {
 			$sql_output .= " MINVALUE $self->{sequences}{$seq}->[1]";
 		}
 		# Max value lower than start value are not allowed
 		if (($self->{sequences}{$seq}->[2] > 0) && ($self->{sequences}{$seq}->[2] < $self->{sequences}{$seq}->[4])) {
-			$self->{sequences}{$seq}->[2] = $self->{sequences}{$seq}->[4];
+			if (!$cycle) {
+				$self->{sequences}{$seq}->[2] = $self->{sequences}{$seq}->[4];
+			} else {
+				$self->{sequences}{$seq}->[4] = $self->{sequences}{$seq}->[1];
+			}
 		}
-		if ($self->{sequences}{$seq}->[2] eq '' || $self->{sequences}{$seq}->[2] > (2**63-1)) {
+		if ($self->{sequences}{$seq}->[2] eq '' || $self->{sequences}{$seq}->[2] >= (2**63/2)-1) {
+			$sql_output .= " NO MAXVALUE";
+		# MSSQL has 32 bit sequences
+		} elsif ($self->{sequences}{$seq}->[2] eq '' || $self->{sequences}{$seq}->[2] >= (2**32/2)-1) {
 			$sql_output .= " NO MAXVALUE";
 		} else {
 			$self->{sequences}{$seq}->[2] = 9223372036854775807 if ($self->{sequences}{$seq}->[2] > 9223372036854775807);
@@ -5013,7 +5291,8 @@ sub export_sequence
 		$sql_output .= " CACHE $cache" if ($cache ne '');
 		$sql_output .= "$cycle;\n";
 
-		if ($self->{force_owner}) {
+		if ($self->{force_owner})
+		{
 			my $owner = $self->{sequences}{$seq}->[7];
 			$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
 			$sql_output .= "ALTER SEQUENCE " . $self->quote_object_name($seq)
@@ -5033,6 +5312,51 @@ sub export_sequence
 	return;
 }
 
+=head2 export_sequence_values
+
+Export Oracle sequence last values into PostgreSQL compatible SQL statements.
+
+=cut
+
+sub export_sequence_values
+{
+	my $self = shift;
+
+	my $sql_header = $self->_set_file_header();
+	my $sql_output = $self->set_search_path() . "\n";
+
+	$self->logit("Add sequences last values...\n", 1);
+
+	# Read DML from file if any
+	if ($self->{input_file}) {
+		$self->read_sequence_from_file();
+	}
+	my $i = 1;
+	my $num_total_sequence = scalar keys %{$self->{sequences}};
+	my $count_seq = 0;
+	my $PGBAR_REFRESH = set_refresh_count($num_total_sequence);
+	foreach my $seq (sort keys %{$self->{sequences}})
+	{
+		if (!$self->{quiet} && !$self->{debug} && ($count_seq % $PGBAR_REFRESH) == 0) {
+			print STDERR $self->progress_bar($i, $num_total_sequence, 25, '=', 'sequences', "generating $seq" ), "\r";
+		}
+		$count_seq++;
+		$sql_output .= "ALTER SEQUENCE " . $self->quote_object_name($seq) . " START WITH $self->{sequences}{$seq}->[4];\n";
+		$i++;
+	}
+	if (!$self->{quiet} && !$self->{debug}) {
+		print STDERR $self->progress_bar($i - 1, $num_total_sequence, 25, '=', 'sequences', 'end of output.'), "\n";
+	}
+	if (!$sql_output) {
+		$sql_output = "-- Nothing found of type $self->{type}\n" if (!$self->{no_header});
+	}
+
+	$self->dump($sql_header . $sql_output);
+
+	return;
+}
+
+
 =head2 export_dblink
 
 Export Oracle sequence into PostgreSQL compatible SQL statements.
@@ -5063,14 +5387,25 @@ sub export_dblink
 		my $srv_name = $self->quote_object_name($db);
 		$srv_name =~ s/^.*\.//;
 		$sql_output .= "CREATE SERVER $srv_name";
-		if (!$self->{is_mysql}) {
-			$sql_output .= " FOREIGN DATA WRAPPER oracle_fdw OPTIONS (dbserver '$self->{dblink}{$db}{host}');\n";
-		} else {
+		if ($self->{is_mysql}) {
 			$sql_output .= " FOREIGN DATA WRAPPER mysql_fdw OPTIONS (host '$self->{dblink}{$db}{host}'";
 			$sql_output .= ", port '$self->{dblink}{$db}{port}'" if ($self->{dblink}{$db}{port});
 			$sql_output .= ");\n";
 		}
-		if ($self->{dblink}{$db}{password} ne 'NONE') {
+		elsif ($self->{is_mssql})
+		{
+			$self->{oracle_dsn} =~ /driver=([^;]+);/;
+			my $driver = $1 || 'msodbcsql18';
+			$sql_output .= " FOREIGN DATA WRAPPER odbc_fdw OPTIONS (odbc_driver '$driver', odbc_server '$self->{dblink}{$db}{host}'";
+			$sql_output .= ", odbc_port '$self->{dblink}{$db}{port}'" if ($self->{dblink}{$db}{port});
+			$sql_output .= ");\n";
+		}
+		else
+		{
+			$sql_output .= " FOREIGN DATA WRAPPER oracle_fdw OPTIONS (dbserver '$self->{dblink}{$db}{host}');\n";
+		}
+		if ($self->{dblink}{$db}{password} ne 'NONE')
+		{
 			$self->{dblink}{$db}{password} ||= 'secret';
 			$self->{dblink}{$db}{password} = ", password '$self->{dblink}{$db}{password}'";
 		}
@@ -5152,6 +5487,21 @@ sub export_directory
 	return;
 }
 
+sub _replace_sql_type
+{
+	my ($self, $str) = @_;
+
+	if ($self->{is_mysql}) {
+		$str = Ora2Pg::MySQL::replace_sql_type($self, $str);
+	} elsif ($self->{is_mssql}) {
+		$str = Ora2Pg::MSSQL::replace_sql_type($self, $str);
+	} else {
+		$str = Ora2Pg::PLSQL::replace_sql_type($self, $str);
+	}
+
+	return $str;
+}
+
 =head2 export_trigger
 
 Export Oracle trigger into PostgreSQL compatible SQL statements.
@@ -5189,7 +5539,7 @@ sub export_trigger
 		if ($self->{file_per_function})
 		{
 			my $schm = '';
-			$schm = $trig->[8] . '-' if ($self->{export_schema} && !$self->{schema});
+			$schm = $trig->[8] . '-' if ($trig->[8] && $self->{export_schema} && !$self->{schema});
 			my $f = "$dirprefix$schm$trig->[0]_$self->{output}";
 			$f =~ s/\.(?:gz|bz2)$//i;
 			$self->dump("\\i$self->{psql_relative_path} $f\n");
@@ -5207,6 +5557,8 @@ sub export_trigger
 
 		$trig->[4] =~ s/([^\*])[;\/]$/$1/;
 
+		# reordering of event when there is a OF keyword to specify columns
+		$trig->[2] =~ s/(\s+OR\s+UPDATE)(\s+.*)(\s+OF\s+)/$2$1$3/i;
 		$self->logit("\tDumping trigger $trig->[0] defined on table $trig->[3]...\n", 1);
 		my $tbname = $self->get_replaced_tbname($trig->[3]);
 
@@ -5301,16 +5653,11 @@ sub export_trigger
 						$trig->[4] =~ s/\b(END[;]*)[\s\/]*$/  END;\n$1/is;
 					}
 					# Add return statement.
-					$trig->[4] =~ s/\b(END[;]*)(\s*\%ORA2PG_COMMENT\d+\%\s*)?[\s\/]*$/$ret_kind\n$1$2/igs;
+					$trig->[4] =~ s/(?:$ret_kind\s+)?\b(END[;]*)(\s*\%ORA2PG_COMMENT\d+\%\s*)?[\s\/]*$/$ret_kind\n$1$2/igs;
 					# Look at function header to convert sql type
 					my @parts = split(/BEGIN/i, $trig->[4]);
-					if ($#parts > 0)
-					{
-						if (!$self->{is_mysql}) {
-							$parts[0] = Ora2Pg::PLSQL::replace_sql_type($parts[0], $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, $self->{varchar_to_text}, %{$self->{data_type}});
-						} else {
-							$parts[0] = Ora2Pg::MySQL::replace_sql_type($parts[0], $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, $self->{varchar_to_text}, %{$self->{data_type}});
-						}
+					if ($#parts > 0) {
+						$parts[0] = $self->_replace_sql_type($parts[0]);
 					}
 					$trig->[4] = join('BEGIN', @parts);
 					$trig->[4] =~ s/\bRETURN\s*;/$ret_kind/igs;
@@ -5327,6 +5674,7 @@ sub export_trigger
 				$revoke = "-- REVOKE ALL ON FUNCTION $trig_fctname() FROM PUBLIC;\n";
 			}
 			$security = " SECURITY INVOKER" if ($self->{force_security_invoker});
+			$trig->[4] =~ s/CREATE TRIGGER (.*?)\sAS\s+//s;
 			if ($self->{pg_supports_when} && $trig->[5])
 			{
 				if (!$self->{preserve_case})
@@ -5348,12 +5696,15 @@ sub export_trigger
 					$trig->[6] =~ s/"([^"]+)"/\L$1\E/gs;
 				}
 				chomp($trig->[6]);
-				# Remove referencing clause, not supported by PostgreSQL
-				$trig->[6] =~ s/REFERENCING\s+(.*?)(FOR\s+EACH\s+)/$2/is;
+				# Remove referencing clause, not supported by PostgreSQL < 10
+				if ($self->{pg_version} < 10) {
+					$trig->[6] =~ s/REFERENCING\s+(.*?)(FOR\s+EACH\s+)/$2/is;
+				}
 				$trig->[6] =~ s/^\s*["]*(?:$trig->[0])["]*//is;
 				$trig->[6] =~ s/\s+ON\s+([^"\s]+)\s+/" ON " . $self->quote_object_name($1) . " "/ies;
 				$sql_output .= "DROP TRIGGER $self->{pg_supports_ifexists} " . $self->quote_object_name($trig->[0]) . " ON " . $self->quote_object_name($1) . ";\n" if ($self->{drop_if_exists});
 				$sql_output .= "CREATE TRIGGER " . $self->quote_object_name($trig->[0]) . "$trig->[6]\n";
+
 				if ($trig->[5])
 				{
 					$self->_remove_comments(\$trig->[5]);
@@ -5365,6 +5716,9 @@ sub export_trigger
 					}
 					$sql_output .= "\tWHEN ($trig->[5])\n";
 				}
+				if ($trig->[6] =~ /REFERENCING/) {
+					$sql_output .= "$trig->[6] ";
+				}
 				$sql_output .= "\tEXECUTE PROCEDURE $trig_fctname();\n\n";
 			}
 			else
@@ -5386,7 +5740,15 @@ sub export_trigger
 				my $statement = 0;
 				$statement = 1 if ($trig->[1] =~ s/ STATEMENT//);
 				$sql_output .= "$trig->[1] $trig->[2]$cols ON " . $self->quote_object_name($tbname) . " ";
-				if ($statement) {
+				if ($trig->[6] =~ /REFERENCING/) {
+					$sql_output .= "$trig->[6] ";
+				}
+				if ($self->{is_mssql}) {
+					my $reftb= "REFERENCING OLD TABLE AS Deleted NEW TABLE AS Inserted";
+					$reftb =~ s/OLD TABLE AS Deleted // if ($trig->[2] eq 'INSERT');
+					$reftb =~ s/NEW TABLE AS Inserted // if ($trig->[2] eq 'DELETE');
+					$sql_output .= "$reftb FOR EACH STATEMENT\n";
+				} elsif ($statement) {
 					$sql_output .= "FOR EACH STATEMENT\n";
 				} else {
 					$sql_output .= "FOR EACH ROW\n";
@@ -5753,13 +6115,38 @@ sub export_function
 	my $i = 0;
 	foreach my $key ( sort keys %{$self->{functions}} )
 	{
+		if ($self->{print_dependencies} && $self->{plsql_pgsql} && !$self->{no_function_metadata})
+		{
+			my $plsql_code = $self->{functions}{$key}{text};
+			$plsql_code =~ s/FUNCTION $key//i;
+			$self->_remove_comments(\$plsql_code);
+			# look for other routines call in the stored function
+			foreach my $sch (sort keys %{ $self->{function_metadata} })
+			{
+				foreach my $pkg_name (sort keys %{ $self->{function_metadata}{$sch} })
+				{
+					foreach my $fname (sort keys %{ $self->{function_metadata}{$sch}{$pkg_name} })
+					{
+						next if ($key =~ /^$fname$/i || $key =~ /^.*\.$fname$/i);
+						if ($plsql_code =~ /\b$fname\b/is)
+						{
+							push(@{ $self->{object_dependencies}{uc("$self->{functions}{$key}{owner}.$key")}{routines} }, uc("$sch.$fname"));
+						}
+					}
+				}
+			}
+			# Look for merge/insert/update/delete
+			@{ $self->{object_dependencies}{uc("$self->{functions}{$key}{owner}.$key")}{merge} } = $plsql_code =~ /\bMERGE\s+INTO\s+([^\(\s;,]+)/igs;
+			@{ $self->{object_dependencies}{uc("$self->{functions}{$key}{owner}.$key")}{insert} } = $plsql_code =~ /\bINSERT\s+INTO\s+([^\(\s;,]+)/igs;
+			@{ $self->{object_dependencies}{uc("$self->{functions}{$key}{owner}.$key")}{update} } = $plsql_code =~ /(?:(?!FOR).)*?\s*\bUPDATE\s+([^\s;,]+)\s+/igs;
+			@{ $self->{object_dependencies}{uc("$self->{functions}{$key}{owner}.$key")}{delete} } = $plsql_code =~ /\b(?:DELETE\s+FROM|TRUNCATE\s+TABLE)\s+([^\s;,]+)\s+/igs;
+		}
 		$fct_group[$i++]{$key} = $self->{functions}{$key};
 		$i = 0 if ($i == $num_chunk);
 	}
 	my $num_cur_fct = 0;
 	for ($i = 0; $i <= $#fct_group; $i++)
 	{
-
 		if ($self->{jobs} > 1) {
 			$self->logit("Creating a new process to translate functions...\n", 1);
 			spawn sub {
@@ -5819,6 +6206,29 @@ sub export_function
 
 	$self->dump($sql_output);
 
+	if (scalar keys %{ $self->{object_dependencies} } > 0)
+	{
+		my $sp_tree = "object_type;object_name;routines_called;insert;update;delete;merge\n";
+		foreach my $caller ( sort keys %{ $self->{object_dependencies} } )
+		{
+			$sp_tree .= "FUNCTION;$caller";
+			$sp_tree .= ";";
+			foreach my $sp (@{ $self->{object_dependencies}{$caller}{routines} }) {
+				my $star = ($#{ $self->{object_dependencies}{$sp}{routines} } >= 0) ? '*' : '';
+				$sp_tree .= "$sp$star,";
+			}
+			$sp_tree =~ s/,$//;
+			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{insert} });
+			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{update} });
+			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{delete} });
+			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{merge} });
+			$sp_tree .= "\n";
+		}
+		my $fhdl = $self->open_export_file("functions_dependencies.csv");
+		$self->dump($sp_tree, $fhdl);
+		$self->close_export_file($fhdl);
+	}
+
 	$self->{functions} = ();
 
 	my $t1 = Benchmark->new;
@@ -5963,13 +6373,41 @@ sub export_procedure
 	my $num_chunk = $self->{jobs} || 1;
 	my @fct_group = ();
 	my $i = 0;
-	foreach my $key (sort keys %{$self->{procedures}} ) {
+	foreach my $key (sort keys %{$self->{procedures}} )
+	{
+		if ($self->{print_dependencies} && $self->{plsql_pgsql} && !$self->{no_function_metadata})
+		{
+			my $plsql_code = $self->{procedures}{$key}{text};
+			$plsql_code =~ s/FUNCTION $key//i;
+			$self->_remove_comments(\$plsql_code);
+			# look for other routines call in the stored procedure
+			foreach my $sch (sort keys %{ $self->{function_metadata} })
+			{
+				foreach my $pkg_name (sort keys %{ $self->{function_metadata}{$sch} })
+				{
+					foreach my $fname (sort keys %{ $self->{function_metadata}{$sch}{$pkg_name} })
+					{
+						next if ($key =~ /^$fname$/i || $key =~ /^.*\.$fname$/i);
+						if ($plsql_code =~ /\b$fname\b/is) {
+							push(@{ $self->{object_dependencies}{uc("$self->{procedures}{$key}{owner}.$key")}{routines} }, uc("$sch.$fname"));
+						}
+					}
+				}
+			}
+			# Look for merge/insert/update/delete
+			@{ $self->{object_dependencies}{uc("$self->{procedures}{$key}{owner}.$key")}{merge} } = $plsql_code =~ /\bMERGE\s+INTO\s+([^\(\s;,]+)/igs;
+			@{ $self->{object_dependencies}{uc("$self->{procedures}{$key}{owner}.$key")}{insert} } = $plsql_code =~ /\bINSERT\s+INTO\s+([^\(\s;,]+)/igs;
+			@{ $self->{object_dependencies}{uc("$self->{procedures}{$key}{owner}.$key")}{update} } = $plsql_code =~ /(?:(?!FOR).)*?\s*\bUPDATE\s+([^\s;,]+)\s+/igs;
+			@{ $self->{object_dependencies}{uc("$self->{procedures}{$key}{owner}.$key")}{delete} } = $plsql_code =~ /\b(?:DELETE\s+FROM|TRUNCATE\s+TABLE)\s+([^\s;,]+)\s+/igs;
+		}
 		$fct_group[$i++]{$key} = $self->{procedures}{$key};
 		$i = 0 if ($i == $num_chunk);
 	}
 	my $num_cur_fct = 0;
-	for ($i = 0; $i <= $#fct_group; $i++) {
-		if ($self->{jobs} > 1) {
+	for ($i = 0; $i <= $#fct_group; $i++)
+	{
+		if ($self->{jobs} > 1)
+		{
 			$self->logit("Creating a new process to translate procedures...\n", 1);
 			spawn sub {
 				$self->translate_function($num_cur_fct, $num_total_function, %{$fct_group[$i]});
@@ -5986,20 +6424,26 @@ sub export_procedure
 	}
 
 	# Wait for all oracle connection terminaison
-	if ($self->{jobs} > 1) {
-		while ($parallel_fct_count) {
+	if ($self->{jobs} > 1)
+	{
+		while ($parallel_fct_count)
+		{
 			my $kid = waitpid(-1, WNOHANG);
-			if ($kid > 0) {
+			if ($kid > 0)
+			{
 				$parallel_fct_count--;
 				delete $RUNNING_PIDS{$kid};
 			}
 			usleep(50000);
 		}
-		if ($self->{estimate_cost}) {
+		if ($self->{estimate_cost})
+		{
 			my $tfh = $self->read_export_file($dirprefix . 'temp_cost_file.dat');
 			flock($tfh, 2) || die "FATAL: can't lock file temp_cost_file.dat\n";
-			if (defined $tfh) {
-				while (my $l = <$tfh>) {
+			if (defined $tfh)
+			{
+				while (my $l = <$tfh>)
+				{
 					chomp($l);
 					my ($fname, $fsize, $fcost) = split(/:/, $l);
 					$total_size += $fsize;
@@ -6014,7 +6458,8 @@ sub export_procedure
 	{
 		print STDERR $self->progress_bar($num_cur_fct, $num_total_function, 25, '=', 'procedures', 'end of procedures export.'), "\n";
 	}
-	if ($self->{estimate_cost}) {
+	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)."."
@@ -6030,6 +6475,29 @@ sub export_procedure
 
 	$self->dump($sql_output);
 
+	if (scalar keys %{ $self->{object_dependencies} } > 0)
+	{
+		my $sp_tree = "object_type;object_name;routines_called;insert;update;delete;merge\n";
+		foreach my $caller ( sort keys %{ $self->{object_dependencies} } )
+		{
+			$sp_tree .= "PROCEDURE;$caller";
+			$sp_tree .= ";";
+			foreach my $sp (@{ $self->{object_dependencies}{$caller}{routines} }) {
+				my $star = ($#{ $self->{object_dependencies}{$sp}{routines} } >= 0) ? '*' : '';
+				$sp_tree .= "$sp$star,";
+			}
+			$sp_tree =~ s/,$//;
+			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{insert} });
+			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{update} });
+			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{delete} });
+			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{merge} });
+			$sp_tree .= "\n";
+		}
+		my $fhdl = $self->open_export_file("procedures_dependencies.csv");
+		$self->dump($sp_tree, $fhdl);
+		$self->close_export_file($fhdl);
+	}
+
 	$self->{procedures} = ();
 
 	my $t1 = Benchmark->new;
@@ -6232,7 +6700,7 @@ sub export_package
 					next if (!$f);
 					my @cnt = $infos{$f}{code} =~ /(\%ORA2PG_COMMENT\d+\%)/i;
 					$total_size_no_comment += (length($infos{$f}{code}) - (17 * length(join('', @cnt))));
-					my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $infos{$f}{code});
+					my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $infos{$f}{code}, $infos{$f}{type});
 					$self->logit("Function $f estimated cost: $cost\n", 1);
 					$cost_value += $cost;
 					$number_fct++;
@@ -6295,8 +6763,32 @@ sub export_package
 
 	$self->dump($sql_output);
 
+	if (scalar keys %{ $self->{object_dependencies} } > 0)
+	{
+		my $sp_tree = "object_type;object_name;routines_called;insert;update;delete;merge\n";
+		foreach my $caller ( sort keys %{ $self->{object_dependencies} } )
+		{
+			$sp_tree .= "PACKAGE;$caller";
+			$sp_tree .= ";";
+			foreach my $sp (@{ $self->{object_dependencies}{$caller}{routines} }) {
+				my $star = ($#{ $self->{object_dependencies}{$sp}{routines} } >= 0) ? '*' : '';
+				$sp_tree .= "$sp$star,";
+			}
+			$sp_tree =~ s/,$//;
+			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{insert} });
+			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{update} });
+			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{delete} });
+			$sp_tree .= ";" . join(',', @{ $self->{object_dependencies}{$caller}{merge} });
+			$sp_tree .= "\n";
+		}
+		my $fhdl = $self->open_export_file("packages_dependencies.csv");
+		$self->dump($sp_tree, $fhdl);
+		$self->close_export_file($fhdl);
+	}
+
 	$self->{packages} = ();
 	$sql_output = '';
+
 	# Create file to load custom variable initialization into postgresql.conf
 	if (scalar keys %{$self->{global_variables}})
 	{
@@ -6379,6 +6871,7 @@ sub export_type
 		if ($self->{plsql_pgsql}) {
 			$tpe->{code} = $self->_convert_type($tpe->{code}, $tpe->{owner});
 		} else {
+			$tpe->{code} =~ s/^CREATE TYPE/TYPE/i;
 			if ($tpe->{code} !~ /^SUBTYPE\s+/) {
 				$tpe->{code} = "CREATE$self->{create_or_replace} $tpe->{code}\n";
 			}
@@ -6430,7 +6923,7 @@ sub export_tablespace
 	my $dirprefix = '';
 	foreach my $tb_type (sort keys %{$self->{tablespaces}})
 	{
-		next if ($tb_type eq 'INDEX PARTITION' || $tb_type eq 'TABLE PARTITION');
+		#next if ($tb_type eq 'INDEX PARTITION' || $tb_type eq 'TABLE PARTITION');
 		# TYPE - TABLESPACE_NAME - FILEPATH - OBJECT_NAME
 		foreach my $tb_name (sort keys %{$self->{tablespaces}{$tb_type}})
 		{
@@ -6454,6 +6947,7 @@ sub export_tablespace
 				}
 				push(@done, $tb_name);
 				foreach my $obj (@{$self->{tablespaces}{$tb_type}{$tb_name}{$tb_path}}) {
+					$tb_type =~ s/ PARTITION//;
 					next if ($self->{file_per_index} && ($tb_type eq 'INDEX'));
 					$sql_output .= "ALTER $tb_type " . $self->quote_object_name($obj)
 							. " SET TABLESPACE " . $self->quote_object_name($tb_name) . ";\n";
@@ -6489,6 +6983,7 @@ sub export_tablespace
 					$loc = $1 . $loc;
 					foreach my $obj (@{$self->{tablespaces}{$tb_type}{$tb_name}{$tb_path}})
 					{
+						$tb_type =~ s/ PARTITION//;
 						next if ($tb_type eq 'TABLE');
 						$sql_output .= "ALTER $tb_type \L$obj\E SET TABLESPACE \L$tb_name\E;\n";
 					}
@@ -6605,9 +7100,12 @@ BEGIN
 			my $create_table_tmp = '';
 			my $create_table_index_tmp = '';
 			my $tb_name = '';
-			if ($self->{prefix_partition}) {
-				$tb_name = $table . "_" . $part;
+			if ($self->{rename_partition}) {
+				$tb_name = $table . '_part' . $pos;
 			} else {
+				if ($tb_name eq 'default') {
+					$tb_name = $table . '_default';
+				}
 				if ($self->{export_schema} && !$self->{schema} && ($table =~ /^([^\.]+)\./)) {
 					$tb_name =  $1 . '.' . $part;
 				} else {
@@ -6746,9 +7244,12 @@ BEGIN
 					{
 						my ($idx, $fts_idx) = $self->_create_indexes($table, 0, %{$self->{tables}{$table}{indexes}});
 						my $tb_name2 = $self->quote_object_name($tb_name);
-						$create_table_index_tmp .= "CREATE INDEX "
+						if ($cindx)
+						{
+							$create_table_index_tmp .= "CREATE INDEX "
 								. $self->quote_object_name("${tb_name}_$colname$pos")
 								. " ON " . $self->quote_object_name($tb_name) . " ($cindx);\n";
+						}
 						if ($idx || $fts_idx)
 						{
 							$idx =~ s/ $table/ $tb_name2/igs;
@@ -6790,16 +7291,24 @@ BEGIN
 						}
 					}
 				}
-				my $deftb = '';
-				$deftb = "${table}_" if ($self->{prefix_partition});
-				if ($self->{partitions_default}{$table} && ($create_table{$table}{index} !~ /ON $deftb$self->{partitions_default}{$table} /))
+				my $deftb = $self->{partitions_default}{$table}{name};
+				$deftb = $table . '_part_default' if ($self->{rename_partition});
+				# Reproduce indexes definition from the main table before PG 11
+				# after they are automatically created on partition tables
+				if ($self->{pg_version} < 11)
 				{
-					$cindx = $self->{partitions}{$table}{$pos}{info}[$i]->{column} || '';
-					$cindx = lc($cindx) if (!$self->{preserve_case});
-					$cindx = Ora2Pg::PLSQL::convert_plsql_code($self, $cindx);
-					$create_table_index_tmp .= "CREATE INDEX " . $self->quote_object_name("$deftb$self->{partitions_default}{$table}_$colname") . " ON " . $self->quote_object_name("$deftb$self->{partitions_default}{$table}") . " ($cindx);\n";
+					if (exists $self->{partitions_default}{$table} && ($create_table{$table}{index} !~ /ON $deftb /))
+					{
+						$cindx = $self->{partitions}{$table}{$pos}{info}[$i]->{column} || '';
+						$cindx = lc($cindx) if (!$self->{preserve_case});
+						$cindx = Ora2Pg::PLSQL::convert_plsql_code($self, $cindx);
+						if ($cindx)
+						{
+							$create_table_index_tmp .= "CREATE INDEX " . $self->quote_object_name($deftb . '_' . $colname) . " ON " . $self->quote_object_name($deftb) . " ($cindx);\n";
+						}
+					}
+					push(@ind_col, $self->{partitions}{$table}{$pos}{info}[$i]->{column}) if (!grep(/^$self->{partitions}{$table}{$pos}{info}[$i]->{column}$/, @ind_col));
 				}
-				push(@ind_col, $self->{partitions}{$table}{$pos}{info}[$i]->{column}) if (!grep(/^$self->{partitions}{$table}{$pos}{info}[$i]->{column}$/, @ind_col));
 				if ($self->{partitions}{$table}{$pos}{info}[$i]->{type} eq 'LIST')
 				{
 					if (!$fct) {
@@ -6840,12 +7349,24 @@ BEGIN
 				if (exists $self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{type})
 				{
 					$create_table_tmp .= "\nPARTITION BY " . $self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{type} . " (";
+					my $expr = '';
 					if (exists $self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{columns})
 					{
-						for (my $j = 0; $j <= $#{$self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{columns}}; $j++)
+						my $len = $#{$self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{columns}};
+						for (my $j = 0; $j <= $len; $j++)
 						{
-							$create_table_tmp .= ', ' if ($j > 0);
-							$create_table_tmp .= $self->quote_object_name($self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{columns}[$j]);
+							if ($self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{type} eq 'LIST') {
+								$expr .= ' || ' if ($j > 0);
+							} else {
+								$expr .= ', ' if ($j > 0);
+							}
+							$expr .= $self->quote_object_name($self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{columns}[$j]);
+							if ($self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{type} eq 'LIST' && $len >= 0) {
+								$expr .= '::text';
+							}
+						}
+						if ($self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{type} eq 'LIST' && $len >= 0) {
+							$expr = '(' . $expr . ')';
 						}
 					}
 					else
@@ -6853,9 +7374,10 @@ BEGIN
 						if ($self->{plsql_pgsql}) {
 							$self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{expression} = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{expression});
 						}
-						$create_table_tmp .= $self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{expression};
+						$expr .= $self->{subpartitions_list}{"\L$table\E"}{"\L$part\E"}{expression};
 					}
-					$create_table_tmp .= 	")";
+					$expr = '(' . $expr . ')' if ($expr =~ /[\(\+\-\*\%:]/ && $expr !~ /^\(.*\)$/);
+					$create_table_tmp .= 	"$expr)";
 				}
 				$create_table_tmp .= ";\n";
 			}
@@ -6874,13 +7396,11 @@ BEGIN
 					my $subpart = $self->{subpartitions}{$table}{$part}{$p}{name};
 					my $sub_tb_name = $subpart;
 					$sub_tb_name =~ s/^[^\.]+\.//; # remove schema part if any
-					if ($self->{prefix_partition})
+					if ($self->{rename_partition})
 					{
-						if ($self->{prefix_sub_partition}) {
-							$sub_tb_name = "${tb_name}_$sub_tb_name";
-						} else {
-							$sub_tb_name = "${table}_$sub_tb_name";
-						}
+						$sub_tb_name = $tb_name . '_subpart' . $p;
+					} else {
+						$sub_tb_name = "${table}_$sub_tb_name";
 					}
 					if (!$self->{quiet} && !$self->{debug} && ($nparts % $PGBAR_REFRESH) == 0)
 					{
@@ -6991,8 +7511,11 @@ BEGIN
 							$cindx = join(',', @ind_col);
 							$cindx = lc($cindx) if (!$self->{preserve_case});
 							$cindx = Ora2Pg::PLSQL::convert_plsql_code($self, $cindx);
-							$create_table_index_tmp .= "CREATE INDEX " . $self->quote_object_name("${sub_tb_name}_$colname$p")
-												 . " ON " . $self->quote_object_name("$sub_tb_name") . " ($cindx);\n";
+							if ($cindx)
+							{
+								$create_table_index_tmp .= "CREATE INDEX " . $self->quote_object_name("${sub_tb_name}_$colname$p")
+									 . " ON " . $self->quote_object_name("$sub_tb_name") . " ($cindx);\n";
+							}
 							my $tb_name2 = $self->quote_object_name("$sub_tb_name");
 							# Reproduce indexes definition from the main table
 							my ($idx, $fts_idx) = $self->_create_indexes($table, 0, %{$self->{tables}{$table}{indexes}});
@@ -7091,17 +7614,20 @@ BEGIN
 						$sub_funct_cond = Ora2Pg::PLSQL::convert_plsql_code($self, $sub_funct_cond);
 						$funct_cond .= "\t$cond ( " . join(' AND ', @condition) . " ) THEN \n";
 						$funct_cond .= $sub_funct_cond;
-						if (exists $self->{subpartitions_default}{$table}{$part})
+						if (exists $self->{subpartitions_default}{$table}{$part}{name})
 						{
-							my $deftb = '';
-							$deftb = "${table}_" if ($self->{prefix_partition});
-							$funct_cond .= "\t\tELSE INSERT INTO " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}")
+							my $deftb = $self->{subpartitions_default}{$table}{$part}{name};
+							$deftb = $table . '_part'. $pos . '_subpart_default' if ($self->{rename_partition});
+							$funct_cond .= "\t\tELSE INSERT INTO " . $self->quote_object_name($deftb)
 										. " VALUES (NEW.*);\n\t\tEND IF;\n";
-							$create_table_tmp .= "DROP TABLE $self->{pg_supports_ifexists} " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}") . ";\n" if ($self->{drop_if_exists});
-							$create_table_tmp .= "CREATE TABLE " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}")
+							$create_table_tmp .= "DROP TABLE $self->{pg_supports_ifexists} " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}{name}") . ";\n" if ($self->{drop_if_exists});
+							$create_table_tmp .= "CREATE TABLE " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}{name}")
 										. " () INHERITS ($table);\n";
-							$create_table_index_tmp .= "CREATE INDEX " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}_$pos")
-										. " ON " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}") . " ($cindx);\n";
+							if ($cindx)
+							{
+								$create_table_index_tmp .= "CREATE INDEX " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}{name}_$pos")
+									. " ON " . $self->quote_object_name("$deftb$self->{subpartitions_default}{$table}{$part}{name}") . " ($cindx);\n";
+							}
 						}
 						else
 						{
@@ -7115,11 +7641,11 @@ BEGIN
 					# With default partition just add default and continue
 					elsif (exists $self->{subpartitions_default}{$table}{$part})
 					{
-						my $tb_name = $self->{subpartitions_default}{$table}{$part};
-						if ($self->{prefix_partition}) {
-							$tb_name = $table . "_" . $self->{subpartitions_default}{$table}{$part};
+						my $tb_name = $self->{subpartitions_default}{$table}{$part}{name};
+						if ($self->{rename_partition}) {
+							$tb_name = $table . '_part' . $pos . '_subpart_default';
 						} elsif ($self->{export_schema} && !$self->{schema} && ($table =~ /^([^\.]+)\./)) {
-							$tb_name =  $1 . '.' . $self->{subpartitions_default}{$table}{$part};
+							$tb_name =  $1 . '.' . $self->{subpartitions_default}{$table}{$part}{name};
 						}
 						$create_table_tmp .= "DROP TABLE $self->{pg_supports_ifexists} " . $self->quote_object_name($tb_name) . ";\n" if ($self->{drop_if_exists});
 						if ($self->{pg_version} >= 11) {
@@ -7143,11 +7669,11 @@ BEGIN
 		{
 			if (!$self->{pg_supports_partition})
 			{
-				if ($self->{partitions_default}{$table})
+				if (exists $self->{partitions_default}{$table})
 				{
-					my $deftb = '';
-					$deftb = "${table}_" if ($self->{prefix_partition});
-					my $pname = $self->quote_object_name("$deftb$self->{partitions_default}{$table}");
+					my $deftb = $self->{partitions_default}{$table}{name};
+					$deftb = $table . '_part_default' if ($self->{rename_partition});
+					my $pname = $self->quote_object_name($deftb);
 					$function .= $funct_cond . qq{	ELSE
 	INSERT INTO $pname VALUES (NEW.*);
 };
@@ -7174,15 +7700,15 @@ LANGUAGE plpgsql;
 				if (exists $self->{partitions_default}{$table})
 				{
 					my $tb_name = '';
-					if ($self->{prefix_partition}) {
-						$tb_name = $table . "_" . $self->{partitions_default}{$table};
+					if ($self->{rename_partition}) {
+						$tb_name = $table . '_part_default';
 					}
 					else
 					{
 						if ($self->{export_schema} && !$self->{schema} && ($table =~ /^([^\.]+)\./)) {
-							$tb_name =  $1 . '.' . $self->{partitions_default}{$table};
+							$tb_name =  $1 . '.' . $self->{partitions_default}{$table}{name};
 						} else {
-							$tb_name =  $self->{partitions_default}{$table};
+							$tb_name =  $self->{partitions_default}{$table}{name};
 						}
 					}
 					$create_table{$table}{table} .= "DROP TABLE $self->{pg_supports_ifexists} " . $self->quote_object_name($tb_name) . ";\n" if ($self->{drop_if_exists});
@@ -7201,23 +7727,23 @@ LANGUAGE plpgsql;
 		{
 			$partition_indexes .= qq{
 -- Create indexes on each partition of table $table
-$create_table{$table}{'index'}
+$create_table{$table}{index}
 
-} if ($create_table{$table}{'index'});
+} if ($create_table{$table}{index});
 			$sql_output .= qq{
 $create_table{$table}{table}
 };
 			my $tb = $self->quote_object_name($table);
 			my $trg = $self->quote_object_name("${table}_insert_trigger");
-			my $defname = $self->{partitions_default}{$table};
-			$defname = $table . '_' .  $defname if ($self->{prefix_partition});
+			my $defname = $self->{partitions_default}{$table}{name};
+			$defname = $table . '_part_default' if ($self->{rename_partition});
 			$defname = $self->quote_object_name($defname);
 			if (!$self->{pg_supports_partition} && $function)
 			{
 				$sql_output .= qq{
 -- Create default table, where datas are inserted if no condition match
 CREATE TABLE $defname () INHERITS ($tb);
-} if ($self->{partitions_default}{$table});
+} if ($self->{partitions_default}{$table}{name});
 				$sql_output .= qq{
 
 $function
@@ -7232,9 +7758,9 @@ FOR EACH ROW EXECUTE PROCEDURE $trg();
 				$owner = $self->{force_owner} if ($self->{force_owner} ne "1");
 				if ($owner)
 				{
-					$sql_output .= "ALTER TABLE " . $self->quote_object_name($self->{partitions_default}{$table})
+					$sql_output .= "ALTER TABLE " . $self->quote_object_name($self->{partitions_default}{$table}{name})
 								. " OWNER TO " . $self->quote_object_name($owner) . ";\n"
-						if ($self->{partitions_default}{$table});
+						if ($self->{partitions_default}{$table}{name});
 					$sql_output .= "ALTER FUNCTION " . $self->quote_object_name("${table}_insert_trigger")
 								. "() OWNER TO " . $self->quote_object_name($owner) . ";\n";
 				}
@@ -7254,7 +7780,7 @@ FOR EACH ROW EXECUTE PROCEDURE $trg();
 		my $fhdl = undef;
 		$self->logit("Dumping partition indexes to file : PARTITION_INDEXES_$self->{output}\n", 1);
 		$sql_header = "-- Generated by Ora2Pg, the Oracle database Schema converter, version $VERSION\n";
-		$sql_header .= "-- Copyright 2000-2022 Gilles DAROLD. All rights reserved.\n";
+		$sql_header .= "-- Copyright 2000-2023 Gilles DAROLD. All rights reserved.\n";
 		$sql_header .= "-- DATASOURCE: $self->{oracle_dsn}\n\n";
 		$sql_header = ''  if ($self->{no_header});
 		$fhdl = $self->open_export_file("PARTITION_INDEXES_$self->{output}");
@@ -7295,10 +7821,15 @@ sub export_synonym
 		}
 		$count_syn++;
 		if ($self->{synonyms}{$syn}{dblink}) {
-			$sql_output .= "-- You need to create foreign table $self->{synonyms}{$syn}{table_owner}.$self->{synonyms}{$syn}{table_name} using foreign server: $self->{synonyms}{$syn}{dblink} (see DBLINK and FDW export type)\n";
+			$sql_output .= "-- You need to create foreign table $self->{synonyms}{$syn}{table_owner}.$self->{synonyms}{$syn}{table_name} using foreign server: '$self->{synonyms}{$syn}{dblink}'\n -- see DBLINK export type to export the server definition\n";
+		}
+		$sql_output .= "CREATE$self->{create_or_replace} VIEW " . $self->quote_object_name($syn)
+			. " AS SELECT * FROM ";
+		if ($self->{synonyms}{$syn}{table_owner} && !$self->{schema} && $self->{export_schema}) {
+			$sql_output .= $self->quote_object_name("$self->{synonyms}{$syn}{table_owner}.$self->{synonyms}{$syn}{table_name}") . ";\n";
+		} else {
+			$sql_output .= $self->quote_object_name($self->{synonyms}{$syn}{table_name}) . ";\n";
 		}
-		$sql_output .= "CREATE$self->{create_or_replace} VIEW " . $self->quote_object_name("$self->{synonyms}{$syn}{owner}.$syn")
-			. " AS SELECT * FROM " . $self->quote_object_name("$self->{synonyms}{$syn}{table_owner}.$self->{synonyms}{$syn}{table_name}") . ";\n";
 		if ($self->{force_owner})
 		{
 			my $owner = $self->{synonyms}{$syn}{owner};
@@ -7339,7 +7870,8 @@ sub export_table
 	{
 		if ($self->{export_schema} && ($self->{schema} || $self->{pg_schema}))
 		{
-			if ($self->{create_schema}) {
+			if ($self->{create_schema})
+			{
 				if ($self->{pg_schema} && $self->{pg_schema} =~ /,/) {
 					$self->logit("FATAL: with export type TABLE you can not set multiple schema to PG_SCHEMA when EXPORT_SCHEMA is enabled.\n", 0, 1);
 				}
@@ -7355,11 +7887,15 @@ sub export_table
 		}
 		elsif ($self->{export_schema})
 		{
-			if ($self->{create_schema}) {
+			if ($self->{create_schema})
+			{
 				my $current_schema = '';
-				foreach my $table (sort keys %{$self->{tables}}) {
-					if ($table =~ /^([^\.]+)\..*/) {
-						if ($1 ne $current_schema) {
+				foreach my $table (sort keys %{$self->{tables}})
+				{
+					if ($table =~ /^([^\.]+)\..*/)
+					{
+						if ($1 ne $current_schema)
+						{
 							$current_schema = $1;
 							$sql_output .= "CREATE SCHEMA IF NOT EXISTS " . $self->quote_object_name($1) . ";\n";
 						}
@@ -7391,6 +7927,15 @@ sub export_table
 	# Stores DDL to restart autoincrement sequences
 	my $sequence_output = '';
 
+	if ($self->{is_mssql})
+	{
+		if (lc($self->{case_insensitive_search}) eq 'citext') {
+			$sql_output .= "CREATE EXTENSION citext;\n";
+		}
+	} else {
+		$self->{case_insensitive_search} = 'none';
+	}
+
 	# Dump all table/index/constraints SQL definitions
 	my $ib = 1;
 	my $count_table = 0;
@@ -7422,6 +7967,9 @@ sub export_table
 			$sql_header .= "CREATE SERVER $srv_name FOREIGN DATA WRAPPER file_fdw;\n\n" if ($sql_header !~ /CREATE SERVER $srv_name FOREIGN DATA WRAPPER file_fdw;/is);
 		}
 
+		# MySQL ON UPDATE clause
+		my %trigger_update = ();
+
 		my $tbname = $self->get_replaced_tbname($table);
 		my $foreign = '';
 		if ( ($self->{type} eq 'FDW') || $self->{oracle_fdw_data_export} || ($self->{external_to_fdw} && (grep(/^$table$/i, keys %{$self->{external_table}}) || $self->{tables}{$table}{table_info}{connection})) ) {
@@ -7473,7 +8021,7 @@ sub export_table
 				push(@collist, $self->{tables}{$table}{column_info}{$k}[0]);
 			}
 
-			# Extract column information following the Oracle position order
+			# Extract column information following the position order
 			foreach my $k (sort { 
 					if (!$self->{reordering_columns}) {
 						$self->{tables}{$table}{column_info}{$a}[11] <=> $self->{tables}{$table}{column_info}{$b}[11];
@@ -7517,6 +8065,7 @@ sub export_table
 					{
 						if ($type !~ s/smallint/smallserial/) {
 							$type =~ s/integer/serial/;
+							$type =~ s/numeric.*/bigserial/;
 						}
 					}
 					if ($type =~ /serial/)
@@ -7604,6 +8153,18 @@ sub export_table
 				}
 				$type = $self->{'modify_type'}{"\L$table\E"}{"\L$f->[0]\E"} if (exists $self->{'modify_type'}{"\L$table\E"}{"\L$f->[0]\E"});
 				$fname = $self->quote_object_name($fname);
+				my $citext_constraint = '';
+				if ($self->{case_insensitive_search} =~ /^citext$/i && $type =~ /^(?:char|varchar|text)/)
+				{
+					if ($type =~ /^(?:char|varchar|text)\s*\((\d+)\)/) {
+						$constraints .= "ALTER TABLE " . $self->quote_object_name($table) . " ADD CHECK (char_length($fname) <= $1);\n";
+					}
+					$type = 'citext';
+				}
+				elsif ($self->{case_insensitive_search} !~ /^none$/i && $type =~ /^(?:char|varchar|text)/)
+				{
+					$type .= ' COLLATE ' . $self->{case_insensitive_search};
+				}
 				$sql_output .= "\t$fname $type";
 				if ($foreign && $self->is_primary_key_column($table, $f->[0])) {
 					 $sql_output .= " OPTIONS (key 'true')";
@@ -7619,7 +8180,7 @@ sub export_table
 				}
 
 				# Autoincremented columns
-				if (!$self->{schema} && $self->{export_schema}) {
+				if (!$self->{schema} && $self->{export_schema} && $f->[8] !~ /\./) {
 					$f->[8] = "$f->[9].$f->[8]";
 				}
 				if (exists $self->{identity_info}{$f->[8]}{$f->[0]} and $self->{type} ne 'FDW' and !$self->{oracle_fdw_data_export})
@@ -7664,11 +8225,12 @@ sub export_table
 						$f->[4] = Ora2Pg::PLSQL::convert_plsql_code($self, $f->[4]);
 					}
 					# Check if the column make reference to an other column
-					my $use_other_col = 0;
-					foreach my $c (@collist) {
-						$use_other_col = 1 if ($f->[4] =~ /\b$c\b/i);
-					}
-					if ($use_other_col && $self->{pg_version} >= 12)
+					#my $use_other_col = 0;
+					#foreach my $c (@collist) {
+					#	$use_other_col = 1 if ($f->[4] =~ /\b$c\b/i);
+					#}
+
+					if ($f->[10] eq 'YES' && $self->{pg_version} >= 12)
 					{
 						$sql_output .= " GENERATED ALWAYS AS (" . $f->[4] . ") STORED";
 					}
@@ -7683,7 +8245,8 @@ sub export_table
 					}
 					else
 					{
-						if (($f->[4] ne '') && ($self->{type} ne 'FDW') && !$self->{oracle_fdw_data_export}) {
+						if (($f->[4] ne '') && ($self->{type} ne 'FDW') && !$self->{oracle_fdw_data_export})
+						{
 							if ($type eq 'boolean')
 							{
 								my $found = 0;
@@ -7750,20 +8313,31 @@ sub export_table
 											$f->[4] = "'$f->[4]'";
 										}
 									}
+									elsif ($type =~ /(char|text)/i && $f->[4] !~ /^'/)
+									{
+										$f->[4] = "'$f->[4]'";
+									}
 								}
 								$f->[4] = 'NULL' if ($f->[4] eq "''" && $type =~ /int|double|numeric/i);
+								$f->[4] =~ s/'((?:session|current)_[^']+)'/$1/;
 								$sql_output .= " DEFAULT $f->[4]";
 							}
 						}
 					}
 				}
 				$sql_output .= ",\n";
+
+				# Replace default generated value on update by a trigger
+				if ($f->[12] =~ /^DEFAULT_GENERATED on update (.*)/i) {
+					$trigger_update{$f->[0]} = $1;
+				}
 			}
 			if ($self->{pkey_in_create}) {
 				$sql_output .= $self->_get_primary_keys($table, $self->{tables}{$table}{unique_key});
 			}
 			$sql_output =~ s/,$//;
 			$sql_output .= ')';
+
 			if (exists $self->{tables}{$table}{table_info}{on_commit})
 			{
 				$sql_output .= ' ' . $self->{tables}{$table}{table_info}{on_commit};
@@ -7773,12 +8347,24 @@ sub export_table
 				if (exists $self->{partitions_list}{"\L$table\E"}{type})
 				{
 					$sql_output .= " PARTITION BY " . $self->{partitions_list}{"\L$table\E"}{type} . " (";
+					my $expr = '';
 					if (exists $self->{partitions_list}{"\L$table\E"}{columns})
 					{
-						for (my $j = 0; $j <= $#{$self->{partitions_list}{"\L$table\E"}{columns}}; $j++)
+						my $len = $#{$self->{partitions_list}{"\L$table\E"}{columns}};
+						for (my $j = 0; $j <= $len; $j++)
 						{
-							$sql_output .= ', ' if ($j > 0);
-							$sql_output .= $self->quote_object_name($self->{partitions_list}{"\L$table\E"}{columns}[$j]);
+							if ($self->{partitions_list}{"\L$table\E"}{type} eq 'LIST') {
+								$expr .= ' || ' if ($j > 0);
+							} else {
+								$expr .= ', ' if ($j > 0);
+							}
+							$expr .= $self->quote_object_name($self->{partitions_list}{"\L$table\E"}{columns}[$j]);
+							if ($self->{partitions_list}{"\L$table\E"}{type} eq 'LIST' && $len >= 0) {
+								$expr .= '::text';
+							}
+						}
+						if ($self->{partitions_list}{"\L$table\E"}{type} eq 'LIST' && $len >= 0) {
+							$expr = '(' . $expr . ')';
 						}
 					}
 					else
@@ -7786,9 +8372,10 @@ sub export_table
 						if ($self->{plsql_pgsql}) {
 							$self->{partitions_list}{"\L$table\E"}{expression} = Ora2Pg::PLSQL::convert_plsql_code($self, $self->{partitions_list}{"\L$table\E"}{expression});
 						}
-						$sql_output .= $self->{partitions_list}{"\L$table\E"}{expression};
+						$expr .= $self->{partitions_list}{"\L$table\E"}{expression};
 					}
-					$sql_output .= 	")";
+					$expr = '(' . $expr . ')' if ($expr =~ /[\(\+\-\*\%:]/ && $expr !~ /^\(.*\)$/);
+					$sql_output .= 	"$expr)";
 				}
 				else
 				{
@@ -7934,6 +8521,34 @@ sub export_table
 			}
 		}
 		$ib++;
+
+		# Add the MySQL ON UPDATE trigger
+		if (scalar keys %trigger_update)
+		{
+			my $objname = $table . '_default_';
+			$objname =~ s/[^a-z0-9_]//ig;
+			$sql_output .= qq{
+DROP TRIGGER IF EXISTS ${objname}_trg ON $tbname;
+CREATE OR REPLACE FUNCTION ${objname}_fct() RETURNS trigger AS \$\$
+BEGIN
+};
+			foreach my $c (sort keys %trigger_update)
+			{	
+				my $colname = $self->quote_object_name($c);
+				$sql_output .= qq{
+    NEW.$colname = $trigger_update{$c};};
+			}
+			$sql_output .= qq{
+    RETURN NEW;
+END;
+\$\$ LANGUAGE plpgsql;
+
+CREATE TRIGGER ${objname}_trg
+    BEFORE UPDATE ON $tbname
+    FOR EACH ROW
+    EXECUTE FUNCTION ${objname}_fct();
+};
+		}
 	}
 
 	if (!$self->{quiet} && !$self->{debug})
@@ -8176,6 +8791,12 @@ sub _get_sql_statements
 		$self->export_sequence();
 	}
 
+	# Process sequences values
+	elsif ($self->{type} eq 'SEQUENCE_VALUES')
+	{
+		$self->export_sequence_values();
+	}
+
 	# Process dblink
 	elsif ($self->{type} eq 'DBLINK')
 	{
@@ -8209,19 +8830,31 @@ sub _get_sql_statements
 	# Process functions only
 	elsif ($self->{type} eq 'FUNCTION')
 	{
+		$self->start_function_json_config($self->{type});
+
 		$self->export_function();
+
+		$self->end_function_json_config($self->{type});
 	}
 
 	# Process procedures only
 	elsif ($self->{type} eq 'PROCEDURE')
 	{
+		$self->start_function_json_config($self->{type});
+
 		$self->export_procedure();
+
+		$self->end_function_json_config($self->{type});
 	}
 
 	# Process packages only
 	elsif ($self->{type} eq 'PACKAGE')
 	{
+		$self->start_function_json_config($self->{type});
+
 		$self->export_package();
+
+		$self->end_function_json_config($self->{type});
 	}
 
 	# Process types only
@@ -8280,6 +8913,8 @@ sub _get_sql_statements
 		# Connect the Oracle database to gather information
 		if ($self->{oracle_dsn} =~ /dbi:mysql/i) {
 			$self->{is_mysql} = 1;
+		} elsif ($self->{oracle_dsn} =~ /dbi:ODBC:driver=msodbcsql/i) {
+			$self->{is_mssql} = 1;
 		}
 		$self->{dbh} = $self->_db_connection();
 
@@ -8433,7 +9068,7 @@ sub _get_sql_statements
 					if (!exists $self->{subpartitions}{$table}{$part_name}) {
 						$tb_name = $part_name;
 					}
-					$tb_name = $table . '_' . $tb_name if ($self->{prefix_partition});
+					$tb_name = $table . '_part' . $pos if ($self->{rename_partition});
 					next if ($self->{allow_partition} && !grep($_ =~ /^$part_name$/i, @{$self->{allow_partition}}));
 
 					if (exists $self->{subpartitions}{$table}{$part_name})
@@ -8444,25 +9079,26 @@ sub _get_sql_statements
 							next if ($self->{allow_partition} && !grep($_ =~ /^$subpart$/i, @{$self->{allow_partition}}));
 							my $sub_tb_name = $subpart;
 							$sub_tb_name =~ s/^[^\.]+\.//; # remove schema part if any
-							$sub_tb_name = "${tb_name}$sub_tb_name" if ($self->{prefix_partition});
-							if ($self->{file_per_table} && !$self->{pg_dsn}) {
-								my $file_name = "$dirprefix${sub_tb_name}_$self->{output}";
+							if ($self->{file_per_table} && !$self->{pg_dsn})
+							{
+								my $file_name = "$dirprefix${table}_${sub_tb_name}_$self->{output}";
 								$file_name =~ s/\.(gz|bz2)$//;
 								$load_file .=  "\\i$self->{psql_relative_path} '$file_name'\n";
 							}
+							$sub_tb_name = $tb_name . '_subpart' . $p if ($self->{rename_partition});
 						}
-						# Now load content of the default partion table
+						# Now load content of the default partition table
 						if ($self->{subpartitions_default}{$table}{$part_name})
 						{
-							if (!$self->{allow_partition} || grep($_ =~ /^$self->{subpartitions_default}{$table}{$part_name}$/i, @{$self->{allow_partition}}))
+							if (!$self->{allow_partition} || grep($_ =~ /^$self->{subpartitions_default}{$table}{$part_name}{name}$/i, @{$self->{allow_partition}}))
 							{
 								if ($self->{file_per_table} && !$self->{pg_dsn})
 								{
-									my $part_name = $self->{subpartitions_default}{$table}{$part_name};
-									$part_name = "${tb_name}$part_name" if ($self->{prefix_partition});
-									my $file_name = "$dirprefix${part_name}_$self->{output}";
+									my $part_name = $self->{subpartitions_default}{$table}{$part_name}{name};
+									my $file_name = "$dirprefix${table}_${part_name}_$self->{output}";
 									$file_name =~ s/\.(gz|bz2)$//;
 									$load_file .=  "\\i$self->{psql_relative_path} '$file_name'\n";
+									$part_name = $tb_name . '_default' if ($self->{rename_partition});
 								}
 							}
 						}
@@ -8478,17 +9114,17 @@ sub _get_sql_statements
 					}
 				}
 				# Now load content of the default partition table
-				if ($self->{partitions_default}{$table})
+				if (exists $self->{partitions_default}{$table})
 				{
-					if (!$self->{allow_partition} || grep($_ =~ /^$self->{partitions_default}{$table}$/i, @{$self->{allow_partition}}))
+					if (!$self->{allow_partition} || grep($_ =~ /^$self->{partitions_default}{$table}{name}$/i, @{$self->{allow_partition}}))
 					{
 						if ($self->{file_per_table} && !$self->{pg_dsn})
 						{
-							my $part_name = $self->{partitions_default}{$table};
-							$part_name = $table . '_' . $part_name if ($self->{prefix_partition});
-							my $file_name = "$dirprefix${part_name}_$self->{output}";
+							my $part_name = $self->{partitions_default}{$table}{name};
+							my $file_name = "$dirprefix${table}_${part_name}_$self->{output}";
 							$file_name =~ s/\.(gz|bz2)$//;
 							$load_file .=  "\\i$self->{psql_relative_path} '$file_name'\n";
+							$part_name = $table . '_part_default' if ($self->{rename_partition});
 						}
 					}
 				}
@@ -8710,6 +9346,8 @@ sub _get_sql_statements
 			$self->{dbh}->disconnect() if (defined $self->{dbh});
 			if ($self->{oracle_dsn} =~ /dbi:mysql/i) {
 				$self->{is_mysql} = 1;
+			} elsif ($self->{oracle_dsn} =~ /dbi:ODBC:driver=msodbcsql/i) {
+				$self->{is_mssql} = 1;
 			}
 			$self->{dbh} = $self->_db_connection();
 		}
@@ -9096,7 +9734,7 @@ sub file_exists
 ####
 sub _dump_table
 {
-	my ($self, $dirprefix, $sql_header, $table, $part_name, $is_subpart) = @_;
+	my ($self, $dirprefix, $sql_header, $table, $part_name, $is_subpart, $tbpart_name, $sub_tb_name) = @_;
 
 	my @cmd_head = ();
 	my @cmd_foot = ();
@@ -9112,10 +9750,13 @@ sub _dump_table
 
 	# Prefix partition name with tablename, if pg_supports_partition is enabled
 	# direct import to partition is not allowed so import to main table.
-	if (!$self->{pg_supports_partition} && $part_name && $self->{prefix_partition}) {
+	if (!$self->{pg_supports_partition} && $part_name && $self->{rename_partition}) {
 		$tmptb = $self->get_replaced_tbname($table . '_' . $part_name);
 	} elsif (!$self->{pg_supports_partition} && $part_name) {
 		$tmptb = $self->get_replaced_tbname($part_name || $table);
+	} elsif ($tbpart_name) {
+		$tmptb = $self->get_replaced_tbname($tbpart_name);
+		$tmptb = $self->get_replaced_tbname($sub_tb_name) if ($sub_tb_name);
 	} else {
 		$tmptb = $self->get_replaced_tbname($table);
 	}
@@ -9145,6 +9786,8 @@ sub _dump_table
 		my $fieldname = ${$self->{tables}{$table}{field_name}}[$i];
 		next if (!$self->is_in_struct($table, $fieldname));
 
+		next if (!exists $self->{tables}{"$table"}{column_info}{"$fieldname"});
+
 		my $f = $self->{tables}{"$table"}{column_info}{"$fieldname"};
 		$f->[2] =~ s/\D//g;
 		if (!$self->{enable_blob_export} && $f->[1] =~ /blob/i)
@@ -9272,6 +9915,9 @@ sub _dump_table
 			}
 			$s_out =~ s/,$//;
 			$s_out .= ")";
+			if ($self->{insert_on_conflict}) {
+				$s_out .= " ON CONFLICT DO NOTHING";
+			}
 			$sprep = $s_out;
 		}
 	}
@@ -9437,7 +10083,9 @@ sub _dump_fdw_table
 
 	$0 = "ora2pg - exporting table $self->{fdw_import_schema}.$fdwtb";
 
+	####
 	# Overwrite the query if REPLACE_QUERY is defined for this table
+	####
 	if ($self->{replace_query}{"\L$table\E"})
 	{
 		$s_out = $self->{replace_query}{"\L$table\E"};
@@ -9554,6 +10202,8 @@ sub _column_comments
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_column_comments($self, $table);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_column_comments($self, $table);
 	} else {
 		return Ora2Pg::Oracle::_column_comments($self, $table);
 	}
@@ -9592,7 +10242,9 @@ sub get_indexname
 			$idxname = substr($idxname,0,63);
 		}
 	}
-	$idxname =~ s/[^a-z0-9_]+//ig; # Remove non alphanumeric character
+	# Remove non alphanumeric character
+	#$idxname =~ s/[^a-z0-9_]+//ig;
+
 	$idxname = $self->quote_object_name("$idxname$self->{indexes_suffix}");
 
 	return $idxname;
@@ -9607,6 +10259,7 @@ and triggers to create for FTS indexes.
 - $indexonly mean no FTS index output
 
 =cut
+
 sub _create_indexes
 {
 	my ($self, $table, $indexonly, %indexes) = @_;
@@ -9983,7 +10636,11 @@ sub _drop_indexes
 				map { s/\b$c\b/$self->{replaced_cols}{"\L$tbsaved\E"}{"\L$c\E"}/i } @{$indexes{$idx}};
 			}
 		}
-		map { if ($_ !~ /\(.*\)/) { $_ = $self->quote_object_name($_) } } @{$indexes{$idx}};
+		map { if ($_ !~ /\(.*\)/) {
+				$_ =~ s/(\s+.*)//; # DESC or ASC
+				$_ = $self->quote_object_name($_);
+				$_ .= $1;
+			} } @{$indexes{$idx}};
 
                 my $columns = '';
                 foreach my $s (@{$indexes{$idx}})
@@ -10355,6 +11012,7 @@ sub _create_check_constraint
 			if (!$converted_as_boolean)
 			{
 				$chkconstraint = Ora2Pg::PLSQL::convert_plsql_code($self, $chkconstraint);
+				$chkconstraint =~ s/,$//;
 				$out .= "ALTER TABLE $table DROP CONSTRAINT $self->{pg_supports_ifexists} $k;\n" if ($self->{drop_if_exists});
 				$out .= "ALTER TABLE $table ADD CONSTRAINT $k CHECK ($chkconstraint)$validate;\n";
 			}
@@ -10517,6 +11175,8 @@ sub _extract_sequence_info
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_extract_sequence_info($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_extract_sequence_info($self);
 	} else {
 		return Ora2Pg::Oracle::_extract_sequence_info($self);
 	}
@@ -10535,11 +11195,34 @@ sub _howto_get_data
 {
 	my ($self, $table, $name, $type, $src_type, $part_name, $is_subpart) = @_;
 
+	####
+	# Overwrite the query if REPLACE_QUERY is defined for this table
+	####
+	if ($self->{replace_query}{"\L$table\E"})
+	{
+		$str = $self->{replace_query}{"\L$table\E"};
+		$self->logit("DEGUG: Query sent to $self->{sgbd_name}: $str\n", 1);
+		return $str;
+	}
+
 	# Fix a problem when the table need to be prefixed by the schema
 	my $realtable = $table;
 	$realtable =~ s/\"//g;
 	# Do not use double quote with mysql, but backquote
-	if (!$self->{is_mysql})
+	if ($self->{is_mysql})
+	{
+		$realtable =~ s/\`//g;
+		$realtable = "\`$realtable\`";
+	}
+	elsif ($self->{is_mssql})
+	{
+		$realtable =~ s/[\[\]]+//g;
+		$realtable = "\[$realtable\]";
+		if (!$self->{schema} && $self->{export_schema}) {
+			$realtable =~ s/\./\].\[/;
+		}
+	}
+	else
 	{
 		if (!$self->{schema} && $self->{export_schema})
 		{
@@ -10558,11 +11241,6 @@ sub _howto_get_data
 			}
 		}
 	}
-	else
-	{
-		$realtable =~ s/\`//g;
-		$realtable = "\`$realtable\`";
-	}
 
 	delete $self->{nullable}{$table};
 
@@ -10586,16 +11264,22 @@ sub _howto_get_data
 			my $realcolname = $name->[$k];
 			my $spatial_srid = '';
 			$self->{nullable}{$table}{$k} = $self->{colinfo}->{$table}{$realcolname}{nullable};
-			if (!$self->{is_mysql})
+			if ($self->{is_mysql})
 			{
-				if ($name->[$k] !~ /"/) {
-					$name->[$k] = '"' . $name->[$k] . '"';
+				if ($name->[$k] !~ /\`/) {
+					$name->[$k] = '`' . $name->[$k] . '`';
+				}
+			}
+			elsif ($self->{is_mssql})
+			{
+				if ($name->[$k] !~ /\[/) {
+					$name->[$k] = '[' . $name->[$k] . ']';
 				}
 			}
 			else
 			{
-				if ($name->[$k] !~ /\`/) {
-					$name->[$k] = '`' . $name->[$k] . '`';
+				if ($name->[$k] !~ /"/) {
+					$name->[$k] = '"' . $name->[$k] . '"';
 				}
 			}
 
@@ -10639,9 +11323,9 @@ sub _howto_get_data
 			elsif ( !$self->{is_mysql} && $src_type->[$k] =~ /^(ST_|STGEOM_)/i)
 			{
 				if ($self->{geometry_extract_type} eq 'WKB') {
-					$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN SDE.ST_ASBINARY($name->[$k]) ELSE NULL END,";
+					$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN $self->{st_asbinary_function}($name->[$k]) ELSE NULL END,";
 				} else {
-					$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN SDE.ST_ASTEXT($name->[$k]) ELSE NULL END,";
+					$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN $self->{st_astext_function}($name->[$k]) ELSE NULL END,";
 				}
 			}
 			# Oracle geometries
@@ -10677,22 +11361,32 @@ sub _howto_get_data
 					}
 				}
 			}
+			# SQL Server geometry
+ 			elsif ( $self->{is_mssql} && $src_type->[$k] =~ /^GEOM(ETRY|GRAPHY)/i)
+ 			{
+				if ($self->{geometry_extract_type} eq 'WKB') {
+					$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN CONCAT('SRID=', $name->[$k].STSrid,';', $name->[$k].STAsText()) ELSE NULL END,";
+				} else {
+					$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN CONCAT('SRID=',$name->[$k].STSrid,';', $name->[$k].STAsText()) ELSE NULL END,";
+				}
+ 			}
+			# MySQL geometry
 			elsif ( $self->{is_mysql} && $src_type->[$k] =~ /geometry/i && $self->{type} ne 'TEST_DATA')
 			{
 				if ($self->{db_version} < '5.7.6')
 				{
 					if ($self->{geometry_extract_type} eq 'WKB') {
-						$str .= "CASE WHEN $name->[$k]->[0] IS NOT NULL THEN CONCAT('SRID=',SRID($name->[$k]),';', AsBinary($name->[$k])) ELSE NULL END,";
+						$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN CONCAT('SRID=',SRID($name->[$k]),';', AsBinary($name->[$k])) ELSE NULL END,";
 					} else {
-						$str .= "CASE WHEN $name->[$k]->[0] IS NOT NULL THEN CONCAT('SRID=',SRID($name->[$k]),';', AsText($name->[$k])) ELSE NULL END,";
+						$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN CONCAT('SRID=',SRID($name->[$k]),';', AsText($name->[$k])) ELSE NULL END,";
 					}
 				}
 				else
 				{
 					if ($self->{geometry_extract_type} eq 'WKB') {
-						$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN CONCAT('SRID=',ST_Srid($name->[$k]),';', ST_AsBinary($name->[$k]->[0])) ELSE NULL END,";
+						$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN CONCAT('SRID=',$self->{st_srid_function}($name->[$k]),';', $self->{st_asbinary_function}($name->[$k])) ELSE NULL END,";
 					} else {
-						$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN CONCAT('SRID=',ST_Srid($name->[$k]),';', ST_AsText($name->[$k]->[0])) ELSE NULL END,";
+						$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN CONCAT('SRID=',$self->{st_srid_function}($name->[$k]),';', $self->{st_astext_function}($name->[$k])) ELSE NULL END,";
 					}
 				}
 			}
@@ -10702,7 +11396,7 @@ sub _howto_get_data
 				if ($self->{db_version} < '5.7.6') {
 					$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN AsText($name->[$k]) ELSE NULL END,";
 				} else {
-					$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN ST_AsText($name->[$k]) ELSE NULL END,";
+					$str .= "CASE WHEN $name->[$k] IS NOT NULL THEN $self->{st_astext_function}($name->[$k]) ELSE NULL END,";
 				}
 			}
 			elsif ( !$self->{is_mysql} && (($src_type->[$k] =~ /clob/i) || ($src_type->[$k] =~ /blob/i)) )
@@ -10724,7 +11418,8 @@ sub _howto_get_data
 			}
 			push(@{$self->{spatial_srid}{$table}}, $spatial_srid);
 			
-			if ($type->[$k] =~ /bytea/i && $self->{enable_blob_export})
+			if ( ($type->[$k] =~ /bytea/i && $self->{enable_blob_export}) ||
+				($self->{clob_as_blob} && $src_type->[$k] =~ /CLOB/i) )
 			{
 				if ($self->{data_limit} >= 1000)
 				{
@@ -10839,7 +11534,7 @@ END;
 
 	if ($part_name)
 	{
-		if ($is_subpart) {
+		if ($is_subpart && !$self->{is_mysql}) {
 			$realtable .= " SUBPARTITION(" . $self->quote_object_name($part_name) . ")";
 		} else {
 			$realtable .= " PARTITION(" . $self->quote_object_name($part_name) . ")";
@@ -10937,7 +11632,7 @@ END;
 		}
 	}
 
-	$self->logit("DEGUG: Query sent to Oracle: $str\n", 1);
+	$self->logit("DEGUG: Query sent to $self->{sgbd_name}: $str\n", 1);
 
 	return $str;
 }
@@ -10953,6 +11648,16 @@ sub _howto_get_fdw_data
 {
 	my ($self, $table, $name, $type, $src_type, $part_name, $is_subpart) = @_;
 
+	####
+	# Overwrite the query if REPLACE_QUERY is defined for this table
+	####
+	if ($self->{replace_query}{"\L$table\E"})
+	{
+		$str = $self->{replace_query}{"\L$table\E"};
+		$self->logit("DEGUG: Query sent to $self->{sgbd_name}: $str\n", 1);
+		return $str;
+	}
+
 	# Fix a problem when the table need to be prefixed by the schema
 	my $realtable = $table;
 	$realtable =~ s/\"//g;
@@ -11113,7 +11818,7 @@ sub _howto_get_fdw_data
 		}
 	}
 
-	$self->logit("DEGUG: Query sent to Oracle: $str\n", 1);
+	$self->logit("DEGUG: Query sent to $self->{sgbd_name}: $str\n", 1);
 
 	return $str;
 }
@@ -11133,6 +11838,8 @@ sub _sql_type
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_sql_type($self, $type, $len, $precision, $scale);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_sql_type($self, $type, $len, $precision, $scale);
 	} else {
 		return Ora2Pg::Oracle::_sql_type($self, $type, $len, $precision, $scale);
 	}
@@ -11158,14 +11865,18 @@ elements for each column the specified table
 
 sub _column_info
 {
-	my ($self, $table, $owner, $objtype, $recurs) = @_;
+	my ($self, $table, $owner, $objtype, @expanded_views) = @_;
 
-	$self->logit("Collecting column information for table $table...\n", 1);
+	$objtype ||= 'TABLE';
+
+	$self->logit("Collecting column information for \L$objtype\E $table...\n", 1);
 
 	if ($self->{is_mysql}) {
-		return Ora2Pg::MySQL::_column_info($self,$table,$owner,'TABLE');
+		return Ora2Pg::MySQL::_column_info($self,$table,$owner,$objtype,0,@expanded_views);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_column_info($self,$table,$owner,$objtype,0,@expanded_views);
 	} else {
-		return Ora2Pg::Oracle::_column_info($self,$table,$owner,'TABLE');
+		return Ora2Pg::Oracle::_column_info($self,$table,$owner,$objtype,0,@expanded_views);
 	}
 }
 
@@ -11175,6 +11886,8 @@ sub _column_attributes
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_column_attributes($self,$table,$owner,'TABLE');
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_column_attributes($self,$table,$owner,'TABLE');
 	} else {
 		return Ora2Pg::Oracle::_column_attributes($self,$table,$owner,'TABLE');
 	}
@@ -11186,6 +11899,8 @@ sub _encrypted_columns
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_encrypted_columns($self,$table,$owner);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_encrypted_columns($self,$table,$owner);
 	} else {
 		return Ora2Pg::Oracle::_encrypted_columns($self,$table,$owner);
 	}
@@ -11214,6 +11929,8 @@ sub _unique_key
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_unique_key($self,$table,$owner, $type);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_unique_key($self,$table,$owner, $type);
 	} else {
 		return Ora2Pg::Oracle::_unique_key($self,$table,$owner, $type);
 	}
@@ -11235,6 +11952,8 @@ sub _check_constraint
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_check_constraint($self, $table, $owner);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_check_constraint($self, $table, $owner);
 	} else {
 		return Ora2Pg::Oracle::_check_constraint($self, $table, $owner);
 	}
@@ -11269,6 +11988,8 @@ sub _foreign_key
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_foreign_key($self,$table,$owner);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_foreign_key($self,$table,$owner);
 	} else {
 		return Ora2Pg::Oracle::_foreign_key($self,$table,$owner);
 	}
@@ -11314,6 +12035,8 @@ sub _get_security_definer
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_security_definer($self, $type);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_security_definer($self, $type);
 	} else {
 		return Ora2Pg::Oracle::_get_security_definer($self, $type);
 	}
@@ -11335,6 +12058,8 @@ sub _get_indexes
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_indexes($self,$table,$owner, $generated_indexes);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_indexes($self,$table,$owner, $generated_indexes);
 	} else {
 		return Ora2Pg::Oracle::_get_indexes($self,$table,$owner, $generated_indexes);
 	}
@@ -11355,6 +12080,8 @@ sub _get_sequences
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_sequences($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_sequences($self);
 	} else {
 		return Ora2Pg::Oracle::_get_sequences($self);
 	}
@@ -11373,6 +12100,8 @@ sub _get_identities
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_identities($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_identities($self);
 	} else {
 		return Ora2Pg::Oracle::_get_identities($self);
 	}
@@ -11392,6 +12121,8 @@ sub _get_external_tables
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_external_tables($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_external_tables($self);
 	} else {
 		return Ora2Pg::Oracle::_get_external_tables($self);
 	}
@@ -11411,6 +12142,8 @@ sub _get_directory
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_directory($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_directory($self);
 	} else {
 		return Ora2Pg::Oracle::_get_directory($self);
 	}
@@ -11431,6 +12164,8 @@ sub _get_dblink
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_dblink($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_dblink($self);
 	} else {
 		return Ora2Pg::Oracle::_get_dblink($self);
 	}
@@ -11453,6 +12188,8 @@ sub _get_job
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_job($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_job($self);
 	} else {
 		return Ora2Pg::Oracle::_get_job($self);
 	}
@@ -11473,6 +12210,8 @@ sub _get_views
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_views($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_views($self);
 	} else {
 		return Ora2Pg::Oracle::_get_views($self);
 	}
@@ -11493,6 +12232,8 @@ sub _get_materialized_views
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_materialized_views($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_materialized_views($self);
 	} else {
 		return Ora2Pg::Oracle::_get_materialized_views($self);
 	}
@@ -11504,6 +12245,8 @@ sub _get_materialized_view_names
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_materialized_view_names($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_materialized_view_names($self);
 	} else {
 		return Ora2Pg::Oracle::_get_materialized_view_names($self);
 	}
@@ -11523,6 +12266,8 @@ sub _get_triggers
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_triggers($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_triggers($self);
 	} else {
 		return Ora2Pg::Oracle::_get_triggers($self);
 	}
@@ -11534,6 +12279,8 @@ sub _list_triggers
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_list_triggers($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_list_triggers($self);
 	} else {
 		return Ora2Pg::Oracle::_list_triggers($self);
 	}
@@ -11555,6 +12302,8 @@ sub _get_plsql_metadata
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_plsql_metadata($self, $owner);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_plsql_metadata($self, $owner);
 	} else {
 		return Ora2Pg::Oracle::_get_plsql_metadata($self, $owner);
 	}
@@ -11596,6 +12345,8 @@ sub _get_functions
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_functions($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_functions($self);
 	} else {
 		return Ora2Pg::Oracle::_get_functions($self);
 	}
@@ -11615,6 +12366,8 @@ sub _get_procedures
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_procedures($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_procedures($self);
 	} else {
 		return Ora2Pg::Oracle::_get_procedures($self);
 	}
@@ -11653,6 +12406,8 @@ sub _get_types
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_types($self, $name);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_types($self, $name);
 	} else {
 		return Ora2Pg::Oracle::_get_types($self, $name);
 	}
@@ -11666,7 +12421,7 @@ This function retrieves real rows count from a table.
 
 sub _count_source_rows
 {
-	my ($self, $dbh, $t) = @_;
+	my ($self, $dbhsrc, $t) = @_;
 
 	$self->logit("DEBUG: pid $$ looking for real row count for source table $t...\n", 1);
 	my $tbname = $t;
@@ -11676,12 +12431,12 @@ sub _count_source_rows
 		$tbname = "[$t]";
 		$tbname =~ s/\./\].\[/;
 	} else {
-		$tbname = "[$t]";
-		$tbname =~ s/\./\].\[/;
+		$tbname = qq{"$t"};
+		$tbname = qq{"$self->{schema}"} . '.' . qq{"$t"} if ($self->{schema} && $t !~ /\./);
 	}
-	my $sql = "SELECT COUNT(*) FROM $t";
-	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 $sql = "SELECT COUNT(*) FROM $tbname";
+	my $sth = $dbhsrc->prepare( $sql ) or $self->logit("FATAL: " . $dbhsrc->errstr . "\n", 0, 1);
+	$sth->execute or $self->logit("FATAL: " . $dbhsrc->errstr . "\n", 0, 1);
 	my $size = $sth->fetch();
 	$sth->finish();
 
@@ -11690,7 +12445,7 @@ sub _count_source_rows
 	my $fh = new IO::File;
 	$fh->open(">>${dirprefix}ora2pg_count_rows") or $self->logit("FATAL: can't write to ${dirprefix}ora2pg_count_rows, $!\n", 0, 1);
 	flock($fh, 2) || die "FATAL: can't lock file ${dirprefix}ora2pg_count_rows\n";
-	$fh->print("$t:$size->[0]\n");
+	$fh->print("$tbname:$size->[0]\n");
 	$fh->close;
 }
 
@@ -11712,6 +12467,8 @@ sub _table_info
 
 	if ($self->{is_mysql}) {
 		%tables_infos = Ora2Pg::MySQL::_table_info($self);
+	} elsif ($self->{is_mssql}) {
+		%tables_infos = Ora2Pg::MSSQL::_table_info($self);
 	} else {
 		%tables_infos = Ora2Pg::Oracle::_table_info($self);
 	}
@@ -11775,7 +12532,12 @@ sub _table_info
 		foreach my $s (@ret)
 		{
 			my ($tb, $cnt) = split(':', $s);
-			$tables_infos{$tb}{num_rows} = $cnt || 0;
+			chomp $cnt;
+			$tb =~ s/"//g;
+			my ($ora_owner, $ora_table) = split('\.', $tb);
+			if ($tables_infos{$ora_table}{owner} eq $ora_owner) {
+				$tables_infos{$ora_table}{num_rows} = $cnt || 0;
+			}
 		}
 
 		my $t2 = Benchmark->new;
@@ -11800,6 +12562,8 @@ sub _global_temp_table_info
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_global_temp_table_info($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_global_temp_table_info($self);
 	} else {
 		return Ora2Pg::Oracle::_global_temp_table_info($self);
 	}
@@ -11906,6 +12670,8 @@ sub _get_partitions
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_partitions($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_partitions($self);
 	} else {
 		return Ora2Pg::Oracle::_get_partitions($self);
 	}
@@ -11924,6 +12690,8 @@ sub _get_subpartitions
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_subpartitions($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_subpartitions($self);
 	} else {
 		return Ora2Pg::Oracle::_get_subpartitions($self);
 	}
@@ -11968,25 +12736,29 @@ sub _get_synonyms
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_synonyms($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_synonyms($self);
 	} else {
 		return Ora2Pg::Oracle::_get_synonyms($self);
 	}
 }
 
-=head2 _get_partitions_type
+=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_type
+sub _get_partitions_list
 {
 	my($self) = @_;
 
 	if ($self->{is_mysql}) {
-		return Ora2Pg::MySQL::_get_partitions_type($self);
+		return Ora2Pg::MySQL::_get_partitions_list($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_partitions_list($self);
 	} else {
-		return Ora2Pg::Oracle::_get_partitions_type($self);
+		return Ora2Pg::Oracle::_get_partitions_list($self);
 	}
 }
 
@@ -12002,6 +12774,8 @@ sub _get_partitioned_table
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_partitioned_table($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_partitioned_table($self);
 	} else {
 		return Ora2Pg::Oracle::_get_partitioned_table($self);
 	}
@@ -12019,6 +12793,8 @@ sub _get_subpartitioned_table
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_subpartitioned_table($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_subpartitioned_table($self);
 	} else {
 		return Ora2Pg::Oracle::_get_subpartitioned_table($self);
 	}
@@ -12032,6 +12808,8 @@ sub _get_custom_types
 	my %all_types = undef;
 	if ($self->{is_mysql}) {
 		%all_types = %Ora2Pg::MySQL::SQL_TYPE;
+	} elsif ($self->{is_mssql}) {
+		%all_types = %Ora2Pg::MSSQL::SQL_TYPE;
 	} else {
 		%all_types = %Ora2Pg::Oracle::SQL_TYPE;
 	}
@@ -12053,6 +12831,8 @@ sub _get_custom_types
 		my $cur_type = '';
 		if ($s =~ /\s+OF\s+([^\s;]+)/) {
 			$cur_type = $1;
+		} elsif ($s =~ /\s+FROM\s+([^\s;]+)/) {
+			$cur_type = uc($1);
 		} elsif ($s =~ /^\s*([^\s]+)\s+([^\s]+)/) {
 			$cur_type = $2;
 		}
@@ -12706,8 +13486,11 @@ sub data_dump
 	my $dirprefix = '';
 	$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
 	my $filename = $self->{output};
-	my $rname = $pname || $tname;
-	if ($self->{file_per_table}) {
+	my $rname = $tname;
+	$rname .= '_' . $pname if (!$self->{rename_partition} && $pname);
+	$rname = $pname if ($self->{rename_partition} && $pname);
+	if ($self->{file_per_table})
+	{
 		$filename = "${rname}_$self->{output}";
 		$filename = "tmp_$filename";
 	}
@@ -12944,7 +13727,7 @@ sub read_config
 		}
 		elsif ($var eq 'DEFINED_PK')
 		{
-			my @defined_pk = split(/[\s,;]+/, $val);
+			my @defined_pk = split(/[\s;]+/, $val);
 			foreach my $r (@defined_pk)
 			{ 
 				my ($table, $col) = split(/:/, $r);
@@ -13434,13 +14217,13 @@ sub _convert_function
 	}
 	return if (!exists $fct_detail{name});
 
-	$fct_detail{name} =~ s/^.*\.//;
+	$fct_detail{name} =~ s/^.*\.// if (!$self->{is_mssql} || $self->{schema}) ;
 	$fct_detail{name} =~ s/"//gs;
 
 	my $sep = '.';
 	$sep = '_' if (!$self->{package_as_schema});
 	my $fname =  $self->quote_object_name($fct_detail{name});
-	$fname =  $self->quote_object_name("$pname$sep$fct_detail{name}") if ($pname && !$self->{is_mysql});
+	$fname =  $self->quote_object_name("$pname$sep$fct_detail{name}") if ($pname && !$self->{is_mysql} && !$self->{is_mssql});
 	$fname =~ s/"_"/_/gs;
 
 	# rewrite argument syntax
@@ -13585,12 +14368,13 @@ sub _convert_function
 		$func_return = " AS \$body\$\n";
 	}
 	$func_return .= $param_comments;
+	$func_return =~ s/\s+AS(\s+AS\s+)/$1/is;
 
 	# extract custom type declared in a stored procedure
 	my $create_type = '';
 	while ($fct_detail{declare} =~ s/\s+TYPE\s+([^\s]+)\s+IS\s+RECORD\s*\(([^;]+)\)\s*;//is)
 	{
-		$create_type .= "DROP TYPE  $self->{pg_supports_ifexists} $1;\n" if ($self->{drop_if_exists});
+		$create_type .= "DROP TYPE  $self->{pg_supports_ifexists} $1;\n";
 		$create_type .= "CREATE TYPE $1 AS ($2);\n";
 	}
 	while ($fct_detail{declare} =~ s/\s+TYPE\s+([^\s]+)\s+(AS|IS)\s*(VARRAY|VARYING ARRAY)\s*\((\d+)\)\s*OF\s*([^;]+);//is) {
@@ -13605,9 +14389,9 @@ sub _convert_function
 			$type_name = "$owner.$type_name";
 		}
 		$internal_name  =~ s/^[^\.]+\.//;
-		my $declar = Ora2Pg::PLSQL::replace_sql_type($tbname, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, $self->{varchar_to_text}, %{$self->{data_type}});
+		my $declar = $self->_replace_sql_type($tbname);
 		$declar =~ s/[\n\r]+//s;
-		$create_type .= "DROP TYPE $self->{pg_supports_ifexists} $1;\n" if ($self->{drop_if_exists});
+		$create_type .= "DROP TYPE $self->{pg_supports_ifexists} $1;\n";
 		$create_type .= "CREATE TYPE $type_name AS ($internal_name $declar\[$size\]);\n";
 	}
 	
@@ -13662,9 +14446,15 @@ sub _convert_function
 	{
 		if ($self->{export_schema} && !$self->{schema})
 		{
-			$function = "\n$create_type\n\n${fct_warning}CREATE$self->{create_or_replace} $type " . $self->quote_object_name("$owner.$fname$at_suffix") . " $fct_detail{args}";
-			$name =  $self->quote_object_name("$owner.$fname");
-			$self->logit("Parsing function " . $self->quote_object_name("$owner.$fname") . "...\n", 1);
+			if ($owner) {
+				$function = "\n$create_type\n\n${fct_warning}CREATE$self->{create_or_replace} $type " . $self->quote_object_name("$owner.$fname$at_suffix") . " $fct_detail{args}";
+				$name =  $self->quote_object_name("$owner.$fname");
+				$self->logit("Parsing function " . $self->quote_object_name("$owner.$fname") . "...\n", 1);
+			} else {
+				$function = "\n$create_type\n\n${fct_warning}CREATE$self->{create_or_replace} $type " . $self->quote_object_name("$fname$at_suffix") . " $fct_detail{args}";
+				$name =  $self->quote_object_name("$fname");
+				$self->logit("Parsing function " . $self->quote_object_name("$fname") . "...\n", 1);
+			}
 		}
 		elsif ($self->{export_schema} && $self->{schema})
 		{
@@ -13929,10 +14719,11 @@ END;
 
 	$fname =~ s/"//g; # Remove case sensitivity quoting
 	$fname =~ s/^$pname\.//i; # remove package name
-	if ($pname && $self->{file_per_function}) {
+	if ($pname && $self->{file_per_function})
+	{
 		$self->logit("\tDumping to one file per function: $dirprefix\L$pname/$fname\E_$self->{output}\n", 1);
 		my $sql_header = "-- Generated by Ora2Pg, the Oracle database Schema converter, version $VERSION\n";
-		$sql_header .= "-- Copyright 2000-2022 Gilles DAROLD. All rights reserved.\n";
+		$sql_header .= "-- Copyright 2000-2023 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";
@@ -13941,6 +14732,29 @@ END;
 		$sql_header .= "SET check_function_bodies = false;\n\n" if (!$self->{function_check});
 		$sql_header = '' if ($self->{no_header});
 
+		if ($self->{print_dependencies} && $self->{plsql_pgsql} && !$self->{no_function_metadata})
+		{
+			# look for other routines call in the stored procedure
+			foreach my $sch (sort keys %{ $self->{function_metadata} })
+			{
+				foreach my $pkg_name (sort keys %{ $self->{function_metadata}{$sch} })
+				{
+					foreach my $fct_name (sort keys %{ $self->{function_metadata}{$sch}{$pkg_name} })
+					{
+						next if ($fct_name =~ /^$fname$/i || $fct_name =~ /^.*\.$fname$/i);
+						if ($fct_detail{code} =~ /\b$fct_name\b/is) {
+							push(@{ $self->{object_dependencies}{uc("$pname.$fname")}{routines} }, uc("$sch.$fct_name"));
+						}
+					}
+				}
+			}
+			# Look for merge/insert/update/delete
+			@{ $self->{object_dependencies}{uc("$pname.$fname")}{merge} } = $function =~ /\bMERGE\s+INTO\s+([^\(\s;,]+)/igs;
+			@{ $self->{object_dependencies}{uc("$pname.$fname")}{insert} } = $function =~ /\bINSERT\s+INTO\s+([^\(\s;,]+)/igs;
+			@{ $self->{object_dependencies}{uc("$pname.$fname")}{update} } = $function =~ /(?:(?!FOR).)*?\s*\bUPDATE\s+([^\s;,]+)\s+/igs;
+			@{ $self->{object_dependencies}{uc("$pname.$fname")}{delete} } = $function =~ /\b(?:DELETE\s+FROM|TRUNCATE\s+TABLE)\s+([^\s;,]+)\s+/igs;
+		}
+
 		my $fhdl = $self->open_export_file("$dirprefix\L$pname/$fname\E_$self->{output}", 1);
 		$self->set_binmode($fhdl) if (!$self->{compress});
 		$self->_restore_comments(\$function);
@@ -14204,15 +15018,20 @@ sub _convert_type
 {
 	my ($self, $plsql, $owner, %pkg_type) = @_;
 
+        my ($package, $filename, $line) = caller;
+
 	my $unsupported = "-- Unsupported, please edit to match PostgreSQL syntax\n";
 	my $content = '';
 	my $type_name = '';
 
+	$plsql =~ s/AUTHID DEFINER//is;
+
 	# Replace SUBTYPE declaration into DOMAIN declaration
         if ($plsql =~ s/SUBTYPE\s+/CREATE DOMAIN /i)
 	{
 		$plsql =~ s/\s+IS\s+/ AS /;
-		$plsql = Ora2Pg::PLSQL::replace_sql_type($plsql, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, $self->{varchar_to_text}, %{$self->{data_type}});
+		$plsql =~ s/^CREATE TYPE/TYPE/i;
+		$plsql = $self->_replace_sql_type($plsql);
 		return $plsql;
 	}
 
@@ -14234,7 +15053,7 @@ sub _convert_type
 		$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->{varchar_to_text}, %{$self->{data_type}});
+			$type_of = $self->_replace_sql_type($type_of);
 			$self->{type_of_type}{'Nested Tables'}++;
 			$content .= "DROP TYPE $self->{pg_supports_ifexists} " . $self->quote_object_name($type_name) . ";\n" if ($self->{drop_if_exists});
 			$content = "CREATE TYPE " . $self->quote_object_name($type_name) . " AS (" . $self->quote_object_name($internal_name) . " $type_of\[\]);\n";
@@ -14272,7 +15091,7 @@ sub _convert_type
 			return "${unsupported}CREATE$self->{create_or_replace} $plsql";
 		}
 		$description =~ s/^\s+//s;
-		my $declar = Ora2Pg::PLSQL::replace_sql_type($description, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, $self->{varchar_to_text}, %{$self->{data_type}});
+		my $declar = $self->_replace_sql_type($description);
 		$type_name =~ s/"//g;
 		if ($self->{export_schema} && !$self->{schema} && $owner && $type_name !~ /\./) {
 			$type_name = "$owner.$type_name";
@@ -14280,7 +15099,7 @@ sub _convert_type
 		if ($notfinal =~ /FINAL/is)
 		{
 			$content = "-- Inherited types are not supported in PostgreSQL, replacing with inherited table\n";
-			$content .= qq{CREATE TABLE " . $self->quote_object_name($type_name) . " (
+			$content .= "CREATE TABLE " . $self->quote_object_name($type_name) . qq{ (
 $declar
 );
 };
@@ -14307,7 +15126,7 @@ $declar
 			return "${unsupported}CREATE$self->{create_or_replace} $plsql";
 		}
 		$description =~ s/^\s+//s;
-		my $declar = Ora2Pg::PLSQL::replace_sql_type($description, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, $self->{varchar_to_text}, %{$self->{data_type}});
+		my $declar = $self->_replace_sql_type($description);
 		$type_name =~ s/"//g;
 		if ($self->{export_schema} && !$self->{schema} && $owner && $type_name !~ /\./) {
 			$type_name = "$owner.$type_name";
@@ -14333,11 +15152,18 @@ $declar
 		my $internal_name = $type_name;
 		chomp($tbname);
 		$internal_name  =~ s/^[^\.]+\.//;
-		my $declar = Ora2Pg::PLSQL::replace_sql_type($tbname, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, $self->{varchar_to_text}, %{$self->{data_type}});
+		my $declar = $self->_replace_sql_type($tbname);
 		$declar =~ s/[\n\r]+//s;
 		$content = "CREATE TYPE " . $self->quote_object_name($type_name) . " AS (" . $self->quote_object_name($internal_name) . " $declar\[$size\]);\n";
 		$self->{type_of_type}{Varrays}++;
 	}
+	elsif ($plsql =~ /TYPE\s+([^\s]+)\s+FROM\s+(.*)( NOT NULL)?;/is)
+	{
+		my $typname = $1;
+		my $notnull = $3;
+		my $dtype = $self->_replace_sql_type($2);
+		$content .= "CREATE DOMAIN $typname AS $dtype$notnull;\n";
+	}
 	else
 	{
 		$self->{type_of_type}{Unknown}++;
@@ -14494,6 +15320,10 @@ sub custom_type_definition
 			{
 				if ($tpe->{code} =~ /AS\s+VARRAY\s*(.*?)\s+OF\s+([^\s;]+);/is) {
 					return $self->custom_type_definition(uc($2), $orig, 1);
+				} elsif ($tpe->{code} =~ /(.*FROM\s+[^;\s\(]+)/is) {
+					%types_def = $self->_get_custom_types($1);
+					push(@{$user_type{pg_types}} , \@{$types_def{pg_types}});
+					push(@{$user_type{src_types}}, \@{$types_def{src_types}});
 				}
 				elsif ($tpe->{code} =~ /\s+([^\s]+)\s+AS\s+TABLE\s+OF\s+([^;]+);/is)
 				{
@@ -14584,7 +15414,17 @@ sub _extract_data
 		}
 
 		# prepare the query before execution
-		if (!$self->{is_mysql})
+		if ($self->{is_mysql})
+		{
+			$query =~ s/^SELECT\s+/SELECT \/\*\!40001 SQL_NO_CACHE \*\/ /s;
+			$sth = $dbh->prepare($query, { mysql_use_result => 1, mysql_use_row => 1 }) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
+		}
+		elsif ($self->{is_mssql})
+		{
+			$sth = $dbh->prepare($query) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
+			$sth->{'LongReadLen'} = $self->{longreadlen};
+		}
+		else
 		{
 			if (!$self->{use_lob_locator}) {
 				$sth = $dbh->prepare($query,{ora_piece_lob => 1, ora_piece_size => $self->{longreadlen}, ora_exe_mode=>OCI_STMT_SCROLLABLE_READONLY, ora_check_sql => 1}) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
@@ -14595,12 +15435,6 @@ sub _extract_data
 				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
 	{
@@ -14611,7 +15445,17 @@ sub _extract_data
 		} 
 
 		# prepare the query before execution
-		if (!$self->{is_mysql})
+		if ($self->{is_mysql})
+		{
+			$query =~ s/^SELECT\s+/SELECT \/\*\!40001 SQL_NO_CACHE \*\/ /s;
+			$sth = $self->{dbh}->prepare($query, { mysql_use_result => 1, mysql_use_row => 1 }) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+		}
+		elsif ($self->{is_mssql})
+		{
+			$sth = $self->{dbh}->prepare($query) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+			$sth->{'LongReadLen'} = $self->{longreadlen};
+		}
+		else
 		{
 			# Set the action name on Oracle side to see which table is exported
 			$self->{dbh}->do("CALL DBMS_APPLICATION_INFO.set_action('$table')") or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
@@ -14635,12 +15479,6 @@ sub _extract_data
 				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
@@ -14666,9 +15504,9 @@ sub _extract_data
 		$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);
+		$sth->execute(@params) or $self->logit("FATAL: " . $dbh->errstr . ", SQL: $query\n", 0, 1);
 	} else {
-		$sth->execute(@params) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+		$sth->execute(@params) or $self->logit("FATAL: " . $self->{dbh}->errstr . ", SQL: $query\n", 0, 1);
 	}
 
 	my $col_cond = $self->hs_cond($tt,$stt, $table);
@@ -14913,7 +15751,7 @@ sub _extract_data
 			}
 		}
 
-		if (@rows && (!$self->{oracle_speed} || $self->{ora2pg_speed}))
+		if (!$self->{oracle_speed} || $self->{ora2pg_speed})
 		{
 			$total_record += @rows;
 			$self->{current_total_row} += @rows;
@@ -15168,9 +16006,14 @@ sub _dump_to_pg
 	elsif (!$sprep)
 	{
 		$sql_out = '';
-		foreach my $row (@$rows) {
+		foreach my $row (@$rows)
+		{
 			$sql_out .= $s_out;
-			$sql_out .= join(',', @$row) . ");\n";
+			$sql_out .= join(',', @$row) . ")";
+			if ($self->{insert_on_conflict}) {
+				$sql_out .= " ON CONFLICT DO NOTHING";
+			}
+			$sql_out .= ";\n";
 		}
 	}
 
@@ -15262,7 +16105,7 @@ sub _dump_to_pg
 	}
 	else
 	{
-		if ($part_name && $self->{prefix_partition})  {
+		if ($part_name && $self->{rename_partition})  {
 			$part_name = $table . '_' . $part_name;
 		}
 		$sql_out = $h_towrite . $sql_out . $e_towrite;
@@ -15437,6 +16280,19 @@ sub _show_infos
 			$self->logit("\tMySQL collation encoding: $collation\n", 0);
 			$self->logit("\tPostgreSQL CLIENT_ENCODING: $client_encoding\n", 0);
 			$self->logit("MySQL SQL mode: $self->{mysql_mode}\n", 0);
+		} elsif ($self->{is_mssql})
+		{
+			$self->logit("Current encoding settings that will be used by Ora2Pg:\n", 0);
+			$self->logit("\tMSSQL database and client encoding: utf8\n", 0);
+			$self->logit("\tMSSQL collation encoding: $self->{nls_nchar}\n", 0);
+			$self->logit("\tPostgreSQL CLIENT_ENCODING: $self->{client_encoding}\n", 0);
+			$self->logit("\tPerl output encoding '$self->{binmode}'\n", 0);
+			$self->logit("\tOra2Pg use UTF8 export to export from MSSQL, change to NSL_LANG and\n", 0);
+			$self->logit("\tNLS_NCHAR have no effect. CLIENT_ENCODING must be set to UFT8\n", 0);
+			$self->logit("Showing current MSSQL encoding and possible PostgreSQL client encoding:\n", 0);
+			$self->logit("\tMSSQL database encoding: $self->{nls_lang}\n", 0);
+			$self->logit("\tMSSQL collation encoding: $self->{nls_nchar}\n", 0);
+			$self->logit("\tPostgreSQL CLIENT_ENCODING: $client_encoding\n", 0);
 		} else {
 			$self->logit("Current encoding settings that will be used by Ora2Pg:\n", 0);
 			$self->logit("\tOracle NLS_LANG $self->{nls_lang}\n", 0);
@@ -15464,10 +16320,24 @@ sub _show_infos
 	}
 	elsif ($type eq 'SHOW_REPORT')
 	{
+		# Get all tables information specified by the DBI method table_info
+		if ($self->{is_mssql})
+		{
+			my $sth = $self->_schema_list() or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+			while ( my @row = $sth->fetchrow()) {
+				push(@{$self->{local_schemas}}, $row[0]);
+			}
+			$sth->finish();
+		}
+		if ($#{$self->{local_schemas}} >= 0) {
+			$self->{local_schemas_regex} = '(' . join('|', @{$self->{local_schemas}}) . ')';
+		}
 		print STDERR "Reporting Oracle Content...\n" if ($self->{debug});
 		my $uncovered_score = 'Ora2Pg::PLSQL::UNCOVERED_SCORE';
 		if ($self->{is_mysql}) {
 			$uncovered_score = 'Ora2Pg::PLSQL::UNCOVERED_MYSQL_SCORE';
+		} elsif ($self->{is_mssql}) {
+			$uncovered_score = 'Ora2Pg::PLSQL::UNCOVERED_MSSQL_SCORE';
 		}
 		# Get Oracle database version and size
 		print STDERR "Looking at Oracle server version...\n" if ($self->{debug});
@@ -15761,36 +16631,36 @@ sub _show_infos
 				my $j = 1;
 				foreach my $t (sort {$self->{tables}{$b}{table_info}{num_rows} <=> $self->{tables}{$a}{table_info}{num_rows}} keys %{$self->{tables}})
 				{
+					next if ($self->{tables}{$t}{table_info}{num_rows} == 0);
 					$report_info{'Objects'}{$typ}{'detail'} .= "\L$t\E has $self->{tables}{$t}{table_info}{num_rows} rows\n";
 					$j++;
 					last if ($j > $self->{top_max});
 				}
-				my %largest_table = ();
-				%largest_table = $self->_get_largest_tables() if ($self->{is_mysql});
-				if ((scalar keys %largest_table > 0) || !$self->{is_mysql})
+				$report_info{'Objects'}{$typ}{'detail'} .= "Top $self->{top_max} of largest tables:\n";
+				$j = 1;
+				if ($self->{is_mssql} || $self->{is_mysql})
 				{
-					$i = 1;
-					if (!$self->{is_mysql})
+					foreach my $t (sort {$self->{tables}{$b}{table_info}{size} <=> $self->{tables}{$a}{table_info}{size}} keys %{$self->{tables}})
 					{
-						$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});
-						}
+						next if ($self->{tables}{$t}{table_info}{size} == 0);
+						$report_info{'Objects'}{$typ}{'detail'} .= "\L$t\E: $self->{tables}{$t}{table_info}{size} MB (" . $self->{tables}{$t}{table_info}{num_rows} . " rows)\n";
+						$j++;
+						last if ($j > $self->{top_max});
 					}
-					else
+				}
+				else
+				{
+					# Because we avoid using JOIN when querying the Oracle catalog, look for all table size
+					my %largest_table = ();
+					%largest_table = $self->_get_largest_tables() if ($self->{is_mysql});
+					foreach my $t (sort { $largest_table{$b} <=> $largest_table{$a} } keys %largest_table)
 					{
-						$report_info{'Objects'}{$typ}{'detail'} .= "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});
-						}
+						$report_info{'Objects'}{$typ}{'detail'} .= "\L$t\E: $largest_table{$t} MB (" . $self->{tables}{$t}{table_info}{num_rows} . " rows)\n";
+						$j++;
+						last if ($j > $self->{top_max});
 					}
 				}
+
 				$comment = "Nothing particular." if (!$comment);
 				$report_info{'Objects'}{$typ}{'cost_value'} =~ s/(\.\d).*$/$1/;
 				if (scalar keys %encrypted_column > 0)
@@ -15835,14 +16705,27 @@ sub _show_infos
 			{
 				my $triggers = $self->_get_triggers();
 				my $total_size = 0;
-				foreach my $trig (@{$triggers}) {
+				foreach my $trig (@{$triggers})
+				{
+                                        # Remove comment and text constant, they are not useful in assessment
+                                        $self->_remove_comments(\$trig->[4]);
+                                        $self->{comment_values} = ();
+                                        $self->{text_values} = ();
+                                        $self->{text_values_pos} = 0;
+                                        if ($self->{is_mysql}) {
+                                                $trig->[4] = $self->_convert_function($trig->[8], $trig->[4], $trig->[0]);
+                                        } else {
+                                                $trig->[4] = $self->_convert_function($trig->[8], $trig->[4]);
+                                        }
 					$total_size += length($trig->[4]);
-					if ($self->{estimate_cost}) {
-						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $trig->[4]);
+					if ($self->{estimate_cost})
+					{
+						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $trig->[4], 'TRIGGER');
 						$report_info{'Objects'}{$typ}{'cost_value'} += $cost;
 						$report_info{'Objects'}{$typ}{'detail'} .= "\L$trig->[0]: $cost\E\n";
 						$report_info{full_trigger_details}{"\L$trig->[0]\E"}{count} = $cost;
-						foreach my $d (sort { $cost_detail{$b} <=> $cost_detail{$a} } keys %cost_detail) {
+						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});
@@ -15863,10 +16746,20 @@ sub _show_infos
 				my $total_size = 0;
 				foreach my $fct (keys %{$functions})
 				{
+                                        # Remove comment and text constant, they are not useful in assessment
+                                        $self->_remove_comments(\$functions->{$fct}{text});
+                                        $self->{comment_values} = ();
+                                        $self->{text_values} = ();
+                                        $self->{text_values_pos} = 0;
+                                        if ($self->{is_mysql}) {
+                                                $functions->{$fct}{text} = $self->_convert_function($functions->{$fct}{owner}, $functions->{$fct}{text}, $fct);
+                                        } else {
+                                                $functions->{$fct}{text} = $self->_convert_function($functions->{$fct}{owner}, $functions->{$fct}{text});
+                                        }
 					$total_size += length($functions->{$fct}{text});
 					if ($self->{estimate_cost})
 					{
-						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $functions->{$fct}{text});
+						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $functions->{$fct}{text}, 'FUNCTION');
 						$report_info{'Objects'}{$typ}{'cost_value'} += $cost;
 						$report_info{'Objects'}{$typ}{'detail'} .= "\L$fct: $cost\E\n";
 						$report_info{full_function_details}{"\L$fct\E"}{count} = $cost;
@@ -15888,20 +16781,30 @@ sub _show_infos
 				my $total_size = 0;
 				foreach my $proc (keys %{$procedures})
 				{
+					# Remove comment and text constant, they are not useful in assessment
+					$self->_remove_comments(\$procedures->{$proc}{text});
+					$self->{comment_values} = ();
+					$self->{text_values} = ();
+					$self->{text_values_pos} = 0;
+					if ($self->{is_mysql}) {
+						$procedures->{$proc}{text} = $self->_convert_function($procedures->{$proc}{owner}, $procedures->{$proc}{text}, $proc);
+					} else {
+						$procedures->{$proc}{text} = $self->_convert_function($procedures->{$proc}{owner}, $procedures->{$proc}{text});
+					}
 					$total_size += length($procedures->{$proc}{text});
 					if ($self->{estimate_cost})
 					{
-						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $procedures->{$proc}{text});
+						my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $procedures->{$proc}{text}, 'PROCEDURE');
 						$report_info{'Objects'}{$typ}{'cost_value'} += $cost;
 						$report_info{'Objects'}{$typ}{'detail'} .= "\L$proc: $cost\E\n";
-						$report_info{full_function_details}{"\L$proc\E"}{count} = $cost;
+						$report_info{full_procedure_details}{"\L$proc\E"}{count} = $cost;
 						foreach my $d (sort { $cost_detail{$b} <=> $cost_detail{$a} } keys %cost_detail)
 						{
 							next if (!$cost_detail{$d});
-							$report_info{full_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{full_procedure_details}{"\L$proc\E"}{info} .= "\t$d => $cost_detail{$d}";
+							$report_info{full_procedure_details}{"\L$proc\E"}{info} .= " (cost: ${$uncovered_score}{$d})" if (${$uncovered_score}{$d});
+							$report_info{full_procedure_details}{"\L$proc\E"}{info} .= "\n";
+							push(@{$report_info{full_procedure_details}{"\L$proc\E"}{keywords}}, $d) if (($d ne 'SIZE') && ($d ne 'TEST')); 
 						}
 					}
 				}
@@ -15933,17 +16836,17 @@ sub _show_infos
 							next if (!$f);
 							if ($self->{estimate_cost})
 							{
-								my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $infos{$f}{code});
+								my ($cost, %cost_detail) = Ora2Pg::PLSQL::estimate_cost($self, $infos{$f}{code}, $infos{$f}{type});
 								$report_info{'Objects'}{$typ}{'cost_value'} += $cost;
 								$report_info{'Objects'}{$typ}{'detail'} .= "\L$f: $cost\E\n";
-								$report_info{full_function_details}{"\L$f\E"}{count} = $cost;
+								$report_info{full_package_details}{"\L$f\E"}{count} = $cost;
 								foreach my $d (sort { $cost_detail{$b} <=> $cost_detail{$a} } keys %cost_detail)
 								{
 									next if (!$cost_detail{$d});
-									$report_info{full_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')); 
+									$report_info{full_package_details}{"\L$f\E"}{info} .= "\t$d => $cost_detail{$d}";
+									$report_info{full_package_details}{"\L$f\E"}{info} .= " (cost: ${$uncovered_score}{$d})" if (${$uncovered_score}{$d});
+									$report_info{full_package_details}{"\L$f\E"}{info} .= "\n";
+									push(@{$report_info{full_package_details}{"\L$f\E"}{keywords}}, $d) if (($d ne 'SIZE') && ($d ne 'TEST')); 
 								}
 							}
 							$number_fct++;
@@ -15958,9 +16861,10 @@ sub _show_infos
 			}
 			elsif ( ($typ eq 'SYNONYM') && !$self->{is_mysql} )
 			{
-				foreach my $t (sort {$a cmp $b} keys %synonyms) {
+				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";
+						$report_info{'Objects'}{$typ}{'detail'} .= "\L$t\E is a link to \L$synonyms{$t}{table_owner}.$synonyms{$t}{table_name}\@$synonyms{$t}{dblink}\E\n";
 					} else {
 						$report_info{'Objects'}{$typ}{'detail'} .= "\L$t\E is an alias to $synonyms{$t}{table_owner}.$synonyms{$t}{table_name}\n";
 					}
@@ -15973,9 +16877,9 @@ sub _show_infos
 			}
 			elsif ($typ eq 'TABLE PARTITION')
 			{
-				my %partitions = $self->_get_partitions_type();
+				my %partitions = $self->_get_partitions_list();
 				foreach my $t (sort keys %partitions) {
-					$report_info{'Objects'}{$typ}{'detail'} .= " $partitions{$t} $t partitions.\n";
+					$report_info{'Objects'}{$typ}{'detail'} .= "$t\n";
 				}
 				$report_info{'Objects'}{$typ}{'comment'} = "Partitions are well supported by PostgreSQL except key partition which will not be exported.";
 			}
@@ -16085,7 +16989,8 @@ sub _show_infos
 		# 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()) {
+		while ( my @row = $sth->fetchrow()) 
+		{
 			my $warning = '';
 			my $ret = $self->is_reserved_words($row[0]);
 			if ($ret == 1) {
@@ -16177,7 +17082,7 @@ sub _show_infos
 		foreach my $t (@ordered_tables)
 		{
 			# Jump to desired extraction
-			if (grep(/^$t$/, @done))
+			if (grep(/^\Q$t\E$/, @done))
 			{
 				$self->logit("Duplicate entry found: $t\n", 1);
 				next;
@@ -16267,6 +17172,7 @@ sub _show_infos
 						{
 							if ($type1 !~ s/smallint/smallserial/) {
 								$type1 =~ s/integer/serial/;
+								$type1 =~ s/numeric.*/bigserial/;
 							}
 						}
 						if ($type1 =~ /serial/) {
@@ -16288,7 +17194,7 @@ sub _show_infos
 					$self->logit("\t$d->[0] : $d->[1]");
 					if ($d->[1] !~ /SDO_GEOMETRY/)
 					{
-						if ($d->[2] && !$d->[5]) {
+						if ($d->[2] && !$d->[5] && $d->[1] !~ /\(\d+\)/) {
 							$self->logit("($d->[2])");
 						}
 						elsif ($d->[5] && ($d->[1] =~ /NUMBER/i) )
@@ -16396,7 +17302,9 @@ sub _show_infos
 
 		$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) {
+		foreach my $t (sort {$tables_infos{$b}{num_rows} <=> $tables_infos{$a}{num_rows}} keys %tables_infos)
+		{
+			next if ($tables_infos{$t}{num_rows} == 0);
 			my $tname = $t;
 			if (!$self->{is_mysql}) {
 				$tname = "$tables_infos{$t}{owner}.$t" if ($self->{debug});
@@ -16407,7 +17315,23 @@ sub _show_infos
 		}
 		$self->logit("Top $self->{top_max} of largest tables:\n", 0);
 		$i = 1;
-		if (!$self->{is_mysql}) {
+		if ($self->{is_mssql} || $self->{is_mysql})
+		{
+			foreach my $t (sort {$tables_infos{$b}{size} <=> $tables_infos{$a}{size}} keys %tables_infos)
+			{
+				next if ($tables_infos{$t}{size} == 0);
+				my $tname = $t;
+				if (!$self->{is_mysql} && !$self->{is_mssql}) {
+					$tname = "$tables_infos{$t}{owner}.$t" if ($self->{debug});
+				}
+				$self->logit("\t[$i] TABLE $tname: $tables_infos{$t}{size} MB (" . $tables_infos{$t}{num_rows} . " rows)\n", 0);
+				$i++;
+				last if ($i > $self->{top_max});
+			}
+		}
+		else
+		{
+			# Because we avoid using JOIN when querying the Oracle catalog, look for all table size
 			my %largest_table = $self->_get_largest_tables();
 			foreach my $t (sort { $largest_table{$b} <=> $largest_table{$a} } keys %largest_table) {
 				last if ($i > $self->{top_max});
@@ -16416,13 +17340,6 @@ sub _show_infos
 				$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++;
-			}
 		}
 	}
 }
@@ -16435,7 +17352,7 @@ sub show_test_errors
 	if ($#errors >= 0)
 	{
 		foreach my $msg (@errors) {
-			print "$msg\n";
+			print "DIFF: $msg\n";
 		}
 	}
 	else
@@ -16459,18 +17376,30 @@ sub set_pg_relation_name
 	$orig = " (origin: $table)" if (lc($cmptb) ne lc($table));
 	my $tbname = $tbmod;
 	$tbname =~ s/[^"\.]+\.//;
-	my $schm = $self->{pg_schema};
+	my $schm = $self->{schema};
+	$schm = $self->{pg_schema} if ($self->{pg_schema});
 	$schm =~ s/"//g;
 	$tbname =~ s/"//g;
 	$schm = $self->quote_object_name($schm);
 	$tbname = $self->quote_object_name($tbname);
-	if ($self->{pg_schema} && $self->{export_schema}) {
-		return ($tbmod, $orig, $self->{pg_schema}, "$schm.$tbname");
-	} elsif ($self->{schema} && $self->{export_schema}) {
-		return ($tbmod, $orig, $self->{schema}, "$schm.$tbname");
+	if ($self->{pg_schema})
+	{
+		if ($self->{preserve_case}) {
+			return ($tbmod, $orig, $self->{pg_schema}, "\"$schm\".\"$tbname\"");
+		} else {
+			return ($tbmod, $orig, $self->{pg_schema}, "$schm.$tbname");
+		}
+	}
+	elsif ($self->{schema} && $self->{export_schema})
+	{
+		if ($self->{preserve_case}) {
+			return ($tbmod, $orig, $self->{schema}, "\"$schm\".\"$tbname\"");
+		} else {
+			return ($tbmod, $orig, $self->{schema}, "$schm.$tbname");
+		}
 	}
 
-	return ($tbmod, $orig, '', $tbmod);
+	return ($tbmod, $orig, '', $tbname);
 }
 
 sub get_schema_condition
@@ -16498,36 +17427,59 @@ sub _count_pg_rows
 {
 	my ($self, $dbhdest, $lbl, $t, $num_rows) = @_;
 
+	chomp($num_rows);
+
+	my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
 	if ($self->{pg_dsn})
 	{
+		my $err = '';
+		my $dirprefix = '';
+		$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
+
 		$self->logit("DEBUG: pid $$ looking for real row count for destination table $t...\n", 1);
 
-		my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
-		my $s = $dbhdest->prepare("SELECT count(*) FROM $both;") or $self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
-		if ($self->{preserve_case}) {
-			$s = $dbhdest->prepare("SELECT count(*) FROM \"$schema\".\"$t\";") or $self->logit("FATAL: " . $dbhdest->errstr . "\n", 0, 1);
-		}
-		if (not $s->execute)
+		my $sql = "SELECT count(*) FROM $both;";
+#		if ($self->{preserve_case}) {
+#			$sql = "SELECT count(*) FROM \"$schema\".\"$t\";";
+#		}
+		my $s = $dbhdest->prepare($sql);
+		if (not defined $s)
 		{
-			push(@errors, "Table $both$orig does not exists in PostgreSQL database.") if ($s->state eq '42P01');
-			next;
+			$err = "Table $both$orig does not exists in PostgreSQL database." if ($s->state eq '42P01');
 		}
-                my $dirprefix = '';
-		$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
-		my $fh = new IO::File;
-		$fh->open(">>${dirprefix}ora2pg_stdout_locker") or $self->logit("FATAL: can't write to ${dirprefix}ora2pg_stdout_locker, $!\n", 0, 1);
-		flock($fh, 2) || die "FATAL: can't lock file ${dirprefix}ora2pg_stdout_locker\n";
-		print "$lbl:$t:$num_rows\n";
-		while ( my @row = $s->fetchrow())
+		else
 		{
-			print "POSTGRES:$both$orig:$row[0]\n";
-			if ($row[0] != $num_rows) {
-				$fh->print("Table $both$orig doesn't have the same number of line in source database ($num_rows) and in PostgreSQL ($row[0]).\n");
+			if (not $s->execute)
+			{
+				$err = "Table $both$orig does not exists in PostgreSQL database." if ($s->state eq '42P01');
+			}
+			else
+			{
+				my $fh = new IO::File;
+				$fh->open(">>${dirprefix}ora2pg_stdout_locker") or $self->logit("FATAL: can't write to ${dirprefix}ora2pg_stdout_locker, $!\n", 0, 1);
+				flock($fh, 2) || die "FATAL: can't lock file ${dirprefix}ora2pg_stdout_locker\n";
+				print "$lbl:$t:$num_rows\n";
+				while ( my @row = $s->fetchrow())
+				{
+					print "POSTGRES:$both$orig:$row[0]\n";
+					if ($row[0] != $num_rows) {
+						$fh->print("Table $both$orig doesn't have the same number of line in source database ($num_rows) and in PostgreSQL ($row[0]).\n");
+					}
+					last;
+				}
+				$s->finish();
+				$fh->close;
 			}
-			last;
 		}
-		$s->finish();
-		$fh->close;
+		if ($err)
+		{
+			my $fh = new IO::File;
+			$fh->open(">>${dirprefix}ora2pg_stdout_locker") or $self->logit("FATAL: can't write to ${dirprefix}ora2pg_stdout_locker, $!\n", 0, 1);
+			flock($fh, 2) || die "FATAL: can't lock file ${dirprefix}ora2pg_stdout_locker\n";
+			print "$lbl:$t:$num_rows\n";
+			$fh->print("$err\n");
+			$fh->close;
+		}
 	}
 }
 
@@ -16537,6 +17489,7 @@ sub _table_row_count
 
 	my $lbl = 'ORACLEDB';
 	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
+	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});
 
 	# Get all tables information specified by the DBI method table_info
 	$self->logit("Looking for real row count in source database and PostgreSQL tables...\n", 1);
@@ -16628,6 +17581,22 @@ sub is_in_struct
 	return 1;
 }
 
+sub _col_count
+{
+	my ($self, $table, $schema) = @_;
+
+	my %col_count = ();
+	if ($self->{is_mysql}) {
+		%col_count = Ora2Pg::MySQL::_col_count($self, $table, $schema);
+	} elsif ($self->{is_mssql}) {
+		%col_count = Ora2Pg::MSSQL::_col_count($self, $table, $schema);
+	} else {
+		%col_count = Ora2Pg::Oracle::_col_count($self, $table, $schema);
+	}
+
+	return %col_count;
+}
+
 sub _test_table
 {
 	my $self = shift;
@@ -16642,24 +17611,20 @@ sub _test_table
 
 	my $lbl = 'ORACLEDB';
 	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
+	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});
 
 	####
 	# Test number of column in tables
 	####
 	print "[TEST COLUMNS COUNT]\n";
-	my %col_count = ();
-	if ($self->{is_mysql}) {
-		%col_count = Ora2Pg::MySQL::_col_count($self, '', $self->{schema});
-	} else {
-		%col_count = Ora2Pg::Oracle::_col_count($self, '', $self->{schema});
-	}
+	my %col_count = $self->_col_count('', $self->{schema});
 	$schema_cond = $self->get_schema_condition('pg_class.relnamespace::regnamespace::text');
 	my $sql = qq{
-SELECT pg_namespace.nspname||'.'||pg_class.relname, pg_attribute.attname
+SELECT upper(pg_namespace.nspname||'.'||pg_class.relname), pg_attribute.attname
 FROM pg_attribute
 JOIN pg_class ON (pg_class.oid=pg_attribute.attrelid)
 JOIN pg_namespace ON (pg_class.relnamespace=pg_namespace.oid)
-WHERE pg_class.relkind = 'r' AND pg_attribute.attnum > 0 AND NOT pg_attribute.attisdropped $schema_cond
+WHERE pg_class.relkind IN ('r', 'p') AND pg_attribute.attnum > 0 AND NOT pg_attribute.attisdropped $schema_cond
 ORDER BY pg_attribute.attnum
 };
 	my %pgret = ();
@@ -16674,7 +17639,7 @@ ORDER BY pg_attribute.attnum
 		while ( my @row = $s->fetchrow())
 		{
 			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
-			push(@{$pgret{"\U$row[0]\E"}}, $row[1]);
+			push(@{$pgret{$row[0]}}, $row[1]);
 		}
 		$s->finish;
 	}
@@ -16683,18 +17648,21 @@ ORDER BY pg_attribute.attnum
 		$pgcount{$t} = $#{$pgret{$t}} + 1;
 	}
 
+	my @tables_names = keys %tables_infos;
 	foreach my $t (sort keys %col_count)
 	{
-		next if (!exists $tables_infos{$t});
+		next if (!grep(/^\Q$t\E$/i, @tables_names));
 		print "$lbl:$t:$col_count{$t}\n";
 		if ($self->{pg_dsn})
 		{
 			my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
-			$pgcount{"\U$both$orig\E"} ||= 0;
-			print "POSTGRES:$both$orig:", $pgcount{"\U$both$orig\E"}, "\n";
-			if ($pgcount{"\U$both$orig\E"} != $col_count{$t}) {
-				push(@errors, "Table $both$orig doesn't have the same number of columns in source database ($col_count{$t}) and in PostgreSQL (" . $pgcount{"\U$both$orig\E"} . ").");
-				push(@errors, "\tPostgreSQL modified struct: \U$both$orig\E(" . join(',', @{$pgret{"\U$both$orig\E"}}) . ")");
+			my $tbname = "\U$both$orig\E";
+			$tbname =~ s/"//g;
+			$pgcount{$tbname} ||= 0;
+			print "POSTGRES:$both$orig:", $pgcount{$tbname}, "\n";
+			if ($pgcount{$tbname} != $col_count{$t}) {
+				push(@errors, "Table $both$orig doesn't have the same number of columns in source database ($col_count{$t}) and in PostgreSQL (" . $pgcount{$tbname} . ").");
+				push(@errors, "\tPostgreSQL modified struct: $both$orig(" . join(',', @{$pgret{$tbname}}) . ")");
 			}
 		}
 	}
@@ -16713,12 +17681,17 @@ ORDER BY pg_attribute.attnum
 	} else {
 		($uniqueness, $indexes, $idx_type, $idx_tbsp) = $self->_get_indexes('', $self->{schema}, 1);
 	}
-	$schema_cond = $self->get_schema_condition('pg_indexes.schemaname');
-	$sql = qq{
-SELECT schemaname||'.'||tablename, count(*)
-FROM pg_indexes
-WHERE 1=1 $schema_cond
-GROUP BY schemaname,tablename
+	$schema_cond = $self->get_schema_condition('tn.nspname');
+	my $exclude_unique = '';
+	$exclude_unique = 'AND NOT i.indisunique' if ($self->{is_mssql});
+	$sql = qq{SELECT tn.nspname||'.'||t.relname, count(*)
+FROM pg_index i
+JOIN pg_class c on c.oid = i.indexrelid
+JOIN pg_namespace n on n.oid = c.relnamespace
+JOIN pg_class t on t.oid = i.indrelid
+JOIN pg_namespace tn on tn.oid = t.relnamespace
+WHERE 1=1 $exclude_unique $schema_cond
+GROUP BY tn.nspname, t.relname
 };
 	%pgret = ();
 	if ($self->{pg_dsn})
@@ -16736,7 +17709,7 @@ GROUP BY schemaname,tablename
 		}
 		$s->finish;
 	}
-	# Initialize when there is not indexes in a table
+	# Initialize when there is no indexes in a table
 	foreach my $t (keys %tables_infos) {
 		$indexes->{$t} = {} if (not exists $indexes->{$t});
 	}
@@ -16765,14 +17738,17 @@ GROUP BY schemaname,tablename
 	print "\n";
 	print "[TEST UNIQUE CONSTRAINTS COUNT]\n";
 	my %unique_keys = $self->_unique_key('',$self->{schema},'U');
-	$schema_cond = $self->get_schema_condition('pg_class.relnamespace::regnamespace::text');
-	$sql = qq{
-SELECT schemaname||'.'||tablename, count(*)
-FROM pg_indexes
-JOIN pg_class ON (pg_class.relname=pg_indexes.indexname)
-JOIN pg_constraint ON (pg_constraint.conname=pg_class.relname AND pg_constraint.connamespace=pg_class.relnamespace)
-WHERE pg_constraint.contype IN ('u') $schema_cond
-GROUP BY schemaname,tablename
+	$schema_cond = $self->get_schema_condition('tn.nspname');
+	$exclude_unique = '';
+	$exclude_unique = 'AND i.indisunique' if ($self->{is_mssql});
+	$sql = qq{SELECT tn.nspname||'.'||t.relname, count(*)
+FROM pg_constraint c
+JOIN pg_class t on t.oid = c.conrelid
+JOIN pg_namespace tn on tn.oid = c.connamespace
+WHERE 1=1 $exclude_unique $schema_cond
+AND c.contype = 'u'
+AND c.conindid > 1
+GROUP BY tn.nspname, t.relname
 };
 	%pgret = ();
 	if ($self->{pg_dsn})
@@ -17208,21 +18184,17 @@ GROUP BY n.nspname,r.conrelid
 	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,
+    nmsp_parent.nspname || '.' || parent.relname,
     COUNT(*)                     
 FROM pg_inherits
     JOIN pg_class parent        ON pg_inherits.inhparent = parent.oid
     JOIN pg_class child     ON pg_inherits.inhrelid   = child.oid
     JOIN pg_namespace nmsp_parent   ON nmsp_parent.oid  = parent.relnamespace
     JOIN pg_namespace nmsp_child    ON nmsp_child.oid   = child.relnamespace
-$schema_cond
-GROUP BY                                                                    
-    parent_schema,
-    parent;
+WHERE child.relkind = 'r' $schema_cond
+GROUP BY nmsp_parent.nspname || '.' || parent.relname                                                                   
 };
 	my %pg_part = ();
 	if ($self->{pg_dsn})
@@ -17235,26 +18207,22 @@ GROUP BY
 		}
 		while ( my @row = $s->fetchrow())
 		{
-			$row[1] =~ s/^[^\.]+\.// if (!$self->{export_schema});
-			$pg_part{$row[1]} = $row[2];
+			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
+			$pg_part{$row[0]} = $row[1];
 		}
 		$s->finish();
 	}
-	foreach my $t (sort keys %partitions)
+
+	foreach my $t (sort keys %tables_infos)
 	{
-		next if (!exists $tables_infos{$t});
-		print "$lbl:$t:", $partitions{"\L$t\E"}{count}, "\n";
 		my ($tbmod, $orig, $schema, $both) = $self->set_pg_relation_name($t);
-		if (exists $pg_part{$tbmod})
-		{
-			print "POSTGRES:$both$orig:$pg_part{$tbmod}\n";
-			if ($pg_part{$tbmod} != $partitions{"\L$t\E"}{count}) {
-				push(@errors, "Table $both$orig doesn't have the same number of partitions in source database (" . $partitions{"\L$t\E"}{count} . ") and in PostgreSQL ($pg_part{$tbmod}).");
-			}
-		}
-		else
-		{
-			push(@errors, "Table $both$orig doesn't have the same number of partitions in source database (" . $partitions{"\L$t\E"}{count} . ") and in PostgreSQL (0).");
+		next if (!exists $partitions{$t} && !exists $pg_part{$tbmod});
+		$partitions{$t}{count} ||= 0;
+		print "$lbl:$t:$partitions{$t}{count}\n";
+		$pg_part{$tbmod} ||= 0;
+		print "POSTGRES:$both$orig:$pg_part{$tbmod}\n";
+		if ($pg_part{$tbmod} != $partitions{$t}{count}) {
+			push(@errors, "Table $both$orig doesn't have the same number of partitions in source database ($partitions{$t}{count}) and in PostgreSQL ($pg_part{$tbmod}).");
 		}
 	}
 	$self->show_test_errors('PARTITION', @errors);
@@ -17269,7 +18237,8 @@ GROUP BY
 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','') AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = c.oid AND d.deptype = 'e')
+WHERE c.relkind IN ('r', 'p') AND NOT c.relispartition
+    AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = c.oid AND d.deptype = 'e')
  $schema_cond
 };
 
@@ -17415,25 +18384,29 @@ WHERE c.relkind = 'v' AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE r
 		while ( my @row = $s->fetchrow())
 		{
 			$row[0] =~ s/^[^\.]+\.// if (!$self->{export_schema});
-			$list_views{$row[0]} = $row[1];
+			$list_views{$row[0]} = $self->{schema} || $row[1];
 		}
 		$s->finish();
 	}
 
 	my $lbl = 'ORACLEDB';
 	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
+	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});
 
 	print "[UNITARY TEST OF VIEWS]\n";
 	foreach my $v (sort keys %list_views)
 	{
 		# Execute init settings if any
 		# Count rows returned by all view on the source database
-		$sql = "SELECT count(*) FROM $self->{schema}.$v";
+		my $vname = $v;
+		$vname = "$list_views{$v}.$v" if (!$self->{schema});
+	        	
+		$sql = "SELECT count(*) FROM $list_views{$v}.$v";
 		my $sth = $self->{dbh}->prepare($sql)  or $self->logit("ERROR: " . $self->{dbh}->errstr . "\n", 0, 0);
 		$sth->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 0);
 		my @row = $sth->fetchrow();
 		my $ora_ct = $row[0];
-		print "$lbl:$v:", join('|', @row), "\n";
+		print "$lbl:$vname:", join('|', @row), "\n";
 		$sth->finish;
 		# Execute view in the PostgreSQL database
 		$sql = "SELECT count(*) FROM " . $self->quote_object_name($list_views{$v}) . '.' . $self->quote_object_name($v);
@@ -17447,7 +18420,7 @@ WHERE c.relkind = 'v' AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend WHERE r
 		@row = $sth->fetchrow();
 		$sth->finish;
 		my $pg_ct = $row[0];
-		print "POSTGRES:$v:", join('|', @row), "\n";
+		print "POSTGRES:$vname:", join('|', @row), "\n";
 		if ($pg_ct != $ora_ct) {
 			print "ERROR: view $v returns different row count [oracle: $ora_ct, postgresql: $pg_ct]\n";
 		}
@@ -17464,6 +18437,7 @@ sub _count_object
 
 	my $lbl = 'ORACLEDB';
 	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
+	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});
 
 	my $schema_clause = $self->get_schema_condition();
 	my $nbobj = 0;
@@ -17507,20 +18481,22 @@ SELECT count(*)
 FROM pg_catalog.pg_class c
      LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
 WHERE c.relkind IN ('S','') AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = c.oid AND d.deptype = 'e')
-      $schema_clause
+     AND NOT EXISTS (SELECT 1 FROM pg_attribute a WHERE NOT a.attisdropped AND a.attidentity IN ('a', 'd') AND c.oid = pg_get_serial_sequence(a.attrelid::regclass::text, a.attname)::regclass::oid)
+     $schema_clause
 };
 	}
 	elsif ($obj_type eq 'TYPE')
 	{
 		my $obj_infos = $self->_get_types();
-		$nbobj = $#{$obj_infos} + 1;
+		$nbobj = scalar @{$obj_infos};
 		$schema_clause .= " AND pg_catalog.pg_type_is_visible(t.oid)" if ($schema_clause =~ /information_schema/);
 		$sql = qq{
 SELECT count(*)
 FROM pg_catalog.pg_type t
      LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
 WHERE (t.typrelid = 0 OR (SELECT c.relkind = 'c' FROM pg_catalog.pg_class c WHERE c.oid = t.typrelid))
-  AND NOT EXISTS(SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid)
+  AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_type el WHERE el.oid = t.typelem AND el.typarray = t.oid)
+  AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = t.oid AND d.deptype = 'e')
   $schema_clause
 };
 	}
@@ -17581,13 +18557,14 @@ sub _test_function
 
 	my $lbl = 'ORACLEDB';
 	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
+	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});
 
 	####
 	# Test number of function
 	####
 	print "\n";
 	print "[TEST FUNCTION COUNT]\n";
-	my @fct_infos = $self->_list_all_funtions();
+	my @fct_infos = $self->_list_all_functions();
 	my $schema_clause = "    AND n.nspname NOT IN ('pg_catalog','information_schema')";
 	$sql = qq{
 SELECT n.nspname,proname,prorettype
@@ -17656,6 +18633,7 @@ sub _test_seq_values
 
 	my $lbl = 'ORACLEDB';
 	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
+	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});
 
 	####
 	# Test number of function
@@ -17674,13 +18652,15 @@ sub _test_seq_values
 	if ($self->{pg_dsn})
 	{
 		# create a function to extract the last value of all sequences
+		my $fqdn = '';
+		$fqdn = "$self->{pg_schema}\." if ($self->{pg_schema});
 		my $sql = qq{
-CREATE OR REPLACE FUNCTION get_sequence_last_values() RETURNS TABLE(seqname text,val bigint) AS
+CREATE OR REPLACE FUNCTION ${fqdn}get_sequence_last_values() RETURNS TABLE(seqname text,val bigint) AS
 \$\$
 DECLARE
     seq_name varchar(128);
 BEGIN
-    FOR seq_name in SELECT quote_ident(relnamespace::regnamespace::text)||'.'||quote_ident(relname::text) FROM pg_class c WHERE (relkind = 'S') AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = c.oid AND d.deptype = 'e')
+    FOR seq_name in SELECT relnamespace::regnamespace::text || '.' || quote_ident(relname::text) FROM pg_class c WHERE (relkind = 'S') AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend d WHERE d.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND d.objid = c.oid AND d.deptype = 'e')
     LOOP
         RETURN QUERY EXECUTE  'SELECT ' || quote_literal(seq_name) || ',last_value FROM ' || seq_name;
     END LOOP;
@@ -17690,7 +18670,7 @@ END
 LANGUAGE 'plpgsql';
 };
 		$self->{dbhdest}->do($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
-		my $s = $self->{dbhdest}->prepare("SELECT * FROM get_sequence_last_values()") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
+		my $s = $self->{dbhdest}->prepare("SELECT * FROM ${fqdn}get_sequence_last_values()") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
 		if (not $s->execute())
 		{
 			push(@errors, "Can not extract information from catalog about last values of sequences.");
@@ -17702,7 +18682,7 @@ LANGUAGE 'plpgsql';
 			$pgret{"\U$row[0]\E"} = $row[1];
 		}
 		$s->finish;
-		$self->{dbhdest}->do("DROP FUNCTION get_sequence_last_values") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
+		$self->{dbhdest}->do("DROP FUNCTION ${fqdn}get_sequence_last_values") or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
 	}
 
 	foreach my $r (sort keys %$obj_infos)
@@ -17736,6 +18716,8 @@ sub _get_version
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_version($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_version($self);
 	} else {
 		return Ora2Pg::Oracle::_get_version($self);
 	}
@@ -17753,6 +18735,8 @@ sub _get_database_size
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_database_size($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_database_size($self);
 	} else {
 		return Ora2Pg::Oracle::_get_database_size($self);
 	}
@@ -17771,19 +18755,23 @@ sub _get_objects
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_objects($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_objects($self);
 	} else {
 		return Ora2Pg::Oracle::_get_objects($self);
 	}
 }
 
-sub _list_all_funtions
+sub _list_all_functions
 {
 	my $self = shift;
 
 	if ($self->{is_mysql}) {
-		return Ora2Pg::MySQL::_list_all_funtions($self);
+		return Ora2Pg::MySQL::_list_all_functions($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_list_all_functions($self);
 	} else {
-		return Ora2Pg::Oracle::_list_all_funtions($self);
+		return Ora2Pg::Oracle::_list_all_functions($self);
 	}
 }
 
@@ -17801,6 +18789,8 @@ sub _schema_list
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_schema_list($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_schema_list($self);
 	} else {
 		return Ora2Pg::Oracle::_schema_list($self);
 	}
@@ -17819,13 +18809,13 @@ sub _table_exists
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_table_exists($self, $schema, $table);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_table_exists($self, $schema, $table);
 	} else {
 		return Ora2Pg::Oracle::_table_exists($self, $schema, $table);
 	}
 }
 
-
-
 =head2 _get_largest_tables
 
 This function retrieves the list of largest table of the Oracle database in MB
@@ -17838,12 +18828,13 @@ sub _get_largest_tables
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_largest_tables($self);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_largest_tables($self);
 	} else {
 		return Ora2Pg::Oracle::_get_largest_tables($self);
 	}
 }
 
-
 =head2 _get_encoding
 
 This function retrieves the Oracle database encoding
@@ -17858,6 +18849,8 @@ sub _get_encoding
 
 	if ($self->{is_mysql}) {
 		return Ora2Pg::MySQL::_get_encoding($self, $self->{dbh});
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_get_encoding($self, $self->{dbh});
 	} else {
 		return Ora2Pg::Oracle::_get_encoding($self, $self->{dbh});
 	}
@@ -18181,9 +19174,6 @@ sub limit_to_objects
 {
 	my ($self, $obj_type, $column) = @_;
 
-	# With reports we don't have object name limitation
-	return if ($self->{type} eq 'SHOW_REPORT');
-
 	my $str = '';
 	$obj_type ||= $self->{type};
 	$column ||= 'TABLE_NAME';
@@ -18235,12 +19225,19 @@ sub limit_to_objects
 					}
 					if ($self->{is_mysql}) {
 						$str .= "upper($colname) RLIKE ?" ;
+					} elsif ($self->{is_mssql}) {
+						$str .= "PATINDEX(?, upper($colname)) != 0" ;
 					} else {
 						$str .= "REGEXP_LIKE(upper($colname), ?)" ;
 					}
+
 					my $objname = $self->{limited}{$arr_type[$i]}->[$j];
 					$objname =~ s/\$/\\\$/g; # support dollar sign
-					push(@{$self->{query_bind_params}}, uc("\^$objname\$"));
+					if (!$self->{is_mssql}) {
+						push(@{$self->{query_bind_params}}, uc("\^$objname\$"));
+					} else {
+						push(@{$self->{query_bind_params}}, uc("$objname"));
+					}
 					if ($j < $#{$self->{limited}{$arr_type[$i]}}) {
 						$str .= " OR ";
 					}
@@ -18268,15 +19265,20 @@ sub limit_to_objects
 						next if ($self->{limited}{$arr_type[$i]}->[$j] !~ /^\!(.+)/);
 						if ($self->{is_mysql}) {
 							$str .= " AND upper($colname) NOT RLIKE ?" ;
+						} elsif ($self->{is_mssql}) {
+							$str .= "PATINDEX(?, upper($colname)) != 0" ;
 						} else {
 							$str .= " AND NOT REGEXP_LIKE(upper($colname), ?)" ;
 						}
 						my $objname = $1;
 						$objname =~ s/\$/\\\$/g; # support dollar sign
-						push(@{$self->{query_bind_params}}, uc("\^$objname\$"));
+						if (!$self->{is_mssql}) {
+							push(@{$self->{query_bind_params}}, uc("\^$objname\$"));
+						} else {
+							push(@{$self->{query_bind_params}}, uc("$objname"));
+						}
 					}
 				}
-
 			}
 			$has_limitation = 1;
 
@@ -18303,10 +19305,16 @@ sub limit_to_objects
 				{
 					if ($self->{is_mysql}) {
 						$str .= "upper($colname) NOT RLIKE ?" ;
+					} elsif ($self->{is_mssql}) {
+						$str .= "PATINDEX(?, upper($colname)) = 0" ;
 					} else {
 						$str .= "NOT REGEXP_LIKE(upper($colname), ?)" ;
 					}
-					push(@{$self->{query_bind_params}}, uc("\^$self->{excluded}{$arr_type[$i]}->[$j]\$"));
+					if (!$self->{is_mssql}) {
+						push(@{$self->{query_bind_params}}, uc("\^$self->{excluded}{$arr_type[$i]}->[$j]\$"));
+					} else {
+						push(@{$self->{query_bind_params}}, uc("$self->{excluded}{$arr_type[$i]}->[$j]"));
+					}
 					if ($j < $#{$self->{excluded}{$arr_type[$i]}}) {
 						$str .= " AND ";
 					}
@@ -18316,7 +19324,7 @@ sub limit_to_objects
 		}
 
 		# Always exclude unwanted tables
-		if (!$self->{is_mysql} && !$self->{no_excluded_table} && !$has_limitation
+		if (!$self->{is_mysql} && !$self->{is_mssql} && !$self->{no_excluded_table} && !$has_limitation
 			&& ($arr_type[$i] =~ /TABLE|SEQUENCE|VIEW|TRIGGER|TYPE|SYNONYM/))
 		{
 			if ($self->{db_version} =~ /Release [89]/)
@@ -18334,8 +19342,13 @@ sub limit_to_objects
 				$str .= ' AND ( ';
 				for (my $j = 0; $j <= $#EXCLUDED_TABLES; $j++)
 				{
-					$str .= " NOT REGEXP_LIKE(upper($colname), ?)" ;
-					push(@{$self->{query_bind_params}}, uc("\^$EXCLUDED_TABLES[$j]\$"));
+					if ($self->{is_mssql}) {
+						$str .= "PATINDEX(?, upper($colname)) = 0" ;
+						push(@{$self->{query_bind_params}}, uc("$EXCLUDED_TABLES[$j]"));
+					} else {
+						$str .= " NOT REGEXP_LIKE(upper($colname), ?)" ;
+						push(@{$self->{query_bind_params}}, uc("\^$EXCLUDED_TABLES[$j]\$"));
+					}
 					if ($j < $#EXCLUDED_TABLES){
 						$str .= " AND ";
 					}
@@ -18458,12 +19471,14 @@ Return a hast with the details of the function
 
 sub _lookup_function
 {
-	my ($self, $plsql, $pname) = @_;
+	my ($self, $plsql, $pname, $meta) = @_;
 
 	if ($self->{is_mysql}) {
-		return Ora2Pg::MySQL::_lookup_function($self, $plsql, $pname);
+		return Ora2Pg::MySQL::_lookup_function($self, $plsql, $pname, $meta);
+	} elsif ($self->{is_mssql}) {
+		return Ora2Pg::MSSQL::_lookup_function($self, $plsql, $pname, $meta);
 	} else {
-		return Ora2Pg::Oracle::_lookup_function($self, $plsql, $pname);
+		return Ora2Pg::Oracle::_lookup_function($self, $plsql, $pname, $meta);
 	}
 }
 
@@ -18581,6 +19596,16 @@ sub difficulty_assessment
 			$difficulty = 5;
 			last;
 		}
+		foreach my $fct (keys %{ $report_info{'full_procedure_details'} } ) {
+			next if (!exists $report_info{'full_procedure_details'}{$fct}{keywords});
+			$difficulty = 5;
+			last;
+		}
+		foreach my $fct (keys %{ $report_info{'full_package_details'} } ) {
+			next if (!exists $report_info{'full_package_details'}{$fct}{keywords});
+			$difficulty = 5;
+			last;
+		}
 	}
 
 	my $tmp = $report_info{'total_cost_value'}/84;
@@ -18693,12 +19718,14 @@ Technical levels:
 			$self->logrep("Total\t$report_info{'total_object_number'}\t$report_info{'total_object_invalid'}\n");
 		}
 		$self->logrep("-------------------------------------------------------------------------------\n");
-		if ($self->{estimate_cost}) {
+		if ($self->{estimate_cost})
+		{
 			$self->logrep("Migration level : $difficulty\n");
 			$self->logrep("-------------------------------------------------------------------------------\n");
 			$self->logrep($lbl_mig_type);
 			$self->logrep("-------------------------------------------------------------------------------\n");
-			if (scalar keys %{ $report_info{'full_function_details'} }) {
+			if (scalar keys %{ $report_info{'full_function_details'} })
+			{
 				$self->logrep("\nDetails of cost assessment per function\n");
 				foreach my $fct (sort { $report_info{'full_function_details'}{$b}{count} <=> $report_info{'full_function_details'}{$a}{count} } keys %{ $report_info{'full_function_details'} } ) {
 					$self->logrep("Function $fct total estimated cost: $report_info{'full_function_details'}{$fct}{count}\n");
@@ -18706,7 +19733,26 @@ Technical levels:
 				}
 				$self->logrep("-------------------------------------------------------------------------------\n");
 			}
-			if (scalar keys %{ $report_info{'full_trigger_details'} }) {
+			if (scalar keys %{ $report_info{'full_procedure_details'} })
+			{
+				$self->logrep("\nDetails of cost assessment per procedure\n");
+				foreach my $fct (sort { $report_info{'full_procedure_details'}{$b}{count} <=> $report_info{'full_procedure_details'}{$a}{count} } keys %{ $report_info{'full_procedure_details'} } ) {
+					$self->logrep("Function $fct total estimated cost: $report_info{'full_procedure_details'}{$fct}{count}\n");
+					$self->logrep($report_info{'full_procedure_details'}{$fct}{info});
+				}
+				$self->logrep("-------------------------------------------------------------------------------\n");
+			}
+			if (scalar keys %{ $report_info{'full_package_details'} })
+			{
+				$self->logrep("\nDetails of cost assessment per package function\n");
+				foreach my $fct (sort { $report_info{'full_package_details'}{$b}{count} <=> $report_info{'full_package_details'}{$a}{count} } keys %{ $report_info{'full_package_details'} } ) {
+					$self->logrep("Function $fct total estimated cost: $report_info{'full_package_details'}{$fct}{count}\n");
+					$self->logrep($report_info{'full_package_details'}{$fct}{info});
+				}
+				$self->logrep("-------------------------------------------------------------------------------\n");
+			}
+			if (scalar keys %{ $report_info{'full_trigger_details'} })
+			{
 				$self->logrep("\nDetails of cost assessment per trigger\n");
 				foreach my $fct (sort { $report_info{'full_trigger_details'}{$b}{count} <=> $report_info{'full_trigger_details'}{$a}{count} } keys %{ $report_info{'full_trigger_details'} } ) {
 					$self->logrep("Trigger $fct total estimated cost: $report_info{'full_trigger_details'}{$fct}{count}\n");
@@ -18714,7 +19760,8 @@ Technical levels:
 				}
 				$self->logrep("-------------------------------------------------------------------------------\n");
 			}
-			if (scalar keys %{ $report_info{'full_view_details'} }) {
+			if (scalar keys %{ $report_info{'full_view_details'} })
+			{
 				$self->logrep("\nDetails of cost assessment per view\n");
 				foreach my $fct (sort { $report_info{'full_view_details'}{$b}{count} <=> $report_info{'full_view_details'}{$a}{count} } keys %{ $report_info{'full_view_details'} } ) {
 					$self->logrep("View $fct total estimated cost: $report_info{'full_view_details'}{$fct}{count}\n");
@@ -18949,7 +19996,8 @@ h2 {
 </ul>
 };
 			$self->logrep($lbl_mig_type);
-			if (scalar keys %{ $report_info{'full_function_details'} }) {
+			if (scalar keys %{ $report_info{'full_function_details'} })
+			{
 				$self->logrep("<h2>Details of cost assessment per function</h2>\n");
 				$self->logrep("<details><summary>Show</summary><ul>\n");
 				foreach my $fct (sort { $report_info{'full_function_details'}{$b}{count} <=> $report_info{'full_function_details'}{$a}{count} } keys %{ $report_info{'full_function_details'} } ) {
@@ -18963,7 +20011,38 @@ h2 {
 				}
 				$self->logrep("</ul></details>\n");
 			}
-			if (scalar keys %{ $report_info{'full_trigger_details'} }) {
+			if (scalar keys %{ $report_info{'full_procedure_details'} })
+			{
+				$self->logrep("<h2>Details of cost assessment per procedure</h2>\n");
+				$self->logrep("<details><summary>Show</summary><ul>\n");
+				foreach my $fct (sort { $report_info{'full_procedure_details'}{$b}{count} <=> $report_info{'full_procedure_details'}{$a}{count} } keys %{ $report_info{'full_procedure_details'} } ) {
+					
+					$self->logrep("<li>Procedure $fct total estimated cost: $report_info{'full_procedure_details'}{$fct}{count}</li>\n");
+					$self->logrep("<ul>\n");
+					$report_info{'full_procedure_details'}{$fct}{info} =~ s/\t/<li>/gs;
+					$report_info{'full_procedure_details'}{$fct}{info} =~ s/\n/<\/li>\n/gs;
+					$self->logrep($report_info{'full_procedure_details'}{$fct}{info});
+					$self->logrep("</ul>\n");
+				}
+				$self->logrep("</ul></details>\n");
+			}
+			if (scalar keys %{ $report_info{'full_package_details'} })
+			{
+				$self->logrep("<h2>Details of cost assessment per package function</h2>\n");
+				$self->logrep("<details><summary>Show</summary><ul>\n");
+				foreach my $fct (sort { $report_info{'full_package_details'}{$b}{count} <=> $report_info{'full_package_details'}{$a}{count} } keys %{ $report_info{'full_package_details'} } ) {
+					
+					$self->logrep("<li>Function $fct total estimated cost: $report_info{'full_package_details'}{$fct}{count}</li>\n");
+					$self->logrep("<ul>\n");
+					$report_info{'full_package_details'}{$fct}{info} =~ s/\t/<li>/gs;
+					$report_info{'full_package_details'}{$fct}{info} =~ s/\n/<\/li>\n/gs;
+					$self->logrep($report_info{'full_package_details'}{$fct}{info});
+					$self->logrep("</ul>\n");
+				}
+				$self->logrep("</ul></details>\n");
+			}
+			if (scalar keys %{ $report_info{'full_trigger_details'} })
+			{
 				$self->logrep("<h2>Details of cost assessment per trigger</h2>\n");
 				$self->logrep("<details><summary>Show</summary><ul>\n");
 				foreach my $fct (sort { $report_info{'full_trigger_details'}{$b}{count} <=> $report_info{'full_trigger_details'}{$a}{count} } keys %{ $report_info{'full_trigger_details'} } ) {
@@ -19568,7 +20647,7 @@ sub register_global_variable
 {
 	my ($self, $pname, $glob_vars) = @_;
 
-	$glob_vars = Ora2Pg::PLSQL::replace_sql_type($glob_vars, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, $self->{varchar_to_text}, %{$self->{data_type}});
+	$glob_vars = $self->_replace_sql_type($glob_vars);
 
 	# Replace PL/SQL code into PL/PGSQL similar code
 	$glob_vars = Ora2Pg::PLSQL::convert_plsql_code($self, $glob_vars);
@@ -19791,7 +20870,9 @@ sub _create_foreign_server
 		if (!$self->{fdw_server}) {
 			$self->logit("FATAL: a foreign server name must be set using FDW_SERVER\n", 0, 1);
 		}
-		if (!$self->{is_mysql} && $self->{oracle_dsn} =~ /(\/\/.*\/.*)/)
+		if (!$self->{is_mysql} && !$self->{is_mssql} &&
+			($self->{oracle_dsn} =~ /(\/\/.*\/.*)/ || $self->{oracle_dsn} =~ /dbi:Oracle:([^=:;]+)$/i)
+		)
 		{
 			$self->{oracle_fwd_dsn} = "dbserver '$1'";
 		}
@@ -19821,6 +20902,10 @@ sub _create_foreign_server
 	my $usrlbl = 'user';
 	$usrlbl = 'username' if ($self->{is_mysql});
 	my $sql = "CREATE USER MAPPING IF NOT EXISTS FOR $self->{pg_user} SERVER $self->{fdw_server} OPTIONS ($usrlbl '$self->{oracle_user}', password '$self->{oracle_pwd}');";
+	if ($self->{oracle_user} eq "__SEPS__" && $self->{oracle_pwd} eq "__SEPS__")  # Replace with empty credentials for an Oracle Wallet connection
+	{
+		$sql =~ s/__SEPS__//g;
+	}
 	$self->{dbhdest}->do($sql) or $self->logit("FATAL: " . $self->{dbhdest}->errstr . "\n", 0, 1);
 }
 
@@ -20006,6 +21091,7 @@ sub compare_data
 
 	my $lbl = 'ORACLEDB';
 	$lbl    = 'MYSQL_DB' if ($self->{is_mysql});
+	$lbl    = 'MSSQL_DB' if ($self->{is_mssql});
 	my $dbhora = undef;
 	my $dbhpg = undef;
 
@@ -20014,12 +21100,18 @@ sub compare_data
 		$self->logit("DEBUG: cloning connection to PostgreSQL.\n", 1);
 		$dbhora = $self->{dbhdest}->clone();
 		$dbhpg = $self->{dbhdest}->clone();
+		# Force execution of initial command on both side
+		$self->_ora_initial_command($dbhora);
+		$self->_pg_initial_command($dbhpg);
 	}
 	else
 	{
 		$self->logit("DEBUG: cloning connection to Oracle.\n", 1);
 		$dbhora = $self->{dbh}->clone();
 		$dbhpg = $self->{dbhdest}->clone();
+		# Force execution of initial command on both side
+		$self->_ora_initial_command($dbhora);
+		$self->_pg_initial_command($dbhpg);
 		if (!$self->{is_mysql})
 		{
 			$dbhora->{'LongReadLen'} = $self->{longreadlen};
@@ -20130,7 +21222,7 @@ ORDER BY attnum};
 		if ($crow[2] eq 'geometry')
 		{
 			if ($self->{is_mysql}) {
-				push(@tlist, "ST_AsText($crow[0])");
+				push(@tlist, "$self->{st_astext_function}($crow[0])");
 			} else {
 				push(@tlist, $crow[0]);
 			}
@@ -20260,7 +21352,7 @@ sub _get_oracle_test_data
 
 	# Prefix partition name with tablename, if pg_supports_partition is enabled
 	# direct import to partition is not allowed so import to main table.
-	if (!$self->{pg_supports_partition} && $part_name && $self->{prefix_partition}) {
+	if (!$self->{pg_supports_partition} && $part_name && $self->{rename_partition}) {
 		$tmptb = $self->get_replaced_tbname($table . '_' . $part_name);
 	} elsif (!$self->{pg_supports_partition} && $part_name) {
 		$tmptb = $self->get_replaced_tbname($part_name || $table);
@@ -20356,7 +21448,7 @@ Gilles Darold <gilles _AT_ darold _DOT_ net>
 
 =head1 COPYRIGHT
 
-Copyright (c) 2000-2022 Gilles Darold - All rights reserved.
+Copyright (c) 2000-2023 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
diff --git a/lib/Ora2Pg/GEOM.pm b/lib/Ora2Pg/GEOM.pm
index 349bf57..78530b0 100644
--- a/lib/Ora2Pg/GEOM.pm
+++ b/lib/Ora2Pg/GEOM.pm
@@ -4,7 +4,7 @@ package Ora2Pg::GEOM;
 # Name     : Ora2Pg/GEOM.pm
 # Language : Perl
 # Authors  : Gilles Darold, gilles _AT_ darold _DOT_ net
-# Copyright: Copyright (c) 2000-2022 : Gilles Darold - All rights reserved -
+# Copyright: Copyright (c) 2000-2023 : Gilles Darold - All rights reserved -
 # Function : Perl module used to convert Oracle SDO_GEOMETRY into PostGis
 # Usage    : See documentation
 #------------------------------------------------------------------------------
@@ -40,7 +40,7 @@ use vars qw($VERSION);
 
 use strict;
 
-$VERSION = '23.2';
+$VERSION = '24.0';
 
 # SDO_ETYPE
 # Second element of triplet in SDO_ELEM_INFO
diff --git a/lib/Ora2Pg/MSSQL.pm b/lib/Ora2Pg/MSSQL.pm
new file mode 100644
index 0000000..4b57bb5
--- /dev/null
+++ b/lib/Ora2Pg/MSSQL.pm
@@ -0,0 +1,2923 @@
+package Ora2Pg::MSSQL;
+
+use vars qw($VERSION);
+use strict;
+
+use DBI;
+use POSIX qw(locale_h);
+use Benchmark;
+
+#set locale to LC_NUMERIC C
+setlocale(LC_NUMERIC,"C");
+
+
+$VERSION = '23.1';
+
+# Some function might be excluded from export and assessment.
+our @EXCLUDED_FUNCTION = ('SQUIRREL_GET_ERROR_OFFSET');
+
+# These definitions can be overriden from configuration
+# file using the DATA_TYPË configuration directive.
+our %SQL_TYPE = (
+	'TINYINT' => 'smallint', # 1 byte
+	'SMALLINT' => 'smallint', # 2 bytes
+	'INT' => 'integer', # 4 bytes
+	'BIGINT' => 'bigint', # 8 bytes
+	'DECIMAL' => 'numeric',
+	'DEC' => 'numeric',
+	'NUMERIC' => 'numeric',
+	'BIT' => 'boolean',
+	'MONEY' => 'numeric(15,4)',
+	'SMALLMONEY' => 'numeric(6,4)',
+	'FLOAT' => 'double precision',
+	'REAL' => 'real',
+	'DATE' => 'date',
+	'SMALLDATETIME' => 'timestamp(0) without time zone',
+	'DATETIME' => 'timestamp(3) without time zone',
+	'DATETIME2' => 'timestamp without time zone',
+	'DATETIMEOFFSET' => 'timestamp with time zone',
+	'TIME' => 'time without time zone',
+	'CHAR' => 'char',
+	'VARCHAR' => 'varchar',
+	'TEXT' => 'text',
+	'NCHAR' => 'char',
+	'NVARCHAR' => 'varchar',
+	'NTEXT' => 'text',
+	'VARBINARY' => 'bytea',
+	'BINARY' => 'bytea',
+	'IMAGE' => 'bytea',
+	'UNIQUEIDENTIFIER' => 'uuid',
+	'ROWVERSION' => 'bytea',
+	'TIMESTAMP' => 'bytea', # synonym of ROWVERSION
+	'XML' => 'xml',
+	'HIERARCHYID' => 'varchar', # The application need to handle the value, no PG equivalent
+	'GEOMETRY' => 'geometry',
+	'GEOGRAPHY' => 'geometry',
+	'SYSNAME' => 'varchar(256)',
+	'SQL_VARIANT' => 'text'
+);
+
+sub _db_connection
+{
+	my $self = shift;
+
+	$self->logit("Trying to connect to database: $self->{oracle_dsn}\n", 1) if (!$self->{quiet});
+
+	if (!defined $self->{oracle_pwd})
+	{
+		eval("use Term::ReadKey;");
+		if (!$@) {
+			$self->{oracle_user} = $self->_ask_username('MSSQL') unless (defined $self->{oracle_user});
+			$self->{oracle_pwd} = $self->_ask_password('MSSQL');
+		}
+	}
+
+	my $dbh = DBI->connect("$self->{oracle_dsn}", $self->{oracle_user}, $self->{oracle_pwd}, {
+			'RaiseError' => 1,
+			AutoInactiveDestroy => 1,
+			odbc_cursortype => 2,
+			PrintError => 0,
+			odbc_utf8_on => 1
+		}
+	);
+	$dbh->{LongReadLen} = $self->{longreadlen} if ($self->{longreadlen});
+	$dbh->{LongTruncOk} = $self->{longtruncok} if (defined $self->{longtruncok});
+
+	# Check for connection failure
+	if (!$dbh) {
+		$self->logit("FATAL: $DBI::err ... $DBI::errstr\n", 0, 1);
+	}
+
+	# Use consistent reads for concurrent dumping...
+	if ($self->{debug} && !$self->{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->_ora_initial_command($dbh);
+
+	# Instruct Ora2Pg that the database engine is mssql
+	$self->{is_mssql} = 1;
+
+	return $dbh;
+}
+
+sub _get_version
+{
+	my $self = shift;
+
+	my $dbver = '';
+	my $sql = "SELECT \@\@VERSION";
+
+        my $sth = $self->{dbh}->prepare( $sql ) or return undef;
+        $sth->execute or return undef;
+	while ( my @row = $sth->fetchrow()) {
+		$dbver = $row[0];
+		last;
+	}
+	$sth->finish();
+
+	$dbver =~ s/ \- .*//;
+	$dbver =~ s/\s+/ /gs;
+
+	return $dbver;
+}
+
+sub _schema_list
+{
+	my $self = shift;
+
+	my $sql = qq{
+SELECT s.name as schema_name, 
+    s.schema_id,
+    u.name as schema_owner
+from sys.schemas s
+    inner join sys.sysusers u
+        on u.uid = s.principal_id
+WHERE s.name NOT IN ('information_schema', 'sys', 'db_accessadmin', 'db_backupoperator', 'db_datareader', 'db_datawriter', 'db_ddladmin', 'db_denydatareader', 'db_denydatawriter', 'db_owner', 'db_securityadmin')
+order by s.name;
+};
+
+	my $sth = $self->{dbh}->prepare( $sql ) or return undef;
+	$sth->execute or return undef;
+	$sth;
+}
+
+sub _table_exists
+{
+	my ($self, $schema, $table) = @_;
+
+	my $ret = '';
+
+	my $sql = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' AND TABLE_SCHEMA = '$schema' AND TABLE_NAME = '$table'";
+
+	my $sth = $self->{dbh}->prepare( $sql ) or return undef;
+	$sth->execute or return undef;
+	while ( my @row = $sth->fetchrow()) {
+		$ret = $row[0];
+	}
+	$sth->finish();
+
+	return $ret;
+}
+
+
+
+=head2 _get_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 = qq{SELECT SERVERPROPERTY('SqlCharSetName') AS 'Instance-SqlCharSetName', SERVERPROPERTY('Collation') AS 'Instance-Collation'};
+        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 $db_encoding = '';
+	my $db_collation = '';
+	while ( my @row = $sth->fetchrow())
+	{
+		$db_encoding = $row[0];
+		$db_collation = $row[1];
+	}
+	$sth->finish();
+
+	my $db_timestamp_format = '';
+	my $db_date_format = '';
+	$sql = qq{SELECT date_format
+FROM sys.dm_exec_sessions
+WHERE session_id = \@\@spid
+	};
+        $sth = $dbh->prepare($sql) or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
+        $sth->execute() or $self->logit("FATAL: " . $dbh->errstr . "\n", 0, 1);
+	while ( my @row = $sth->fetchrow()) {
+		$db_date_format = $row[0];
+	}
+	$sth->finish();
+
+	my $pg_encoding = auto_set_encoding($db_encoding);
+
+	return ($db_encoding, $db_collation, $pg_encoding, $db_timestamp_format, $db_date_format);
+}
+
+=head2 auto_set_encoding
+
+This function is used to find the PostgreSQL charset corresponding to the
+MSSQL encoding valie
+
+=cut
+
+sub auto_set_encoding
+{
+	my $mssql_encoding = shift;
+
+	my %ENCODING = (
+		"iso_1" => "LATIN1",
+	);
+
+	return $ENCODING{$mssql_encoding};
+}
+
+
+=head2 _table_info
+
+This function retrieves all MSSQL tables information.
+
+Returns a handle to a DB query statement.
+
+=cut
+
+sub _table_info
+{
+	my $self = shift;
+	my $do_real_row_count = shift;
+
+	# First register all tablespace/table in memory from this database
+	my %tbspname = ();
+
+        my $schema_clause = '';
+        $schema_clause = " AND s.name='$self->{schema}'" if ($self->{schema});
+	my $sql = qq{SELECT t.NAME AS TABLE_NAME, NULL AS comment, t.type_desc as TABLE_TYPE, p.rows AS RowCounts, SUM(a.used_pages)  * 8 / 1024 AS UsedSpaceMB, CONVERT(DECIMAL,SUM(a.total_pages)) * 8 / 1024 AS TotalSpaceMB, s.Name AS TABLE_SCHEMA, SCHEMA_NAME(t.principal_id)
+FROM sys.tables t
+INNER JOIN sys.indexes i ON t.OBJECT_ID = i.object_id
+INNER JOIN sys.partitions p ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id
+INNER JOIN sys.allocation_units a ON p.partition_id = a.container_id
+LEFT OUTER JOIN sys.schemas s ON t.schema_id = s.schema_id
+WHERE t.is_ms_shipped = 0 AND i.OBJECT_ID > 255 AND t.type='U' AND t.NAME NOT LIKE '#%' $schema_clause
+};
+	my %tables_infos = ();
+	my %comments = ();
+	$sql .= $self->limit_to_objects('TABLE', 't.Name');
+	$sql .= " GROUP BY t.type_desc, s.Name, t.Name, SCHEMA_NAME(t.principal_id), p.Rows ORDER BY s.Name, t.Name";
+	my $sth = $self->{dbh}->prepare( $sql ) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	while (my $row = $sth->fetch)
+	{
+		if (!$self->{schema} && $self->{export_schema}) {
+			$row->[0] = "$row->[6].$row->[0]";
+		}
+		$row->[2] =~ s/^USER_//;
+		$comments{$row->[0]}{comment} = $row->[1];
+		$comments{$row->[0]}{table_type} = $row->[2];
+		$tables_infos{$row->[0]}{owner} = $row->[7] || $row->[6];
+		$tables_infos{$row->[0]}{num_rows} = $row->[3] || 0;
+		$tables_infos{$row->[0]}{comment} = ''; # SQL Server doesn't have COMMENT and we don't play with "Extended Properties"
+		$tables_infos{$row->[0]}{type} =  $comments{$row->[0]}{table_type} || '';
+		$tables_infos{$row->[0]}{nested} = 'NO';
+		$tables_infos{$row->[0]}{size} = sprintf("%.3f", $row->[5]) || 0;
+		$tables_infos{$row->[0]}{tablespace} = 0;
+		$tables_infos{$row->[0]}{auto_increment} = 0;
+		$tables_infos{$row->[0]}{tablespace} = $tbspname{$row->[0]} || '';
+		$tables_infos{$row->[0]}{partitioned} = 1 if (exists $self->{partitions_list}{"\L$row->[0]\E"});
+	}
+	$sth->finish();
+
+	if ($do_real_row_count)
+	{
+		foreach my $t (keys %tables_infos)
+		{
+			$self->logit("DEBUG: looking for real row count for table $t (aka using count(*))...\n", 1);
+			my $tbname = "[$t]";
+			$tbname =~ s/\./\].\[/;
+			$sql = "SELECT COUNT(*) FROM $tbname";
+			$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 $size = $sth->fetch();
+			$sth->finish();
+			$tables_infos{$t}{num_rows} = $size->[0];
+		}
+	}
+
+	return %tables_infos;
+}
+
+sub _column_comments
+{
+	my ($self, $table) = @_;
+
+	return ; # SQL Server doesn't have COMMENT and we don't play with "Extended Properties"
+}
+
+sub _column_info
+{
+	my ($self, $table, $owner, $objtype, $recurs, @expanded_views) = @_;
+
+	$objtype ||= 'TABLE';
+
+	my $condition = '';
+	if ($self->{schema}) {
+		$condition .= "AND s.name='$self->{schema}' ";
+	}
+	$condition .= "AND tb.name='$table' " if ($table);
+	if (!$table) {
+		$condition .= $self->limit_to_objects('TABLE', 'tb.name');
+	} else {
+		@{$self->{query_bind_params}} = ();
+	}
+	$condition =~ s/^\s*AND\s/ WHERE /;
+
+	my $str = qq{SELECT 
+    c.name 'Column Name',
+    t.Name 'Data type',
+    c.max_length 'Max Length',
+    c.is_nullable,
+    object_definition(c.default_object_id),
+    c.precision ,
+    c.scale ,
+    '',
+    tb.name,
+    s.name,
+    '',
+    c.column_id,
+    NULL as AUTO_INCREMENT,
+    NULL AS ENUM_INFO,
+    object_definition(c.rule_object_id),
+    t.is_user_defined
+FROM sys.columns c
+INNER JOIN sys.types t ON t.user_type_id = c.user_type_id
+INNER JOIN sys.tables AS tb ON tb.object_id = c.object_id
+INNER JOIN sys.schemas AS s ON s.schema_id = tb.schema_id
+$condition
+ORDER BY c.column_id};
+
+	my $sth = $self->{dbh}->prepare($str);
+	if (!$sth) {
+		$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	}
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	# Expected columns information stored in hash 
+	# COLUMN_NAME,DATA_TYPE,DATA_LENGTH,NULLABLE,DATA_DEFAULT,DATA_PRECISION,DATA_SCALE,CHAR_LENGTH,TABLE_NAME,OWNER,VIRTUAL_COLUMN,POSITION,AUTO_INCREMENT,ENUM_INFO
+	my %data = ();
+	my $pos = 0;
+	while (my $row = $sth->fetch)
+	{
+		next if ($self->{drop_rowversion} && ($row->[1] eq 'rowversion' || $row->[1] eq 'timestamp'));
+
+		if (!$self->{schema} && $self->{export_schema}) {
+			$row->[8] = "$row->[9].$row->[8]";
+		}
+		if (!$row->[15])
+		{
+			if ($row->[4]) {
+				$row->[4] =~ s/\s*CREATE\s+DEFAULT\s+.*\s+AS\s*//is;
+				$row->[4] =~ s/[\[\]]+//g;
+			}
+			if ($row->[14]) {
+				$row->[14] =~ s/[\[\]]+//g;
+				$row->[14] =~ s/\s*CREATE\s+RULE\s+.*\s+AS\s*//is;
+				$row->[14] =~ s/\@[a-z0-1_\$\#]+/VALUE/igs;
+				$row->[14] = " CHECK ($row->[14])";
+				$row->[14] =~ s/[\r\n]+/ /gs;
+			}
+		}
+		push(@{$data{"$row->[8]"}{"$row->[0]"}}, @$row);
+		$pos++;
+	}
+
+	return %data;
+}
+
+sub _get_indexes
+{
+	my ($self, $table, $owner, $generated_indexes) = @_;
+
+	my $condition = '';
+	$condition .= "AND OBJECT_NAME(Id.object_id, DB_ID())='$table' " if ($table);
+	if ($owner) {
+		$condition .= "AND s.name='$owner' ";
+	} else {
+		$condition .= " AND s.name NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
+	}
+	if (!$table) {
+		$condition .= $self->limit_to_objects('TABLE|INDEX', "OBJECT_NAME(Id.object_id, DB_ID())|Id.NAME");
+	} else {
+		@{$self->{query_bind_params}} = ();
+	}
+
+	# When comparing number of index we need to retrieve generated index (mostly PK)
+	my $generated = '';
+	$generated = " AND Id.auto_created = 0" if (!$generated_indexes);
+
+	my $t0 = Benchmark->new;
+	my $sth = '';
+	my $sql = qq{SELECT Id.name AS index_name, AC.name AS column_name, Id.is_unique AS UNIQUENESS, AC.column_id AS COLUMN_POSITION, Id.type AS INDEX_TYPE, 'U' AS TABLE_TYPE, Id.auto_created AS GENERATED, NULL AS JOIN_INDEX, t.name AS TABLE_NAME, s.name as TABLE_SCHEMA, Id.data_space_id AS TABLESPACE_NAME, Id.type_desc AS ITYP_NAME, Id.filter_definition AS PARAMETERS, IC.is_descending_key AS DESCEND, id.is_primary_key PRIMARY_KEY, typ.name AS COL_TYPE_NAME
+FROM sys.tables AS T
+INNER JOIN sys.indexes Id ON T.object_id = Id.object_id 
+INNER JOIN sys.index_columns IC ON Id.object_id = IC.object_id
+INNER JOIN sys.all_columns AC ON T.object_id = AC.object_id AND IC.column_id = AC.column_id 
+INNER JOIN sys.types typ ON typ.user_type_id = AC.user_type_id
+LEFT OUTER JOIN sys.schemas s ON t.schema_id = s.schema_id
+WHERE T.is_ms_shipped = 0 $generated $condition
+ORDER BY T.name, Id.index_id, IC.key_ordinal
+};
+
+	$sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %data = ();
+	my %unique = ();
+	my %idx_type = ();
+	my %index_tablespace = ();
+	my $nidx = 0;
+	while (my $row = $sth->fetch)
+	{
+		next if ($self->{drop_rowversion} && ($row->[15] eq 'rowversion' || $row->[15] eq 'timestamp'));
+
+		# Handle case where indexes name include the schema at create time
+		$row->[0] =~ s/^$self->{schema}\.//i if ($self->{schema});
+
+		next if (!$row->[0]);
+
+		my $save_tb = $row->[8];
+		if (!$self->{schema} && $self->{export_schema}) {
+			$row->[8] = "$row->[9].$row->[8]";
+		}
+		next if (!$self->is_in_struct($row->[8], $row->[1]));
+
+		# Show a warning when an index has the same name as the table
+		if ( !$self->{indexes_renaming} && !$self->{indexes_suffix} && (lc($row->[0]) eq lc($table)) ) {
+			 print STDERR "WARNING: index $row->[0] has the same name as the table itself. Please rename it before export or enable INDEXES_RENAMING.\n";
+		}
+		$unique{$row->[8]}{$row->[0]} = $row->[2];
+
+		# Save original column name
+		my $colname = $row->[1];
+		# Quote column with unsupported symbols
+		$row->[1] = $self->quote_object_name($row->[1]);
+		# Replace function based index type
+		if ( $row->[13] )
+		{
+			# Append DESC sort order when not default to ASC
+			if ($row->[13] eq 'DESC') {
+				$row->[1] .= " DESC";
+			}
+		}
+
+		$idx_type{$row->[8]}{$row->[0]}{type_name} = $row->[11];
+		$idx_type{$row->[8]}{$row->[0]}{type} = $row->[4];
+
+#		my $idx_name = $row->[0];
+#		if (!$self->{schema} && $self->{export_schema}) {
+#			$idx_name = "$row->[9].$row->[0]";
+#		}
+#		if ($row->[11] =~ /SPATIAL_INDEX/) {
+#			$idx_type{$row->[8]}{$row->[0]}{type} = 'SPATIAL INDEX';
+#			if ($row->[12] =~ /layer_gtype=([^\s,]+)/i) {
+#				$idx_type{$row->[9]}{$row->[0]}{type_constraint} = uc($1);
+#			}
+#			if ($row->[12] =~ /sdo_indx_dims=(\d+)/i) {
+#				$idx_type{$row->[8]}{$row->[0]}{type_dims} = $1;
+#			}
+#		}
+#		if ($row->[4] eq 'BITMAP') {
+#			$idx_type{$row->[8]}{$row->[0]}{type} = $row->[4];
+#		}
+		push(@{$data{$row->[8]}{$row->[0]}}, $row->[1]);
+		$index_tablespace{$row->[8]}{$row->[0]} = $row->[10];
+		$nidx++;
+	}
+	$sth->finish();
+	my $t1 = Benchmark->new;
+	my $td = timediff($t1, $t0);
+	$self->logit("Collecting $nidx indexes in sys.indexes took: " . timestr($td) . "\n", 1);
+
+	return \%unique, \%data, \%idx_type, \%index_tablespace;
+}
+
+
+sub _count_indexes
+{
+	my ($self, $table, $owner) = @_;
+
+	my $condition = '';
+	$condition = " FROM $self->{schema}" if ($self->{schema});
+	if (!$table) {
+		$condition .= $self->limit_to_objects('TABLE|INDEX', "`Table`|`Key_name`");
+	} else {
+		@{$self->{query_bind_params}} = ();
+	}
+	$condition =~ s/ AND / WHERE /;
+
+	my %tables_infos = ();
+	if ($table) {
+		$tables_infos{$table} = 1;
+	} else {
+		%tables_infos = Ora2Pg::MSSQL::_table_info($self);
+	}
+	my %data = ();
+
+	# Retrieve all indexes for the given table
+	foreach my $t (keys %tables_infos)
+	{
+		my $sql = "SHOW INDEX FROM `$t` $condition";
+		my $sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+		$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+		my $i = 1;
+		while (my $row = $sth->fetch)
+		{
+			push(@{$data{$row->[0]}{$row->[2]}}, $row->[4]);
+		}
+	}
+
+	return \%data;
+}
+
+
+sub _foreign_key
+{
+        my ($self, $table, $owner) = @_;
+
+        my $condition = '';
+        $condition .= " AND OBJECT_NAME (f.referenced_object_id) = '$table' " if ($table);
+        $condition .= " AND SCHEMA_NAME(f.schema_id) = '$self->{schema}' " if ($self->{schema});
+	$condition =~ s/^ AND / WHERE /;
+
+        my $deferrable = $self->{fkey_deferrable} ? "'DEFERRABLE' AS DEFERRABLE" : "DEFERRABLE";
+	my $sql = qq{SELECT f.name ConsName, SCHEMA_NAME(f.schema_id) SchemaName, COL_NAME(fc.parent_object_id,fc.parent_column_id) ColName, OBJECT_NAME(f.parent_object_id) TableName, t.name as ReferencedTableName, COL_NAME(f.referenced_object_id, key_index_id) as ReferencedColumnName,update_referential_action_desc UPDATE_RULE, delete_referential_action_desc DELETE_RULE, SCHEMA_NAME(t.schema_id)
+FROM sys.foreign_keys AS f
+INNER JOIN sys.foreign_key_columns AS fc ON f.OBJECT_ID = fc.constraint_object_id
+INNER JOIN sys.tables t ON t.OBJECT_ID = fc.referenced_object_id
+LEFT OUTER JOIN sys.schemas s ON f.principal_id = s.schema_id
+$condition};
+
+        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 = ();
+	my $i = 1;
+        my %data = ();
+        my %link = ();
+        while (my $r = $sth->fetch)
+	{
+		my $key_name = $r->[3] . '_' . $r->[2] . '_fk' . $i;
+		if ($r->[0]) {
+			$key_name = uc($r->[0]);
+		}
+		if (!$self->{schema} && $self->{export_schema}) {
+			$r->[3] = "$r->[1].$r->[3]";
+			$r->[4] = "$r->[8].$r->[4]";
+		}
+		push(@{$link{$r->[3]}{$key_name}{local}}, $r->[2]);
+		push(@{$link{$r->[3]}{$key_name}{remote}{$r->[4]}}, $r->[5]);
+		#SELECT ConsName, SchemaName, ColName, TableName, ReferencedTableName, ReferencedColumnName, UPDATE_RULE, DELETE_RULE, SCHEMA_NAME
+		$r->[7] =~ s/_/ /;
+                push(@{$data{$r->[3]}}, [ ($key_name, $key_name, '', $r->[7], 'DEFERRABLE', 'Y', '', $r->[3], '', $r->[6]) ]);
+		$i++;
+        }
+	$sth->finish();
+
+        return \%link, \%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) = @_;
+
+        my $condition = '';
+        $condition .= "AND TABLE_SCHEMA='$self->{schema}' " if ($self->{schema});
+
+	my %comments = ();
+	# Retrieve all views
+	my $str = qq{select
+       v.name as view_name,
+       schema_name(v.schema_id) as schema_name,
+       m.definition,
+       v.with_check_option
+from sys.views v
+join sys.sql_modules m on m.object_id = v.object_id
+WHERE NOT EXISTS (SELECT 1 FROM sys.indexes i WHERE i.object_id = v.object_id and i.index_id = 1 and i.ignore_dup_key = 0) AND is_date_correlation_view=0};
+
+	if (!$self->{schema}) {
+		$str .= " AND schema_name(v.schema_id) NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
+	} else {
+		$str .= " AND schema_name(v.schema_id) = '$self->{schema}'";
+	}
+	$str .= $self->limit_to_objects('VIEW', 'v.name');
+	$str .= " ORDER BY schema_name, view_name";
+
+
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %ordered_view = ();
+	my %data = ();
+	while (my $row = $sth->fetch)
+	{
+		if (!$self->{schema} && $self->{export_schema}) {
+			$row->[0] = "$row->[1].$row->[0]";
+		}
+		$row->[2] =~ s/
//g;
+		$row->[2] =~ s/[\[\]]//g;
+		$row->[2] =~ s/^.*\bCREATE VIEW\s+[^\s]+\s+AS\s+//is;
+		$data{$row->[0]}{text} = $row->[2];
+		$data{$row->[0]}{owner} = '';
+		$data{$row->[0]}{comment} = '';
+		$data{$row->[0]}{check_option} = $row->[3];
+		$data{$row->[0]}{updatable} = 'Y';
+		$data{$row->[0]}{definer} = '';
+		$data{$row->[0]}{security} = '';
+		if ($self->{plsql_pgsql}) {
+			$data{$row->[0]}{text} =~ s/\s+WITH\s+(ENCRYPTION|SCHEMABINDING|VIEW_METADATA)\s+AS\s+//is;
+			$data{$row->[0]}{text} =~ s/^\s*AS\s+//is;
+		}
+	}
+	return %data;
+}
+
+sub _get_triggers
+{
+	my($self) = @_;
+
+	my $str = qq{SELECT 
+     o.name AS trigger_name 
+    ,USER_NAME(o.uid) AS trigger_owner 
+    ,OBJECT_NAME(o.parent_obj) AS table_name 
+    ,s.name AS table_schema 
+    ,OBJECTPROPERTY( o.id, 'ExecIsAfterTrigger') AS isafter 
+    ,OBJECTPROPERTY( o.id, 'ExecIsInsertTrigger') AS isinsert 
+    ,OBJECTPROPERTY( o.id, 'ExecIsUpdateTrigger') AS isupdate 
+    ,OBJECTPROPERTY( o.id, 'ExecIsDeleteTrigger') AS isdelete 
+    ,OBJECTPROPERTY( o.id, 'ExecIsInsteadOfTrigger') AS isinsteadof 
+    ,OBJECTPROPERTY( o.id, 'ExecIsTriggerDisabled') AS [disabled]
+    , c.text
+FROM sys.sysobjects o
+INNER JOIN sys.syscomments AS c ON o.id = c.id
+INNER JOIN sys.tables t ON o.parent_obj = t.object_id 
+INNER JOIN sys.schemas s ON t.schema_id = s.schema_id 
+WHERE o.type = 'TR'
+};
+
+	if ($self->{schema}) {
+		$str .= " AND s.name = '$self->{schema}'";
+	}
+	$str .= " " . $self->limit_to_objects('TABLE|VIEW|TRIGGER','t.name|t.name|o.name');
+
+	$str .= " ORDER BY t.name, o.name";
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my @triggers = ();
+	while (my $row = $sth->fetch)
+	{
+		$row->[4] = 'AFTER'; # only FOR=AFTER trigger in this field, no BEFORE
+		$row->[4] = 'INSTEAD OF' if ($row->[8]);
+		my @actions = ();
+		push(@actions, 'INSERT') if ($row->[5]);
+		push(@actions, 'UPDATE') if ($row->[6]);
+		push(@actions, 'DELETE') if ($row->[7]);
+		my $act = join(' OR ', @actions);
+		if (!$self->{schema} && $self->{export_schema}) {
+			$row->[2] = "$row->[3].$row->[2]";
+		}
+		$row->[10] =~ s/
//g;
+		$row->[10] =~ s/^(?:.*?)\sAS\s(.*)\s*;\s*$/$1/is;
+		push(@triggers, [ ($row->[0], $row->[4], $act, $row->[2], $row->[10], '', 'ROW', $row->[1]) ]);
+	}
+
+	return \@triggers;
+}
+
+sub _unique_key
+{
+	my($self, $table, $owner) = @_;
+
+	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 $condition = '';
+        $condition .= " AND t.name = '$table' " if ($table);
+        $condition .= " AND sh.name = '$self->{schema}' " if ($self->{schema});
+	if (!$table) {
+		$condition .= $self->limit_to_objects('TABLE|INDEX', "t.name|i.name");
+	} else {
+		@{$self->{query_bind_params}} = ();
+	}
+
+
+	my $sql = qq{SELECT sh.name AS schema_name,
+   i.name AS constraint_name,
+   t.name AS table_name,
+   c.name AS column_name,
+   ic.key_ordinal AS column_position,
+   ic.is_descending_key AS is_desc,
+   i.is_unique_constraint AS unique_key,
+   i.is_primary_key AS primary_key,
+   typ.name
+FROM sys.indexes i
+   INNER JOIN sys.index_columns ic ON i.index_id = ic.index_id AND i.object_id = ic.object_id
+   INNER JOIN sys.tables AS t ON t.object_id = i.object_id
+   INNER JOIN sys.columns c ON t.object_id = c.object_id AND ic.column_id = c.column_id
+   INNER JOIN sys.types typ ON typ.user_type_id = c.user_type_id
+   INNER JOIN sys.objects AS syso ON syso.object_id = t.object_id AND syso.is_ms_shipped = 0 
+   INNER JOIN sys.schemas AS sh ON sh.schema_id = t.schema_id 
+WHERE (i.is_unique_constraint = 1 OR i.is_primary_key = 1) $condition
+ORDER BY sh.name, i.name, ic.key_ordinal;
+};
+
+	my %tables_infos = ();
+	if ($table) {
+		$tables_infos{$table} = 1;
+	} else {
+		%tables_infos = Ora2Pg::MSSQL::_table_info($self);
+	}
+
+	my $sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my $i = 1;
+	while (my $row = $sth->fetch)
+	{
+		next if ($self->{drop_rowversion} && ($row->[9] eq 'rowversion' || $row->[9] eq 'timestamp'));
+
+		my $name = $row->[2];
+		if (!$self->{schema} && $self->{export_schema}) {
+			$name = "$row->[0].$row->[2]";
+		}
+
+		my $idxname = $row->[3] . '_idx' . $i;
+		$idxname = $row->[2] if ($row->[2]);
+		my $key_type = 'U';
+		$key_type = 'P' if ($row->[7]);
+
+		next if (!grep(/$key_type/, @accepted_constraint_types));
+
+		if (!exists $result{$name}{$idxname})
+		{
+			my %constraint = (type => $key_type, 'generated' => 'N', 'index_name' => $idxname, columns => [ ($row->[3]) ] );
+			$result{$name}{$idxname} = \%constraint if ($row->[3]);
+			$i++;
+		} else {
+			push(@{$result{$name}{$idxname}->{columns}}, $row->[3]);
+		}
+	}
+
+	return %result;
+}
+
+sub _check_constraint
+{
+	my ($self, $table, $owner) = @_;
+
+	my $condition = '';
+	$condition .= " AND t.name = '$table' " if ($table);
+	$condition .= " AND s.name = '$self->{schema}' " if ($self->{schema});
+	if (!$table) {
+		$condition .= $self->limit_to_objects('TABLE|INDEX', "t.name|i.name");
+	} else {
+		@{$self->{query_bind_params}} = ();
+	}
+
+	my $sql = qq{SELECT
+    s.name SchemaName,
+    t.name as TableName,
+    col.name as column_name,
+    con.name as constraint_name,
+    con.definition,
+    con.is_disabled 
+FROM sys.check_constraints con
+LEFT OUTER JOIN sys.objects t ON con.parent_object_id = t.object_id
+JOIN sys.schemas AS s ON t.schema_id = s.schema_id
+LEFT OUTER JOIN sys.all_columns col ON con.parent_column_id = col.column_id AND con.parent_object_id = col.object_id
+$condition
+ORDER BY SchemaName, t.Name, col.name
+};
+
+        my $sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+        $sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+        my %data = ();
+        while (my $row = $sth->fetch)
+	{
+                if ($self->{export_schema} && !$self->{schema}) {
+                        $row->[1] = "$row->[0].$row->[1]";
+                }
+		$row->[4] =~ s/[\[\]]//gs;
+		$row->[4] =~ s/^\(//s;
+		$row->[4] =~ s/\)$//s;
+                $data{$row->[1]}{constraint}{$row->[3]}{condition} = $row->[4];
+		if ($row->[5]) {
+			$data{$row->[1]}{constraint}{$row->[3]}{validate}  = 'NOT VALIDATED';
+		} else {
+			$data{$row->[1]}{constraint}{$row->[3]}{validate}  = 'VALIDATED';
+		}
+        }
+
+	return %data;
+}
+
+sub _get_external_tables
+{
+	my ($self) = @_;
+
+	# There is no external table in MSSQL
+	return;
+}
+
+sub _get_directory
+{
+	my ($self) = @_;
+
+	# There is no external table in MSSQL
+	return;
+}
+
+sub _get_functions
+{
+	my $self = shift;
+
+	# Retrieve all functions 
+	my $str = qq{SELECT
+    O.name, M.definition, O.type_desc, s.name, M.null_on_null_input,
+    M.execute_as_principal_id
+FROM sys.sql_modules M
+JOIN sys.objects O ON M.object_id=O.object_id
+JOIN sys.schemas AS s ON o.schema_id = s.schema_id
+WHERE O.type IN ('IF','TF','FN')
+};
+	if ($self->{schema}) {
+		$str .= " AND s.name = '$self->{schema}'";
+	}
+	$str .= " " . $self->limit_to_objects('FUNCTION','O.name');
+	$str .= " ORDER BY O.name";
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %functions = ();
+	while (my $row = $sth->fetch)
+	{
+		my $kind = 'FUNCTION';
+		next if ( ($kind ne $self->{type}) && ($self->{type} ne 'SHOW_REPORT') );
+		my $fname = $row->[0];
+		if ($self->{export_schema} && !$self->{schema}) {
+			$row->[0] = "$row->[3].$row->[0]";
+		}
+		$functions{"$row->[0]"}{name} = $row->[0];
+		$functions{"$row->[0]"}{text} = $row->[1];
+		$functions{"$row->[0]"}{kind} = $row->[2];
+		$functions{"$row->[0]"}{strict} = $row->[4];
+		$functions{"$row->[0]"}{security} = ($row->[5] == -2) ? 'DEFINER' : 'EXECUTER';
+		$functions{"$row->[0]"}{text} =~ s/
//gs;
+		if ($self->{plsql_pgsql})
+		{
+			$functions{"$row->[0]"}{text} =~ s/[\[\]]//gs;
+		}
+	}
+
+	return \%functions;
+}
+
+sub _get_procedures
+{
+	my $self = shift;
+
+	# Retrieve all functions 
+	my $str = qq{SELECT
+    O.name, M.definition, O.type_desc, s.name, M.null_on_null_input,
+    M.execute_as_principal_id
+FROM sys.sql_modules M
+JOIN sys.objects O ON M.object_id=O.object_id
+JOIN sys.schemas AS s ON o.schema_id = s.schema_id
+WHERE O.type = 'P'
+};
+	if ($self->{schema}) {
+		$str .= " AND s.name = '$self->{schema}'";
+	}
+	$str .= " " . $self->limit_to_objects('PROCEDURE','O.name');
+	$str .= " ORDER BY O.name";
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %functions = ();
+	while (my $row = $sth->fetch)
+	{
+		my $kind = 'PROCEDURE';
+		next if ( ($kind ne $self->{type}) && ($self->{type} ne 'SHOW_REPORT') );
+		my $fname = $row->[0];
+		if ($self->{export_schema} && !$self->{schema}) {
+			$row->[0] = "$row->[3].$row->[0]";
+		}
+		$functions{"$row->[0]"}{name} = $row->[0];
+		$functions{"$row->[0]"}{text} = $row->[1];
+		$functions{"$row->[0]"}{kind} = $row->[2];
+		$functions{"$row->[0]"}{strict} = $row->[4];
+		$functions{"$row->[0]"}{security} = ($row->[5] == -2) ? 'DEFINER' : 'EXECUTER';
+		$functions{"$row->[0]"}{text} =~ s/
//gs;
+		if ($self->{plsql_pgsql}) {
+			$functions{"$row->[0]"}{text} =~ s/[\[\]]//gs;
+		}
+	}
+
+	return \%functions;
+}
+
+sub _lookup_function
+{
+	my ($self, $code, $fctname) = @_;
+
+	my $type = 'functions';
+	$type = lc($self->{type}) . 's' if ($self->{type} eq 'FUNCTION' or $self->{type} eq 'PROCEDURE');
+
+	# Replace all double quote with single quote
+	$code =~ s/"/'/g;
+	# replace backquote with double quote
+	$code =~ s/`/"/g;
+	# Remove some unused code
+	$code =~ s/\s+READS SQL DATA//igs;
+	$code =~ s/\s+UNSIGNED\b((?:.*?)\bFUNCTION\b)/$1/igs;
+	while ($code =~ s/(\s*DECLARE\s+)([^\r\n]+?),\s*\@/$1 $2\n$1 \@/is) {};
+
+        my %fct_detail = ();
+        $fct_detail{func_ret_type} = 'OPAQUE';
+
+        # Split data into declarative and code part
+        ($fct_detail{declare}, $fct_detail{code}) = split(/\b(?:BEGIN|SET|SELECT|INSERT|UPDATE|IF)\b/i, $code, 2);
+	return if (!$fct_detail{code});
+
+	# Look for table variables in code and rewrite them as temporary tables
+	my $records = '';
+	while ($fct_detail{code} =~ s/DECLARE\s+\@([^\s]+)\s+TABLE\s+(\(.*?[\)\w]\s*\))\s*([^,])/"CREATE TEMPORARY TABLE v_$1 $2" . (($3 eq ")") ? $3 : "") . ";"/eis)
+	{
+		my $varname = $1;
+		$fct_detail{code} =~ s/\@$varname\b/v_$varname/igs;
+	}
+
+	# Move all DECLARE statements found in the code into the DECLARE section
+	my @lines = split(/\n/, $fct_detail{code});
+	$fct_detail{code} = '';
+	foreach my $l (@lines)
+	{
+		if ($l !~ /^\s*DECLARE\s+.*CURSOR/ && $l =~ /^\s*DECLARE\s+(.*)/i) {
+			$fct_detail{declare} .= "\n$1";
+			$fct_detail{declare} .= ";" if ($1 !~ /;$/);
+		} else {
+			$fct_detail{code} .= "$l\n";
+		}
+	}
+
+	# Fix DECLARE section
+	$fct_detail{declare} =~ s/\bDECLARE\s+//igs;
+	if ($fct_detail{declare} !~ /\bDECLARE\b/i)
+	{
+		if ($fct_detail{declare} !~ s/(FUNCTION|PROCEDURE|PROC)\s+([^\s\(]+)[\)\s]+AS\s+(.*)/$1 $2\nDECLARE\n$3/is) {
+			$fct_detail{declare} =~ s/(FUNCTION|PROCEDURE|PROC)\s+([^\s\(]+)\s+(.*\@.*?[\)\s]+)(RETURNS|AS)\s+(.*)/$1 $2 ($3)\n$4\nDECLARE\n$5/is;
+		}
+	}
+	# Remove any label that was before the main BEGIN block
+	$fct_detail{declare} =~ s/\s+[^\s\:]+:\s*$//gs;
+	$fct_detail{declare} =~ s/(RETURNS.*TABLE.*\))\s*\)\s*AS\b/) $1 AS/is;
+
+        @{$fct_detail{param_types}} = ();
+	if ( ($fct_detail{declare} =~ s/(.*?)\b(FUNCTION|PROCEDURE|PROC)\s+([^\s]+)\s+((?:RETURNS|AS)\s+.*)//is)
+		|| ($fct_detail{declare} =~ s/(.*?)\b(FUNCTION|PROCEDURE|PROC)\s+([^\s\(]+)(.*?)\s+((?:RETURNS|AS)\s+.*)//is)
+		|| ($fct_detail{declare} =~ s/(.*?)\b(FUNCTION|PROCEDURE|PROC)\s+(.*?)\s+((?:RETURNS|AS)\s+.*)//is)
+		|| ($fct_detail{declare} =~ s/(.*?)\b(FUNCTION|PROCEDURE|PROC)\s+([^\s\(]+)\s*(\(.*\))//is)
+		|| ($fct_detail{declare} =~ s/(.*?)\b(FUNCTION|PROCEDURE|PROC)\s+([^\s\(]+)\s*DECLARE/DECLARE/is)
+	)
+	{
+                $fct_detail{before} = $1;
+                $fct_detail{type} = uc($2);
+                $fct_detail{name} = $3;
+                $fct_detail{args} = $4;
+		my $tmp_returned = $5;
+
+		if ($fct_detail{args} !~ /^\(/ && !$tmp_returned)
+		{
+			$tmp_returned = $fct_detail{args};
+			$fct_detail{args} = '';
+		}
+		$fct_detail{type} = 'PROCEDURE' if ($fct_detail{type} eq 'PROC');
+		$type = lc($fct_detail{type} . 's');
+		$tmp_returned =~ s/RETURNS\s+DECLARE/RETURNS /is;
+		if ($tmp_returned =~ s/\s+AS\s+(DECLARE.*)//is) {
+			$fct_detail{declare} .= "$1\n";
+		}
+		$tmp_returned =~ s/RETURNS\s+(.*)\s+AS\s+.*/$1/is;
+		if ($fct_detail{args} =~ s/\b(AS|DECLARE)\s+(.*)//is) {
+			$tmp_returned = "DECLARE\n$1";
+		}
+		chomp($tmp_returned);
+
+		$tmp_returned =~ s/[\)\s]AS\s+.*//is;
+		$fct_detail{code} = "\n" . $fct_detail{code};
+
+		$tmp_returned =~ s/\)\)$/\)/;
+		$tmp_returned =~ s/\(MAX\)$//i;
+		$fct_detail{args} =~ s/^\s*\(\s*\((.*)\)\s*\)$/$1/s;
+		$fct_detail{args} =~ s/^\s*\(\s*(.*)\s*\)$/$1/s;
+		#$fct_detail{code} =~ s/^DECLARE\b//is;
+		if ($fct_detail{declare} =~ s/\s*COMMENT\s+(\?TEXTVALUE\d+\?|'[^\']+')//) {
+			$fct_detail{comment} = $1;
+		}
+		$fct_detail{immutable} = 1 if ($fct_detail{declare} =~ s/\s*\bDETERMINISTIC\b//is);
+		$fct_detail{before} = ''; # There is only garbage for the moment
+
+                $fct_detail{name} =~ s/['"]//g;
+                $fct_detail{fct_name} = $fct_detail{name};
+		if (!$fct_detail{args}) {
+			$fct_detail{args} = '()';
+		}
+		$fct_detail{immutable} = 1 if ($fct_detail{return} =~ s/\s*\bDETERMINISTIC\b//is);
+		$fct_detail{immutable} = 1 if ($tmp_returned =~ s/\s*\bDETERMINISTIC\b//is);
+		$tmp_returned =~ s/^\s+//;
+		$tmp_returned =~ s/\s+$//;
+
+		$fctname = $fct_detail{name} || $fctname;
+		if ($type eq 'functions' && exists $self->{$type}{$fctname}{return} && $self->{$type}{$fctname}{return})
+		{
+			$fct_detail{hasreturn} = 1;
+			$fct_detail{func_ret_type} = $self->_sql_type($self->{$type}{$fctname}{return});
+		}
+		elsif ($type eq 'functions' && !exists $self->{$type}{$fctname}{return} && $tmp_returned)
+		{
+			$tmp_returned =~ s/\s+CHARSET.*//is;
+			#$fct_detail{func_ret_type} = $self->_sql_type($tmp_returned);
+			$fct_detail{func_ret_type} = replace_sql_type($self, $tmp_returned);
+			$fct_detail{hasreturn} = 1;
+		}
+		$fct_detail{language} = $self->{$type}{$fctname}{language};
+		$fct_detail{immutable} = 1 if ($self->{$type}{$fctname}{immutable} eq 'YES');
+		$fct_detail{security} = $self->{$type}{$fctname}{security};
+
+                if ($fct_detail{func_ret_type} =~ s/RETURNS\s+\@(.*?)\s+TABLE/TABLE/is) {
+			$fct_detail{declare} .= "v_$1 record;\n";
+		}
+                $fct_detail{func_ret_type} =~ s/RETURNS\s*//is;
+
+		# Procedure that have out parameters are functions with PG
+		if ($type eq 'procedures' && $fct_detail{args} =~ /\b(OUT|INOUT)\b/) {
+			# set return type to empty to avoid returning void later
+			$fct_detail{func_ret_type} = ' ';
+		}
+
+		# IN OUT should be INOUT
+		$fct_detail{args} =~ s/\bIN\s+OUT/INOUT/igs;
+
+		# Move the DECLARE statement from code to the declare section.
+		#$fct_detail{declare} = '';
+		while ($fct_detail{code} =~ s/DECLARE\s+([^;\n\r]+)//is)
+		{
+			my $var = $1;
+			$fct_detail{declare} .= "\n$var" if ($fct_detail{declare} !~ /v_$var /is);
+		}
+		# Rename arguments with @ replaced by p_
+		($fct_detail{args}, $fct_detail{declare}, $fct_detail{code}) = replace_mssql_params($self, $fct_detail{args}, $fct_detail{declare}, $fct_detail{code});
+
+		# Now convert types
+		if ($fct_detail{args}) {
+			$fct_detail{args} = replace_sql_type($self, $fct_detail{args});
+		}
+		if ($fct_detail{declare}) {
+			$fct_detail{declare} = replace_sql_type($self, $fct_detail{declare});
+		}
+
+		$fct_detail{args} =~ s/\s+/ /gs;
+		push(@{$fct_detail{param_types}}, split(/\s*,\s*/, $fct_detail{args}));
+
+		# Store type used in parameter list to lookup later for custom types
+		map { s/^\(//; } @{$fct_detail{param_types}};
+		map { s/\)$//; } @{$fct_detail{param_types}};
+		map { s/\%ORA2PG_COMMENT\d+\%//gs; }  @{$fct_detail{param_types}};
+		map { s/^\s*[^\s]+\s+(IN|OUT|INOUT)/$1/i; s/^((?:IN|OUT|INOUT)\s+[^\s]+)\s+[^\s]*$/$1/i; s/\(.*//; s/\s*\)\s*$//; s/\s+$//; } @{$fct_detail{param_types}};
+	}
+	else
+	{
+                delete $fct_detail{func_ret_type};
+                delete $fct_detail{declare};
+                $fct_detail{code} = $code;
+	}
+
+	# Mark the function as having out parameters if any
+	my @nout = $fct_detail{args} =~ /\bOUT\s+([^,\)]+)/igs;
+	my @ninout = $fct_detail{args} =~ /\bINOUT\s+([^,\)]+)/igs;
+	my $nbout = $#nout+1 + $#ninout+1;
+	$fct_detail{inout} = 1 if ($nbout > 0);
+
+	# Append TABLE declaration to the declare section
+	$fct_detail{declare} .= "\n$records" if ($records);
+	# Rename variables with @ replaced by v_
+	($fct_detail{code}, $fct_detail{declare}) = replace_mssql_variables($self, $fct_detail{code}, $fct_detail{declare});
+
+	$fct_detail{args} =~ s/\s*$//s;
+	$fct_detail{args} =~ s/^\s*//s;
+	$fct_detail{code} =~ s/^[\r\n]*/\n/s;
+
+
+	# Remove %ROWTYPE from return type
+	$fct_detail{func_ret_type} =~ s/\%ROWTYPE//igs;
+	return %fct_detail;
+}
+
+sub replace_mssql_params
+{
+	my ($self, $args, $declare, $code) = @_;
+
+	if ($args =~ s/\s+(?:DECLARE|AS)\s+(.*)//is) {
+		$declare .= "\n$1";
+	}
+	while ($args =~ s/\@([^\s]+)\b/p_$1/s)
+	{
+		my $p = $1;
+		$code =~ s/\@$p\b/p_$p/gis;
+	}
+
+	return ($args, $declare, $code);
+}
+
+sub replace_mssql_variables
+{
+	my ($self, $code, $declare) = @_;
+
+	# Look for mssql global variables and add them to the custom variable list
+	while ($code =~ s/\b(?:SET\s+)?\@\@(?:SESSION\.)?([^\s:=]+)\s*:=\s*([^;]+);/PERFORM set_config('$2', $2, false);/is)
+	{
+		my $n = $1;
+		my $v = $2;
+		$self->{global_variables}{$n}{name} = lc($n);
+		# Try to set a default type for the variable
+		$self->{global_variables}{$n}{type} = 'bigint';
+		if ($v =~ /'[^\']*'/) {
+			$self->{global_variables}{$n}{type} = 'varchar';
+		}
+		if ($n =~ /datetime/i) {
+			$self->{global_variables}{$n}{type} = 'timestamp';
+		} elsif ($n =~ /time/i) {
+			$self->{global_variables}{$n}{type} = 'time';
+		} elsif ($n =~ /date/i) {
+			$self->{global_variables}{$n}{type} = 'date';
+		} 
+	}
+
+	my @to_be_replaced = ();
+	# Look for local variable definition and append them to the declare section
+	while ($code =~ s/SET\s+\@([^\s=]+)\s*=\s*/v_$1 := /is)
+	{
+		my $n = $1;
+		push(@to_be_replaced, $n);
+	}
+
+	# Look for local variable definition and append them to the declare section
+	while ($code =~ s/(^|[^\@])\@([^\s:=,]+)\s*:=\s*(.*)/$1v_$2 := $3/is)
+	{
+		my $n = $2;
+		my $v = $3;
+		# Try to set a default type for the variable
+		my $type = 'integer';
+		$type = 'varchar' if ($v =~ /'[^']*'/);
+		if ($n =~ /datetime/i) {
+			$type = 'timestamp';
+		} elsif ($n =~ /time/i) {
+			$type = 'time';
+		} elsif ($n =~ /date/i) {
+			$type = 'date';
+		} 
+		$declare .= "v_$n $type;\n" if ($declare !~ /\b$n $type;/s);
+		push(@to_be_replaced, $n);
+	}
+
+	# Fix other call to the same variable in the code
+	foreach my $n (@to_be_replaced) {
+		$code =~ s/\@$n\b/v_$n/gs;
+	}
+
+	# Look for variable definition in DECLARE section and rename them in the code too
+	while ($declare =~ s/(^|[^\@])\@([a-z0-9_]+)/$1v_$2/is)
+	{
+		my $n = $2;
+		# Fix other call to the same variable in the code
+		$code =~ s/\@$n\b/v_$n/gs;
+	}
+
+	# Look for some global variable definition and append them to the declare section
+	while ($code =~ /\@\@(ROWCOUNT|VERSION|LANGUAGE|SPID|MICROSOFTVERSION)/is)
+	{
+		my $v = uc($1);
+		if ($v eq 'VERSION') {
+			$code =~ s/\@\@$v/version()/igs;
+		} elsif ($v eq 'LANGUAGE') {
+			$code =~ s/\@\@$v/current_setting('client_encoding')/igs;
+		} elsif ($v eq 'ROWCOUNT') {
+			$declare .= "v_v_rowcount bigint;\n" if ($declare !~ /v_v_rowcount/s);
+			$code =~ s/([\r\n])([^\r\n]+?)\@\@$v/\nGET DIAGNOSTICS v_v_rowcount := ROWCOUNT;\n$2 v_v_rowcount/igs;
+		} elsif ($v eq 'SPID') {
+			$code =~ s/\@\@$v/pg_backend_pid()/igs;
+		} elsif ($v eq 'MICROSOFTVERSION') {
+			$code =~ s/\@\@$v/current_setting('server_version')/igs;
+		}
+	}
+
+	# Look for local variable definition and append them to the declare section
+	while ($code =~ s/(^|[^\@])\@([a-z0-9_\$]+)/$1v_$2/is)
+	{
+		my $n = $2;
+		next if ($n =~ /^v_/);
+		# Try to set a default type for the variable
+		my $type = 'varchar';
+		if ($n =~ /datetime/i) {
+			$type = 'timestamp';
+		} elsif ($n =~ /time/i) {
+			$type = 'time';
+		} elsif ($n =~ /date/i) {
+			$type = 'date';
+		} 
+		$declare .= "v_$n $type;\n" if ($declare !~ /v_$n ($type|record);/is);
+		# Fix other call to the same variable in the code
+		$code =~ s/\@$n\b/v_$n/gs;
+	}
+
+	# Look for variable definition with SELECT statement
+	$code =~ s/\bSET\s+([^\s=]+)\s*=\s*([^;]+\bSELECT\b[^;]+);/$1 = $2;/igs;
+
+	return ($code, $declare);
+}
+
+sub _list_all_functions
+{
+	my $self = shift;
+
+	# Retrieve all functions 
+	# ROUTINE_SCHEMA           | varchar(64)   | NO   |     |                     |       |
+	# ROUTINE_NAME             | varchar(64)   | NO   |     |                     |       |
+	# ROUTINE_TYPE             | varchar(9)    | NO   |     |                     |       |
+
+	my $str = "SELECT ROUTINE_NAME,DATA_TYPE FROM INFORMATION_SCHEMA.ROUTINES";
+	if ($self->{schema}) {
+		$str .= " AND ROUTINE_SCHEMA = '$self->{schema}'";
+	}
+	if ($self->{db_version} < '5.5.0') {
+		$str =~ s/\bDATA_TYPE\b/DTD_IDENTIFIER/;
+	}
+	$str .= " " . $self->limit_to_objects('FUNCTION','ROUTINE_NAME');
+	$str =~ s/ AND / WHERE /;
+	$str .= " ORDER BY ROUTINE_NAME";
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my @functions = ();
+	while (my $row = $sth->fetch) {
+		push(@functions, $row->[0]);
+	}
+	$sth->finish();
+
+	return @functions;
+}
+
+sub _sql_type
+{
+        my ($self, $type, $len, $precision, $scale, $default, $no_blob_to_oid) = @_;
+
+	my $data_type = '';
+	chomp($type);
+
+	# Some length and scale may have not been extracted before
+	if ($type =~ s/\(\s*(\d+)\s*\)//) {
+		$len   = $1;
+	} elsif ($type =~ s/\(\s*(\d+)\s*,\s*(\d+)\s*\)//) {
+		$len   = $1;
+		$scale = $2;
+	}
+
+	if ($type !~ /CHAR/i) {
+		$precision = $len if (!$precision);
+	}
+        # Override the length
+        if (exists $self->{data_type}{uc($type)})
+	{
+		$type = uc($type); # Force uppercase
+		if ($len)
+		{
+			if ( $type =~ /CHAR|TEXT/ )
+			{
+				# 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"));
+				if ($self->{data_type}{$type} =~ /text/i) {
+					return "$self->{data_type}{$type}";
+				} else {
+					return "$self->{data_type}{$type}($len)";
+				}
+			}
+			elsif ($type eq 'BIT')
+			{
+				if ($precision > 1) {
+					return "bit($precision)";
+				} else {
+					return $self->{data_type}{$type};
+				}
+			}
+			elsif ($type =~ /(TINYINT|SMALLINT|INTEGER|BIGINT|INT|REAL|FLOAT|DECIMAL|NUMERIC|SMALLMONEY|MONEY)/i)
+			{
+				# This is an integer
+				if (!$scale)
+				{
+					if ($precision)
+					{
+						if ($self->{pg_integer_type})
+						{
+							if ($precision < 5) {
+								return 'smallint';
+							} elsif ($precision <= 9) {
+								return 'integer'; # The speediest in PG
+							} else {
+								return 'bigint';
+							}
+						}
+						return "numeric($precision)";
+					}
+					else
+					{
+						# Most of the time interger should be enought?
+						return $self->{data_type}{$type};
+					}
+				}
+				else
+				{
+					if ($precision)
+					{
+						if ($self->{pg_numeric_type})
+						{
+							if ($precision <= 6) {
+								return 'real';
+							} else {
+								return 'double precision';
+							}
+						}
+						return "decimal($precision,$scale)";
+					}
+				}
+			}
+			$self->{use_uuid} = 1 if ($type =~ /UNIQUEIDENTIFIER/);
+			return $self->{data_type}{$type};
+		} else {
+			return $self->{data_type}{$type};
+		}
+        }
+
+        return $type;
+}
+
+sub replace_sql_type
+{
+        my ($self,  $str) = @_;
+
+	$str =~ s/with local time zone/with time zone/igs;
+	$str =~ s/([A-Z])ORA2PG_COMMENT/$1 ORA2PG_COMMENT/igs;
+	$str =~ s/\(\s*MAX\s*\)//igs;
+
+	# Replace type with precision
+	my $mssqltype_regex = '';
+	foreach (keys %{$self->{data_type}}) {
+		$mssqltype_regex .= quotemeta($_) . '|';
+	}
+	$mssqltype_regex =~ s/\|$//;
+
+	while ($str =~ /(.*)\b($mssqltype_regex)\s*\(([^\)]+)\)/i)
+	{
+		my $backstr = $1;
+		my $type = uc($2);
+		my $args = $3;
+		if (exists $self->{data_type}{"$type($args)"}) {
+			$str =~ s/\b$type\($args\)/$self->{data_type}{"$type($args)"}/igs;
+			next;
+		}
+		if ($backstr =~ /_$/) {
+		    $str =~ s/\b($mssqltype_regex)\s*\(([^\)]+)\)/$1\%\|$2\%\|\%/is;
+		    next;
+		}
+
+		my ($precision, $scale) = split(/,/, $args);
+		$scale ||= 0;
+		my $len = $precision || 0;
+		$len =~ s/\D//;
+		if ( $type =~ /CHAR|TEXT/i )
+		{
+			# Type CHAR have default length set to 1
+			# Type VARCHAR must have a specified length
+			$len = 1 if (!$len && ($type eq 'CHAR' || $type eq 'NCHAR'));
+			$str =~ s/\b$type\b\s*\([^\)]+\)/$self->{data_type}{$type}\%\|$len\%\|\%/is;
+		}
+		elsif ($type eq 'BIT')
+		{
+			if ($precision > 1) {
+				return "bit($precision)";
+			} else {
+				return $self->{data_type}{$type};
+			}
+		}
+		elsif ($precision && ($type =~ /(TINYINT|SMALLINT|INTEGER|BIGINT|INT|REAL|FLOAT|DECIMAL|NUMERIC|SMALLMONEY|MONEY)/))
+		{
+			if (!$scale)
+			{
+				if ($type =~ /(TINYINT|SMALLINT|INTEGER|BIGINT|INT)/)
+				{
+					if ($self->{pg_integer_type})
+					{
+						if ($precision < 5) {
+							$str =~ s/\b$type\b\s*\([^\)]+\)/smallint/is;
+						} elsif ($precision <= 9) {
+							$str =~ s/\b$type\b\s*\([^\)]+\)/integer/is;
+						} else {
+							$str =~ s/\b$type\b\s*\([^\)]+\)/bigint/is;
+						}
+					}
+					else {
+						$str =~ s/\b$type\b\s*\([^\)]+\)/numeric\%\|$precision\%\|\%/i;
+					}
+				}
+				else {
+					$str =~ s/\b$type\b\s*\([^\)]+\)/$self->{data_type}{$type}\%\|$precision\%\|\%/is;
+				}
+			}
+			else
+			{
+				$str =~ s/\b$type\b\s*\([^\)]+\)/$self->{data_type}{$type}\%\|$args\%\|\%/is;
+			}
+		}
+		else
+		{
+			# Prevent from infinit loop
+			$str =~ s/\(/\%\|/s;
+			$str =~ s/\)/\%\|\%/s;
+		}
+	}
+	$str =~ s/\%\|\%/\)/gs;
+	$str =~ s/\%\|/\(/gs;
+
+	# Replace datatype even without precision
+	my %recover_type = ();
+	my $i = 0;
+	foreach my $type (sort { length($b) <=> length($a) } keys %{$self->{data_type}})
+	{
+		while ($str =~ s/\b$type\b/%%RECOVER_TYPE$i%%/is)
+		{
+			$recover_type{$i} = $self->{data_type}{$type};
+			$i++;
+		}
+	}
+
+	foreach $i (keys %recover_type) {
+		$str =~ s/\%\%RECOVER_TYPE$i\%\%/$recover_type{$i}/;
+	}
+
+	if (($self->{type} eq 'COPY' || $self->{type} eq 'INSERT') && exists $SQL_TYPE{uc($str)}) {
+		$str = $SQL_TYPE{uc($str)};
+	}
+	# Set varchar without length to text
+	$str =~ s/\bVARCHAR(\s*(?!\())/text$1/igs;
+
+        return $str;
+}
+
+sub _get_job
+{
+	my($self) = @_;
+
+	# Don't work with Azure
+	#return if ($self->{db_version} =~ /Microsoft SQL Azure/);
+	return;
+
+	# Retrieve all database job from user_jobs table
+	my $str = qq{SELECT
+     job.job_id,
+     notify_level_email,
+     name,
+     enabled,
+     description,
+     step_name,
+     command,
+     server,
+     database_name
+FROM
+    msdb.dbo.sysjobs job
+INNER JOIN 
+    msdb.dbo.sysjobsteps steps        
+ON
+    job.job_id = steps.job_id
+WHERE
+    job.enabled = 1 AND database_name = $self->{database}
+};
+	$str .= $self->limit_to_objects('JOB', 'NAME');
+	$str .= " ORDER BY NAME";
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %data = ();
+	while (my $row = $sth->fetch) {
+		$data{$row->[0]}{what} = $row->[6];
+		$data{$row->[0]}{interval} = $row->[0];
+	}
+
+	return %data;
+}
+
+sub _get_dblink
+{
+	my($self) = @_;
+
+	# Don't work with Azure
+	return if ($self->{db_version} =~ /Microsoft SQL Azure/);
+
+	# Retrieve all database link from dba_db_links table
+	my $str = qq{SELECT 
+  name,
+  provider,
+  data_source,
+  catalog
+FROM sys.servers
+WHERE is_linked = 1
+};
+	$str .= $self->limit_to_objects('DBLINK', 'name');
+	$str .= " ORDER BY name";
+
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %data = ();
+	while (my $row = $sth->fetch)
+	{
+		my $port = '1433';
+		if ($row->[2] =~ s/,(\d+)$//) {
+			$port = $1;
+		}
+		$data{$row->[0]}{owner} = 'unknown';
+		$data{$row->[0]}{username} = 'unknown';
+		$data{$row->[0]}{host} = $row->[2];
+		$data{$row->[0]}{db} = $row->[3] || 'unknown';
+		$data{$row->[0]}{port} = $port;
+		$data{$row->[0]}{backend} = $row->[1] || 'SQL Server';
+	}
+
+	return %data;
+}
+
+=head2 _get_partitions
+
+This function implements an MSSQL-native partitions information.
+Return two hash ref with partition details and partition default.
+=cut
+
+sub _get_partitions
+{
+	my($self) = @_;
+
+	# Retrieve all partitions.
+	my $str = qq{
+SELECT sch.name AS SchemaName, t.name AS TableName, i.name AS IndexName,
+    p.partition_number, p.partition_id, i.data_space_id, f.function_id, f.type_desc,
+    r.boundary_id, r.value AS BoundaryValue, ic.column_id AS PartitioningColumnID,
+    c.name AS PartitioningColumnName
+FROM sys.tables AS t
+JOIN sys.indexes AS i ON t.object_id = i.object_id AND i.[type] <= 1
+JOIN sys.partitions AS p ON i.object_id = p.object_id AND i.index_id = p.index_id
+JOIN sys.partition_schemes AS s ON i.data_space_id = s.data_space_id
+JOIN sys.index_columns AS ic ON ic.[object_id] = i.[object_id] AND ic.index_id = i.index_id AND ic.partition_ordinal >= 1 -- because 0 = non-partitioning column
+JOIN sys.columns AS c ON t.[object_id] = c.[object_id] AND ic.column_id = c.column_id
+JOIN sys.partition_functions AS f ON s.function_id = f.function_id
+LEFT JOIN sys.partition_range_values AS r ON f.function_id = r.function_id and r.boundary_id = p.partition_number
+LEFT OUTER JOIN sys.schemas sch ON t.schema_id = sch.schema_id
+};
+
+	$str .= $self->limit_to_objects('TABLE|PARTITION','t.name|t.name');
+	if ($self->{schema}) {
+		$str .= " WHERE sch.name ='$self->{schema}'";
+	}
+	$str .= " ORDER BY sch.name, t.name, i.name, p.partition_number, ic.column_id\n";
+
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %parts = ();
+	my %default = ();
+	my $i = 1;
+	while (my $row = $sth->fetch)
+	{
+		my $tbname = $row->[1];
+		if ($self->{export_schema} && !$self->{schema}) {
+			$row->[1] = "$row->[0].$row->[1]";
+		}
+		#dbo | PartitionTable | PK__Partitio__357D0D3E1290FD9F | 2 | 72057594048872448 | 65601 | 65536 | RANGE | 2 | 2022-05-01 00:00:00 | 1 | col1
+		$parts{$row->[1]}{$row->[3]}{name} = $tbname . '_part_' . $i++;
+		$row->[9] = "'$row->[9]'" if ($row->[9] && $row->[9] =~ /[^\d\.]/);
+		$row->[9] = 'MAXVALUE' if ($row->[9] eq '');
+		push(@{$parts{$row->[1]}{$row->[3]}{info}}, { 'type' => 'RANGE', 'value' => $row->[9], 'column' => $row->[11], 'colpos' => $row->[10], 'tablespace' => '', 'owner' => ''});
+	}
+	$sth->finish;
+
+	return \%parts, \%default;
+}
+
+=head2 _get_subpartitions
+
+This function implements a MSSQL subpartitions information.
+Return two hash ref with partition details and partition default.
+=cut
+
+sub _get_subpartitions
+{
+	my($self) = @_;
+
+	my %subparts = ();
+	my %default = ();
+
+	# For what I know, subpartition is not supported by MSSQL
+	return \%subparts, \%default;
+}
+
+=head2 _get_partitions_list
+
+This function implements a MSSQL-native partitions information.
+Return a hash of the partition table_name => type
+
+=cut
+
+sub _get_partitions_list
+{
+	my($self) = @_;
+
+	# Retrieve all partitions.
+	my $str = qq{
+SELECT sch.name AS SchemaName, t.name AS TableName, i.name AS IndexName,
+    p.partition_number, p.partition_id, i.data_space_id, f.function_id, f.type_desc,
+    r.boundary_id, r.value AS BoundaryValue, ic.column_id AS PartitioningColumnID,
+    c.name AS PartitioningColumnName
+FROM sys.tables AS t
+JOIN sys.indexes AS i ON t.object_id = i.object_id AND i.[type] <= 1
+JOIN sys.partitions AS p ON i.object_id = p.object_id AND i.index_id = p.index_id
+JOIN sys.partition_schemes AS s ON i.data_space_id = s.data_space_id
+JOIN sys.index_columns AS ic ON ic.[object_id] = i.[object_id] AND ic.index_id = i.index_id AND ic.partition_ordinal >= 1 -- because 0 = non-partitioning column
+JOIN sys.columns AS c ON t.[object_id] = c.[object_id] AND ic.column_id = c.column_id
+JOIN sys.partition_functions AS f ON s.function_id = f.function_id
+LEFT JOIN sys.partition_range_values AS r ON f.function_id = r.function_id and r.boundary_id = p.partition_number
+LEFT OUTER JOIN sys.schemas sch ON t.schema_id = sch.schema_id
+};
+
+	$str .= $self->limit_to_objects('TABLE|PARTITION','t.name|t.name');
+	if ($self->{schema}) {
+		$str .= " WHERE sch.name ='$self->{schema}'";
+	}
+	$str .= " ORDER BY sch.name, t.name, i.name, p.partition_number, ic.column_id\n";
+
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %parts = ();
+	while (my $row = $sth->fetch)
+	{
+		$parts{"\L$row->[1]\E"}++;
+#		if ($self->{export_schema} && !$self->{schema}) {
+#			$row->[1] = "$row->[0].$row->[1]";
+#		}
+#               $parts{"\L$row->[1]\E"}{count}++;
+#               $parts{"\L$row->[1]\E"}{composite} = 0;
+#		$parts{"\L$row->[1]\E"}{type} = 'RANGE';
+#		push(@{ $parts{"\L$row->[1]\E"}{columns} }, $row->[11]) if (!grep(/^$row->[11]$/, @{ $parts{"\L$row->[1]\E"}{columns} }));
+	}
+	$sth->finish;
+
+	return %parts;
+}
+
+=head2 _get_partitioned_table
+
+Return a hash of the partitioned table with the number of partition
+
+=cut
+
+sub _get_partitioned_table
+{
+	my ($self, %subpart) = @_;
+
+	# Retrieve all partitions.
+	my $str = qq{
+SELECT sch.name AS SchemaName, t.name AS TableName, i.name AS IndexName,
+    p.partition_number, p.partition_id, i.data_space_id, f.function_id, f.type_desc,
+    r.boundary_id, r.value AS BoundaryValue, ic.column_id AS PartitioningColumnID,
+    c.name AS PartitioningColumnName
+FROM sys.tables AS t
+JOIN sys.indexes AS i ON t.object_id = i.object_id AND i.[type] <= 1
+JOIN sys.partitions AS p ON i.object_id = p.object_id AND i.index_id = p.index_id
+JOIN sys.partition_schemes AS s ON i.data_space_id = s.data_space_id
+JOIN sys.index_columns AS ic ON ic.[object_id] = i.[object_id] AND ic.index_id = i.index_id AND ic.partition_ordinal >= 1 -- because 0 = non-partitioning column
+JOIN sys.columns AS c ON t.[object_id] = c.[object_id] AND ic.column_id = c.column_id
+JOIN sys.partition_functions AS f ON s.function_id = f.function_id
+LEFT JOIN sys.partition_range_values AS r ON f.function_id = r.function_id and r.boundary_id = p.partition_number
+LEFT OUTER JOIN sys.schemas sch ON t.schema_id = sch.schema_id
+};
+
+	$str .= $self->limit_to_objects('TABLE|PARTITION','t.name|t.name');
+	if ($self->{schema}) {
+		$str .= " WHERE sch.name ='$self->{schema}'";
+	}
+	$str .= " ORDER BY sch.name, t.name, i.name, p.partition_number, ic.column_id\n";
+
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %parts = ();
+	while (my $row = $sth->fetch)
+	{
+		if ($self->{export_schema} && !$self->{schema}) {
+			$row->[1] = "$row->[0].$row->[1]";
+		}
+                $parts{$row->[1]}{count}++;
+                $parts{$row->[1]}{composite} = 0;
+		$parts{$row->[1]}{type} = 'RANGE';
+		push(@{ $parts{$row->[1]}{columns} }, $row->[11]) if (!grep(/^$row->[11]$/, @{ $parts{$row->[1]}{columns} }));
+		#dbo | PartitionTable | PK__Partitio__357D0D3E1290FD9F | 2 | 72057594048872448 | 65601 | 65536 | RANGE | 2 | 2022-05-01 00:00:00 | 1 | col1
+	}
+	$sth->finish;
+
+	return %parts;
+}
+
+
+=head2 _get_objects
+
+This function retrieves all object the Oracle information
+
+=cut
+
+sub _get_objects
+{
+	my $self = shift;
+
+	my %infos = ();
+
+	# TABLE
+	my $sql = "SELECT t.name FROM sys.tables t INNER JOIN sys.indexes i ON t.OBJECT_ID = i.object_id INNER JOIN sys.partitions p ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id LEFT OUTER JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE t.is_ms_shipped = 0 AND i.OBJECT_ID > 255 AND t.type='U' AND t.NAME NOT LIKE '#%'";
+	if (!$self->{schema}) {
+		$sql .= " AND s.name NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
+	} else {
+		$sql.= " AND s.name = '$self->{schema}'";
+	}
+	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->fetchrow()) {
+		push(@{$infos{TABLE}}, { ( name => $row[0], invalid => 0) });
+	}
+	$sth->finish();
+	# VIEW
+	$sql = "SELECT v.name from sys.views v join sys.sql_modules m on m.object_id = v.object_id WHERE NOT EXISTS (SELECT 1 FROM sys.indexes i WHERE i.object_id = v.object_id and i.index_id = 1 and i.ignore_dup_key = 0) AND is_date_correlation_view=0";
+	if (!$self->{schema}) {
+		$sql .= " AND schema_name(v.schema_id) NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
+	} else {
+		$sql.= " AND schema_name(v.schema_id) = '$self->{schema}'";
+	}
+	$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->fetchrow()) {
+		push(@{$infos{VIEW}}, { ( name => $row[0], invalid => 0) });
+	}
+	$sth->finish();
+	# TRIGGER
+	$sql = "SELECT o.name FROM sys.sysobjects o INNER JOIN sys.tables t ON o.parent_obj = t.object_id INNER JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE o.type = 'TR'";
+	if (!$self->{schema}) {
+		$sql .= " AND s.name NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
+	} else {
+		$sql.= " AND s.name = '$self->{schema}'";
+	}
+	$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->fetchrow()) {
+		push(@{$infos{TRIGGER}}, { ( name => $row[0], invalid => 0) });
+	}
+	$sth->finish();
+	# INDEX
+	foreach my $t (@{$infos{TABLE}})
+	{
+		my $sql = "SELECT Id.name AS index_name FROM sys.tables AS T INNER JOIN sys.indexes Id ON T.object_id = Id.object_id LEFT OUTER JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE T.is_ms_shipped = 0 AND Id.auto_created = 0 AND OBJECT_NAME(Id.object_id, DB_ID())='$t' AND Id.is_primary_key = 0";
+		if (!$self->{schema}) {
+			$sql .= " AND s.name NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
+		} else {
+			$sql.= " AND s.name = '$self->{schema}'";
+		}
+		$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->fetchrow()) {
+			next if ($row[2] eq 'PRIMARY');
+			push(@{$infos{INDEX}}, { ( name => $row[2], invalid => 0) });
+		}
+	}
+	# FUNCTION
+	$sql = "SELECT O.name FROM sys.sql_modules M JOIN sys.objects O ON M.object_id=O.object_id JOIN sys.schemas AS s ON o.schema_id = s.schema_id WHERE O.type IN ('IF','TF','FN')";
+	if (!$self->{schema}) {
+		 $sql .= " AND s.name NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
+	} else {
+		$sql .= " AND s.name = '$self->{schema}'";
+	}
+	$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->fetchrow()) {
+		push(@{$infos{FUNCTION}}, { ( name => $row[0], invalid => 0) });
+	}
+	$sth->finish();
+
+	# PROCEDURE
+	$sql = "SELECT O.name FROM sys.sql_modules M JOIN sys.objects O ON M.object_id=O.object_id JOIN sys.schemas AS s ON o.schema_id = s.schema_id WHERE O.type = 'P'";
+	if (!$self->{schema}) {
+		 $sql .= " AND s.name NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
+	} else {
+		$sql .= " AND s.name = '$self->{schema}'";
+	}
+	$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->fetchrow()) {
+		push(@{$infos{PROCEDURE}}, { ( name => $row[0], invalid => 0) });
+	}
+	$sth->finish();
+
+	# PARTITION.
+	$sql = qq{
+SELECT sch.name AS SchemaName, t.name AS TableName, i.name AS IndexName,
+    p.partition_number, p.partition_id, i.data_space_id, f.function_id, f.type_desc,
+    r.boundary_id, r.value AS BoundaryValue, ic.column_id AS PartitioningColumnID,
+    c.name AS PartitioningColumnName
+FROM sys.tables AS t
+JOIN sys.indexes AS i ON t.object_id = i.object_id AND i.[type] <= 1
+JOIN sys.partitions AS p ON i.object_id = p.object_id AND i.index_id = p.index_id
+JOIN sys.partition_schemes AS s ON i.data_space_id = s.data_space_id
+JOIN sys.index_columns AS ic ON ic.[object_id] = i.[object_id] AND ic.index_id = i.index_id AND ic.partition_ordinal >= 1 -- because 0 = non-partitioning column
+JOIN sys.columns AS c ON t.[object_id] = c.[object_id] AND ic.column_id = c.column_id
+JOIN sys.partition_functions AS f ON s.function_id = f.function_id
+LEFT JOIN sys.partition_range_values AS r ON f.function_id = r.function_id and r.boundary_id = p.partition_number
+LEFT OUTER JOIN sys.schemas sch ON t.schema_id = sch.schema_id
+};
+	if ($self->{schema}) {
+		$sql .= " WHERE sch.name ='$self->{schema}'";
+	}
+
+	$sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	while ( my @row = $sth->fetchrow()) {
+		push(@{$infos{'TABLE PARTITION'}}, { ( name => $row[0], invalid => 0) });
+	}
+	$sth->finish;
+
+	# MATERIALIZED VIEW
+	$sql = qq{select
+       v.name as view_name,
+       schema_name(v.schema_id) as schema_name,
+       i.name as index_name,
+       m.definition
+from sys.views v
+join sys.indexes i on i.object_id = v.object_id and i.index_id = 1 and i.ignore_dup_key = 0
+join sys.sql_modules m on m.object_id = v.object_id
+};
+
+	if (!$self->{schema}) {
+		$sql .= " WHERE schema_name(v.schema_id) NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
+	} else {
+		$sql .= " WHERE schema_name(v.schema_id) = '$self->{schema}'";
+	}
+	$sth = $self->{dbh}->prepare($sql) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	while ( my @row = $sth->fetchrow()) {
+		push(@{$infos{'MATERIALIZED VIEW'}}, { ( name => $row[0], invalid => 0) });
+	}
+	$sth->finish;
+
+	return %infos;
+}
+
+sub _get_privilege
+{
+	my($self) = @_;
+
+	my %privs = ();
+	my %roles = ();
+
+	# Retrieve all privilege per table defined in this database
+	my $str = "SELECT GRANTEE,TABLE_NAME,PRIVILEGE_TYPE,IS_GRANTABLE FROM INFORMATION_SCHEMA.TABLE_PRIVILEGES";
+	if ($self->{schema}) {
+		$str .= " WHERE TABLE_SCHEMA = '$self->{schema}'";
+	}
+	$str .= " " . $self->limit_to_objects('GRANT|TABLE|VIEW|FUNCTION|PROCEDURE|SEQUENCE', 'GRANTEE|TABLE_NAME|TABLE_NAME|TABLE_NAME|TABLE_NAME|TABLE_NAME');
+	$str .= " ORDER BY TABLE_NAME, GRANTEE";
+	my $error = "\n\nFATAL: You must be connected as an oracle dba user to retrieved grants\n\n";
+	my $sth = $self->{dbh}->prepare($str) or $self->logit($error . "FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	while (my $row = $sth->fetch) {
+		# Remove the host part of the user
+		$row->[0] =~ s/\@.*//;
+		$row->[0] =~ s/'//g;
+		$privs{$row->[1]}{type} = $row->[2];
+		if ($row->[3] eq 'YES') {
+			$privs{$row->[1]}{grantable} = $row->[3];
+		}
+		$privs{$row->[1]}{owner} = '';
+		push(@{$privs{$row->[1]}{privilege}{$row->[0]}}, $row->[2]);
+		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 GRANTEE,TABLE_NAME,PRIVILEGE_TYPE,COLUMN_NAME,IS_GRANTABLE FROM INFORMATION_SCHEMA.COLUMN_PRIVILEGES";
+	if ($self->{schema}) {
+		$str .= " WHERE TABLE_SCHEMA = '$self->{schema}'";
+	}
+	$str .= " " . $self->limit_to_objects('GRANT|TABLE|VIEW|FUNCTION|PROCEDURE|SEQUENCE', 'GRANTEE|TABLE_NAME|TABLE_NAME|TABLE_NAME|TABLE_NAME|TABLE_NAME');
+
+	$sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	while (my $row = $sth->fetch) {
+		$row->[0] =~ s/\@.*//;
+		$row->[0] =~ s/'//g;
+		$privs{$row->[1]}{owner} = '';
+		push(@{$privs{$row->[1]}{column}{$row->[3]}{$row->[0]}}, $row->[2]);
+		push(@{$roles{grantee}}, $row->[0]) if (!grep(/^$row->[0]$/, @{$roles{grantee}}));
+	}
+	$sth->finish();
+
+	return (\%privs, \%roles);
+}
+
+=head2 _get_database_size
+
+This function retrieves the size of the MSSQL database in MB
+
+=cut
+
+sub _get_database_size
+{
+	my $self = shift;
+
+	# Don't work with Azure
+	return if ($self->{db_version} =~ /Microsoft SQL Azure/);
+
+	my $mb_size = '';
+	my $condition = '';
+
+       my $sql = qq{SELECT
+   d.name,
+   m.size * 8 / 1024
+FROM sys.master_files m JOIN sys.databases d ON d.database_id = m.database_id and m.type = 0
+};
+        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[1]);
+		last;
+	}
+	$sth->finish();
+
+	return $mb_size;
+}
+
+=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;
+
+	my %table_size = ();
+
+        my $schema_clause = '';
+        $schema_clause = " AND s.name='$self->{schema}'" if ($self->{schema});
+	my $sql = qq{SELECT t.NAME AS TABLE_NAME, p.rows AS RowCounts, SUM(a.used_pages)  * 8 / 1024 AS UsedSpaceMB, CONVERT(DECIMAL,SUM(a.total_pages)) * 8 / 1024 AS TotalSpaceMB, s.Name AS TABLE_SCHEMA
+FROM sys.tables t
+INNER JOIN sys.indexes i ON t.OBJECT_ID = i.object_id
+INNER JOIN sys.partitions p ON i.object_id = p.OBJECT_ID AND i.index_id = p.index_id
+INNER JOIN sys.allocation_units a ON p.partition_id = a.container_id
+LEFT OUTER JOIN sys.schemas s ON t.schema_id = s.schema_id
+WHERE t.is_ms_shipped = 0 AND i.OBJECT_ID > 255 AND t.type='U' $schema_clause
+};
+
+	$sql .= $self->limit_to_objects('TABLE', 't.Name');
+	$sql .= " GROUP BY t.NAME ORDER BY TotalSpaceMB";
+	$sql .= " LIMIT $self->{top_max}" if ($self->{top_max});
+
+        my $sth = $self->{dbh}->prepare( $sql ) or return undef;
+        $sth->execute(@{$self->{query_bind_params}}) or return undef;
+	while ( my @row = $sth->fetchrow()) {
+		$table_size{$row[0]} = $row[1];
+	}
+	$sth->finish();
+
+	return %table_size;
+}
+
+sub _get_audit_queries
+{
+	my($self) = @_;
+
+	return if (!$self->{audit_user});
+
+	my @users = ();
+	push(@users, split(/[,;\s]/, lc($self->{audit_user})));
+
+	# Retrieve all object with tablespaces.
+	my $str = "SELECT argument FROM mysql.general_log WHERE command_type='Query' AND argument REGEXP '^(INSERT|UPDATE|DELETE|SELECT)'";
+	if (($#users >= 0) && !grep(/^all$/, @users)) {
+		$str .= " AND user_host REGEXP '(" . join("'|'", @users) . ")'";
+	}
+	my $error = "\n\nFATAL: You must be connected as an oracle dba user to retrieved audited queries\n\n";
+	my $sth = $self->{dbh}->prepare($str) or $self->logit($error . "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;
+}
+
+sub _get_synonyms
+{
+	my ($self) = shift;
+
+	# Retrieve all synonym
+	my $str = qq{SELECT
+	n.name AS SchemaName, 
+	sy.name AS synonym_name,
+	sy.base_object_name AS synonym_definition,
+	COALESCE(PARSENAME(sy.base_object_name, 4), \@\@servername) AS server_name,
+	COALESCE(PARSENAME(sy.base_object_name, 3), DB_NAME(DB_ID())) AS DB_name,
+	COALESCE(PARSENAME (sy.base_object_name, 2), SCHEMA_NAME(SCHEMA_ID ())) AS schema_name,
+	PARSENAME(sy.base_object_name, 1) AS table_name,
+	\@\@servername AS local_server
+FROM sys.synonyms sy
+LEFT OUTER JOIN sys.schemas n ON sy.schema_id = n.schema_id
+};
+	if ($self->{schema}) {
+		$str .= " WHERE n.name='$self->{schema}' ";
+	} else {
+		$str .= " WHERE n.name NOT IN ('" . join("','", @{$self->{sysusers}}) . "') ";
+	}
+	$str .= $self->limit_to_objects('SYNONYM','sy.name');
+
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %synonyms = ();
+	while (my $row = $sth->fetch)
+	{
+                if (!$self->{schema} && $self->{export_schema}) {
+                        $row->[1] = $row->[0] . '.' . $row->[1];
+                }
+		$synonyms{$row->[1]}{owner} = $row->[0];
+		$synonyms{$row->[1]}{table_owner} = $row->[5];
+		$synonyms{$row->[1]}{table_name} = $row->[6];
+		if ($row->[3] ne $row->[7]) {
+			$synonyms{$row->[1]}{dblink} = $row->[3];
+		}
+	}
+	$sth->finish;
+
+	return %synonyms;
+}
+
+sub _get_tablespaces
+{
+	my ($self) = shift;
+
+	return;
+}
+
+sub _list_tablespaces
+{
+	my ($self) = shift;
+
+	return;
+}
+
+sub _get_sequences
+{
+	my ($self) = shift;
+
+        my $str = qq{SELECT
+  s.name,
+  s.minimum_value AS minimum_value,
+  s.maximum_value AS maximum_value,
+  s.increment AS increment,
+  s.current_value AS current_value,
+  s.cache_size AS cache_size,
+  s.is_cycling AS cycling,
+  n.name,
+  s.is_cached AS cached
+FROM sys.sequences s
+LEFT OUTER JOIN sys.schemas n ON s.schema_id = n.schema_id
+};
+	
+        if (!$self->{schema}) {
+                $str .= " WHERE n.name NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
+        } else {
+                $str .= " WHERE n.name = '$self->{schema}'";
+        }
+        $str .= $self->limit_to_objects('SEQUENCE', 's.name');
+        #$str .= " ORDER BY SEQUENCE_NAME";
+
+
+        my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+        $sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+        my %seqs = ();
+        while (my $row = $sth->fetch)
+        {
+		$row->[5] = '' if ($row->[8]);
+                if (!$self->{schema} && $self->{export_schema}) {
+                        $row->[0] = $row->[7] . '.' . $row->[0];
+                }
+                push(@{$seqs{$row->[0]}}, @$row);
+        }
+
+        return \%seqs;
+}
+
+sub _extract_sequence_info
+{
+	my ($self) = shift;
+
+        my $str = qq{SELECT
+  s.name,
+  s.minimum_value AS minimum_value,
+  s.maximum_value AS maximum_value,
+  s.increment AS increment,
+  s.current_value AS current_value,
+  s.cache_size AS cache_size,
+  s.is_cycling AS cycling,
+  n.name,
+  s.is_cached AS cached
+FROM sys.sequences s
+LEFT OUTER JOIN sys.schemas n ON s.schema_id = n.schema_id
+};
+	
+        if (!$self->{schema}) {
+                $str .= " WHERE n.name NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
+        } else {
+                $str .= " WHERE n.name = '$self->{schema}'";
+        }
+        $str .= $self->limit_to_objects('SEQUENCE', 's.name');
+
+        my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+        $sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my @script = ();
+        while (my $row = $sth->fetch)
+        {
+		$row->[5] = '' if ($row->[8]);
+                if (!$self->{schema} && $self->{export_schema}) {
+                        $row->[0] = $row->[7] . '.' . $row->[0];
+                }
+		my $nextvalue = $row->[4] + $row->[3];
+		my $alter = "ALTER SEQUENCE $self->{pg_supports_ifexists} " .  $self->quote_object_name($row->[0]) . " RESTART WITH $nextvalue;";
+		push(@script, $alter);
+		$self->logit("Extracted sequence information for sequence \"$row->[0]\", nextvalue: $nextvalue\n", 1);
+	}
+	$sth->finish();
+
+	return @script;
+}
+
+# MSSQL does not have sequences but we count auto_increment as sequences
+sub _count_sequences
+{
+	my $self = shift;
+
+	# Table: information_schema.tables
+	# TABLE_CATALOG   | varchar(512)        | NO   |     |         |       |
+	# TABLE_SCHEMA    | varchar(64)         | NO   |     |         |       |
+	# TABLE_NAME      | varchar(64)         | NO   |     |         |       |
+	# TABLE_TYPE      | varchar(64)         | NO   |     |         |       |
+	# ENGINE          | varchar(64)         | YES  |     | NULL    |       |
+	# VERSION         | bigint(21) unsigned | YES  |     | NULL    |       |
+	# ROW_FORMAT      | varchar(10)         | YES  |     | NULL    |       |
+	# TABLE_ROWS      | bigint(21) unsigned | YES  |     | NULL    |       |
+	# AVG_ROW_LENGTH  | bigint(21) unsigned | YES  |     | NULL    |       |
+	# DATA_LENGTH     | bigint(21) unsigned | YES  |     | NULL    |       |
+	# MAX_DATA_LENGTH | bigint(21) unsigned | YES  |     | NULL    |       |
+	# INDEX_LENGTH    | bigint(21) unsigned | YES  |     | NULL    |       |
+	# DATA_FREE       | bigint(21) unsigned | YES  |     | NULL    |       |
+	# AUTO_INCREMENT  | bigint(21) unsigned | YES  |     | NULL    |       |
+	# CREATE_TIME     | datetime            | YES  |     | NULL    |       |
+	# UPDATE_TIME     | datetime            | YES  |     | NULL    |       |
+	# CHECK_TIME      | datetime            | YES  |     | NULL    |       |
+	# TABLE_COLLATION | varchar(32)         | YES  |     | NULL    |       |
+	# CHECKSUM        | bigint(21) unsigned | YES  |     | NULL    |       |
+	# CREATE_OPTIONS  | varchar(255)        | YES  |     | NULL    |       |
+	# TABLE_COMMENT   | varchar(2048)       | NO   |     |         |       |
+
+	my %seqs = ();
+	my $sql = "SELECT TABLE_NAME, AUTO_INCREMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' AND TABLE_SCHEMA = '$self->{schema}' AND AUTO_INCREMENT IS NOT NULL";
+	$sql .= $self->limit_to_objects('TABLE', 'TABLE_NAME');
+	my $sth = $self->{dbh}->prepare( $sql ) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	while (my $row = $sth->fetch) {
+		push(@{$seqs{$row->[0]}}, @$row);
+	}
+	$sth->finish();
+
+	return \%seqs;
+}
+
+sub _column_attributes
+{
+	my ($self, $table, $owner, $objtype) = @_;
+
+	
+	my $condition = '';
+	if ($self->{schema}) {
+		$condition .= "AND s.name='$self->{schema}' ";
+	}
+	$condition .= "AND tb.name='$table' " if ($table);
+	if (!$table) {
+		$condition .= $self->limit_to_objects('TABLE', 'tb.name');
+	} else {
+		@{$self->{query_bind_params}} = ();
+	}
+	$condition =~ s/^\s*AND\s/ WHERE /;
+
+	my $sql = qq{SELECT 
+    c.name 'Column Name',
+    c.is_nullable,
+    object_definition(c.default_object_id),
+    tb.name,
+    t.Name 'Data type',
+    c.column_id,
+    s.name
+FROM sys.columns c
+INNER JOIN sys.types t ON t.user_type_id = c.user_type_id
+INNER JOIN sys.tables AS tb ON tb.object_id = c.object_id
+INNER JOIN sys.schemas AS s ON s.schema_id = tb.schema_id
+$condition
+ORDER BY c.column_id};
+
+	my $sth = $self->{dbh}->prepare($sql);
+	if (!$sth) {
+		$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	}
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %data = ();
+	while (my $row = $sth->fetch)
+	{
+		next if ($self->{drop_rowversion} && ($row->[4] eq 'rowversion' || $row->[4] eq 'timestamp'));
+                if (!$self->{schema} && $self->{export_schema}) {
+                        $row->[3] = $row->[6] . '.' . $row->[3];
+                }
+		$data{$row->[3]}{$row->[0]}{nullable} = 'N';
+		if ($row->[1]) {
+			$data{$row->[3]}{$row->[0]}{nullable} = 'Y';
+		}
+		$row->[2] =~ s/[\[\]]+//g;
+		$data{$row->[3]}{$row->[0]}{default} = $row->[2];
+		# Store the data type of the column following its position
+		$data{$row->[3]}{data_type}{$row->[5]} = $row->[4];
+	}
+
+	return %data;
+}
+
+sub _list_triggers
+{
+        my ($self) = @_;
+
+	my $str = qq{SELECT 
+     o.name AS trigger_name 
+    ,OBJECT_NAME(o.parent_obj) AS table_name 
+    ,s.name AS table_schema 
+FROM sys.sysobjects o
+INNER JOIN sys.tables t ON o.parent_obj = t.object_id 
+INNER JOIN sys.schemas s ON t.schema_id = s.schema_id 
+WHERE o.type = 'TR'
+};
+
+	if ($self->{schema}) {
+		$str .= " AND s.name = '$self->{schema}'";
+	}
+	$str .= " " . $self->limit_to_objects('TABLE|VIEW|TRIGGER','t.name|t.name|o.name');
+
+	$str .= " ORDER BY t.name, o.name";
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %triggers = ();
+	while (my $row = $sth->fetch)
+	{
+		if (!$self->{schema} && $self->{export_schema}) {
+			$row->[1] = "$row->[2].$row->[1]";
+		}
+		push(@{$triggers{$row->[1]}}, $row->[0]);
+	}
+
+	return %triggers;
+}
+
+sub _global_temp_table_info
+{
+        my($self) = @_;
+
+	# Useless, SQL Server has automatic removal of the GTT
+	# when the session that create it is closed so it might
+	# not persist.
+
+	return;
+}
+
+sub _encrypted_columns
+{
+        my ($self, $table, $owner) = @_;
+
+	return;
+}
+
+sub _get_subpartitioned_table
+{
+        my($self) = @_;
+
+	return;
+}
+
+# Replace IF("user_status"=0,"username",NULL)
+# PostgreSQL (CASE WHEN "user_status"=0 THEN "username" ELSE NULL END)
+sub replace_if
+{
+	my $str = shift;
+
+	# First remove all IN (...) before processing
+	my %in_clauses = ();
+	my $j = 0;
+	while ($str =~ s/\b(IN\s*\([^\(\)]+\))/,\%INCLAUSE$j\%/is) {
+		$in_clauses{$j} = $1;
+		$j++;
+	}
+
+	while ($str =~ s/\bIF\s*\(((?:(?!\)\s*THEN|\s*SELECT\s+|\bIF\s*\().)*)$/\%IF\%$2/is || $str =~ s/\bIF\s*\(([^\(\)]+)\)(\s+AS\s+)/(\%IF\%)$2/is) {
+		my @if_params = ('');
+		my $stop_learning = 0;
+		my $idx = 1;
+		foreach my $c (split(//, $1)) {
+			$idx++ if (!$stop_learning && $c eq '(');
+			$idx-- if (!$stop_learning && $c eq ')');
+		
+			if ($idx == 0) {
+				# Do not copy last parenthesis in the output string
+				$c = '' if (!$stop_learning);
+				# Inform the loop that we don't want to process any charater anymore
+				$stop_learning = 1;
+				# We have reach the end of the if() parameter
+				# next character must be restored to the final string.
+				$str .= $c;
+			} elsif ($idx > 0) {
+				# We are parsing the if() parameter part, append
+				# the caracter to the right part of the param array.
+				if ($c eq ',' && ($idx - 1) == 0) {
+					# we are switching to a new parameter
+					push(@if_params, '');
+				} elsif ($c ne "\n") {
+					$if_params[-1] .= $c;
+				}
+			}
+		}
+		my $case_str = 'CASE ';
+		for (my $i = 1; $i <= $#if_params; $i+=2) {
+			$if_params[$i] =~ s/^\s+//gs;
+			$if_params[$i] =~ s/\s+$//gs;
+			if ($i < $#if_params) {
+				if ($if_params[$i] !~ /INCLAUSE/) {
+					$case_str .= "WHEN $if_params[0] THEN $if_params[$i] ELSE $if_params[$i+1] ";
+				} else {
+					$case_str .= "WHEN $if_params[0] $if_params[$i] THEN $if_params[$i+1] ";
+				}
+			} else {
+				$case_str .= " ELSE $if_params[$i] ";
+			}
+		}
+		$case_str .= 'END ';
+
+		$str =~ s/\%IF\%/$case_str/s;
+	}
+	$str =~ s/\%INCLAUSE(\d+)\%/$in_clauses{$1}/gs;
+	$str =~ s/\s*,\s*IN\s*\(/ IN \(/igs;
+
+	return $str;
+}
+
+sub _get_plsql_metadata
+{
+        my $self = shift;
+        my $owner = shift;
+
+        my $schema_clause = '';
+        $schema_clause = "WHERE s.name='$self->{schema}'" if ($self->{schema});
+
+	# Retrieve all functions
+	my $str = qq{SELECT
+   OBJECT_NAME(sm.object_id) AS object_name,
+   SCHEMA_NAME(o.schema_id),
+   o.type_desc,   
+   sm.definition,  
+   o.type,   
+   sm.uses_ansi_nulls,  
+   sm.uses_quoted_identifier,  
+   sm.is_schema_bound,  
+   sm.execute_as_principal_id  
+FROM sys.sql_modules AS sm  
+JOIN sys.objects AS o ON sm.object_id = o.object_id
+LEFT OUTER JOIN sys.schemas s ON o.schema_id = s.schema_id $schema_clause
+ORDER BY 1;};
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %functions = ();
+	my @fct_done = ();
+	push(@fct_done, @EXCLUDED_FUNCTION);
+	while (my $row = $sth->fetch)
+	{
+		next if (grep(/^$row->[0]$/i, @fct_done));
+		push(@fct_done, "$row->[0]");
+		$self->{function_metadata}{'unknown'}{'none'}{$row->[0]}{type} = $row->[2];
+		$self->{function_metadata}{'unknown'}{'none'}{$row->[0]}{text} = $row->[3];
+	}
+	$sth->finish();
+
+	# Look for functions/procedures
+	foreach my $name (sort keys %{$self->{function_metadata}{'unknown'}{'none'}})
+	{
+		# Retrieve metadata for this function after removing comments
+		$self->_remove_comments(\$self->{function_metadata}{'unknown'}{'none'}{$name}{text}, 1);
+		$self->{comment_values} = ();
+		$self->{function_metadata}{'unknown'}{'none'}{$name}{text} =~ s/\%ORA2PG_COMMENT\d+\%//gs;
+		my %fct_detail = $self->_lookup_function($self->{function_metadata}{'unknown'}{'none'}{$name}{text}, $name);
+		if (!exists $fct_detail{name}) {
+			delete $self->{function_metadata}{'unknown'}{'none'}{$name};
+			next;
+		}
+		delete $fct_detail{code};
+		delete $fct_detail{before};
+		%{$self->{function_metadata}{'unknown'}{'none'}{$name}{metadata}} = %fct_detail;
+		delete $self->{function_metadata}{'unknown'}{'none'}{$name}{text};
+	}
+}
+
+sub _get_security_definer
+{
+	my ($self, $type) = @_;
+
+	# Not supported by SQL Server
+	return;
+}
+
+=head2 _get_identities
+
+This function retrieve information about IDENTITY columns that must be
+exported as PostgreSQL serial.
+
+=cut
+
+sub _get_identities
+{
+	my ($self) = @_;
+
+	# Retrieve all indexes 
+	my $str = qq{SELECT
+	s.name As SchemaName,
+	t.name As TableName,
+	i.name as ColumnName,
+	i.seed_value,
+	i.increment_value,
+	i.last_value
+FROM sys.tables t
+JOIN sys.identity_columns i ON t.object_id=i.object_id
+LEFT OUTER JOIN sys.schemas s ON t.schema_id = s.schema_id
+};
+	if (!$self->{schema}) {
+		$str .= " WHERE s.name NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
+	} else {
+		$str .= " WHERE s.name = '$self->{schema}'";
+	}
+	$str .= $self->limit_to_objects('TABLE', 't.name');
+
+	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %seqs = ();
+	while (my $row = $sth->fetch)
+	{
+		if (!$self->{schema} && $self->{export_schema}) {
+			$row->[1] = "$row->[0].$row->[1]";
+		}
+		# GENERATION_TYPE can be ALWAYS, BY DEFAULT and BY DEFAULT ON NULL
+		$seqs{$row->[1]}{$row->[2]}{generation} = 'BY DEFAULT';
+		# SEQUENCE options
+		$row->[5] = $row->[3] || 1 if ($row->[5] eq '');
+		$seqs{$row->[1]}{$row->[2]}{options} = "START WITH $row->[5]";
+		$seqs{$row->[1]}{$row->[2]}{options} .= " INCREMENT BY $row->[4]";
+		$seqs{$row->[1]}{$row->[2]}{options} .= " MINVALUE $row->[3]" if ($row->[3] ne '');
+		# For default values don't use option at all
+		if ( $seqs{$row->[1]}{$row->[2]}{options} eq 'START WITH 1 INCREMENT BY 1 MINVALUE 1') {
+			delete $seqs{$row->[1]}{$row->[2]}{options};
+		}
+	}
+
+	return %seqs;
+}
+
+=head2 _get_materialized_views
+
+This function implements a mssql-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) = @_;
+
+	my $str = qq{select
+       v.name as view_name,
+       schema_name(v.schema_id) as schema_name,
+       i.name as index_name,
+       m.definition
+from sys.views v
+join sys.indexes i on i.object_id = v.object_id and i.index_id = 1 and i.ignore_dup_key = 0
+join sys.sql_modules m on m.object_id = v.object_id
+};
+
+	if (!$self->{schema}) {
+		$str .= " WHERE schema_name(v.schema_id) NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
+	} else {
+		$str .= " WHERE schema_name(v.schema_id) = '$self->{schema}'";
+	}
+	$str .= $self->limit_to_objects('MVIEW', 'v.name');
+	$str .= " ORDER BY schema_name, view_name";
+
+	my $sth = $self->{dbh}->prepare($str);
+	if (not defined $sth) {
+		$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	}
+	if (not $sth->execute(@{$self->{query_bind_params}})) {
+		$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+		return ();
+	}
+
+	my %data = ();
+	while (my $row = $sth->fetch)
+	{
+		if (!$self->{schema} && $self->{export_schema}) {
+			$row->[0] = "$row->[1].$row->[0]";
+		}
+		$row->[3] =~ s/
//g;
+		$row->[3] =~ s/[\[\]]//g;
+		$row->[3] =~ s/^CREATE VIEW [^\s]+//;
+		$data{$row->[0]}{text} = $row->[3];
+		$data{$row->[0]}{updatable} = 0;
+		$data{$row->[0]}{refresh_mode} = '';
+		$data{$row->[0]}{refresh_method} = '';
+		$data{$row->[0]}{no_index} = 0;
+		$data{$row->[0]}{rewritable} = 0;
+		$data{$row->[0]}{build_mode} = '';
+		$data{$row->[0]}{owner} = $row->[1];
+	}
+
+	return %data;
+}
+
+sub _get_materialized_view_names
+{
+	my($self) = @_;
+
+	my $str = qq{select
+       v.name as view_name,
+       schema_name(v.schema_id) as schema_name,
+       i.name as index_name,
+from sys.views v
+join sys.indexes i on i.object_id = v.object_id and i.index_id = 1 and i.ignore_dup_key = 0
+};
+	if (!$self->{schema}) {
+		$str .= " WHERE schema_name(v.schema_id) NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
+	} else {
+		$str .= " WHERE schema_name(v.schema_id) = '$self->{schema}'";
+	}
+	$str .= $self->limit_to_objects('MVIEW', 'v.name');
+	$str .= " ORDER BY schema_name, view_name";
+	my $sth = $self->{dbh}->prepare($str);
+	if (not defined $sth) {
+		$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	}
+	if (not $sth->execute(@{$self->{query_bind_params}})) {
+		$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+	}
+
+	my @data = ();
+	while (my $row = $sth->fetch)
+	{
+		if (!$self->{schema} && $self->{export_schema}) {
+			$row->[0] = "$row->[1].$row->[0]";
+		}
+		push(@data, uc($row->[0]));
+	}
+
+	return @data;
+}
+
+sub _get_package_function_list
+{
+	my ($self, $owner) = @_;
+
+	# not package in MSSQL
+	return;
+}
+
+sub _get_types
+{
+	my ($self, $name) = @_;
+
+	# Retrieve all user defined types => PostgreSQL DOMAIN
+	my $idx = 1;
+	my $str = qq{SELECT
+	t1.name, s.name, t2.name, t1.precision, t1.scale, t1.max_length, t1.is_nullable,
+	object_definition(t1.default_object_id), object_definition(t1.rule_object_id), t1.is_table_type
+FROM sys.types t1
+JOIN sys.types t2 ON t2.system_type_id = t1.system_type_id AND t2.is_user_defined = 0
+LEFT OUTER JOIN sys.schemas s ON t1.schema_id = s.schema_id
+WHERE t1.is_user_defined = 1 AND t2.name <> 'sysname'};
+
+	if ($name) {
+		$str .= " AND t1.name='$name'";
+	}
+	if ($self->{schema}) {
+		$str .= "AND s.name='$self->{schema}' ";
+	}
+	if (!$name) {
+		$str .= $self->limit_to_objects('TYPE', 't1.name');
+	} else {
+		@{$self->{query_bind_params}} = ();
+	}
+	$str .= " ORDER BY t1.name";
+	# use a separeate connection
+	my $local_dbh = _db_connection($self);
+
+	my $sth = $local_dbh->prepare($str) or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);
+
+	my @types = ();
+	my @fct_done = ();
+	while (my $row = $sth->fetch)
+	{
+		my $origname = $row->[0];
+		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 = ();
+		if (!$row->[9])
+		{
+			my $precision = '';
+			if ($row->[3]) {
+				$precision .= "($row->[3]";
+				$precision .= ",$row->[4]" if ($row->[4]);
+			} elsif ($row->[5]) {
+				$precision .= "($row->[5]";
+			}
+			$precision .= ")" if ($precision);
+			my $notnull = '';
+			$notnull = ' NOT NULL' if (!$row->[6]);
+			my $default = '';
+			if ($row->[7]) {
+				$row->[7] =~ s/\s*CREATE\s+DEFAULT\s+.*\s+AS\s*//is;
+				$default = " DEFAULT $row->[7]";
+			}
+			my $rule = '';
+			if ($row->[8]) {
+				$row->[8] =~ s/\s*CREATE\s+RULE\s+.*\s+AS\s*//is;
+				$row->[8] =~ s/\@[a-z0-1_\$\#]+/VALUE/igs;
+				$rule = " CHECK ($row->[8])";
+				$rule =~ s/[\r\n]+/ /gs;
+			}
+			$tmp{code} = "CREATE TYPE $row->[0] FROM $row->[2]$precision$notnull$default$rule;";
+			# Add domain type to main type convertion hash
+			if ($self->{is_mssql} && !exists $self->{data_type}{uc($origname)}
+					&& ($self->{type} eq 'COPY' || $self->{type} eq 'INSERT')
+			)
+			{
+				$self->{data_type}{uc($origname)} = replace_sql_type($self, $row->[2]);
+			}
+		}
+		$tmp{name} = $row->[0];
+		$tmp{owner} = $row->[1];
+		$tmp{pos} = $idx++;
+		if (!$self->{preserve_case})
+		{
+			$tmp{code} =~ s/(TYPE\s+)"[^"]+"\."[^"]+"/$1\L$row->[0]\E/igs;
+			$tmp{code} =~ s/(TYPE\s+)"[^"]+"/$1\L$row->[0]\E/igs;
+		}
+		else
+		{
+			$tmp{code} =~ s/((?:CREATE|REPLACE|ALTER)\s+TYPE\s+)([^"\s]+)\s/$1"$2" /igs;
+		}
+		$tmp{code} =~ s/\s+ALTER/;\nALTER/igs;
+		push(@types, \%tmp);
+	}
+	$sth->finish();
+
+	# Retrieve all user defined table types => PostgreSQL TYPE
+	$str = qq{SELECT
+	t1.name AS table_Type, s.name SchemaName, c.name AS ColName, c.column_id, y.name AS DataType,
+	c.precision, c.scale, c.max_length, c.is_nullable, object_definition(t1.default_object_id),
+	object_definition(t1.rule_object_id)
+FROM sys.table_types t1
+INNER JOIN sys.columns c ON c.object_id = t1.type_table_object_id
+INNER JOIN sys.types y ON y.user_type_id = c.user_type_id
+LEFT OUTER JOIN sys.schemas s ON t1.schema_id = s.schema_id
+};
+	if ($name) {
+		$str .= " AND t1.name='$name'";
+	}
+	if ($self->{schema}) {
+		$str .= "AND s.name='$self->{schema}' ";
+	}
+	if (!$name) {
+		$str .= $self->limit_to_objects('TYPE', 't1.name');
+	} else {
+		@{$self->{query_bind_params}} = ();
+	}
+	$str =~ s/ AND / WHERE /s;
+	$str .= " ORDER BY t1.name, c.column_id";
+
+	$sth = $local_dbh->prepare($str) or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $local_dbh->errstr . "\n", 0, 1);
+
+	my $current_type = '';
+	my $old_type = '';
+	while (my $row = $sth->fetch)
+	{
+		next if ($self->{drop_rowversion} && ($row->[4] eq 'rowversion' || $row->[4] eq 'timestamp'));
+
+		if (!$self->{schema} && $self->{export_schema}) {
+			$row->[0] = "$row->[1].$row->[0]";
+		}
+		if ($old_type ne $row->[0]) {
+			$self->logit("\tFound Type: $row->[0]\n", 1);
+
+			if ($current_type ne '') {
+				$current_type =~ s/,$//s;
+				$current_type .= ");";
+
+				my %tmp = (
+					code => $current_type,
+					name => $old_type,
+					owner => '',
+					pos => $idx++
+				);
+				if (!$self->{preserve_case})
+				{
+					$tmp{code} =~ s/(TYPE\s+)"[^"]+"\."[^"]+"/$1\L$old_type\E/igs;
+					$tmp{code} =~ s/(TYPE\s+)"[^"]+"/$1\L$old_type\E/igs;
+				}
+				else
+				{
+					$tmp{code} =~ s/((?:CREATE|REPLACE|ALTER)\s+TYPE\s+)([^"\s]+)\s/$1"$2" /igs;
+				}
+				$tmp{code} =~ s/\s+ALTER/;\nALTER/igs;
+				push(@types, \%tmp);
+				$current_type = '';
+			}
+			$old_type = $row->[0];
+		}
+		if ($current_type eq '') {
+			$current_type = "CREATE TYPE $row->[0] AS OBJECT ("
+		}
+		
+		my $precision = '';
+		if ($row->[5]) {
+			$precision .= "($row->[5]";
+			$precision .= ",$row->[6]" if ($row->[6]);
+		} elsif ($row->[7]) {
+			$precision .= "($row->[7]";
+		}
+		$precision .= ")" if ($precision);
+		my $notnull = '';
+		$notnull = 'NOT NULL' if (!$row->[8]);
+		my $default = '';
+		if ($row->[9]) {
+			$row->[9] =~ s/\s*CREATE\s+DEFAULT\s+.*\s+AS\s*//is;
+			$default = " DEFAULT $row->[9]";
+		}
+		my $rule = '';
+		if ($row->[10]) {
+			$row->[10] =~ s/\s*CREATE\s+RULE\s+.*\s+AS\s*//is;
+			$row->[10] =~ s/\@[a-z0-1_\$\#]+/VALUE/igs;
+			$rule = " CHECK ($row->[10])";
+		}
+		$current_type .= "\n\t$row->[2] $row->[4]$precision $notnull$default$rule,"
+	}
+	$sth->finish();
+
+	$local_dbh->disconnect() if ($local_dbh);
+
+	# Process last table type
+	if ($current_type ne '')
+	{
+		$current_type =~ s/,$//s;
+		$current_type .= ");";
+
+		my %tmp = (
+			code => $current_type,
+			name => $old_type,
+			owner => '',
+			pos => $idx++
+		);
+		if (!$self->{preserve_case})
+		{
+			$tmp{code} =~ s/(TYPE\s+)"[^"]+"\."[^"]+"/$1\L$old_type\E/igs;
+			$tmp{code} =~ s/(TYPE\s+)"[^"]+"/$1\L$old_type\E/igs;
+		}
+		else
+		{
+			$tmp{code} =~ s/((?:CREATE|REPLACE|ALTER)\s+TYPE\s+)([^"\s]+)\s/$1"$2" /igs;
+		}
+		$tmp{code} =~ s/\s+ALTER/;\nALTER/igs;
+		push(@types, \%tmp);
+	}
+
+	return \@types;
+}
+
+sub _col_count
+{
+	my ($self, $table, $schema) = @_;
+
+	my $condition = '';
+	if ($schema) {
+		$condition .= "AND s.name='$self->{schema}' ";
+	}
+	$condition .= "AND t.name='$table' " if ($table);
+	if (!$table) {
+		$condition .= $self->limit_to_objects('TABLE', 't.name');
+	} else {
+		@{$self->{query_bind_params}} = ();
+	}
+	if ($self->{drop_rowversion}) {
+		$condition .= "AND typ.name NOT IN ( 'rowversion', 'timestamp')";
+	}
+	$condition =~ s/^\s*AND\s/ WHERE /;
+
+	my $sql = qq{SELECT 
+    s.name,
+    t.name,
+    count(*)
+FROM sys.columns c
+INNER JOIN sys.tables AS t ON t.object_id = c.object_id
+INNER JOIN sys.schemas AS s ON s.schema_id = t.schema_id
+INNER JOIN sys.types AS typ ON c.user_type_id = typ.user_type_id
+$condition
+GROUP BY s.name, t.name};
+
+	my $sth = $self->{dbh}->prepare($sql) || $self->logit("FATAL: _col_count() " . $self->{dbh}->errstr . "\n", 0, 1);
+	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: _column_attributes() " . $self->{dbh}->errstr . "\n", 0, 1);
+
+	my %data = ();
+	while (my $row = $sth->fetch)
+	{
+		if ($self->{export_schema} && !$self->{schema}) {
+			$data{"$row->[0].$row->[1]"} = $row->[2];
+		} else {
+			$data{$row->[1]} = $row->[2];
+		}
+	}
+
+	return %data;
+}
+
+1;
+
diff --git a/lib/Ora2Pg/MySQL.pm b/lib/Ora2Pg/MySQL.pm
index dada4c2..f25be66 100644
--- a/lib/Ora2Pg/MySQL.pm
+++ b/lib/Ora2Pg/MySQL.pm
@@ -10,7 +10,7 @@ use POSIX qw(locale_h);
 setlocale(LC_NUMERIC,"C");
 
 
-$VERSION = '23.2';
+$VERSION = '24.0';
 
 # Some function might be excluded from export and assessment.
 our @EXCLUDED_FUNCTION = ('SQUIRREL_GET_ERROR_OFFSET');
@@ -338,7 +338,9 @@ sub _column_comments
 
 	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
 	my %data = ();
-	while (my $row = $sth->fetch) {
+	while (my $row = $sth->fetch)
+	{
+		next if (!$self->is_in_struct($row->[2], $row->[0]));
 		$data{$row->[2]}{$row->[0]} = $row->[1];
 	}
 	return %data;
@@ -346,7 +348,7 @@ sub _column_comments
 
 sub _column_info
 {
-	my ($self, $table, $owner, $objtype, $recurs) = @_;
+	my ($self, $table, $owner, $objtype, $recurs, @expanded_views) = @_;
 
 	$objtype ||= 'TABLE';
 
@@ -391,6 +393,9 @@ ORDER BY ORDINAL_POSITION};
 	if ($self->{db_version} < '5.5.0') {
 		$str =~ s/\bDATA_TYPE\b/DTD_IDENTIFIER/;
 	}
+	if ($self->{db_version} < '5.7.0') {
+		$str =~ s/, GENERATION_EXPRESSION//;
+	}
 	my $sth = $self->{dbh}->prepare($str);
 	if (!$sth) {
 		$self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
@@ -404,10 +409,14 @@ ORDER BY ORDINAL_POSITION};
 	my $pos = 0;
 	while (my $row = $sth->fetch)
 	{
+		next if (!$self->is_in_struct($row->[8], $row->[0]));
+		$row->[4] =~ s/^_[^']+\\'(.*)\\'/'$1'/; # fix collation on string
 		if ($row->[1] eq 'enum') {
 			$row->[1] = $row->[-2];
 		}
-		if ($row->[13] =~ /unsigned/) {
+		if ($row->[13] =~ s/(decimal.*)\s+unsigned//) {
+			$row->[1] = $1;
+		} elsif ($row->[13] =~ /unsigned/) {
 			$row->[1] .= ' unsigned';
 		}
 
@@ -505,6 +514,8 @@ sub _get_indexes
 		#Null : Contains YES if the column may contain NULL values and '' if not.
 		#Index_type : The index method used (BTREE, FULLTEXT, HASH, RTREE).
 		#Comment : Information about the index not described in its own column, such as disabled if the index is disabled. 
+			next if (!$self->is_in_struct($row->[0], $row->[4]));
+
 			my $idxname = $row->[2];
 			$row->[1] = 'UNIQUE' if (!$row->[1]);
 			$unique{$row->[0]}{$idxname} = $row->[1];
@@ -616,6 +627,8 @@ sub _foreign_key
 		if ($self->{schema} && (lc($r->[7]) ne lc($self->{schema}))) {
 			print STDERR "WARNING: Foreign key $r->[2].$r->[0] point to an other database: $r->[7].$r->[3].$r->[4], please fix it.\n";
 		}
+		next if (!$self->is_in_struct($r->[2], $r->[0]));
+		next if (!$self->is_in_struct($r->[3], $r->[4]));
 		push(@{$link{$r->[2]}{$key_name}{local}}, $r->[0]);
 		push(@{$link{$r->[2]}{$key_name}{remote}{$r->[3]}}, $r->[4]);
 		$r->[8] = 'SIMPLE'; # See pathetical documentation of mysql
@@ -666,8 +679,10 @@ sub _get_views
 
 	my %ordered_view = ();
 	my %data = ();
-	while (my $row = $sth->fetch) {
+	while (my $row = $sth->fetch)
+	{
 		$row->[1] =~ s/`$self->{schema}`\.//g;
+		$row->[1] =~ s/ AS `([^`]+\([^`]+)`//ig;
 		$row->[1] =~ s/`([^\s`,]+)`/$1/g;
 		$row->[1] =~ s/"/'/g;
 		$row->[1] =~ s/`/"/g;
@@ -830,7 +845,7 @@ sub _check_constraint
 	while (my $row = $sth->fetch)
 	{
 		# Pour chaque retour SHOW CREATE TABLE xxxx;
-		my $sql2 = "SHOW CREATE TABLE $row->[1];";
+		my $sql2 = "SHOW CREATE TABLE `$row->[1]`;";
 		my $sth2 = $self->{dbh}->prepare($sql2) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
 		$sth2->execute() or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
 		# Parsing de   CONSTRAINT `CHK_CONSTR` CHECK (((`Age` >= 18) and (`City` = _utf8mb4'Bangalore')))
@@ -925,7 +940,7 @@ sub _get_functions
 	{
 		my $kind = $row->[7]; # FUNCTION or PROCEDURE
 		next if ( ($kind ne $self->{type}) && ($self->{type} ne 'SHOW_REPORT') );
-		my $sth2 = $self->{dbh}->prepare("SHOW CREATE $kind $row->[0]") or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+		my $sth2 = $self->{dbh}->prepare("SHOW CREATE $kind `$row->[0]`") or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
 		$sth2->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
 		while (my $r = $sth2->fetch)
 		{
@@ -951,7 +966,8 @@ sub _lookup_function
 {
 	my ($self, $code, $fctname) = @_;
 
-	my $type = lc($self->{type}) . 's';
+	my $type = 'functions';
+	$type = lc($self->{type}) . 's' if ($self->{type} eq 'FUNCTION' or $self->{type} eq 'PROCEDURE');
 
 	# Replace all double quote with single quote
 	$code =~ s/"/'/g;
@@ -972,27 +988,29 @@ sub _lookup_function
 			$fct_detail{code} = "BEGIN\n    $1;\nEND\n";
 		}
 	}
-	return if (!$fct_detail{code});
 
 	# Remove any label that was before the main BEGIN block
 	$fct_detail{declare} =~ s/\s+[^\s\:]+:\s*$//gs;
 
         @{$fct_detail{param_types}} = ();
 
-        if ( ($fct_detail{declare} =~ s/(.*?)\b(FUNCTION|PROCEDURE)\s+([^\s\(]+)\s*(\(.*\))\s+RETURNS\s+(.*)//is) ||
-			($fct_detail{declare} =~ s/(.*?)\b(FUNCTION|PROCEDURE)\s+([^\s\(]+)\s*(\(.*\))//is) )
+        if ( ($fct_detail{declare} =~ s/(.*?)\b(FUNCTION|PROCEDURE)\s+([^\(]+)\s*(\(.*\))\s+RETURNS\s+(.*)//is) ||
+		($fct_detail{declare} =~ s/(.*?)\b(FUNCTION|PROCEDURE)\s+([^\(]+)\s*(\(.*\))\s*(.*)//is) )
 	{
                 $fct_detail{before} = $1;
                 $fct_detail{type} = uc($2);
                 $fct_detail{name} = $3;
                 $fct_detail{args} = $4;
 		my $tmp_returned = $5;
+		$type = lc($fct_detail{type} . 's');
 		chomp($tmp_returned);
 		if ($tmp_returned =~ s/\b(DECLARE\b.*)//is) {
 			$fct_detail{code} = $1 . $fct_detail{code};
 		}
 		if ($fct_detail{declare} =~ s/\s*COMMENT\s+(\?TEXTVALUE\d+\?|'[^\']+')//) {
 			$fct_detail{comment} = $1;
+		} elsif ($tmp_returned =~ s/\s*COMMENT\s+(\?TEXTVALUE\d+\?|'[^\']+')//) {
+			$fct_detail{comment} = $1;
 		}
 		$fct_detail{immutable} = 1 if ($fct_detail{declare} =~ s/\s*\bDETERMINISTIC\b//is);
 		$fct_detail{before} = ''; # There is only garbage for the moment
@@ -1018,6 +1036,11 @@ sub _lookup_function
 		$fct_detail{immutable} = 1 if ($self->{$type}{$fctname}{immutable} eq 'YES');
 		$fct_detail{security} = $self->{$type}{$fctname}{security};
 
+		if (!$fct_detail{code} && $tmp_returned) {
+			$tmp_returned .= ';' if ($tmp_returned !~ /;$/s);
+			$fct_detail{code} = "\n$tmp_returned\nEND;"
+		}
+
 		# Procedure that have out parameters are functions with PG
 		if ($type eq 'procedures' && $fct_detail{args} =~ /\b(OUT|INOUT)\b/) {
 			# set return type to empty to avoid returning void later
@@ -1033,10 +1056,10 @@ sub _lookup_function
 		}
 		# Now convert types
 		if ($fct_detail{args}) {
-			$fct_detail{args} = replace_sql_type($fct_detail{args}, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, %{ $self->{data_type} });
+			$fct_detail{args} = replace_sql_type($self, $fct_detail{args});
 		}
 		if ($fct_detail{declare}) {
-			$fct_detail{declare} = replace_sql_type($fct_detail{declare}, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, %{ $self->{data_type} });
+			$fct_detail{declare} = replace_sql_type($self, $fct_detail{declare});
 		}
 
 		$fct_detail{args} =~ s/\s+/ /gs;
@@ -1155,7 +1178,7 @@ sub replace_mysql_variables
 	return ($code, $declare);
 }
 
-sub _list_all_funtions
+sub _list_all_functions
 {
 	my $self = shift;
 
@@ -1191,6 +1214,7 @@ sub _sql_type
         my ($self, $type, $len, $precision, $scale, $default, $no_blob_to_oid) = @_;
 
 	my $data_type = '';
+	chomp($type);
 
 	# Simplify timestamp type
 	$type =~ s/TIMESTAMP\s*\(\s*\d+\s*\)/TIMESTAMP/i;
@@ -1236,10 +1260,14 @@ sub _sql_type
 			}
 		       	elsif ($type =~ /(TINYINT|SMALLINT|MEDIUMINT|INTEGER|BIGINT|INT|REAL|DOUBLE|FLOAT|DECIMAL|NUMERIC)/i)
 			{
+				if ($type =~ /(TINYINT|SMALLINT|MEDIUMINT|INTEGER|BIGINT|INT)/i) {
+					$scale = 0;
+					$precision = 0;
+				}
 				# This is an integer
 				if (!$scale)
 				{
-					if ($type =~ /UNSIGNED/&& $precision)
+					if ($type =~ /UNSIGNED/ && $precision)
 					{
 						# Replace MySQL type UNSIGNED in cast
 						$type =~ s/TINYINT UNSIGNED/smallint/igs;
@@ -1251,7 +1279,11 @@ sub _sql_type
 					}
 					elsif ($precision)
 					{
-						if ($self->{pg_integer_type})
+						if ($type =~ /(REAL|DOUBLE|FLOAT)/i)
+						{
+							return $self->{data_type}{$type};
+						}
+						elsif ($self->{pg_integer_type})
 						{
 							if ($precision < 5) {
 								return 'smallint';
@@ -1298,14 +1330,14 @@ sub _sql_type
 
 sub replace_sql_type
 {
-        my ($str, $pg_numeric_type, $default_numeric, $pg_integer_type, %data_type) = @_;
+        my ($self, $str) = @_;
 
 	$str =~ s/with local time zone/with time zone/igs;
 	$str =~ s/([A-Z])ORA2PG_COMMENT/$1 ORA2PG_COMMENT/igs;
 
 	# Remove any reference to UNSIGNED AND ZEROFILL
 	# but translate CAST( ... AS unsigned) before.
-	$str =~ s/(\s+AS\s+)UNSIGNED/$1$data_type{'UNSIGNED'}/gis;
+	$str =~ s/(\s+AS\s+)UNSIGNED/$1$self->{data_type}{'UNSIGNED'}/gis;
 	$str =~ s/\b(UNSIGNED|ZEROFILL)\b//gis;
 
 	# Remove BINARY from CHAR(n) BINARY and VARCHAR(n) BINARY
@@ -1314,7 +1346,7 @@ sub replace_sql_type
 
 	# Replace type with precision
 	my $mysqltype_regex = '';
-	foreach (keys %data_type) {
+	foreach (keys %{$self->{data_type}}) {
 		$mysqltype_regex .= quotemeta($_) . '|';
 	}
 	$mysqltype_regex =~ s/\|$//;
@@ -1330,8 +1362,8 @@ sub replace_sql_type
 			$str =~ s/\)/\%\|\%/s;
 			next;
 		}
-		if (exists $data_type{"$type($args)"}) {
-			$str =~ s/\b$type\($args\)/$data_type{"$type($args)"}/igs;
+		if (exists $self->{data_type}{"$type($args)"}) {
+			$str =~ s/\b$type\($args\)/$self->{data_type}{"$type($args)"}/igs;
 			next;
 		}
 		if ($backstr =~ /_$/) {
@@ -1347,11 +1379,11 @@ sub replace_sql_type
 			# Type CHAR have default length set to 1
 			# Type VARCHAR must have a specified length
 			$len = 1 if (!$len && ($type eq "CHAR"));
-			$str =~ s/\b$type\b\s*\([^\)]+\)/$data_type{$type}\%\|$len\%\|\%/is;
+			$str =~ s/\b$type\b\s*\([^\)]+\)/$self->{data_type}{$type}\%\|$len\%\|\%/is;
 		} elsif ($precision && ($type =~ /(BIT|TINYINT|SMALLINT|MEDIUMINT|INTEGER|BIGINT|INT|REAL|DOUBLE|FLOAT|DECIMAL|NUMERIC)/)) {
 			if (!$scale) {
 				if ($type =~ /(BIT|TINYINT|SMALLINT|MEDIUMINT|INTEGER|BIGINT|INT)/) {
-					if ($pg_integer_type) {
+					if ($self->{pg_integer_type}) {
 						if ($precision < 5) {
 							$str =~ s/\b$type\b\s*\([^\)]+\)/smallint/is;
 						} elsif ($precision <= 9) {
@@ -1363,13 +1395,13 @@ sub replace_sql_type
 						$str =~ s/\b$type\b\s*\([^\)]+\)/numeric\%\|$precision\%\|\%/i;
 					}
 				} else {
-					$str =~ s/\b$type\b\s*\([^\)]+\)/$data_type{$type}\%\|$precision\%\|\%/is;
+					$str =~ s/\b$type\b\s*\([^\)]+\)/$self->{data_type}{$type}\%\|$precision\%\|\%/is;
 				}
 			} else {
 				if ($type =~ /DOUBLE/) {
 					$str =~ s/\b$type\b\s*\([^\)]+\)/decimal\%\|$args\%\|\%/is;
 				} else {
-					$str =~ s/\b$type\b\s*\([^\)]+\)/$data_type{$type}\%\|$args\%\|\%/is;
+					$str =~ s/\b$type\b\s*\([^\)]+\)/$self->{data_type}{$type}\%\|$args\%\|\%/is;
 				}
 			}
 		} else {
@@ -1384,13 +1416,13 @@ sub replace_sql_type
 	# Replace datatype even without precision
 	my %recover_type = ();
 	my $i = 0;
-	foreach my $type (sort { length($b) <=> length($a) } keys %data_type)
+	foreach my $type (sort { length($b) <=> length($a) } keys %{$self->{data_type}})
 	{
 		# Keep enum as declared, we are not in table definition
 		next if (uc($type) eq 'ENUM');
 		while ($str =~ s/\b$type\b/%%RECOVER_TYPE$i%%/is)
 		{
-			$recover_type{$i} = $data_type{$type};
+			$recover_type{$i} = $self->{data_type}{$type};
 			$i++;
 		}
 	}
@@ -1494,7 +1526,32 @@ WHERE PARTITION_NAME IS NOT NULL
 		$row->[6] =~ s/\`//g;
 		$row->[3] =~ s/\`//g;
 
-		$row->[5] = 'HASH' if ($row->[5] =~ /KEY/);
+		# Partition by KEY can be converted to HASH partition
+		# but we need to gather the PK or UK definition
+		my @ucol = ();
+		if ($row->[5] =~ /KEY/)
+		{
+			$row->[5] = 'HASH';
+			if ($row->[6])
+			{
+				$row->[5] = 'HASH COLUMNS';
+			}
+			else
+			{
+				my $sql = "SHOW INDEX FROM `$row->[0]`";
+				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);
+				while (my $r = $sth2->fetch)
+				{
+					if ($r->[2] eq 'PRIMARY') {
+						push(@ucol, $r->[4]) if (!grep(/^$r->[4]$/, @ucol));
+					} elsif (!$r->[1]) {
+						push(@ucol, $r->[4]) if (!grep(/^$r->[4]$/, @ucol));
+					}
+				}
+				$sth2->finish;
+			}
+		}
 
 		if ($row->[5] =~ s/ COLUMNS//)
 		{
@@ -1505,6 +1562,12 @@ WHERE PARTITION_NAME IS NOT NULL
 				$i++;
 			}
 		}
+		elsif ($row->[5] eq 'HASH')
+		{
+			for (my $i = 0; $i <= $#ucol;$i++) {
+				push(@{$parts{$row->[0]}{$row->[1]}{info}}, { 'type' => $row->[5], 'value' => $row->[3], 'column' => $ucol[$i], 'colpos' => $i, 'tablespace' => $row->[4], 'owner' => '' });
+			}
+		}
 		else
 		{
 			@{$parts{$row->[0]}{$row->[1]}{info}} = ( { 'type' => $row->[5], 'value' => $row->[3], 'expression' => $row->[6], 'colpos' => 0, 'tablespace' => $row->[4], 'owner' => '' } );
@@ -1564,14 +1627,14 @@ WHERE SUBPARTITION_NAME IS NOT NULL AND SUBPARTITION_EXPRESSION IS NOT NULL
 	return \%subparts, \%default;
 }
 
-=head2 _get_partitions_type
+=head2 _get_partitions_list
 
 This function implements a MySQL-native partitions information.
 Return a hash of the partition table_name => type
 
 =cut
 
-sub _get_partitions_type
+sub _get_partitions_list
 {
 	my($self) = @_;
 
@@ -1644,50 +1707,60 @@ FROM INFORMATION_SCHEMA.PARTITIONS WHERE PARTITION_NAME IS NOT NULL
 		if ($row->[1] =~ /KEY/)
 		{
 			$parts{"\L$row->[0]\E"}{type} = 'HASH';
-			my $sql = "SHOW INDEX FROM `$row->[0]`";
-			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 @ucol = ();
-			while (my $r = $sth2->fetch)
+
+			if ($row->[6])
 			{
-				#Table : The name of the table.
-				#Non_unique : 0 if the index cannot contain duplicates, 1 if it can.
-				#Key_name : The name of the index. If the index is the primary key, the name is always PRIMARY.
-				#Seq_in_index : The column sequence number in the index, starting with 1.
-				#Column_name : The column name.
-				#Collation : How the column is sorted in the index. In MySQL, this can have values “A” (Ascending) or NULL (Not sorted).
-				#Cardinality : An estimate of the number of unique values in the index.
-				#Sub_part : The number of indexed characters if the column is only partly indexed, NULL if the entire column is indexed.
-				#Packed : Indicates how the key is packed. NULL if it is not.
-				#Null : Contains YES if the column may contain NULL values and '' if not.
-				#Index_type : The index method used (BTREE, FULLTEXT, HASH, RTREE).
-				#Comment : Information about the index not described in its own column, such as disabled if the index is disabled. 
-				if ($r->[2] eq 'PRIMARY') {
-					push(@{ $parts{"\L$row->[0]\E"}{columns} }, $r->[4]) if (!grep(/^$r->[4]$/, @{ $parts{"\L$row->[0]\E"}{columns} }));
-				} elsif (!$r->[1]) {
-					push(@ucol, $r->[4]) if (!grep(/^$r->[4]$/, @ucol));
-				}
+				$parts{"\L$row->[0]\E"}{type} = 'HASH COLUMNS';
 			}
-			$sth2->finish;
-			if ($#{ $parts{"\L$row->[0]\E"}{columns} } < 0) {
-				if ($#ucol >= 0) {
-					push(@{ $parts{"\L$row->[0]\E"}{columns} }, @ucol);
-				} else {
-					$row->[6] =~ s/[\(\)\s]//g;
-					@{ $parts{"\L$row->[0]\E"}{columns} } = split(',', $row->[6]);
+			else
+			{
+				my $sql = "SHOW INDEX FROM `$row->[0]`";
+				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 @ucol = ();
+				while (my $r = $sth2->fetch)
+				{
+					#Table : The name of the table.
+					#Non_unique : 0 if the index cannot contain duplicates, 1 if it can.
+					#Key_name : The name of the index. If the index is the primary key, the name is always PRIMARY.
+					#Seq_in_index : The column sequence number in the index, starting with 1.
+					#Column_name : The column name.
+					#Collation : How the column is sorted in the index. In MySQL, this can have values “A” (Ascending) or NULL (Not sorted).
+					#Cardinality : An estimate of the number of unique values in the index.
+					#Sub_part : The number of indexed characters if the column is only partly indexed, NULL if the entire column is indexed.
+					#Packed : Indicates how the key is packed. NULL if it is not.
+					#Null : Contains YES if the column may contain NULL values and '' if not.
+					#Index_type : The index method used (BTREE, FULLTEXT, HASH, RTREE).
+					#Comment : Information about the index not described in its own column, such as disabled if the index is disabled. 
+					if ($r->[2] eq 'PRIMARY') {
+						push(@{ $parts{"\L$row->[0]\E"}{columns} }, $r->[4]) if (!grep(/^$r->[4]$/, @{ $parts{"\L$row->[0]\E"}{columns} }));
+					} elsif (!$r->[1]) {
+						push(@ucol, $r->[4]) if (!grep(/^$r->[4]$/, @ucol));
+					}
+				}
+				$sth2->finish;
+				if ($#{ $parts{"\L$row->[0]\E"}{columns} } < 0) {
+					if ($#ucol >= 0) {
+						push(@{ $parts{"\L$row->[0]\E"}{columns} }, @ucol);
+					} else {
+						$row->[6] =~ s/[\(\)\s]//g;
+						@{ $parts{"\L$row->[0]\E"}{columns} } = split(',', $row->[6]);
+					}
 				}
 			}
 
 		}
+
 		if ($parts{"\L$row->[0]\E"}{type} =~ s/ COLUMNS//)
 		{
 			$row->[6] =~ s/[\(\)\s]//g;
 			@{ $parts{"\L$row->[0]\E"}{columns} } = split(',', $row->[6]);
 		}
-		else
+		elsif ($row->[6])
 		{
 			$parts{"\L$row->[0]\E"}{expression} = $row->[6];
 		}
+
 	}
 	$sth->finish;
 
@@ -2084,6 +2157,7 @@ ORDER BY ORDINAL_POSITION
 	my %data = ();
 	while (my $row = $sth->fetch)
 	{
+		$row->[2] =~ s/^_[^']+\\'(.*)\\'/'$1'/; # fix collation on string
 		$data{$row->[3]}{"$row->[0]"}{nullable} = $row->[1];
 		$data{$row->[3]}{"$row->[0]"}{default} = $row->[2];
 		# Store the data type of the column following its position
@@ -2156,36 +2230,43 @@ FROM INFORMATION_SCHEMA.PARTITIONS WHERE SUBPARTITION_NAME IS NOT NULL
 		$parts{"\L$row->[0]\E"}{"\L$row->[3]\E"}{type} = $row->[1];
 		$row->[7] =~ s/\`//g;
 
-		if ($parts{"\L$row->[0]\E"}{type} =~ /KEY/)
+		if ($parts{"\L$row->[0]\E"}{"\L$row->[3]\E"}{type} =~ /KEY/)
 		{
-			$parts{"\L$row->[0]\E"}{type} = 'HASH';
-			my $sql = "SHOW INDEX FROM `$row->[0]`";
-			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 @ucol = ();
-			while (my $r = $sth2->fetch)
+			$parts{"\L$row->[0]\E"}{"\L$row->[3]\E"}{type} = 'HASH';
+			if ($row->[7])
 			{
-				#Table : The name of the table.
-				#Non_unique : 0 if the index cannot contain duplicates, 1 if it can.
-				#Key_name : The name of the index. If the index is the primary key, the name is always PRIMARY.
-				#Seq_in_index : The column sequence number in the index, starting with 1.
-				#Column_name : The column name.
-				#Collation : How the column is sorted in the index. In MySQL, this can have values “A” (Ascending) or NULL (Not sorted).
-				#Cardinality : An estimate of the number of unique values in the index.
-				#Sub_part : The number of indexed characters if the column is only partly indexed, NULL if the entire column is indexed.
-				#Packed : Indicates how the key is packed. NULL if it is not.
-				#Null : Contains YES if the column may contain NULL values and '' if not.
-				#Index_type : The index method used (BTREE, FULLTEXT, HASH, RTREE).
-				#Comment : Information about the index not described in its own column, such as disabled if the index is disabled. 
-				if ($r->[2] eq 'PRIMARY') {
-					push(@{ $parts{"\L$row->[0]\E"}{columns} }, $r->[4]) if (!grep(/^$r->[4]$/, @{ $parts{"\L$row->[0]\E"}{columns} }));
-				} elsif (!$row->[1]) {
-					push(@ucol, $r->[4]) if (!grep(/^$r->[4]$/, @ucol));
-				}
+				$parts{"\L$row->[0]\E"}{"\L$row->[3]\E"}{type} = 'HASH COLUMNS';
 			}
-			$sth2->finish;
-			if ($#{ $parts{"\L$row->[0]\E"}{columns} } < 0) {
-				push(@{ $parts{"\L$row->[0]\E"}{columns} }, @ucol);
+			else
+			{
+				my $sql = "SHOW INDEX FROM `$row->[0]`";
+				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 @ucol = ();
+				while (my $r = $sth2->fetch)
+				{
+					#Table : The name of the table.
+					#Non_unique : 0 if the index cannot contain duplicates, 1 if it can.
+					#Key_name : The name of the index. If the index is the primary key, the name is always PRIMARY.
+					#Seq_in_index : The column sequence number in the index, starting with 1.
+					#Column_name : The column name.
+					#Collation : How the column is sorted in the index. In MySQL, this can have values “A” (Ascending) or NULL (Not sorted).
+					#Cardinality : An estimate of the number of unique values in the index.
+					#Sub_part : The number of indexed characters if the column is only partly indexed, NULL if the entire column is indexed.
+					#Packed : Indicates how the key is packed. NULL if it is not.
+					#Null : Contains YES if the column may contain NULL values and '' if not.
+					#Index_type : The index method used (BTREE, FULLTEXT, HASH, RTREE).
+					#Comment : Information about the index not described in its own column, such as disabled if the index is disabled. 
+					if ($r->[2] eq 'PRIMARY') {
+						push(@{ $parts{"\L$row->[0]\E"}{"\L$row->[3]\E"}{columns} }, $r->[4]) if (!grep(/^$r->[4]$/, @{ $parts{"\L$row->[0]\E"}{columns} }));
+					} elsif (!$row->[1]) {
+						push(@ucol, $r->[4]) if (!grep(/^$r->[4]$/, @ucol));
+					}
+				}
+				$sth2->finish;
+				if ($#{ $parts{"\L$row->[0]\E"}{columns} } < 0) {
+					push(@{ $parts{"\L$row->[0]\E"}{"\L$row->[3]\E"}{columns} }, @ucol);
+				}
 			}
 		}
 
@@ -2194,7 +2275,7 @@ FROM INFORMATION_SCHEMA.PARTITIONS WHERE SUBPARTITION_NAME IS NOT NULL
 			$row->[7] =~ s/[\(\)\s]//g;
 			@{ $parts{"\L$row->[0]\E"}{"\L$row->[3]\E"}{columns} } = split(',', $row->[7]);
 		}
-		else
+		elsif ($row->[7])
 		{
 			$parts{"\L$row->[0]\E"}{"\L$row->[3]\E"}{expression} = $row->[7];
 		}
@@ -2291,7 +2372,7 @@ sub _get_plsql_metadata
 		push(@fct_done, "$row->[0]");
 		$self->{function_metadata}{'unknown'}{'none'}{$row->[0]}{type} = $row->[2];
 		$self->{function_metadata}{'unknown'}{'none'}{$row->[0]}{text} = $row->[3];
-		my $sth2 = $self->{dbh}->prepare("SHOW CREATE $row->[2] $row->[0]") or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
+		my $sth2 = $self->{dbh}->prepare("SHOW CREATE $row->[2] `$row->[0]`") or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
 		$sth2->execute or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
 		while (my $r = $sth2->fetch) {
 			$self->{function_metadata}{'unknown'}{'none'}{$row->[0]}{text} = $r->[2];
@@ -2302,7 +2383,8 @@ sub _get_plsql_metadata
 	$sth->finish();
 
 	# Look for functions/procedures
-	foreach my $name (sort keys %{$self->{function_metadata}{'unknown'}{'none'}}) {
+	foreach my $name (sort keys %{$self->{function_metadata}{'unknown'}{'none'}})
+	{
 		# Retrieve metadata for this function after removing comments
 		$self->_remove_comments(\$self->{function_metadata}{'unknown'}{'none'}{$name}{text}, 1);
 		$self->{comment_values} = ();
@@ -2414,8 +2496,22 @@ sub _col_count
 {
         my ($self, $name) = @_;
 
-	# Not supported
-	return;
+	my $sql = qq{SELECT TABLE_SCHEMA, TABLE_NAME, count(*)
+FROM INFORMATION_SCHEMA.COLUMNS
+GROUP BY TABLE_SCHEMA, TABLE_NAME
+};
+	my $sth = $self->{dbh}->prepare($sql);
+	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) {
+		$data{$row->[1]} = $row->[2];
+	}
+
+	return %data;
 }
 
 1;
diff --git a/lib/Ora2Pg/Oracle.pm b/lib/Ora2Pg/Oracle.pm
index 594f311..66f92d6 100644
--- a/lib/Ora2Pg/Oracle.pm
+++ b/lib/Ora2Pg/Oracle.pm
@@ -11,7 +11,7 @@ use Encode;
 #set locale to LC_NUMERIC C
 setlocale(LC_NUMERIC,"C");
 
-$VERSION = '23.2';
+$VERSION = '24.0';
 
 # Some function might be excluded from export and assessment.
 our @EXCLUDED_FUNCTION = ('SQUIRREL_GET_ERROR_OFFSET');
@@ -38,7 +38,7 @@ our %SQL_TYPE = (
 	'STRING' => 'varchar',
 	# The DATE data type is used to store the date and time
 	# information. PG type timestamp should match all needs.
-	'DATE' => 'timestamp',
+	'DATE' => 'timestamp(0)',
 	# 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
@@ -81,6 +81,7 @@ our %SQL_TYPE = (
 	'TIMESTAMP WITH TIME ZONE' => 'timestamp with time zone',
 	'TIMESTAMP WITH LOCAL TIME ZONE' => 'timestamp with time zone',
 	'SDO_GEOMETRY' => 'geometry',
+	'ST_GEOMETRY' => 'geometry',
 );
 
 our %GTYPE = (
@@ -133,7 +134,9 @@ sub _db_connection
 	$self->logit("NLS_NCHAR = $ENV{NLS_NCHAR}\n", 1);
 	$self->logit("Trying to connect to database: $self->{oracle_dsn}\n", 1) if (!$self->{quiet});
 
-	my $dbh = DBI->connect($self->{oracle_dsn}, $self->{oracle_user}, $self->{oracle_pwd},
+	my $dbh = DBI->connect($self->{oracle_dsn}, 
+		( $self->{oracle_user} eq "__SEPS__" ? "" : $self->{oracle_user} ),
+		( $self->{oracle_pwd}  eq "__SEPS__" ? "" : $self->{oracle_pwd}  ),
 		{
 			ora_envhp => 0,
 			LongReadLen=>$self->{longreadlen},
@@ -508,7 +511,7 @@ sub _column_comments
 
 sub _column_info
 {
-	my ($self, $table, $owner, $objtype, $recurs) = @_;
+	my ($self, $table, $owner, $objtype, $recurs, @expanded_views) = @_;
 
 	$objtype ||= 'TABLE';
 
@@ -534,7 +537,7 @@ sub _column_info
 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
-WHERE 1=1 $condition
+WHERE A.TABLE_NAME NOT LIKE 'BIN\$%' $condition
 ORDER BY A.COLUMN_ID
 };
 		$sth = $self->{dbh}->prepare($sql);
@@ -545,7 +548,7 @@ ORDER BY A.COLUMN_ID
 			{
 				$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);
+				return $self->_column_info($table, $owner, $objtype, 1, @expanded_views);
 			}
 			$self->logit("FATAL: _column_info() " . $self->{dbh}->errstr . "\n", 0, 1);
 		}
@@ -557,7 +560,7 @@ ORDER BY A.COLUMN_ID
 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
-    $condition
+WHERE A.TABLE_NAME NOT LIKE 'BIN\$%' $condition
 ORDER BY A.COLUMN_ID
 };
 		$sth = $self->{dbh}->prepare($sql);
@@ -568,7 +571,7 @@ ORDER BY A.COLUMN_ID
 			{
 				$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);
+				return $self->_column_info($table, $owner, $objtype, 1, @expanded_views);
 			}
 			$self->logit("FATAL: _column_info() " . $self->{dbh}->errstr . "\n", 0, 1);
 		}
@@ -581,10 +584,10 @@ ORDER BY A.COLUMN_ID
 	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;
-	my $st_spatial_gtype =  'SELECT DISTINCT ST_GeometryType(c.%s) FROM %s c WHERE ROWNUM < ' . $max_lines;
+	my $st_spatial_gtype =  "SELECT DISTINCT $self->{st_geometrytype_function}(c.\%s) 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=?";
-	my $st_spatial_srid = "SELECT ST_SRID(c.%s) FROM %s c";
+	my $st_spatial_srid = "SELECT $self->{st_srid_function}(c.\%s) FROM \%s c";
 	if ($self->{convert_srid})
 	{
 		# Translate SRID to standard EPSG SRID, may return 0 because there's lot of Oracle only SRID.
@@ -592,7 +595,7 @@ ORDER BY A.COLUMN_ID
 	}
 	# 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 $st_spatial_dim = "SELECT ST_DIMENSION(c.%s) FROM %s c";
+	my $st_spatial_dim = "SELECT $self->{st_dimension_function}(c.\%s) FROM \%s c";
 
 	my $is_virtual_col = "SELECT V.VIRTUAL_COLUMN FROM $self->{prefix}_TAB_COLS V WHERE V.OWNER=? AND V.TABLE_NAME=? AND V.COLUMN_NAME=?";
 	my $sth3 = undef;
@@ -606,14 +609,14 @@ ORDER BY A.COLUMN_ID
 	my $ncols = 0;
 	while (my $row = $sth->fetch)
 	{
-		my $tmptable = "$row->[9].$row->[8]";
+		my $tmptable = $row->[8];
+		$tmptable = "$row->[9].$row->[8]" if (!$self->{schema} && $self->{export_schema});
+
 		# Skip object if it is not in the object list and if this is not
 		# a view or materialized view that must be exported as table.
-		next if (!exists $self->{all_objects}{$tmptable}
-				|| ($self->{all_objects}{$tmptable} eq 'VIEW'
-					&& !grep(/^$row->[8]$/i, @{$self->{view_as_table}}))
-				|| ($self->{all_objects}{$tmptable} eq 'MATERIALIZED VIEW'
-					&& !grep(/^$row->[8]$/i, @{$self->{mview_as_table}}))
+		next if (!exists $self->{all_objects}{"$row->[9].$row->[8]"}
+				|| ($self->{all_objects}{"$row->[9].$row->[8]"} =~ /^(VIEW|MATERIALIZED VIEW)$/)
+					&& !grep(/^$row->[8]$/i, @expanded_views)
 			);
 
 		$row->[2] = $row->[7] if $row->[1] =~ /char/i;
@@ -623,9 +626,6 @@ ORDER BY A.COLUMN_ID
 			$row->[2] = 38;
 		}
 
-		# Use FQDN table name otherwise a table not exist error can occurs.
-		$tmptable = "$row->[9].$row->[8]";
-
 		# In case we have a default value, check if this is a virtual column
 		my $virtual = 'NO';
 		if ($self->{pg_supports_virtualcol} and defined $sth3 and $row->[4])
@@ -647,7 +647,7 @@ ORDER BY A.COLUMN_ID
 			{
 				my @result = ();
 				if ($row->[1] =~ /^ST_|STGEOM_/) {
-					$spatial_srid = sprintf($st_spatial_srid, $row->[0], $tmptable);
+					$spatial_srid = sprintf($st_spatial_srid, $row->[0], "$row->[9].$row->[8]");
 				}
 				my $sth2 = $self->{dbh}->prepare($spatial_srid);
 				if (!$sth2)
@@ -706,7 +706,7 @@ ORDER BY A.COLUMN_ID
 			if (!$found_dims)
 			{
 				if ($row->[1] =~ /^ST_|STGEOM_/) {
-					$spatial_dim = sprintf($st_spatial_dim, $row->[0], $tmptable);
+					$spatial_dim = sprintf($st_spatial_dim, $row->[0], "$row->[9].$row->[8]");
 				}
 				my $sth2 = $self->{dbh}->prepare($spatial_dim);
 				if (!$sth2) {
@@ -733,9 +733,9 @@ ORDER BY A.COLUMN_ID
 			if (!$found_contraint && $self->{autodetect_spatial_type})
 			{
 				# Get spatial information
-				my $squery = sprintf($spatial_gtype, $row->[0], $tmptable);
+				my $squery = sprintf($spatial_gtype, $row->[0], "$row->[9].$row->[8]");
 				if ($row->[1] =~ /^ST_|STGEOM_/) {
-					$squery = sprintf($st_spatial_gtype, $row->[0], $tmptable);
+					$squery = sprintf($st_spatial_gtype, $row->[0], "$row->[9].$row->[8]");
 				}
 				my $sth2 = $self->{dbh}->prepare($squery);
 				if (!$sth2) {
@@ -1269,7 +1269,12 @@ sub _get_triggers
 	my($self) = @_;
 
 	# 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'";
+	my $str = "SELECT TRIGGER_NAME, TRIGGER_TYPE, TRIGGERING_EVENT, TABLE_NAME, TRIGGER_BODY, WHEN_CLAUSE, DESCRIPTION, ACTION_TYPE, OWNER FROM $self->{prefix}_TRIGGERS WHERE 1=1";
+	if (!$self->{export_invalid}) {
+		$str .= " AND STATUS='ENABLED'";
+	} elsif ($self->{export_invalid} == 2) {
+		$str .= " AND STATUS <> 'ENABLED'";
+	}
 	if (!$self->{schema}) {
 		$str .= " AND OWNER NOT IN ('" . join("','", @{$self->{sysusers}}) . "')";
 	} else {
@@ -1538,7 +1543,7 @@ sub _get_functions
 
 sub _lookup_function
 {
-	my ($self, $plsql, $pname) = @_;
+	my ($self, $plsql, $pname, $meta) = @_;
 
 	my %fct_detail = ();
 
@@ -1599,6 +1604,7 @@ sub _lookup_function
 			$fct_detail{hasreturn} = 1;
 			my $ret_typ = $2 || '';
 			$ret_typ =~ s/(\%ORA2PG_COMMENT\d+\%)+//i;
+			$fct_detail{orig_func_ret_type} = $ret_typ;
 			$fct_detail{func_ret_type} = $self->_sql_type($ret_typ) || 'OPAQUE';
 		}
 		if ($fct_detail{declare} =~ s/(.*?)(USING|AS|IS)(\s+(?!REF\s+))/$3/is)
@@ -1619,9 +1625,65 @@ sub _lookup_function
 				$fct_detail{library_fct} = $1;
 			}
 		}
+		####
 		# rewrite argument syntax
+		####
 		# Replace alternate syntax for default value
 		$fct_detail{args} =~ s/:=/DEFAULT/igs;
+		my $json_arg = $fct_detail{args};
+		$json_arg =~ s/^\s*\(\s*//s;
+		$json_arg =~ s/\s*\)\s*$//s;
+		my %params_rewrite = ();
+		my $y = 0;
+		while ($json_arg =~ s/(\([^\(\)]+\))/%ARGPARAM$y%/s) {
+			$params_rewrite{$y} = $1;
+			$y++;
+		}
+		my $routine_name = $fct_detail{name};
+		if ($self->{type} eq 'PACKAGE' && $pname) {
+			$routine_name = $pname . '.' . $fct_detail{name};
+		}
+		$self->{json_config} = qq/    {
+      "routine_type": "\U$fct_detail{type}\E",
+      "ora": {
+        "routine_name": "\U$routine_name\E",
+        "return_type": "$fct_detail{orig_func_ret_type}",
+        "args_list": [
+          {
+            "0": [
+/;
+		my @json_args = split(/\s*,\s*/, $json_arg);
+		map { s/\%ARGPARAM(\d+)\%/$params_rewrite{$1}/sg; } @json_args;
+		foreach my $a (@json_args)
+		{
+			my $arg_name = '';
+			my $arg_kind = 'IN';
+			my $arg_default = '';
+			if ($a =~ s/^([^\s]+)\s+//s) {
+				$arg_name = $1;
+			}
+			if ($a =~ s/^(IN\s+OUT|OUT|IN)\s+//s) {
+				$arg_kind = $1;
+			}
+			if ($a =~ s/\s*DEFAULT\s+(.*)//s) {
+				$arg_default = $1;
+			}
+			my $arg_type = $a;
+
+			$self->{json_config} .= qq/              {
+                "name": "$arg_name",
+                "mode": "$arg_kind",
+                "type": "$arg_type",
+                "default": "$arg_default",
+                "value": ""
+              },
+/;
+		}
+		$self->{json_config} =~ s/,$//s;
+		$self->{json_config} .= qq/	      ]
+	  } ]
+      },
+/;
 		# NOCOPY not supported
 		$fct_detail{args} =~ s/\s*NOCOPY//igs;
 		# IN OUT should be INOUT
@@ -1633,8 +1695,58 @@ sub _lookup_function
 		$fct_detail{args} =~ s/\s+DEFAULT\s+EMPTY_[CB]LOB\(\)/DEFAULT NULL/igs;
 
 		# Now convert types
-		$fct_detail{args} = Ora2Pg::PLSQL::replace_sql_type($fct_detail{args}, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, $self->{varchar_to_text}, %{$self->{data_type}});
-		$fct_detail{declare} = Ora2Pg::PLSQL::replace_sql_type($fct_detail{declare}, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, $self->{varchar_to_text}, %{$self->{data_type}});
+		$fct_detail{args} = Ora2Pg::PLSQL::replace_sql_type($self, $fct_detail{args}, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, $self->{varchar_to_text}, %{$self->{data_type}});
+		$fct_detail{declare} = Ora2Pg::PLSQL::replace_sql_type($self, $fct_detail{declare}, $self->{pg_numeric_type}, $self->{default_numeric}, $self->{pg_integer_type}, $self->{varchar_to_text}, %{$self->{data_type}});
+
+		$json_arg = $fct_detail{args};
+		$json_arg =~ s/^\s*\(\s*//s;
+		$json_arg =~ s/\s*\)\s*$//s;
+		my %params_rewrite = ();
+		my $y = 0;
+		while ($json_arg =~ s/(\([^\(\)]+\))/%ARGPARAM$y%/s) {
+			$params_rewrite{$y} = $1;
+			$y++;
+		}
+		$self->{json_config} .= qq/      "pg": {
+        "routine_name": "\L$routine_name\E",
+        "return_type": "$fct_detail{func_ret_type}",
+        "args_list": [
+          {
+            "0": [
+/;
+		@json_args = split(/\s*,\s*/, $json_arg);
+		map { s/\%ARGPARAM(\d+)\%/$params_rewrite{$1}/sg; } @json_args;
+		foreach my $a (@json_args)
+		{
+			my $arg_name = '';
+			my $arg_kind = 'IN';
+			my $arg_default = '';
+			if ($a =~ s/^([^\s]+)\s+//s) {
+				$arg_name = $1;
+			}
+			if ($a =~ s/^(IN\s+OUT|OUT|IN)\s+//s) {
+				$arg_kind = $1;
+			}
+			if ($a =~ s/\s*DEFAULT\s+(.*)//s) {
+				$arg_default = $1;
+			}
+			my $arg_type = $a;
+
+			$self->{json_config} .= qq/              {
+                "name": "$arg_name",
+                "mode": "$arg_kind",
+                "type": "$arg_type",
+                "default": "$arg_default",
+                "value": ""
+              },
+/;
+		}
+		$self->{json_config} =~ s/,$//s;
+		$self->{json_config} .= qq/	      ]
+	  } ]
+      }
+    },
+/;
 
 		# Sometime variable used in FOR ... IN SELECT loop is not declared
 		# Append its RECORD declaration in the DECLARE section.
@@ -1722,9 +1834,9 @@ sub _lookup_function
 		next if (!$n || ($pname && (uc($n) !~ /^\U$pname\E\./)));
 		my $tmpname = $n;
 		$tmpname =~ s/^$pname\.//i;
-		next if ($fct_detail{code} !~ /\b$tmpname\b/is);
+		next if ($fct_detail{code} !~ /\b\Q$tmpname\E\b/is);
 		my $i = 0;
-		while ($fct_detail{code} =~ s/(SELECT\s+(?:.*?)\s+)INTO\s+$tmpname\s+([^;]+);/PERFORM set_config('$n', ($1$2), false);/is) { last if ($i++ > 100); };
+		while ($fct_detail{code} =~ s/(SELECT\s+(?:.*?)\s+)INTO\s+\Q$tmpname\E\s+([^;]+);/PERFORM set_config('$n', ($1$2), false);/is) { last if ($i++ > 100); };
 		$i = 0;
 		while ($fct_detail{code} =~ s/\b$n\s*:=\s*([^;]+)\s*;/PERFORM set_config('$n', $1, false);/is) { last if ($i++ > 100); };
 		$i = 0;
@@ -1759,10 +1871,20 @@ sub _lookup_function
 	# Remove %ROWTYPE from return type
 	$fct_detail{func_ret_type} =~ s/\%ROWTYPE//igs;
 
+	if ($self->{json_test} && !$meta && ($self->{type} eq uc($fct_detail{type}) || $self->{type} eq 'PACKAGE'))
+	{
+		my $dirprefix = '';
+		$dirprefix = "$self->{output_dir}/" if ($self->{output_dir});
+		my $tfh = $self->append_export_file($dirprefix . "$self->{type}.json", 1);
+		flock($tfh, 2) || die "FATAL: can't lock file ${dirprefix}$self->{type}.json\n";
+		$tfh->print($self->{json_config});
+		$self->close_export_file($tfh, 1);
+	}
+
 	return %fct_detail;
 }
 
-sub _list_all_funtions
+sub _list_all_functions
 {
 	my $self = shift;
 
@@ -1827,6 +1949,7 @@ sub _sql_type
         my ($self, $type, $len, $precision, $scale, $default, $no_blob_to_oid) = @_;
 
 	my $data_type = '';
+	chomp($type);
 
 	# Simplify timestamp type
 	$type =~ s/TIMESTAMP\(\d+\)/TIMESTAMP/;
@@ -2135,7 +2258,8 @@ WHERE
 		}
 		if ( ($row->[3] eq 'DEFAULT'))
 		{
-			$default{$row->[0]} = $row->[2];
+			$default{$row->[0]}{name} = $row->[2];
+			$default{$row->[0]}{tablespace} = $row->[4];
 			next;
 		}
 		$parts{$row->[0]}{$row->[1]}{name} = $row->[2];
@@ -2211,7 +2335,8 @@ WHERE
 			$row->[0] = "$row->[9].$row->[0]";
 		}
 		if ( ($row->[3] eq 'MAXVALUE') || ($row->[3] eq 'DEFAULT')) {
-			$default{$row->[0]}{$row->[10]} = $row->[2];
+			$default{$row->[0]}{$row->[10]}{name} = $row->[2];
+			$default{$row->[0]}{$row->[10]}{tablespace} = $row->[4];
 			next;
 		}
 
@@ -2223,14 +2348,14 @@ WHERE
 	return \%subparts, \%default;
 }
 
-=head2 _get_partitions_type
+=head2 _get_partitions_list
 
 This function implements a MySQL-native partitions information.
 Return a hash of the partition table_name => type
 
 =cut
 
-sub _get_partitions_type
+sub _get_partitions_list
 {
 	my ($self) = @_;
 
@@ -2692,8 +2817,12 @@ sub _get_synonyms
 	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
 
 	my %synonyms = ();
-	while (my $row = $sth->fetch) {
+	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];
@@ -2710,9 +2839,9 @@ sub _get_tablespaces
 
 	# Retrieve all object with tablespaces.
 my $str = qq{
-SELECT a.SEGMENT_NAME,a.TABLESPACE_NAME,a.SEGMENT_TYPE,c.FILE_NAME, a.OWNER
+SELECT a.SEGMENT_NAME,a.TABLESPACE_NAME,a.SEGMENT_TYPE,c.FILE_NAME, a.OWNER, a.PARTITION_NAME, b.SUBOBJECT_NAME
 FROM DBA_SEGMENTS a, $self->{prefix}_OBJECTS b, DBA_DATA_FILES c
-WHERE a.SEGMENT_TYPE IN ('INDEX', 'TABLE', 'INDEX PARTITION', 'TABLE PARTITION')
+WHERE a.SEGMENT_TYPE IN ('INDEX', 'TABLE', 'TABLE PARTITION')
 AND a.SEGMENT_NAME = b.OBJECT_NAME
 AND a.SEGMENT_TYPE = b.OBJECT_TYPE
 AND a.OWNER = b.OWNER
@@ -2724,17 +2853,81 @@ AND a.TABLESPACE_NAME = c.TABLESPACE_NAME
 		$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";
+	$str .= " ORDER BY a.SEGMENT_NAME";
 	my $sth = $self->{dbh}->prepare($str) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
 	$sth->execute(@{$self->{query_bind_params}}) or $self->logit("FATAL: " . $self->{dbh}->errstr . "\n", 0, 1);
 
 	my %tbs = ();
-	while (my $row = $sth->fetch) {
-		# TYPE - TABLESPACE_NAME - FILEPATH - OBJECT_NAME
+	my @done = ();
+	while (my $row = $sth->fetch)
+	{
+		my $table = $row->[0];
 		if ($self->{export_schema} && !$self->{schema}) {
 			$row->[0] = "$row->[4].$row->[0]";
 		}
+		next if (grep(/^$row->[0]$/, @done));
+		push(@done, $row->[0]);
+
 		push(@{$tbs{$row->[2]}{$row->[1]}{$row->[3]}}, $row->[0]);
+
+		# With partitioned table, add tablespace info for table partition
+		if (exists $self->{partitions}{$table})
+		{
+			foreach my $pos (sort {$self->{partitions}{$table}{$a} <=> $self->{partitions}{$table}{$b}} keys %{$self->{partitions}{$table}})
+			{
+				my $part_name = $self->{partitions}{$table}{$pos}{name};
+				my $tbpart_name = $part_name;
+				$tbpart_name = $table . '_part' . $pos if ($self->{rename_partition});
+				next if ($self->{allow_partition} && !grep($_ =~ /^$tbpart_name$/i, @{$self->{allow_partition}}));
+				my $tbspace = $self->{partitions}{$table}{$pos}{info}[0]->{tablespace};
+				push(@{$tbs{$row->[2]}{$tbspace}{$row->[3]}}, $tbpart_name);
+
+				if (exists $self->{subpartitions}{$table}{$part_name})
+				{
+					foreach my $p (sort {$a <=> $b} keys %{$self->{subpartitions}{$table}{$part_name}})
+					{
+						my $subpart = $self->{subpartitions}{$table}{$part_name}{$p}{name};
+						next if ($self->{allow_partition} && !grep($_ =~ /^$subpart$/i, @{$self->{allow_partition}}));
+						my $sub_tb_name = $subpart;
+						$sub_tb_name =~ s/^[^\.]+\.//; # remove schema part if any
+						$sub_tb_name = $table . '_part' . $pos . '_subpart' . $p if ($self->{rename_partition});
+						if ($#{$self->{tables}{$table}{field_name}} < 0) {
+							$self->logit("Table $table has no column defined, skipping...\n", 1);
+							next;
+						}
+						my $tbspace = $self->{subpartitions}{$table}{$part_name}{$p}{info}[0]->{tablespace};
+						push(@{$tbs{$row->[2]}{$tbspace}{$row->[3]}}, $sub_tb_name);
+
+					}
+
+					# process default subpartition table
+					if (exists $self->{subpartitions_default}{$table}{$part_name})
+					{
+						if (!$self->{allow_partition} || grep($_ =~ /^$self->{subpartitions_default}{$table}{$part_name}{name}$/i, @{$self->{allow_partition}}))
+						{
+							my $sub_tb_name = $self->{subpartitions_default}{$table}{$part_name}{name};
+							$sub_tb_name =~ s/^[^\.]+\.//; # remove schema part if any
+							next if ($self->{allow_partition} && !grep($_ =~ /^$sub_tb_name$/i, @{$self->{allow_partition}}));
+							$sub_tb_name = $table . '_part' . $pos . '_subpart_default' if ($self->{rename_partition});
+							my $tbspace = $self->{subpartitions_default}{$table}{$part_name}{tablespace};
+							push(@{$tbs{$row->[2]}{$tbspace}{$row->[3]}}, $sub_tb_name);
+						}
+					}
+				}
+			}
+
+			# Add the default partition table
+			if (exists $self->{partitions_default}{$table})
+			{
+				if (!$self->{allow_partition} || grep($_ =~ /^$self->{partitions_default}{$table}{name}$/i, @{$self->{allow_partition}}))
+				{
+					my $tbpart_name = $table . '_' . $self->{partitions_default}{$table}{name};
+					$tbpart_name = $table . '_part_default' if ($self->{rename_partition});
+					my $tbspace = $self->{partitions_default}{$table}{tablespace};
+					push(@{$tbs{$row->[2]}{$tbspace}{$row->[3]}}, $tbpart_name);
+				}
+			}
+		}
 	}
 	$sth->finish;
 
@@ -2784,7 +2977,7 @@ sub _get_sequences
 	}
 	# Exclude sequence used for IDENTITY columns
 	$str .= " AND SEQUENCE_NAME NOT LIKE 'ISEQ\$\$_%'";
-	$str .= $self->limit_to_objects('SEQUENCE', 'SEQUENCE_NAME');
+	$str .= $self->limit_to_objects($self->{type}, 'SEQUENCE_NAME');
 	#$str .= " ORDER BY SEQUENCE_NAME";
 
 
@@ -2861,7 +3054,7 @@ sub _column_attributes
 		$sth = $self->{dbh}->prepare(<<END);
 SELECT A.COLUMN_NAME, A.NULLABLE, A.DATA_DEFAULT, A.TABLE_NAME, A.OWNER, A.COLUMN_ID, A.DATA_TYPE
 FROM $self->{prefix}_TAB_COLUMNS A, $self->{prefix}_OBJECTS O
-WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='$objtype'
+WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='$objtype' and A.TABLE_NAME NOT LIKE 'BIN\$%'
     $condition
 ORDER BY A.COLUMN_ID
 END
@@ -2875,7 +3068,7 @@ END
 		$sth = $self->{dbh}->prepare(<<END);
 SELECT A.COLUMN_NAME, A.NULLABLE, A.DATA_DEFAULT, A.TABLE_NAME, A.OWNER, A.COLUMN_ID, A.DATA_TYPE
 FROM $self->{prefix}_TAB_COLUMNS A, $self->{prefix}_OBJECTS O
-WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='$objtype'
+WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='$objtype' and A.TABLE_NAME NOT LIKE 'BIN\$%'
     $condition
 ORDER BY A.COLUMN_ID
 END
@@ -3225,7 +3418,7 @@ sub _get_plsql_metadata
 				$self->_remove_comments(\$self->{function_metadata}{$sch}{'none'}{$name}{text}, 1);
 				$self->{comment_values} = ();
 				$self->{function_metadata}{$sch}{'none'}{$name}{text} =~  s/\%ORA2PG_COMMENT\d+\%//gs;
-				my %fct_detail = $self->_lookup_function($self->{function_metadata}{$sch}{'none'}{$name}{text});
+				my %fct_detail = $self->_lookup_function($self->{function_metadata}{$sch}{'none'}{$name}{text}, undef, 1);
 				if (!exists $fct_detail{name})
 				{
 					delete $self->{function_metadata}{$sch}{'none'}{$name};
@@ -3762,7 +3955,7 @@ sub _col_count
 	if ($self->{db_version} !~ /Release 8/) {
 		$sth = $self->{dbh}->prepare(<<END);
 SELECT A.OWNER, A.TABLE_NAME, COUNT(*)
-FROM $self->{prefix}_TAB_COLUMNS A, $self->{prefix}_OBJECTS O WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='TABLE' $condition
+FROM $self->{prefix}_TAB_COLUMNS A, $self->{prefix}_OBJECTS O WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='TABLE' and A.TABLE_NAME NOT LIKE 'BIN\$%' $condition
 GROUP BY A.OWNER, A.TABLE_NAME
 END
 		if (!$sth) {
@@ -3772,7 +3965,7 @@ END
 		# an 8i database.
 		$sth = $self->{dbh}->prepare(<<END);
 SELECT A.OWNER, A.TABLE_NAME, COUNT(*)
-FROM $self->{prefix}_TAB_COLUMNS A, $self->{prefix}_OBJECTS O WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='TABLE' $condition
+FROM $self->{prefix}_TAB_COLUMNS A, $self->{prefix}_OBJECTS O WHERE A.OWNER=O.OWNER and A.TABLE_NAME=O.OBJECT_NAME and O.OBJECT_TYPE='TABLE' and A.TABLE_NAME NOT LIKE 'BIN\$%' $condition
 GROUP BY A.OWNER, A.TABLE_NAME
 END
 		if (!$sth) {
diff --git a/lib/Ora2Pg/PLSQL.pm b/lib/Ora2Pg/PLSQL.pm
index 06e73cc..05f04ce 100644
--- a/lib/Ora2Pg/PLSQL.pm
+++ b/lib/Ora2Pg/PLSQL.pm
@@ -4,7 +4,7 @@ package Ora2Pg::PLSQL;
 # Name     : Ora2Pg/PLSQL.pm
 # Language : Perl
 # Authors  : Gilles Darold, gilles _AT_ darold _DOT_ net
-# Copyright: Copyright (c) 2000-2022 : Gilles Darold - All rights reserved -
+# Copyright: Copyright (c) 2000-2023 : Gilles Darold - All rights reserved -
 # Function : Perl module used to convert Oracle PLSQL code into PL/PGSQL
 # Usage    : See documentation
 #------------------------------------------------------------------------------
@@ -24,14 +24,14 @@ package Ora2Pg::PLSQL;
 # 
 #------------------------------------------------------------------------------
 
-use vars qw($VERSION %OBJECT_SCORE $SIZE_SCORE $FCT_TEST_SCORE $QUERY_TEST_SCORE %UNCOVERED_SCORE %UNCOVERED_MYSQL_SCORE @ORA_FUNCTIONS @MYSQL_SPATIAL_FCT @MYSQL_FUNCTIONS %EXCEPTION_MAP %MAX_SCORE);
+use vars qw($VERSION %OBJECT_SCORE $SIZE_SCORE $FCT_TEST_SCORE $QUERY_TEST_SCORE %UNCOVERED_SCORE %UNCOVERED_MYSQL_SCORE @ORA_FUNCTIONS @MYSQL_SPATIAL_FCT @MYSQL_FUNCTIONS %EXCEPTION_MAP %MAX_SCORE %MSSQL_STYLE %UNCOVERED_MSSQL_SCORE);
 use POSIX qw(locale_h);
 
 #set locale to LC_NUMERIC C
 setlocale(LC_NUMERIC,"C");
 
 
-$VERSION = '23.2';
+$VERSION = '24.0';
 
 #----------------------------------------------------
 # Cost scores used when converting PLSQL to PLPGSQL
@@ -176,6 +176,7 @@ $QUERY_TEST_SCORE = 0.1;
 	'FND_ORG' => 3,
 	'FND_STANDARD' => 3,
 	'FND_UTILITIES' => 3,
+	'ADD CONSTRAINT' => 3, ## need stability in constraint name
 );
 
 @ORA_FUNCTIONS = qw(
@@ -321,7 +322,7 @@ $QUERY_TEST_SCORE = 0.1;
 
 # Scores associated to each code difficulties after replacement.
 %UNCOVERED_MYSQL_SCORE = (
-	'ARRAY_AGG_DISTINCT' => 1, # array_agg(distinct
+	'ARRAY_AGG_DISTINCT' => 1, # array_agg(distinct)
 	'SOUNDS LIKE' => 1,
 	'CHARACTER SET' => 1,
 	'COUNT(DISTINCT)' => 2,
@@ -331,6 +332,68 @@ $QUERY_TEST_SCORE = 0.1;
 	'@VAR' => 0.1,
 );
 
+@MSSQL_FUNCTIONS = (
+	'ATN2',
+	'CHARINDEX',
+	'Concat with +',
+	'CONVERT',
+	'DATEADD',
+	'DATEDIFF',
+	'DATEFROMPARTS',
+	'DATENAME',
+	'DATEPART',
+	'DAY',
+	'ERROR_LINE',
+	'ERROR_MESSAGE',
+	'ERROR_NUMBER',
+	'ERROR_PROCEDURE',
+	'ERROR_SEVERITY',
+	'ERROR_STATE',
+	'GETDATE',
+	'GETUTCDATE',
+	'IIF',
+	'ISDATE',
+	'ISNULL',
+	'ISNUMERIC',
+	'MONTH',
+	'NCHAR',
+	'NULLIF',
+	'PATINDEX',
+	'PRINT',
+	'RAISERROR',
+	'QUOTENAME',
+	'RAND',
+	'REPLICATE',
+	'SESSIONPROPERTY',
+	'SOUNDEX',
+	'SPACE',
+	'SQUARE',
+	'STR',
+	'STUFF',
+	'SYSDATETIME',
+	'SYSTEM_USER',
+	'UNICODE',
+	'YEAR',
+);
+
+%UNCOVERED_MSSQL_SCORE = (
+	'ARRAY_AGG_DISTINCT' => 1, # array_agg(distinct)
+	'FOREIGN_OBJECT' => 6,
+	'SYS_OBJECT' => 6,
+	'INTO_TEMP_TABLE' => 1,
+	'GLOBAL_TEMP_TABLE' => 2,
+	'SELECT_TOP' => 0.2,
+	'COLLATE' => 0.2,
+	'RETURNS_TABLE' => 1,
+	'TODATETIMEOFFSET' => 3,
+	'CURSOR' => 0.2,
+	'GLOBAL_VARIABLE' => 1,
+	'PIVOT' => 12,
+	'TRY_CATCH' => 3,
+	'SP_FCT' => 3,
+	'XML_FCT' => 3,
+);
+
 %EXCEPTION_MAP = (
 	'INVALID_CURSOR' => 'invalid_cursor_state',
 	'ZERO_DIVIDE' => 'division_by_zero',
@@ -346,6 +409,29 @@ $QUERY_TEST_SCORE = 0.1;
 	# 'ROWTYPE_MISMATCH' => 'DATATYPE MISMATCH'
 );
 
+%MSSQL_STYLE = (
+	'100' => 'mon dd yyyy hh:miAM/PM',
+	'101' => 'mm/dd/yyyy',
+	'102' => ' yyyy.mm.dd',
+	'103' => ' dd/mm/yyyy',
+	'104' => 'dd.mm.yyyy',
+	'105' => ' dd-mm-yyyy',
+	'106' => 'dd mon yyyy',
+	'107' => 'Mon dd, yyyy',
+	'108' => 'hh:mm:ss',
+	'109' => 'mon dd yyyy hh:mi:ss:mmmAM (or PM)',
+	'110' => 'mm-dd-yyyy',
+	'111' => ' yyyy/mm/dd',
+	'112' => ' yyyymmdd',
+	'113' => 'dd mon yyyy hh:mi:ss:mmm',
+	'114' => 'hh:mi:ss:mmm',
+	'120' => 'yyyy-mm-dd hh:mi:ss',
+	'121' => 'yyyy-mm-dd hh:mi:ss.mmm',
+	'126' => 'yyyy-mm-ddThh:mi:ss.mmm',
+	'127' => 'yyyy-mm-ddThh:mi:ss.mmmZ',
+	'130' => 'dd mon yyyy hh:mi:ss:mmmAM',
+	'131' => 'dd/mm/yy hh:mi:ss:mmmAM',
+);
 
 =head1 NAME
 
@@ -388,7 +474,7 @@ sub convert_plsql_code
 	%{$class->{single_fct_call}} = ();
 	$class->{replace_out_params} = '';
 
-	if (uc($class->{type}) ne 'SHOW_REPORT')
+	if (!$self->{use_orafce} && uc($class->{type}) ne 'SHOW_REPORT')
 	{
 		# Rewrite all decode() call before
 		$str = replace_decode($str);
@@ -459,22 +545,11 @@ sub convert_plsql_code
 		foreach my $k (keys %{$class->{single_fct_call}})
 		{
 			$class->{single_fct_call}{$k} = replace_oracle_function($class, $class->{single_fct_call}{$k}, @strings);
-			if ($class->{single_fct_call}{$k} =~ /^CAST\s*\(/i)
-			{
-				if (!$class->{is_mysql})
-				{
-					$class->{single_fct_call}{$k} = Ora2Pg::PLSQL::replace_sql_type($class->{single_fct_call}{$k}, $class->{pg_numeric_type}, $class->{default_numeric}, $class->{pg_integer_type}, $class->{varchar_to_text}, %{$class->{data_type}});
-				} else {
-					$class->{single_fct_call}{$k} = Ora2Pg::MySQL::replace_sql_type($class->{single_fct_call}{$k}, $class->{pg_numeric_type}, $class->{default_numeric}, $class->{pg_integer_type}, $class->{varchar_to_text}, %{$class->{data_type}});
-				}
+			if ($class->{single_fct_call}{$k} =~ /^CAST\s*\(/i) {
+				$class->{single_fct_call}{$k} = replace_sql_type($class, $class->{single_fct_call}{$k});
 			}
-			if ($class->{single_fct_call}{$k} =~ /^CAST\s*\(.*\%\%REPLACEFCT(\d+)\%\%/i)
-			{
-				if (!$class->{is_mysql}) {
-					$class->{single_fct_call}{$1} = Ora2Pg::PLSQL::replace_sql_type($class->{single_fct_call}{$1}, $class->{pg_numeric_type}, $class->{default_numeric}, $class->{pg_integer_type}, $class->{varchar_to_text}, %{$class->{data_type}});
-				} else {
-					$class->{single_fct_call}{$1} = Ora2Pg::MySQL::replace_sql_type($class->{single_fct_call}{$1}, $class->{pg_numeric_type}, $class->{default_numeric}, $class->{pg_integer_type}, $class->{varchar_to_text}, %{$class->{data_type}});
-				}
+			if ($class->{single_fct_call}{$k} =~ /^CAST\s*\(.*\%\%REPLACEFCT(\d+)\%\%/i) {
+				$class->{single_fct_call}{$1} = replace_sql_type($class, $class->{single_fct_call}{$1});
 			}
 		}
 		while ($code_parts[$i] =~ s/\%\%REPLACEFCT(\d+)\%\%/$class->{single_fct_call}{$1}/) {};
@@ -663,10 +738,14 @@ sub plsql_to_plpgsql
 	return if ($str eq '');
 
 	return mysql_to_plpgsql($class, $str, @strings) if ($class->{is_mysql});
+	return mssql_to_plpgsql($class, $str, @strings) if ($class->{is_mssql});
 
 	my $field = '\s*([^\(\),]+)\s*';
 	my $num_field = '\s*([\d\.]+)\s*';
 
+	# Rewrite variable calls
+	$str =~ s/([a-z=<>\*\+\-\/\( ]): ([a-z0-9_\$]+)/$1 :'$2'/igs if ($class->{type} eq 'QUERY');
+
 	my $conv_current_time = 'clock_timestamp()';
 	if (!grep(/$class->{type}/i, 'FUNCTION', 'PROCEDURE', 'PACKAGE')) {
 		$conv_current_time = 'statement_timestamp()';
@@ -863,8 +942,13 @@ sub plsql_to_plpgsql
 	# Raise information to the client
 	if (!$class->{use_orafce}) {
 		$str =~ s/DBMS_OUTPUT\.(put_line|put|new_line)\s*\((.*?)\)\s*;/&raise_output($class, $2) . ';'/isge;
+	} else {
+		$str =~ s/(DBMS_OUTPUT\.)/PERFORM $1/igs;
 	}
 
+	# DBMS_LOCK.SLEEP can be replaced by pg_sleep
+	$str =~ s/DBMS_LOCK\.SLEEP/PERFORM pg_sleep/igs;
+
 	# Simply remove this as not supported
 	$str =~ s/\bDEFAULT\s+NULL\b//igs;
 
@@ -992,11 +1076,7 @@ sub plsql_to_plpgsql
 	}
 
 	# Replace type in sub block
-	if (!$class->{is_mysql}) {
-		$str =~ s/(BEGIN.*?DECLARE\s+)(.*?)(\s+BEGIN)/$1 . Ora2Pg::PLSQL::replace_sql_type($2, $class->{pg_numeric_type}, $class->{default_numeric}, $class->{pg_integer_type}, $class->{varchar_to_text}, %{$class->{data_type}}) . $3/iges;
-	} else {
-		$str =~ s/(BEGIN.*?DECLARE\s+)(.*?)(\s+BEGIN)/$1 . Ora2Pg::MySQL::replace_sql_type($2, $class->{pg_numeric_type}, $class->{default_numeric}, $class->{pg_integer_type}, $class->{varchar_to_text}, %{$class->{data_type}}) . $3/iges;
-	}
+	$str =~ s/(BEGIN.*?DECLARE\s+)(.*?)(\s+BEGIN)/$1 . replace_sql_type($class, $2) . $3/iges;
 
 	# Remove any call to MDSYS schema in the code
 	$str =~ s/\bMDSYS\.//igs;
@@ -1093,8 +1173,8 @@ sub plsql_to_plpgsql
 	$str =~ s/TIMESTAMP\s*('[^']+')/$1/igs;
 	
 	# Replace call to SQL%ROWCOUNT
-	$str =~ s/([^\s]+)\s*:=\s*SQL\%ROWCOUNT/GET DIAGNOSTICS $1 = ROW_COUNT/igs;
-	if ($str =~ s/(IF\s+)SQL\%ROWCOUNT/GET DIAGNOSTICS ora2pg_rowcount = ROW_COUNT;\n$1ora2pg_rowcount/igs) {
+	$str =~ s/([^\s:]+)\s*:=\s*SQL\%ROWCOUNT/GET DIAGNOSTICS $1 = ROW_COUNT/igs;
+	if ($str =~ s/(IF\s+)[^\%\s;]+\%ROWCOUNT/GET DIAGNOSTICS ora2pg_rowcount = ROW_COUNT;\n$1ora2pg_rowcount/igs) {
 		$class->{get_diagnostics} = 'ora2pg_rowcount int;';
 	} elsif ($str =~ s/;(\s+)([^;]+)SQL\%ROWCOUNT/;$1GET DIAGNOSTICS ora2pg_rowcount = ROW_COUNT;\n$1$2 ora2pg_rowcount/igs) {
 		$class->{get_diagnostics} = 'ora2pg_rowcount int;';
@@ -1102,6 +1182,9 @@ sub plsql_to_plpgsql
 	# SQL%ROWCOUNT with concatenated string
 	$str =~ s/(\s+)(GET DIAGNOSTICS )([^\s]+)( = ROW_COUNT)(\s+\|\|[^;]+);/$1$2$3$4;$1$3 := $3 $5;/;
 
+	# Replace call of ROWNUM in the target list with row_number() over ()
+	$str =~ s/(PERFORM|SELECT|,|\()\s*ROWNUM\b((?:.*?)\s+FROM\s+)/$1row_number() over ()$2/igs;
+
 	# Sometime variable used in FOR ... IN SELECT loop is not declared
 	# Append its RECORD declaration in the DECLARE section.
 	my $tmp_code = $str;
@@ -1132,6 +1215,25 @@ sub plsql_to_plpgsql
 	# Restore non converted outer join
 	$str =~ s/\%OUTERJOIN\d+\%/\(\+\)/igs;
 
+	# Rewrite some SQL script setting from Oracle
+	$str =~ s/\bset\s+timing\s+(on|off)/\\timing $1/igs;
+	$str =~ s/\b(set\s+(?:array|arraysize)\s+\d+)/-- $1/igs;
+	$str =~ s/\bset\s+(?:auto|autocommit)\s+(on|off)/\\set AUTOCOMMIT $1/igs;
+	$str =~ s/\bset\s+echo\s+on/\\set ECHO queries/igs;
+	$str =~ s/\bset\s+echo\s+off/\\set ECHO none/igs;
+	$str =~ s/\bset\s+(?:heading|head)\s+(on|off)/\\pset tuples_only $1/igs;
+	$str =~ s/\bset\s+(?:trim|trimout)\s+on/\\pset format unaligned/igs;
+	$str =~ s/\bset\s+(?:trim|trimout)\s+off/\\pset format aligned/igs;
+	$str =~ s/\bset\s+colsep\s+([^\s]+)/\\pset fieldsep $1/igs;
+	$str =~ s/\bspool\s+off/\\o/igs;
+	$str =~ s/\bspool\s+([^\&']+[^\s]+)/\\o $1/igs;
+	$str =~ s/\bttitle\s+/\\pset title /igs;
+	$str =~ s/\bprompt\s+/\\qecho /igs;
+	$str =~ s/\b(set\s+(?:linesize|pagesize|feedback|verify)\s+)/--$1/igs;
+	$str =~ s/\b(disconnect)\b/--$1/igs;
+	$str =~ s/\b(connect\s+)/--$1/igs;
+	$str =~ s/\b(quit)\b/\\$1/igs;
+
 	return $str;
 }
 
@@ -1415,8 +1517,10 @@ sub replace_rownum_with_limit
 		my $clause = $2;
 		if ($clause =~ /\%SUBQUERY\d+\%/) {
 			$tmp_val = $clause;
-		} else {
+		} elsif ($clause !~ /\D/) {
 			$tmp_val = $clause - 1;
+		} else {
+			$tmp_val = "$clause - 1";
 		}
         }
 	if ($str =~ s/\s+AND\s+(?:\(\s*)?ROWNUM\s*<=\s*([^\s\)]+)(\s*\)\s*)?([^;]*)/ $2$3/is) {
@@ -1427,8 +1531,10 @@ sub replace_rownum_with_limit
 		my $clause = $1;
 		if ($clause =~ /\%SUBQUERY\d+\%/) {
 			$tmp_val = $clause;
-		} else {
+		} elsif ($clause !~ /\D/) {
 			$tmp_val = $clause - 1;
+		} else {
+			$tmp_val = "$clause - 1";
 		}
         }
 	$str =~ s/\s+WHERE\s+ORDER\s+/ ORDER /is;
@@ -1657,6 +1763,8 @@ sub replace_oracle_function
 
 	if ($class->{is_mysql}) {
 		$str = mysql_to_plpgsql($class, $str);
+	} elsif ($class->{is_mssql}) {
+		$str = mssql_to_plpgsql($class, $str);
 	}
 
 	# Change NVL to COALESCE
@@ -1676,6 +1784,9 @@ sub replace_oracle_function
 	# TO_CLOB(), we just remove it
 	$str =~ s/TO_CLOB\s*\(/\(/igs;
 
+	# DBMS_LOCK.SLEEP can be replaced by pg_sleep
+	$str =~ s/DBMS_LOCK\.SLEEP/pg_sleep/igs;
+
 	# Replace call to SYS_GUID() function
 	$str =~ s/\bSYS_GUID\s*\(\s*\)/$class->{uuid_function}()/igs;
 	$str =~ s/\bSYS_GUID\b/$class->{uuid_function}()/igs;
@@ -1783,7 +1894,7 @@ sub replace_oracle_function
 		if ($class->{to_number_conversion} =~ /(numeric|bigint|integer|int)/i)
 		{
 			my $cast = lc($1);
-			if ($class->{type} ne 'TABLE') {
+			if ($class->{type} ne 'TABLE' && $class->{type} ne 'COPY') {
 				$str =~ s/\bTO_NUMBER\s*\(\s*([^,\)]+)\s*\)\s?/($1)\:\:$cast /is;
 			} else {
 				$str =~ s/\bTO_NUMBER\s*\(\s*([^,\)]+)\s*\)\s?/(nullif($1, '')\:\:$cast) /is;
@@ -1837,7 +1948,7 @@ sub replace_oracle_function
 
 	# Replace some sys_context call to the postgresql equivalent
 	if ($str =~ /SYS_CONTEXT/is) {
-		replace_sys_context($str);
+		$str = replace_sys_context($str);
 	}
 
 	return $str;
@@ -2231,7 +2342,7 @@ sub raise_output
 
 sub replace_sql_type
 {
-        my ($str, $pg_numeric_type, $default_numeric, $pg_integer_type, $varchar_to_text, %data_type) = @_;
+        my ($self, $str) = @_;
 
 	# Remove the SYS schema from type name
 	$str =~ s/\bSYS\.//igs;
@@ -2250,7 +2361,7 @@ sub replace_sql_type
 	$str =~ s/\b(RAW|BLOB)\s*\(\s*\d+\s*\)/$1/igs;
 
 	# Replace type with precision
-	my @ora_type = keys %data_type;
+	my @ora_type = keys %{$self->{data_type}};
 	map { s/\(/\\\(/; s/\)/\\\)/; } @ora_type;
 	my $oratype_regex = join('|', @ora_type);
 
@@ -2278,7 +2389,7 @@ sub replace_sql_type
 			# 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")));
-			$str =~ s/\b$type\b\s*\([^\)]+\)/$data_type{$type}\%\|$len\%\|\%/is;
+			$str =~ s/\b$type\b\s*\([^\)]+\)/$self->{data_type}{$type}\%\|$len\%\|\%/is;
 		}
 		elsif ($type =~ /TIMESTAMP/i)
 		{
@@ -2306,7 +2417,7 @@ sub replace_sql_type
 			{
 				if ($precision)
 				{
-					if ($pg_integer_type)
+					if ($self->{pg_integer_type})
 					{
 						if ($precision < 5) {
 							$str =~ s/\b$type\b\s*\([^\)]+\)/smallint/is;
@@ -2321,15 +2432,15 @@ sub replace_sql_type
 						$str =~ s/\b$type\b\s*\([^\)]+\)/numeric\%\|$precision\%\|\%/i;
 					}
 				}
-				elsif ($pg_integer_type)
+				elsif ($self->{pg_integer_type})
 				{
-					my $tmp = $default_numeric || 'bigint';
+					my $tmp = $self->{default_numeric} || 'bigint';
 					$str =~ s/\b$type\b\s*\([^\)]+\)/$tmp/is;
 				}
 			}
 			else
 			{
-				if ($pg_numeric_type)
+				if ($self->{pg_numeric_type})
 				{
 					if ($precision eq '') {
 						$str =~ s/\b$type\b\s*\([^\)]+\)/decimal(38, $scale)/is;
@@ -2365,14 +2476,14 @@ sub replace_sql_type
 	$str =~ s/\%\|/\(/gs;
 
         # Replace datatype without precision
-	my $number = $data_type{'NUMBER'};
-	$number = $default_numeric if ($pg_integer_type);
+	my $number = $self->{data_type}{'NUMBER'};
+	$number = $self->{default_numeric} if ($self->{pg_integer_type});
 	$str =~ s/\bNUMBER\b/$number/igs;
 
 	# Set varchar without length to text
 	$str =~ s/\bVARCHAR2\b/VARCHAR/igs;
 	$str =~ s/\bSTRING\b/VARCHAR/igs;
-	if ($varchar_to_text) {
+	if ($self->{varchar_to_text}) {
 		$str =~ s/\bVARCHAR\b(\s*(?!\())/text$1/igs;
 	} else {
 		$str =~ s/\bVARCHAR\b(\s*(?!\())/varchar$1/igs;
@@ -2381,26 +2492,23 @@ sub replace_sql_type
 	foreach my $t ('DATE','LONG RAW','LONG','NCLOB','CLOB','BLOB','BFILE','RAW','ROWID','UROWID','FLOAT','DOUBLE PRECISION','INTEGER','INT','REAL','SMALLINT','BINARY_FLOAT','BINARY_DOUBLE','BINARY_INTEGER','BOOLEAN','XMLTYPE','SDO_GEOMETRY','PLS_INTEGER','NUMBER')
 	{
 		if ($t eq 'DATE') {
-			$str =~ s/\b$t\s*\(\d\)/$data_type{$t}/igs;
+			$str =~ s/\b$t\s*\(\d\)/$self->{data_type}{$t}/igs;
 		}
 		elsif ($t eq 'NUMBER')
 		{
-			if ($pg_integer_type)
+			if ($self->{pg_integer_type})
 			{
-				my $tmp = $default_numeric || 'bigint';
+				my $tmp = $self->{default_numeric} || 'bigint';
 				$str =~ s/\b$t\b/$tmp/igs;
 				next;
 			}
 		}
-		$str =~ s/\b$t\b/$data_type{$t}/igs;
+		$str =~ s/\b$t\b/$self->{data_type}{$t}/igs;
 	}
 
 	# Translate cursor declaration
 	$str = replace_cursor_def($str);
 
-	# Remove remaining %ROWTYPE in other prototype declaration
-	#$str =~ s/\%ROWTYPE//isg;
-
 	$str =~ s/;[ ]+/;/gs;
 
         return $str;
@@ -2483,7 +2591,8 @@ sub estimate_cost
 {
 	my ($class, $str, $type) = @_;
 
-	return mysql_estimate_cost($str, $type) if ($class->{is_mysql});
+	return mysql_estimate_cost($class, $str, $type) if ($class->{is_mysql});
+	return mssql_estimate_cost($class, $str, $type) if ($class->{is_mssql});
 
 	my %cost_details = ();
 
@@ -2692,9 +2801,13 @@ sub estimate_cost
 	$cost_details{'TOOBJECT'} += $n;
 	$n = () = $str =~ m/TRANSFORM\(/igs;
 	$cost_details{'TRANSFORM'} += $n;
+	$n = () = $str =~ m/ADD CONSTRAINT/igs;
+	$cost_details{'ADD CONSTRAINT'} += $n;
 
-	foreach my $f (@ORA_FUNCTIONS) {
-		if ($str =~ /\b$f\b/igs) {
+	foreach my $f (@ORA_FUNCTIONS)
+	{
+		if ($str =~ /\b$f\b/igs)
+		{
 			$cost += 1;
 			$cost_details{$f} += 1;
 		}
@@ -2848,9 +2961,10 @@ sub mysql_to_plpgsql
 	$str =~ s/\bTRUNCATE\(\s*([^,]+)\s*,\s*([^\(\)]+)\s*\)/trunc\($1, $2\)/igs;
 	$str =~ s/\bUSER\(\s*\)/CURRENT_USER/igs;
 
+
 	# Date/time related function
 	$str =~ s/\b(CURDATE|CURRENT_DATE)\(\s*\)/CURRENT_DATE/igs;
-	$str =~ s/\b(CURTIME|CURRENT_TIME)\(\s*\)/LOCALTIME(0)/igs;
+	$str =~ s/\b(CURTIME|CURRENT_TIME)\(\s*\)/CURRENT_TIMESTAMP::timestamp(0) without time zone/igs;
 	$str =~ s/\bCURRENT_TIMESTAMP\(\s*\)/CURRENT_TIMESTAMP::timestamp(0) without time zone/igs;
 	$str =~ s/\b(LOCALTIMESTAMP|LOCALTIME)\(\s*\)/CURRENT_TIMESTAMP::timestamp(0) without time zone/igs;
 	$str =~ s/\b(LOCALTIMESTAMP|LOCALTIME)\b/CURRENT_TIMESTAMP::timestamp(0) without time zone/igs;
@@ -2896,7 +3010,7 @@ sub mysql_to_plpgsql
 	$str =~ s/\bSUBTIME\(\s*([^,]+)\s*,\s*([^\(\)]+)\s*\)/($1)::timestamp - ($2)::interval/igs;
 	$str =~ s/\bTIME(\([^\(\)]+\))/($1)::time/igs;
 	$str =~ s/\bTIMEDIFF\(\s*([^,]+)\s*,\s*([^\(\)]+)\s*\)/($1)::timestamp - ($2)::timestamp/igs;
-	$str =~ s/\bTIMESTAMP\(\s*([^\(\)]+)\s*\)/($1)::timestamp/igs;
+	#$str =~ s/\bTIMESTAMP\(\s*([^\(\)]+)\s*\)/($1)::timestamp/igs;
 	$str =~ s/\bTIMESTAMP\(\s*([^,]+)\s*,\s*([^\(\)]+)\s*\)/($1)::timestamp + ($2)::time/igs;
 	$str =~ s/\bTIMESTAMPADD\(\s*([^,]+)\s*,\s*([^,]+)\s*,\s*([^\(\)]+)\s*\)/($3)::timestamp + ($1 * interval '1 $2')/igs;
 	$str =~ s/\bTIMESTAMPDIFF\(\s*YEAR\s*,\s*([^,]+)\s*,\s*([^\(\),]+)\s*\)/extract(year from ($2)::timestamp) - extract(year from ($1)::timestamp)/igs;
@@ -2975,7 +3089,8 @@ sub mysql_to_plpgsql
 	# Rewrite REPEAT loop
 	my %repl_repeat = ();
 	$i = 0;
-	while ($str =~ s/\bREPEAT\s+(.*?)\bEND REPEAT\s*;/%REPREPEATLBL$i%/igs) {
+	while ($str =~ s/\bREPEAT\s+(.*?)\bEND REPEAT\s*;/%REPREPEATLBL$i%/igs)
+	{
 		my $code = $1;
 		$code =~ s/\bUNTIL(.*)//;
 		$repl_repeat{$i} = "LOOP ${code}EXIT WHEN $1;\nEND LOOP;";
@@ -3026,6 +3141,21 @@ sub mysql_to_plpgsql
 		$str =~ s/\bCALL\s+//igs;
 	}
 
+	if ($str =~ /\s+FROM\s+(.*?)\s+WHERE/is)
+	{
+		my @joins = split(/\bJOIN\b/i, $1);
+		my $res = '';
+		foreach my $j (@joins) {
+			if ($res eq '') {
+				$res = $j;
+			} elsif ($j !~ /\bON\b/i) {
+				$res .= ',' . $j;
+			} else {
+				$res .= 'JOIN' . $j;
+			}
+		}
+		$str =~ s/(\s+FROM\s+)(.*?)(\s+WHERE)/$1$res$3/is;
+	}
 	return $str;
 }
 
@@ -3206,8 +3336,7 @@ sub _mysql_dateformat_to_pgsql
 
 sub mysql_estimate_cost
 {
-	my $str = shift;
-	my $type = shift;
+	my ($class, $str, $type) = @_;
 
 	my %cost_details = ();
 
@@ -3250,7 +3379,8 @@ sub mysql_estimate_cost
 	foreach my $t (keys %UNCOVERED_MYSQL_SCORE) {
 		$cost += $UNCOVERED_MYSQL_SCORE{$t}*$cost_details{$t};
 	}
-	foreach my $f (@MYSQL_FUNCTIONS) {
+	foreach my $f (@MYSQL_FUNCTIONS)
+	{
 		if ($str =~ /\b$f\b/igs) {
 			$cost += 2;
 			$cost_details{$f} += 2;
@@ -3260,6 +3390,95 @@ sub mysql_estimate_cost
 	return $cost, %cost_details;
 }
 
+sub mssql_estimate_cost
+{
+	my ($class, $str, $type) = @_;
+
+	my %cost_details = ();
+
+	# TSQL do not use ; as statements separator and condition use begin instead of then/loop...
+	# this require manual editing so decrease the number of lines for cost of the code review.
+	$SIZE_SCORE = 400;
+
+	# Default cost is testing that mean it at least must be tested
+	my $cost = $FCT_TEST_SCORE;
+	# When evaluating queries tests must not be included here
+	if ($type eq 'QUERY') {
+		$cost = 0;
+	}
+	$cost_details{'TEST'} = $cost;
+
+	# Set cost following code length
+	my $cost_size = int(length($str)/$SIZE_SCORE) || 1;
+	# When evaluating queries size must not be included here
+	if ($type eq 'QUERY') {
+		$cost_size = 0;
+	}
+
+	$cost += $cost_size;
+	$cost_details{'SIZE'} = $cost_size;
+
+	# Try to figure out the manual work
+	# Not accurate for now
+	my $n = () = $str =~ m/(ARRAY_AGG|GROUP_CONCAT)\(\s*DISTINCT/igs;
+	$cost_details{'ARRAY_AGG_DISTINCT'} += $n*$UNCOVERED_MSSQL_SCORE{'ARRAY_AGG_DISTINCT'};
+	# Look for access to objects in other database, require FDW or dblink.
+	$n = () = $str =~ /\b[a-z0-9_\$]+\.[a-z0-9_\$]+\.[a-z0-9_\$]+\b/igs;
+	$cost_details{'FOREIGN_OBJECT'} += $n*$UNCOVERED_MSSQL_SCORE{'FOREIGN_OBJECT'};
+	$n = () = $str =~ /\b(master|model|msdb|tempdb)\.\b/igs;
+	$cost_details{'SYS_OBJECT'} += $n*$UNCOVERED_MSSQL_SCORE{'SYS_OBJECT'};
+	$cost_details{'FOREIGN_OBJECT'} -= $n*$UNCOVERED_MSSQL_SCORE{'FOREIGN_OBJECT'};
+	if ($class->{local_schemas_regex})
+	{
+		$n = () = $str =~ /\b$class->{local_schemas_regex}\.[a-z0-9_\$]+\.[a-z0-9_\$]+\b/igs;
+		$cost_details{'FOREIGN_OBJECT'} -= $n*$UNCOVERED_MSSQL_SCORE{'FOREIGN_OBJECT'};
+	}
+	$n = () = $str =~ /[\s,]\s*sys[a-z]+/igs;
+	$cost_details{'SYS_OBJECT'} += $n*$UNCOVERED_MSSQL_SCORE{'SYS_OBJECT'};
+	$n = () = $str =~ /\b\#\#[a-z0-9_\$]+\b/igs;
+	$cost_details{'GLOBAL_TEMP_TABLE'} += $n*$UNCOVERED_MSSQL_SCORE{'GLOBAL_TEMP_TABLE'};
+	$n = () = $str =~ /(?<!INSERT)\s+INTO\s+[\#]+[a-z0-9_\$]+\b/igs;
+	$cost_details{'INTO_TEMP_TABLE'} += $n*$UNCOVERED_MSSQL_SCORE{'INTO_TEMP_TABLE'};
+	$n = () = $str =~ /\bSELECT\s+TOP\s+\d+\b/igs;
+	$cost_details{'SELECT_TOP'} += $n*$UNCOVERED_MSSQL_SCORE{'SELECT_TOP'};
+	$n = () = $str =~ /\sCOLLATE\s/igs;
+	$cost_details{'COLLATE'} += $n*$UNCOVERED_MSSQL_SCORE{'COLLATE'};
+	$n = () = $str =~ /\bRETURNS\s+TABLE\s/igs;
+	$cost_details{'RETURNS_TABLE'} += $n*$UNCOVERED_MSSQL_SCORE{'RETURNS_TABLE'};
+	$n = () = $str =~ /\bTODATETIMEOFFSET\s*\(/igs;
+	$cost_details{'TODATETIMEOFFSET'} += $n*$UNCOVERED_MSSQL_SCORE{'TODATETIMEOFFSET'};
+	$n = () = $str =~ /\bCURSOR\s*\(/igs;
+	$cost_details{'CURSOR'} += $n*$UNCOVERED_MSSQL_SCORE{'CURSOR'};
+	$n = () = $str =~ /\b\@\@[a-z0-9_\$]+/igs;
+	$cost_details{'GLOBAL_VARIABLE'} += $n*$UNCOVERED_MSSQL_SCORE{'GLOBAL_VARIABLE'};
+	$n = () = $str =~ /\b\@\@(ROWCOUNT|VERSION|LANGUAGE)/igs;
+	$cost_details{'GLOBAL_VARIABLE'} -= $n*$UNCOVERED_MSSQL_SCORE{'GLOBAL_VARIABLE'};
+	$n = () = $str =~ /\s+(UNPIVOT|PIVOT)\s*/igs;
+	$cost_details{'PIVOT'} += $n*$UNCOVERED_MSSQL_SCORE{'PIVOT'};
+	$n = () = $str =~ /\bBEGIN\s+TRY\s/igs;
+	$cost_details{'TRY_CATCH'} += $n*$UNCOVERED_MSSQL_SCORE{'TRY_CATCH'};
+	$n = () = $str =~ /EXEC.*(\.|\s)SP_[A-Z_]+\b/igs;
+	$cost_details{'SP_FCT'} += $n*$UNCOVERED_MSSQL_SCORE{'SP_FCT'};
+	$n = () = $str =~ /\.(value|nodes|query|exists|modify)\s*\(/igs;
+	$cost_details{'XML_FCT'} += $n*$UNCOVERED_MSSQL_SCORE{'XML_FCT'};
+
+	foreach my $t (keys %UNCOVERED_MSSQL_SCORE) {
+		$cost += $cost_details{$t} if (exists $cost_details{$t});
+	}
+
+	foreach my $f (@MSSQL_FUNCTIONS)
+	{
+		next if ($class->{use_mssqlfce} && $f =~ /^(DATEDIFF|STUFF|PATINDEX|ISNUMERIC|ISDATE|LEN|PRINT)$/);
+		if ($str =~ /\b$f\s*\(/igs)
+		{
+			$cost += 2;
+			$cost_details{$f} += 2;
+		}
+	}
+
+	return $cost, %cost_details;
+}
+
 sub replace_outer_join
 {
 	my ($class, $str, $type) = @_;
@@ -3567,6 +3786,11 @@ sub replace_connect_by
 
 	return $str if ($str !~ /\bCONNECT\s+BY\b/is);
 
+	my $into_clause = '';
+	if ($str =~ s/\s+INTO\s+(.*?)(\s+FROM\s+)/$2/is) {
+		$into_clause = " INTO $1";
+	}
+
 	my $final_query = "WITH RECURSIVE cte AS (\n";
 
 	# Remove NOCYCLE, not supported at now
@@ -3820,7 +4044,7 @@ sub replace_connect_by
 		$order_by =~ s/^, //s;
 		$order_by = " ORDER BY $order_by";
 	}
-	$final_query .= "\n) SELECT * FROM cte$where_clause$union$group_by$order_by";
+	$final_query .= "\n) SELECT *$into_clause FROM cte$where_clause$union$group_by$order_by";
 
 	return $final_query;
 }
@@ -3845,6 +4069,104 @@ sub replace_without_function
 	return $str;
 }
 
+=head2 mssql_to_plpgsql
+
+This function turn a MSSQL function code into a PLPGSQL code
+
+=cut
+
+sub mssql_to_plpgsql
+{
+        my ($class, $str) = @_;
+
+	# Replace getdate() with CURRENT_TIMESTAMP
+	$str =~ s/\bgetdate\s*\(\s*\)/date_trunc('millisecond', CURRENT_TIMESTAMP::timestamp)/ig;
+	# Replace user_name() with CURRENT_USER
+	$str =~ s/\buser_name\s*\(\s*\)/CURRENT_USER/gi;
+
+	# Remove call to with(nolock) from queries
+	$str =~ s/\bwith\s*\(\s*nolock\s*\)//ig;
+	$str =~ s/\bwith\s*schemabinding//ig;
+
+	# Replace call to SYS_GUID() function
+	$str =~ s/\bnewid\s*\(\s*\)/$class->{uuid_function}()/ig;
+
+	# Remove COUNT setting
+	$str =~ s/SET NOCOUNT (ON|OFF)[;\s]*//ig;
+
+	# Remove quoted definer order
+	$str =~ s/SET QUOTED_IDENTIFIER (ON|OFF)//igs;
+
+	# Replace BREAK by EXIT
+	$str =~ s/\bBREAK\s*[;]*$/EXIT;/ig;
+
+	# Rewrite call to sequences
+	while ($str =~ /NEXT VALUE FOR ([^\s]+)/i)
+	{
+		my $seqname = $1;
+		$seqname =~ s/[\[\]\)]+//g;
+		$str =~ s/[\(]*NEXT VALUE FOR ([^\s]+)/nextval('$seqname')/i;
+	}
+
+	####
+	# Replace some function with their PostgreSQL syntax
+	####
+	$str =~ s/\bDATALENGTH\s*\(/LENGTH(/gi;
+	$str =~ s/\bLEN\s*\(([^\)]+)\)/LENGTH(RTRIM($1))/gi;
+	$str =~ s/ISNULL\s*\(/COALESCE(/gi;
+	$str =~ s/SPACE\s*\(/REPEAT(' ', /gi;
+	$str =~ s/REPLICATE\s*\(/REPEAT(/gi;
+	$str =~ s/CHARINDEX\s*\(\s*(.*?)\s*,\s*(.*?)\s*,\s*(\d+)\)/position('$1' in substring($2 from $3))/gi;
+	$str =~ s/CHARINDEX\s*\(\s*(.*?)\s*,\s*(.*?)\s*\)/position('$1' in $2)/gi;
+	$str =~ s/DATEPART\s*\(\s*(.*?)\s*,\s*(.*?)\s*\)/date_part('$1', $2)/gi;
+	$str =~ s/DATEADD\s*\(\s*(.*?)\s*\,\s*(.*?)\s*,\s*(.*?)\s*\)/$3 + INTERVAL '$2 $1'/gi;
+	$str =~ s/CONVERT\s*\(\s*(.*?)\s*,\s*(.*?)\s*,\s*(\d+)\)/TO_CHAR($2, '$MSSQL_STYLE{$3}')::$1/gi;
+	$str =~ s/CONVERT\s*\(\s*NVARCHAR\s*(.*?)\s*\(\s*(.*?)\s*\s*\)\,\s*(.*?)\s*\)/CAST($3 AS varchar($2))/gi;
+	$str =~ s/CONVERT\s*\(\s*(.*?)\s*\(\s*(.*?)\s*\s*\),\s*(.*?)\s*\)/CAST($3 AS $1($2))/gi;
+	$str =~ s/CONVERT\s*\(\s*(.*?)\s*\,\s*(.*?)\s*\)/CAST($2 AS $1)/gi;
+	$str =~ s/\bRAND\s*\(/random(/gi;
+	$str =~ s/\bYEAR\s*\(/date_part('year', /gi;
+	$str =~ s/\bMONTH\s*\(/date_part('month', /gi;
+	$str =~ s/\bDAY\s*\(/date_part('day', /gi;
+	$str =~ s/\bSYSDATETIMEOFFSET\s*\(/now(/gi;
+	$str =~ s/\bSYSDATETIME\s*\(\s*\)/now()::timestamp/gi;
+	$str =~ s/\bSYSUTCDATETIME\s*\(\s*\)/now() at time zone 'UTC'/gi;
+	$str =~ s/\bDATENAME\s*\(\s*(?:weekday|dw|w)\s*,\s*([^\)]+)\s*\)/to_char($1, 'day')/gi;
+	$str =~ s/\bDATENAME\s*\(\s*(?:dayofyear|dy|y)\s*,\s*([^\)]+)\s*\)/date_part('doy', $1)/gi;
+	$str =~ s/\bDATENAME\s*\(\s*(?:month|mm|m)\s*,\s*([^\)]+)\s*\)/to_char($1, 'month')/gi;
+	$str =~ s/\bDATENAME\s*\(\s*(?:year|yy|y)\s*,/date_part('year',/gi;
+	$str =~ s/\bDATENAME\s*\(\s*(?:quarter|qq|q)\s*,/date_part('quarter',/gi;
+	$str =~ s/\bDATENAME\s*\(\s*(?:day|dd|d)\s*,/date_part('day',/gi;
+	$str =~ s/\bDATENAME\s*\(\s*(?:week|ww|wk)\s*,\s*([^\)]+)\s*\)/date_part('week', $1)+1/gi;
+	$str =~ s/\bDATENAME\s*\(\s*hour\s*,/date_part('hour',/gi;
+	$str =~ s/\bDATENAME\s*\(\s*(?:minute|mi|n)\s*,/date_part('minute',/gi;
+	$str =~ s/\bDATENAME\s*\(\s*(?:second|ss|s)\s*,/date_part('second',/gi;
+	$str =~ s/\bDATENAME\s*\(\s*(?:millisecond|ms)\s*,/date_part('millisecond',/gi;
+	$str =~ s/\bDATENAME\s*\(\s*(?:microsecond|mcs)\s*,/date_part('microsecond',/gi;
+	$str =~ s/\bDATENAME\s*\(\s*(?:nanosecond|ns)\s*,/date_part('nanosecond',/gi; # will fail, should be microsecond
+	$str =~ s/\bDATENAME\s*\(\s*(?:ISO_WEEK|ISOWK|ISOWW)\s*,\s*([^\)]+)\s*\)/date_part('week', $1)/gi;
+	$str =~ s/\bDATENAME\s*\(\s*(?:TZoffset|tz)\s*,\s*([^\)]+)\s*\)/to_char($1, 'TZH:TZM')/gi;
+
+	# Rewrite expression like SET p_MatchExpression =  '%'+p_MatchExpression+'%'
+	$str =~ s/\bSET\s+([^\s=;:]+)\s*=/$1 :=/igs;
+
+	# Fix IF ... BEGIN into IF ... THEN on single line
+	$str =~ s/(\s+IF[\s\(]+(?:.*?))\s+BEGIN\b/$1 THEN/igs;
+	# Fix WHILE ... BEGIN into IF ... THEN
+	$str =~ s/(\s+WHILE[\s\(]+(?:.*?))\s+BEGIN\b/$1 LOOP/igs;
+	# Fix ELSE IF into ELSIF
+	$str =~ s/\bELSE\s+IF\b/ELSIF/igs;
+
+	# Fix temporary table creation. We keep the # so that they can be identified in the code
+	$str =~ s/CREATE\s+TABLE\s+#/CREATE TEMPORARY TABLE #/igs;
+
+	# Replace identity function
+	$str =~ s/int identity\(\s*([\d+])\s*,\s*([\d+])\s*\)/int GENERATED BY DEFAULT AS IDENTITY (START WITH $1 INCREMENT BY $2)/igs;
+
+	return $str;
+}
+
+
 1;
 
 __END__
@@ -3857,7 +4179,7 @@ Gilles Darold <gilles@darold.net>
 
 =head1 COPYRIGHT
 
-Copyright (c) 2000-2022 Gilles Darold - All rights reserved.
+Copyright (c) 2000-2023 Gilles Darold - All rights reserved.
 
 This program is free software; you can redistribute it and/or modify it under
 the same terms as Perl itself.
diff --git a/packaging/README b/packaging/README
index 8e9c741..0fb3dff 100644
--- a/packaging/README
+++ b/packaging/README
@@ -12,13 +12,13 @@ RPM/
 
 	The binary package may be found here:
 
-		~/rpmbuild/RPMS/noarch/ora2pg-23.2-1.noarch.rpm
+		~/rpmbuild/RPMS/noarch/ora2pg-24.0-1.noarch.rpm
 	or
-		/usr/src/redhat/RPMS/i386/ora2pg-23.2-1.noarch.rpm
+		/usr/src/redhat/RPMS/i386/ora2pg-24.0-1.noarch.rpm
 
 	To install run:
 
-		rpm -i ~/rpmbuild/RPMS/noarch/ora2pg-23.2-1.noarch.rpm
+		rpm -i ~/rpmbuild/RPMS/noarch/ora2pg-24.0-1.noarch.rpm
 
 
 slackbuild/
@@ -30,11 +30,11 @@ slackbuild/
 	then take a look at /tmp/build/ to find the Slackware package.
 	To install run the following command:
 
-		installpkg /tmp/build/ora2pg-23.2-i386-1gda.tgz
+		installpkg /tmp/build/ora2pg-24.0-i386-1gda.tgz
 
 	or
 
-		installpkg /tmp/build/ora2pg-23.2-x86_64-1gda.tgz
+		installpkg /tmp/build/ora2pg-24.0-x86_64-1gda.tgz
 
 	following the architecture.
 
diff --git a/packaging/debian/ora2pg/DEBIAN/control b/packaging/debian/ora2pg/DEBIAN/control
index d1e2ca4..b17a059 100644
--- a/packaging/debian/ora2pg/DEBIAN/control
+++ b/packaging/debian/ora2pg/DEBIAN/control
@@ -1,5 +1,5 @@
 Package: ora2pg
-Version: 23.2
+Version: 24.0
 Priority: optional
 Architecture: all
 Essential: no
diff --git a/packaging/debian/ora2pg/DEBIAN/copyright b/packaging/debian/ora2pg/DEBIAN/copyright
index 211ce09..d7d1a59 100644
--- a/packaging/debian/ora2pg/DEBIAN/copyright
+++ b/packaging/debian/ora2pg/DEBIAN/copyright
@@ -13,7 +13,7 @@ Upstream Author(s):
 
 Copyright:
 
-    Copyright (c) 2000-2022 : Gilles Darold - All rights reserved
+    Copyright (c) 2000-2023 : Gilles Darold - All rights reserved
 
 License:
 
diff --git a/packaging/slackbuild/Ora2Pg.SlackBuild b/packaging/slackbuild/Ora2Pg.SlackBuild
index e32e332..d113b0d 100644
--- a/packaging/slackbuild/Ora2Pg.SlackBuild
+++ b/packaging/slackbuild/Ora2Pg.SlackBuild
@@ -12,7 +12,7 @@
 
 ## Fill these variables to your needs ##
 NAMESRC=${NAMESRC:-ora2pg}
-VERSION=${VERSION:-23.2}
+VERSION=${VERSION:-24.0}
 EXT=${EXT:-tar.bz2}
 NAMEPKG=${NAMEPKG:-ora2pg}
 PKGEXT=${PKGEXT:-tgz/txz}
diff --git a/packaging/slackbuild/Ora2Pg.info b/packaging/slackbuild/Ora2Pg.info
index a1d7b78..9c4ee64 100644
--- a/packaging/slackbuild/Ora2Pg.info
+++ b/packaging/slackbuild/Ora2Pg.info
@@ -1,7 +1,7 @@
 PRGNAM="Ora2Pg"
-VERSION="23.2"
+VERSION="24.0"
 HOMEPAGE="http://ora2pg.darold.net/"
-DOWNLOAD="http://downloads.sourceforge.net/ora2pg/ora2pg-23.2.tar.gz"
+DOWNLOAD="http://downloads.sourceforge.net/ora2pg/ora2pg-24.0.tar.gz"
 MD5SUM=""
 DOWNLOAD_x86_64="UNTESTED"
 MD5SUM_x86_64=""
diff --git a/scripts/ora2pg b/scripts/ora2pg
index a38fefb..5478b6b 100644
--- a/scripts/ora2pg
+++ b/scripts/ora2pg
@@ -3,7 +3,7 @@
 # Project  : Oracle to Postgresql converter
 # Name     : ora2pg
 # Author   : Gilles Darold, gilles _AT_ darold _DOT_ net
-# Copyright: Copyright (c) 2000-2022 : Gilles Darold - All rights reserved -
+# Copyright: Copyright (c) 2000-2023 : Gilles Darold - All rights reserved -
 # Function : Script used to convert Oracle Database to PostgreSQL
 # Usage    : ora2pg configuration_file
 #------------------------------------------------------------------------------
@@ -31,7 +31,7 @@ use POSIX qw(locale_h sys_wait_h _exit);
 setlocale(LC_NUMERIC, '');
 setlocale(LC_ALL,     'C');
 
-my $VERSION = '23.2';
+my $VERSION = '24.0';
 
 $| = 1;
 
@@ -73,6 +73,7 @@ my $PROJECT_BASE = '.';
 my $PRINT_HEADER = '';
 my $HUMAN_DAY_LIMIT;
 my $IS_MYSQL = 0;
+my $IS_MSSQL = 0;
 my $AUDIT_USER = '';
 my $PG_DSN = '';
 my $PG_USER = '';
@@ -93,7 +94,7 @@ my $CDC_READY = '';
 my $CDC_FILE = 'TABLES_SCN.log';
 my $DROP_IF_EXISTS = 0;
 
-my @SCHEMA_ARRAY  = qw( SEQUENCE TABLE PACKAGE VIEW GRANT TRIGGER FUNCTION PROCEDURE TABLESPACE PARTITION TYPE MVIEW DBLINK SYNONYM DIRECTORY );
+my @SCHEMA_ARRAY  = qw( SEQUENCE SEQUENCE_VALUES TABLE PACKAGE VIEW GRANT TRIGGER FUNCTION PROCEDURE TABLESPACE PARTITION TYPE MVIEW DBLINK SYNONYM DIRECTORY );
 my @EXTERNAL_ARRAY  = qw( KETTLE FDW );
 my @REPORT_ARRAY  = qw( SHOW_VERSION SHOW_REPORT SHOW_SCHEMA SHOW_TABLE SHOW_COLUMN SHOW_ENCODING  );
 my @TEST_ARRAY  = qw( TEST TEST_COUNT TEST_VIEW TEST_DATA);
@@ -104,6 +105,9 @@ my @CAPABILITIES  = qw( QUERY LOAD );
 my @MYSQL_SCHEMA_ARRAY  = qw( TABLE VIEW GRANT TRIGGER FUNCTION PROCEDURE PARTITION DBLINK );
 my @MYSQL_SOURCES_ARRAY = qw( VIEW TRIGGER FUNCTION PROCEDURE PARTITION );
 
+my @MSSQL_SCHEMA_ARRAY  = qw( SEQUENCE SEQUENCE_VALUES TABLE PACKAGE VIEW GRANT TRIGGER FUNCTION PROCEDURE TABLESPACE PARTITION TYPE MVIEW DBLINK SYNONYM DIRECTORY );
+my @MSSQL_SOURCES_ARRAY = qw( PACKAGE VIEW TRIGGER FUNCTION PROCEDURE PARTITION TYPE MVIEW );
+
 my @GRANT_OBJECTS_ARRAY = ('USER','TABLE','VIEW','MATERIALIZED VIEW','SEQUENCE','PROCEDURE','FUNCTION','PACKAGE BODY','TYPE','SYNONYM','DIRECTORY');
 
 my $TMP_DIR      = File::Spec->tmpdir() || '/tmp';
@@ -125,6 +129,7 @@ GetOptions (
         'l|log=s' => \$LOGFILE,
 	'L|limit=i' => \$DATA_LIMIT,
 	'm|mysql!' => \$IS_MYSQL,
+	'M|mssql!' => \$IS_MSSQL,
 	'n|namespace=s' => \$SCHEMA,
 	'N|pg_schema=s' => \$PG_SCHEMA,
         'o|out=s' => \$OUTFILE,
@@ -184,6 +189,11 @@ if ($IS_MYSQL)
 	@SCHEMA_ARRAY = @MYSQL_SCHEMA_ARRAY;
 	@SOURCES_ARRAY = @MYSQL_SOURCES_ARRAY;
 	@EXTERNAL_ARRAY = ();
+} elsif ($IS_MSSQL)
+{
+	@SCHEMA_ARRAY = @MSSQL_SCHEMA_ARRAY;
+	@SOURCES_ARRAY = @MSSQL_SOURCES_ARRAY;
+	@EXTERNAL_ARRAY = ();
 }
 
 # Create project repository and useful stuff
@@ -359,6 +369,7 @@ my $schema = new Ora2Pg (
 	print_header => $PRINT_HEADER,
 	human_days_limit => $HUMAN_DAY_LIMIT,
 	is_mysql => $IS_MYSQL,
+	is_mssql => $IS_MSSQL,
 	audit_user => $AUDIT_USER,
 	temp_dir => $TMP_DIR,
 	pg_dsn => $PG_DSN,
@@ -456,6 +467,7 @@ Usage: ora2pg [-dhpqv --estimate_cost --dump_as_html] [--option value]
     -L | --limit num  : Number of tuples extracted from Oracle and stored in
                         memory before writing, default: 10000.
     -m | --mysql      : Export a MySQL database instead of an Oracle schema.
+    -M | --mssql      : Export a Microsoft SQL Server database.
     -n | --namespace schema : Set the Oracle schema to extract from.
     -N | --pg_schema schema : Set PostgreSQL's search_path.
     -o | --out file   : Set the path to the output file where SQL will
@@ -573,8 +585,13 @@ sub create_project
 	foreach my $exp (sort @SCHEMA_ARRAY ) {
 		my $tpath = lc($exp);
 		$tpath =~ s/y$/ie/;
-		mkdir("$base_path/schema/" . $tpath . 's');
-		print "\t\t" . $tpath . "s/\n";
+		if ($exp ne 'SEQUENCE_VALUES') {
+			mkdir("$base_path/schema/" . $tpath . 's');
+			print "\t\t" . $tpath . "s/\n";
+		} else {
+			mkdir("$base_path/schema/" . $tpath);
+			print "\t\t" . $tpath . "/\n";
+		}
 	}
 	mkdir("$base_path/sources");
 	print "\tsources/\n";
@@ -639,6 +656,7 @@ for etype in \$(echo \$EXPORT_TYPE | tr " " "\\n")
 do
         ltype=`echo \$etype | tr '[:upper:]' '[:lower:]'`
         ltype=`echo \$ltype | sed 's/y\$/ie/'`
+        ltype=`echo \$ltype | sed 's/s\$//'`
         echo "Running: ora2pg -p -t \$etype -o \$ltype.sql -b \$namespace/schema/\$\{ltype\}s -c \$namespace/config/ora2pg.conf"
         ora2pg -p -t \$etype -o \$ltype.sql -b \$namespace/schema/\$\{ltype\}s -c \$namespace/config/ora2pg.conf
 	ret=`grep "Nothing found" \$namespace/schema/\$\{ltype\}s/\$ltype.sql 2> /dev/null`
@@ -700,6 +718,7 @@ ora2pg -t SHOW_REPORT -c \$namespace/config/ora2pg.conf --dump_as_html --cost_un
 foreach (\$etype in \$EXPORT_TYPE)
 {
 	\$ltype =  \$etype.ToLower() -replace 'y\$', 'ie'  
+	\$ltype =  \$etype.ToLower() -replace 's\$', ''
 	\$cmd="ora2pg -p -t \$etype -o \$ltype.sql -b \$namespace/schema/\${ltype}s -c \$namespace/config/ora2pg.conf"  
 	Write-Host  "Running: \$cmd"
 	Invoke-Expression \$cmd
@@ -760,6 +779,9 @@ sub make_config_generic
 			$conf_arr->[$i] =~ s/^(ORACLE_DSN.*dbi):Oracle:(.*);sid=SIDNAME/$1:mysql:$2;database=dbname/;
 			$conf_arr->[$i] =~ s/CHANGE_THIS_SCHEMA_NAME/CHANGE_THIS_DB_NAME/;
 			$conf_arr->[$i] =~ s/#REPLACE_ZERO_DATE.*/REPLACE_ZERO_DATE\t-INFINITY/;
+		} elsif ($IS_MSSQL) {
+			$conf_arr->[$i] =~ s/^# Set Oracle database/# Set MSSQL database/;
+			$conf_arr->[$i] =~ s/^(ORACLE_DSN.*dbi):Oracle:(.*);sid=SIDNAME/$1:ODBC:driver=msodbcsql18;$2;database=dbname/;
 		} elsif ($ENV{ORACLE_HOME}) {
 			$conf_arr->[$i] =~ s/^ORACLE_HOME.*/ORACLE_HOME\t$ENV{ORACLE_HOME}/;
 		}
@@ -1145,6 +1167,7 @@ if [ $IMPORT_DATA -eq 0 ]; then
 
 		ltype=`echo $etype | tr '[:upper:]' '[:lower:]'`
 		ltype=`echo $ltype | sed 's/y$/ie/'`
+		ltype=`echo $ltype | sed 's/s$//'`
 		if [ -r "$NAMESPACE/schema/${ltype}s/$ltype.sql" ]; then
 			if confirm "Would you like to import $etype from $NAMESPACE/schema/${ltype}s/$ltype.sql?" ; then
 				echo "Running: psql --single-transaction $DB_HOST$DB_PORT -U $DB_OWNER -d $DB_NAME -f $NAMESPACE/schema/${ltype}s/$ltype.sql"
diff --git a/scripts/ora2pg_scanner b/scripts/ora2pg_scanner
index b7b670e..ee1e8b6 100644
--- a/scripts/ora2pg_scanner
+++ b/scripts/ora2pg_scanner
@@ -3,7 +3,7 @@
 # Project  : Oracle to Postgresql converter
 # Name     : ora2pg_scanner
 # Author   : Gilles Darold, gilles _AT_ darold _DOT_ net
-# Copyright: Copyright (c) 2000-2022 : Gilles Darold - All rights reserved -
+# Copyright: Copyright (c) 2000-2023 : Gilles Darold - All rights reserved -
 # Function : Script used to scan a list of DSN and generate reports
 # Usage    : ora2pg_scanner -l dsn_csv_file -o outdir
 #------------------------------------------------------------------------------
@@ -26,7 +26,7 @@ use strict;
 
 use Getopt::Long qw(:config no_ignore_case bundling);
 
-my $VERSION = '23.2';
+my $VERSION = '24.0';
 
 my @DB_DNS = ();
 my $OUTDIR = '';
@@ -82,10 +82,11 @@ while (my $l = <IN>)
 	#"type","schema/database","dsn","user","password","audit users"
 	#"MYSQL","sakila","dbi:mysql:host=192.168.1.10;database=sakila;port=3306","root","mysecret"
 	#"ORACLE","HR","dbi:Oracle:host=192.168.1.10;sid=XE;port=1521","system","manager","hr;system;scott"
+	#"MSSQL","HR","dbi:ODBC:driver=msodbcsql18;server=srv.database.windows.net;database=testdb","usrname","pwd"
 	# skip header line
 	chomp($l);
 	$l =~ s/\r//;
-	next if ($l !~ /^["]*(MYSQL|ORACLE)["]*,/i);
+	next if ($l !~ /^["]*(MYSQL|ORACLE|MSSQL)["]*,/i);
 	$l =~ s/"//gs;
 	my @data = split(/,/, $l);
 	if ($#data < 4)
@@ -137,6 +138,7 @@ for (my $i = 0; $i < @DB_DNS; $i++)
 	my $info = '';
 	# Set RDBMS type
 	$info = ' -m' if ($DB_DNS[$i]->{type} eq 'MYSQL');
+	$info = ' -M' if ($DB_DNS[$i]->{type} eq 'MSSQL');
 	# Add custom configuration file if set
 	$info .= ' -c ' . $CONF_FILE if ($CONF_FILE);
 	my $cmd_ora2pg = $ORA2PG_CMD . $info;
@@ -165,7 +167,7 @@ for (my $i = 0; $i < @DB_DNS; $i++)
 	}
 
 	# Extract host
-	if ($DB_DNS[$i]->{dsn} =~ m/host=([^;]+)/ || $DB_DNS[$i]->{dsn} =~ m/dbi:Oracle:\/\/([^\/]+)/)
+	if ($DB_DNS[$i]->{dsn} =~ m/(?:host|server)=([^;]+)/ || $DB_DNS[$i]->{dsn} =~ m/dbi:Oracle:\/\/([^\/]+)/)
 	{
 		$DB_DNS[$i]->{host} = $1;
 		$DB_DNS[$i]->{host} =~ s/:\d$+//;
@@ -258,6 +260,7 @@ Usage: ora2pg_scanner -l CSVFILE [-o OUTDIR]
 	"type","schema/database","dsn","user","password"
 	"MYSQL","sakila","dbi:mysql:host=192.168.1.10;database=sakila;port=3306","root","secret"
 	"ORACLE","HR","dbi:Oracle:host=192.168.1.10;sid=XE;port=1521","system","manager"
+	"MSSQL","HR","dbi:ODBC:driver=msodbcsql18;server=srv.database.windows.net;database=testdb","system","manager"
 
    The CSV field separator must be a comma.
 
@@ -268,6 +271,7 @@ Usage: ora2pg_scanner -l CSVFILE [-o OUTDIR]
    For example:
 
 	"ORACLE","","dbi:Oracle:host=192.168.1.10;sid=XE;port=1521","system","manager"
+	"MSSQL","","dbi:ODBC:driver=msodbcsql18;server=srv.database.windows.net;database=testdb","system","manager"
 
    will generate a report for all schema in the XE instance. Note that in this
    case the SCHEMA directive in ora2pg.conf must not be set.

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/perl5/Ora2Pg/MSSQL.pm

No differences were encountered in the control files

More details

Full run details