# FastCopyHelper.java

    /*
    ** Module   : FastCopyHelper.java
    ** Abstract : The core of fast-copy technique implementation
    **
    ** Copyright (c) 2020-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 AIL 20201201 First revision.
    ** 002 AIL 20201203 Added append mode support.
    **         20201215 Temporarily avoided fast-copy for loose-copy-mode.
    **         20201216 Read support for loose-copy-mode.
    **     OM  20210223 Invalidate index-based cache in the event of low-level SQL operations. Replaced iterated
    **                  string concatenation with append chaining.
    **     CA  20210506 Unique constraints sub-selects must use a different alias for the table.
    **     ECF 20210506 Use RecordBuffer.getDmoInfo() getter instead of direct field access to prevent NPE in
    **                  proxy case.
    **     CA  20210526 Fixed computeUniqueConstraintsClauses - the dst/src lists were reversed.
    **     CA  20210928 Fixed getMapping - the pkMappingTableName parameter must be used.
    **     OM  20211020 Added _DATASOURCE_ROWID property.
    **     OM  20220727 FieldId and PropertyId are different for denormalized extent fields.
    **     DDF 20220823 Replaced .nextvalue with next value for.
    **     ECF 20221006 Minor optimization.
    **     OM  20220913 Enhanced dialect API for sorting nulls.
    **     OM  20221012 Fixed regression in previous commit.
    **     RAA 20230109 Changed inline statement(s) to prepared statement(s).
    **     SVL 20230110 P2JIndex.components() provides direct access to the components array in order to improve
    **                  performance.
    **     SVL 20230113 Improved performance by replacing some "for-each" loops with indexed "for" loops.
    **     RAA 20230314 Multiplex id is now an argument rather than being hard-coded. This takes effect in
    **                  copyTable and safeTableCopy.
    **     DDF 20230306 Made a change so that temp-tables don't use computed columns anymore (refs: #7108).
    ** 003 DDF 20230523 Use wildcard when possible instead of using all columns into the copy statement.
    **     DDF 20230524 Fix uid usage and replace ALLOW_WILDCARD with dialect specific method.
    **     DDF 20230525 Add flag that enables uid usage. At the moment, usage of uid for intermediate table
    **                  increases the size of the psCache.
    ** 004 DDF 20230606 Added a new copy method that doesn't use mapping.
    **     DDF 20230612 Fix missing records when doing a safe copy without mapping.
    **     SR  20230721 Let append be false when copying tables if the destination is empty.
    **     AL2 20230826 Reset loose-copy flag if the property matching is right (source matches destination).
    **     SR  20230726 Updated updatePeerRecords to use preparedStatements instead of plain SQL statements.
    **     RAA 20230807 Created FastCopyBuilder and a cache for instances of FastCopyHelper.
    **                  Refactored class so it doesn't contain static methods anymore (apart from cache usage).
    **     RAA 20230823 Merged master and before processes and added a bundle that stores the SQLs which will be
    **                  executed.
    **     RAA 20230828 Removed SQL storing variables. Got rid of the propagated "append" flag.
    **     RAA 20230830 Removed tryExecuteCopy and changed so that before copy can access safeTableCopy.
    **     RAA 20230904 Renamed finishCopy into executeCopyImpl, which is now called from executeCopy.
    **     AL2 20230905 Fixed noMapping flag to be part of the helper, not the context.
    **                  Removed replaceMode from fast-copy as it is not supported; removed CopyMeta.
    ** 005 AL2 20230918 Fixed regression when safeTableCopyNoMapping was not honoring unique constraints.
    **     AL2 20230919 Fixed regression where updatePeerRecords may prematurely stop.
    ** 006 AL2 20231012 Replaced getCurrentPrimaryKey with nextPrimaryKey.
    ** 007 AL2 20231101 Created the intermediary tables only once per session and reuse between copies.
    ** 008 AL2 20231103 Made the dropMappings use the session instead of the persistence.
    ** 009 AL2 20231103 Replaced IF NOT EXISTS with IF EXISTS from drop.
    ** 010 AL2 20240512 Allow H2 to provide the PK instead of a separate sequence to honor reclaiming.
    **     AL2 20240513 Fast copy with non-expanded extent fields should also create an in-memory PK mapping.
    **     AL2 20240514 Removed FastCopyBundlePKElement.
    ** 011 SP  20240523 Fixed a for loop in safeTableCopy() when setting arguments for generatePkMappingArgs
    ** 012 OM  20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
    **                  based on [sharedDb] parameter.
    ** 013 DDF 20240205 Added the mandatory value of the index component to orderByNulls call.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.persist;

    import java.sql.Savepoint;
    import java.util.*;
    import java.util.concurrent.atomic.AtomicLong;
    import java.util.function.Function;
    import java.util.function.Supplier;

    import com.goldencode.p2j.persist.RecordBuffer.DatumAccess;
    import com.goldencode.p2j.persist.dialect.Dialect;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.util.*;

    /**
     * This helper is used when we need to copy records from one temp-table to another in bulk mode.
     * Using statements like "insert into select from" significantly improves the time performance.
     * However this technique can be used only when the source and destination tables are compatible.
     * Also, this helper operates directly with the persistence layer (without using intermediate buffer
     * validations and such). This means that we need to ensure at SQL level that the records are copied
     * correctly.
     */
    public class FastCopyHelper
    {
       /** Name of the multiplex ID field in temp table DMO classes */
       private static final String MULTIPLEX_FIELD_NAME = TemporaryBuffer.MULTIPLEX_FIELD_NAME;

       /** Name of a generic use intermediate table, used for storing the indexes of an extent */
       private static final String LIST__INDEX__TABLE = "LIST__INDEX__TABLE";

       /** Name of the field in a database-level pk mapping, which stores a destination pk */
       private static final String DST__PK = "DST__PK";

       /** Name of the field in a database-level pk mapping, which stores a source pk */
       private static final String SRC__PK = "SRC__PK";

       /** The name of the db level mapping used intermediary by the copy process for master tables*/
       private static final String MASTER_PK_MAPPING = "MASTER__PK__MAPPING";

       /** The name of the db level mapping used intermediary by the copy process for before tables*/
       private static final String BEFORE_PK_MAPPING = "BEFORE__PK__MAPPING";

       /** Flag used to enable usage of uid for intermediate tables */
       private static final boolean ALLOW_UID = false;

       /** A value used to increment the uid upon instantiation of FastCopyHelper. */
       private static final AtomicLong uidCounter = new AtomicLong(0);

       /** The bundle of SQLs that will be executed to perform the copy process. */
       private final FastCopyBundle fastCopyBundle = new FastCopyBundle();

       /** Flag which indicates that this is a simpleCopy (no need for property mapping) */
       private final boolean simpleCopy;

       /** Flag that indicates whether copying should be executed in APPEND mode or not. */
       private final boolean appendMode;

       /** Flag that indicates if this is a loose copy process. */
       private final boolean looseCopyMode;

       /** Variable that stores the result of the {@code canUseFastCopy} function. */
       private Boolean canFastCopy;

       /** Name of a generic use temporary table, used for intermediary operations. */
       private final String pkMappingName;

       /** Flag set when a database level mapping exists for the copy operation. */
       private boolean generatesPkMapping;

       /** The persistence of the source. */
       private Persistence srcPersistence;

       /** The meta information of the source. */
       private DmoMeta srcMeta;

       /** The meta information of the destination. */
       private DmoMeta dstMeta;

       /** The DMO interface of the source. */
       private Class<? extends DataModelObject> srcDmoInterface;

       /** The DMO interface of the destination. */
       private Class<? extends DataModelObject> dstDmoInterface;

       /** The dialect of the source. */
       private Dialect srcDialect;

       /** The source table name. */
       private String srcTableName;

       /** The destination table name. */
       private String dstTableName;

       /** The source index name. */
       private String srcImplicitIndexName;

       /** The source index name used in SQLs. */
       private String srcImplicitSqlIndexName;

       /** A property mapping in case this is not a simple copy. */
       private Map<DatumAccess, DatumAccess> propsMap;

       /** The list of properties specific to the source temp-table. */
       private List<Property> srcSimpleProps;

       /** The list of properties specific to the destination temp-table. */
       private List<Property> dstSimpleProps;

       /**
        * A collection of extent fields. The key is the extent.
        * The value is a list of two properties (the source property and destination property).
        */
       private Map<Integer, List<Property[]>> extentFields;

       /** A cache for the sort clause used when copying tables. */
       private String sort;

       /** A value used to generate unique names for the tables used for intermediate operations and storing. */
       private long uid;

       /** {@code true} if this fast-copy helper will force the creation of an intermediate PK mapping */
       private boolean forceMapping;

       /**
        * Create the in-database mappings which are used for intermediary steps in the fast-copy process.
        * Complex copy attempts implying extent tables and before tables, require a PK mapping to join with.
        * This PK mapping is stored in these intermediary tables.
        *
        * Creating only one pair of tables per session (one for master and one for before) is an optimization.
        * Creating and dropping at each fast-copy bundle will force the reparsing of the statements using the
        * PK mapping tables.
        *
        * @param   persistence
        *          The persistence in which the tables should be created.
        */
       public static void createMappings(Persistence persistence)
       {
          try
          {
             String fields = " (" + DST__PK + " bigint, " + SRC__PK + " bigint, " + TempRecord._PEER_ROWID + " bigint)";
             persistence.executeSQL(
                   "create local temporary table if not exists " + MASTER_PK_MAPPING + fields + " transactional",
                   Persistence.TEMP_CTX);
             persistence.executeSQL(
                   "create local temporary table if not exists " + BEFORE_PK_MAPPING + fields + " transactional",
                   Persistence.TEMP_CTX);
          }
          catch (PersistenceException exc)
          {
             throw new IllegalStateException(exc);
          }
       }

       /**
        * Drop the in-database mappings which are used for intermediary steps in the fast-copy process.
        * Use the session instead of the persistence as this is usually called in a clean-up routine where we don't have
        * access to the context anymore
        *
        * @param   session
        *          The persistence from which the tables should be dropped.
        *
        * @throws  PersistenceException
        *          This is thrown if the persistence layer fails
        */
       public static void dropMappings(Session session)
       throws PersistenceException
       {
          Session.createSQLQuery("drop table if exists " + MASTER_PK_MAPPING + " transactional").executeUpdate(session);
          Session.createSQLQuery("drop table if exists " + BEFORE_PK_MAPPING + " transactional").executeUpdate(session);
       }

       /**
        * Constructor that is based on the context given.
        *
        * @param   fastCopyContext
        *          The context that has the information required.
        */
       private FastCopyHelper(FastCopyContext fastCopyContext)
       {
          this.simpleCopy = fastCopyContext.simpleCopy;
          this.propsMap = fastCopyContext.propsMap;
          this.appendMode = fastCopyContext.appendMode;
          this.looseCopyMode = fastCopyContext.looseCopyMode;
          this.forceMapping = fastCopyContext.forceMapping;
          this.uid = uidCounter.getAndIncrement();
          this.pkMappingName = fastCopyContext.isBefore ? BEFORE_PK_MAPPING : MASTER_PK_MAPPING;

          initialize(fastCopyContext.srcBuf, fastCopyContext.dstBuf);
       }

       /**
        * This method will update the peer values in the master and before tables. This is mandatory
        * as long as before tables exist. Otherwise, inconsistencies may appear.
        *
        * @param   masterCopyHelper
        *          The master instance that handled the master copy.
        * @param   masterCopyContext
        *          The master context that handled the master copy.
        * @param   beforeCopyHelper
        *          The before instance that handled the before copy.
        * @param   beforeCopyContext
        *          The before context that handled the before copy.
        */
       static void updatePeerRecords(FastCopyHelper  masterCopyHelper,
                                     FastCopyContext masterCopyContext,
                                     FastCopyHelper  beforeCopyHelper,
                                     FastCopyContext beforeCopyContext)
       throws PersistenceException
       {
          // TODO: check if we can't update peer records using the database level mapping
          //       in order to avoid additional mapping query and sequential update statements
          Map<Long, Long[]> srcToDstPks = masterCopyHelper.getMapping(masterCopyContext);
          Map<Long, Long[]> srcB4ToDstB4Pks = beforeCopyHelper.getMapping(beforeCopyContext);

          Iterator<Long[]> it = srcToDstPks.values().iterator();
          Iterator<Long[]> itB4 = srcB4ToDstB4Pks.values().iterator();

          String sql = beforeCopyHelper.getDstTableSql();

          Supplier<Object[]> supp = () ->
          {
             Long[] result = null;
             while (it.hasNext() && (result == null || result[1] == null))
             {
                result = it.next();
             }

             if (result == null || result[1] == null)
             {
                return null;
             }

             Long dstPk = result[0];
             Long srcPeerId = result[1];
             Long dstB4Pk = srcB4ToDstB4Pks.get(srcPeerId)[0];
             return new Object[] {dstPk, dstB4Pk};

          };
          beforeCopyHelper.srcPersistence.executeSQLBatch(sql, Persistence.TEMP_CTX, supp);

          sql = masterCopyHelper.getDstTableSql();

          Supplier<Object[]> b4Supp = () ->
          {
             Long[] result = null;
             while (itB4.hasNext() && (result == null || result[1] == null))
             {
                result = itB4.next();
             }

             if (result == null || result[1] == null)
             {
                return null;
             }

             Long dstB4Pk = result[0];
             Long srcB4PeerId = result[1];
             Long dstPk = srcToDstPks.get(srcB4PeerId)[0];
             return new Object[] {dstB4Pk, dstPk};
          };
          masterCopyHelper.srcPersistence.executeSQLBatch(sql, Persistence.TEMP_CTX, b4Supp);
       }

       /**
        * Performance improved variant, when we know the destination table can have its
        * records inserted via <code>INSERT INTO ... SELECT FROM</code>, without having to do any validation on
        * the records, as the destination table will accept them all.
        *
        * If the copy bundle is already computed, then this method will simply determine the execution of the
        * SQLs. Otherwise, It will generate them and then it will execute.
        *
        * @param   fastCopyContext
        *          The context that has the required information.
        * @param   srcRecId
        *          A source record id for which a destination record id should be computed at the end.
        *          If this is null, then null will be returned.
        *
        * @return  The recid in the destination equivalent to the provided source recid if not {@code null}.
        */
       rowid executeCopy(FastCopyContext fastCopyContext, rowid srcRecId)
       throws PersistenceException
       {
          if (fastCopyBundle.isBundleComputed())
          {
             return executeCopyImpl(fastCopyContext, srcRecId);
          }

          boolean loose = !processProperties();
          copyTable(fastCopyContext, loose);
          if (extentFields != null && !extentFields.isEmpty())
          {
             copyExtentFields(fastCopyContext);
          }

          return executeCopyImpl(fastCopyContext, srcRecId);
       }

       /**
        * This should be used to determine if this helper can be used in order to execute a fast copy
        * on the provided temp-tables.
        *
        * @return  {@code true} if this helper can be used for fast-copy.
        */
       boolean canUseFastCopy()
       {
          if (canFastCopy != null)
          {
             return canFastCopy;
          }

          if (!DMOSignatureHelper.validTempTableBulkCopy(srcMeta.getSignature(),
                                                         dstMeta.getSignature(),
                                                         looseCopyMode))
          {
             canFastCopy = false;
             return false;
          }

          if (looseCopyMode)
          {
             // make sure that all unique constraints are satisfied by the copied fields
             // in other words, avoid fast copy if we know that there are fields in the unique constraints
             // which will be set on the default value
             Set<Integer> destinationProps = new HashSet<>();
             if (propsMap != null)
             {
                for (Map.Entry<DatumAccess, DatumAccess> entry : propsMap.entrySet())
                {
                   destinationProps.add(entry.getValue().getPropertyMeta().getId());
                }
             }

             ArrayList<List<Property>> uniqueConstraints = dstMeta.getUniqueConstraintsAsProps();
             for (int i = 0; i < uniqueConstraints.size(); i++)
             {
                List<Property> props = uniqueConstraints.get(i);
                for (int j = 0; j < props.size(); j++)
                {
                   Property prop = props.get(j);
                   if (!destinationProps.contains(prop.propId))
                   {
                      canFastCopy = false;
                      return false;
                   }
                }
             }
          }

          canFastCopy = true;
          return true;
       }

       /**
        * Particular method which clears the intermediate database-level pk mapping for the table copy.
        * This will eventually drop any such table.
        *
        * @param   ctx
        *          The fast-copy context in which this pk mapping clear is done.
        */
       void clear(FastCopyContext ctx)
       throws PersistenceException
       {
          if (ctx.hasMapping)
          {
             // TODO: replace with truncate table once FWD-H2 supports transactional for TRUNCATE TABLE
             srcPersistence.executeSQL(
                   "delete from " + assembleUidMapping(pkMappingName) /* + transactional */,
                   Persistence.TEMP_CTX);
             ctx.hasMapping = false;
          }
       }

       /**
        * Given the buffers used, initialize the fields that will be required in this class.
        *
        * @param   srcBuf
        *          The source buffer.
        * @param   dstBuf
        *          The destination buffer.
        */
       private void initialize(TemporaryBuffer srcBuf, TemporaryBuffer dstBuf)
       {
          if (srcBuf != null)
          {
             srcMeta = srcBuf.getDmoInfo();
             srcPersistence = srcBuf.getPersistence();
             srcDmoInterface = srcBuf.getDMOInterface();
             srcDialect = srcBuf.getDialect();
             srcTableName = srcBuf.getDmoInfo().getSqlTableName();
             srcImplicitIndexName = srcBuf.getImplicitIndexName(srcDmoInterface);
             srcImplicitSqlIndexName = srcBuf.getImplicitSqlIndexName(srcDmoInterface);
          }

          if (dstBuf != null)
          {
             dstMeta = dstBuf.getDmoInfo();
             dstDmoInterface = dstBuf.getDMOInterface();
             dstTableName = dstBuf.getDmoInfo().getSqlTableName();
          }
       }

       /**
        * Method used to execute the SQLs for the copy process and, if needed, set the destination rowid.
        *
        * @param   fastCopyContext
        *          The context with the required information.
        * @param   srcRecId
        *          The {@link rowid} of the current record in the source buffer - if not null, it will be set in
        *          the destination buffer, too.
        *
        * @return  The destination {@code rowid}.
        */
       private rowid executeCopyImpl(FastCopyContext fastCopyContext, rowid srcRecId)
       throws PersistenceException
       {
          // execute the SQL bundle
          fastCopyBundle.executeSqls(fastCopyContext);

          if (generatesPkMapping)
          {
             fastCopyContext.hasMapping = true;
          }

          rowid result = null;
          if (srcRecId != null)
          {
             Map<Long, Long[]> pkMapping = getMapping(fastCopyContext);
             result = new rowid(pkMapping.get(srcRecId.getValue())[0]);
          }

          return result;
       }

       /**
        * The main procedure involved when copying the records from a source to a destination.
        * This processes the tables and executes a fast table copy SQL meant to insert into the destination
        * a record selection from the source.
        * The properties given as parameters are already ordered such that the insert SQL will automatically
        * match the correct fields.
        *
        * @param   fastCopyContext
        *          The context that has the required information.
        * @param   srcSimpleProps
        *          A list of properties in the source table.
        * @param   dstSimpleProps
        *          A list of properties in the destination table.
        * @param   sort
        *          The sort clause which should be used when computing the insert SQL.
        * @param   looseCopy
        *          If the copy is in loose mode.
        *
        * @return  The bundle element whose execution will do the copy.
        */
       private FastCopyBundleElement copyTable(FastCopyContext fastCopyContext,
                                               List<Property> srcSimpleProps,
                                               List<Property> dstSimpleProps,
                                               String sort,
                                               boolean looseCopy)
       {
          // compute the columns which should be involved in the insertion in the right order
          StringBuilder columnListSrc;
          StringBuilder columnListDst;

          List<Object> args = new ArrayList<>();
          if (!srcDialect.allowSelectWildcard() ||
              looseCopy ||
              srcMeta.hasComputedColumns(false) ||
              dstMeta.hasComputedColumns(false))
          {
             columnListSrc = getSrcTempColumns(dstTableName);
             columnListDst = getDstTempColumns();
             for (int i = 0; i < srcSimpleProps.size(); i++)
             {
                appendColumnName(srcMeta,
                                 dstMeta,
                                 srcSimpleProps.get(i),
                                 dstSimpleProps.get(i),
                                 columnListSrc,
                                 columnListDst,
                                 args);
             }
          }
          else
          {
             columnListSrc = getSrcTempWildcard(dstTableName, srcTableName);
             columnListDst = new StringBuilder();
          }

          String sqlInsert = getFastTableCopySql(srcTableName,
                                                 dstTableName,
                                                 columnListSrc.toString(),
                                                 columnListDst.toString(),
                                                 srcImplicitSqlIndexName,
                                                 sort);

          Function<FastCopyContext, Object[]> generateArgs = (context) ->
          {
             int argSize = args.size();
             Object[] arguments = new Object[argSize + 3];
             arguments[0] = context.dstBuf.getMultiplexID();
             arguments[1] = context.dstBuf.getMultiplexID();
             for (int i = 0; i < argSize; i++)
             {
                arguments[i + 2] = args.get(i);
             }

             arguments[argSize + 2] = context.srcBuf.getMultiplexID();
             return arguments;
          };

          return new FastCopyBundleElement(sqlInsert, generateArgs);
       }

       /**
        * A more complex way to copy a temp-table which ensures that the unique constraints won't
        * be violated after copy. This only checks that the new records are not already in the
        * destination (according to any unique constraint). It does not check for duplicates in the
        * source; this should be ruled out by the compatibility checker {@link #canUseFastCopy()}.
        * This technique uses a pre-copy pk mapping, which means that at first we will have a
        * preselection of the records from the source. Afterwards, we apply a "shadow" copy, meaning
        * that we won't check the constraints anymore; we just copy the preselected records from
        * the source to the destination.
        * This will create a pk mapping. This won't allow post-copy pk mappings as they won't be safe.
        *
        * @param   fastCopyContext
        *          The context that has the required information.
        * @param   srcSimpleProps
        *          A list of properties in the source table.
        * @param   dstSimpleProps
        *          A list of properties in the destination table.
        * @param   sort
        *          The sort clause which should be used when computing the insert SQL.
        * @param   pkMappingTableName
        *          The name of the intermediate table which will be used for pk mapping.
        * @param   looseCopy
        *          If the copy is in loose mode.
        *
        * @return  The bundle element whose execution will do the copy.
        */
       private FastCopyBundleElement safeTableCopy(FastCopyContext fastCopyContext,
                                                   List<Property> srcSimpleProps,
                                                   List<Property> dstSimpleProps,
                                                   String sort,
                                                   String pkMappingTableName,
                                                   boolean looseCopy)
       {
          List<String> whereClauses = computeUniqueConstraintsClauses(srcSimpleProps, dstSimpleProps);

          String pkMappingSql = getPkMappingSql(srcTableName,
                                                dstTableName,
                                                srcImplicitSqlIndexName,
                                                whereClauses,
                                                sort,
                                                pkMappingTableName);

          Function<FastCopyContext, Object[]> generatePkMappingArgs = (context) ->
          {
             int size = whereClauses.size();
             Object[] arguments = new Object[size + 2];
             Integer multiplexId = context.dstBuf.getMultiplexID();
             arguments[0] = multiplexId;
             arguments[1] = context.srcBuf.getMultiplexID();
             for (int i = 0; i < size; i++)
             {
                arguments[i + 2] = multiplexId;
             }

             return arguments;
          };

          fastCopyBundle.addBundleElement(new FastCopyBundleElement(pkMappingSql, generatePkMappingArgs));

          // we start preparing the shadow copy sql
          StringBuilder columnListSrc;
          StringBuilder columnListDst;

          List<Object> args = new ArrayList<>();
          if (!srcDialect.allowSelectWildcard() ||
              looseCopy ||
              srcMeta.hasComputedColumns(false) ||
              dstMeta.hasComputedColumns(false))
          {
             columnListSrc = getSrcMappedColumns(pkMappingTableName);
             columnListDst = getDstTempColumns();
             for (int i = 0; i < srcSimpleProps.size(); i++)
             {
                appendColumnName(srcMeta,
                                 dstMeta,
                                 srcSimpleProps.get(i),
                                 dstSimpleProps.get(i),
                                 columnListSrc,
                                 columnListDst,
                                 args);
             }
          }
          else
          {
             columnListSrc = getSrcMappedWildcard(pkMappingTableName, srcTableName);
             columnListDst = new StringBuilder();
          }

          // we already have a pk mapping at database-level
          // using that pk mapping, copy rows from source to destination, considering that
          // the pk mapping is constraint safe (the unique constraints are already checked,
          // the order is correct and the _multiplex is the one desired)

          String shadowCopySQL = getShadowCopySql(srcTableName,
                                                  dstTableName,
                                                  columnListSrc.toString(),
                                                  columnListDst.toString(),
                                                  sort,
                                                  pkMappingTableName);

          Function<FastCopyContext, Object[]> generateShadowCopyArgs = (context) ->
          {
             int size = args.size();
             Object[] arguments = new Object[size + 1];
             arguments[0] = context.dstBuf.getMultiplexID();
             for (int i = 0; i < size; i++)
             {
                arguments[i + 1] = args.get(i);
             }

             return arguments;
          };

          return new FastCopyBundleElement(shadowCopySQL,
                                           generateShadowCopyArgs);
       }

       /**
        * Handles the copy of the records from a source to a destination without using
        * intermediate tables. This type of copy is done only in append mode when there
        * is no need for mapping afterwards for extents/before table/destRecId. The mapping
        * is replaced by "inlining" it into the where clause and using an additional
        * condition for the recid to avoid an infinite loop.
        *
        * @param   fastCopyContext
        *          The context that has the required information.
        * @param   srcSimpleProps
        *          A list of properties in the source table.
        * @param   dstSimpleProps
        *          A list of properties in the destination table.
        * @param   sort
        *          The sort clause which should be used when computing the insert SQL.
        * @param   looseCopy
        *          If the copy is in loose mode.
        *
        * @return  The bundle element whose execution will do the copy.
        */
       private FastCopyBundleElement safeTableCopyNoMapping(FastCopyContext fastCopyContext,
                                                            List<Property> srcSimpleProps,
                                                            List<Property> dstSimpleProps,
                                                            String sort,
                                                            boolean looseCopy)
       {
          StringBuilder columnListSrc;
          StringBuilder columnListDst;

          List<Object> args = new ArrayList<>();

          if (!srcDialect.allowSelectWildcard() ||
              looseCopy                         ||
              srcMeta.hasComputedColumns(false) ||
              dstMeta.hasComputedColumns(false))
          {
             columnListSrc = getSrcTempColumns(dstTableName);
             columnListDst = getDstTempColumns();
             for (int i = 0; i < srcSimpleProps.size(); i++)
             {
                appendColumnName(srcMeta,
                                 dstMeta,
                                 srcSimpleProps.get(i),
                                 dstSimpleProps.get(i),
                                 columnListSrc,
                                 columnListDst,
                                 args);
             }
          }
          else
          {
             columnListSrc = getSrcTempWildcard(dstTableName, srcTableName);
             columnListDst = new StringBuilder();
          }

          List<String> whereClauses = computeUniqueConstraintsClauses(srcSimpleProps, dstSimpleProps);
          String insertSQL = getSafeTableCopySqlNoMapping(srcTableName,
                                                          dstTableName,
                                                          columnListSrc.toString(),
                                                          columnListDst.toString(),
                                                          srcImplicitSqlIndexName,
                                                          whereClauses,
                                                          sort);

          Function<FastCopyContext, Object[]> generateArgs = (context) ->
          {
             int argsSize = args.size();
             Object[] arguments = new Object[argsSize + whereClauses.size() + 3];
             Integer multiplexId = context.dstBuf.getMultiplexID();

             arguments[0] = multiplexId;
             arguments[1] = multiplexId;
             for (int i = 0; i < argsSize; i++)
             {
                arguments[i + 2] = args.get(i);
             }

             arguments[argsSize + 2] = context.srcBuf.getMultiplexID();
             // arguments[argsSize + 3] = context.srcBuf.nextPrimaryKey(true);

             for (int i = 0; i < whereClauses.size(); i++)
             {
                arguments[argsSize + 3 + i] = multiplexId;
             }

             return arguments;
          };

          return new FastCopyBundleElement(insertSQL, generateArgs);

       }

       /**
        * Identify a list of where clauses which rule out duplicate records when copying from a source
        * to a destination. A duplicate record is one which will violate destination's unique constraints.
        * There are multiple such clauses, one for each unique constraint. Each clause will check the
        * multiplex value and the properties which take part in the constraint.
        *
        * @param   srcSimpleProps
        *          A list of properties in the source table.
        * @param   dstSimpleProps
        *          A list of properties in the destination table.
        *
        * @return  A list of SQL where clauses which can preselect records from the source which
        *          won't violate the unique constraints from the destination.
        */
       private List<String> computeUniqueConstraintsClauses(List<Property> srcSimpleProps,
                                                            List<Property> dstSimpleProps)
       {
          List<String> uniqueConstraintsClauses = new ArrayList<>();
          String dstAlias = dstTableName + "___dst";

          ArrayList<List<Property>> constraints = dstMeta.getUniqueConstraintsAsProps();

          for (int i = 0; i < constraints.size(); i++)
          {
             List<Property> index = constraints.get(i);
             if (index == null || index.isEmpty())
             {
                continue;
             }

             StringBuilder sb = new StringBuilder("not exists (");
             sb.append("select 0 from ").append(dstTableName).append(" ").append(dstAlias).append(" where ");
             sb.append(dstAlias).append(".").append(MULTIPLEX_FIELD_NAME).append(" = ?");

             for (int j = 0; j < index.size(); j++)
             {
                Property prop = index.get(j);
                String dstPropName = prop.column;
                String srcPropName = getMappedPropName(dstPropName, dstSimpleProps, srcSimpleProps);

                sb.append(" and ");
                sb.append(srcTableName).append(".").append(srcPropName).append(" = ");
                sb.append(dstAlias).append(".").append(dstPropName);
             }

             sb.append(")");
             uniqueConstraintsClauses.add(sb.toString());
          }

          return uniqueConstraintsClauses;
       }

       /**
        * Identify the name of the property in the destination which should match the provided property
        * from the source. They should be on the same position in the list of properties. Note that
        * the result may be the same as the input (in case of a simple copy; the DMO implementation of
        * the source is the same for destination), but generally they can be different.
        *
        * @param   propName
        *          The property name for which we search a correspondent.
        * @param   from
        *          The list of properties for the DMO containing the provided property name.
        * @param   to
        *          The list of properties in which we search for a correspondent.
        *
        * @return  The name of the correspondent property or null if not found.
        */
       private String getMappedPropName(String propName, List<Property> from, List<Property> to)
       {
          for (int i = 0; i < from.size(); i++)
          {
             if (from.get(i).column.equals(propName))
             {
                return to.get(i).column;
             }
          }

          return null;
       }

       /**
        * The procedure which handles the copy between extent tables (only when source and destination properties
        * are both normalized). For this part to work fast (so use the "insert into select from" paradigm), we
        * will need a primary key mapping. Unless one is already created, this method will create a database
        * level pkMapping (which can be used further in other scenarios).
        *
        * @param    extentFields
        *           The mapping between the source extent fields and destination extent fields. This mapping
        *           is done per extent value.
        * @param    pkMappingTableName
        *           The name of the table which will be used as a database level primary key mapping.
        * @param    uid
        *           Value used to generate unique names for intermediate tables.
        */
       private void copyExtentFields(Map<Integer, List<Property[]>> extentFields,
                                     String pkMappingTableName,
                                     long uid)
       {
          // insert records in all extent tables
          for (Map.Entry<Integer, List<Property[]>> extent : extentFields.entrySet())
          {
             // prepare the columns which will be part of the copy - in the right order
             StringBuilder extColumnListSrc = new StringBuilder(DST__PK + ", " +
                                              ReservedProperty.LIST__INDEX.column);
             StringBuilder extColumnListDst = new StringBuilder(ReservedProperty.PARENT__ID.column + ", " +
                                              ReservedProperty.LIST__INDEX.column);

             // there is a possibility that there is no extent table for the source
             // according to the property mapping, destination's extent field is unmapped
             // but we still need to set it on a default value
             // so make sure don't copy from an inexistent table (source extent table)
             boolean srcExtTableExists = false;
             List<Object> args = new ArrayList<>();
             for (Property[] props : extent.getValue())
             {
                if (props[0] != null)
                {
                   srcExtTableExists = true;
                }
                appendColumnName(srcMeta,
                                 dstMeta,
                                 props[0],
                                 props[1],
                                 extColumnListSrc,
                                 extColumnListDst,
                                 args);
             }

             String insertSql;
             if (srcExtTableExists)
             {
                // insert into the destination extent table all records from source
                // but with re-mapped id (based on pkMappingTableName)
                insertSql = getExtentInsertSql(srcTableName + "__" + extent.getKey(),
                                               dstTableName + "__" + extent.getKey(),
                                               extColumnListSrc.toString(),
                                               extColumnListDst.toString(),
                                               pkMappingTableName);
             }
             else
             {
                // insert into the destination extent table defaults
                insertSql = getSimpleExtentInsertSql(extent.getKey(),
                                                     dstTableName + "__" + extent.getKey(),
                                                     extColumnListSrc.toString(),
                                                     extColumnListDst.toString(),
                                                     pkMappingTableName,
                                                     uid);
             }

             Function<FastCopyContext, Object[]> generateArgs = (context) ->
             {
                return args.toArray();
             };

             fastCopyBundle.addBundleElement(new FastCopyBundleElement(insertSql, generateArgs));
          }
       }

       /**
        * This will create a database-level primary key mapping which can be used in other operations
        * (through join) in order to speed up the copy process.
        *
        * @param   fastCopyContext
        *          The context that has the required information.
        * @param   sort
        *          The sort clause used when the records were copied between main tables.
        * @param   pkMappingTableName
        *          The name which should be used by the table which will store the primary key mapping.
        *          This does not check if the pkMappingTableName already exists.
        * @param   addToBundle
        *          {@code true} if instead of immediately executing the SQLs, they can be added to the bundle
        *          and be executed at the end, {@code false} otherwise.
        */
       private void createPkMapping(FastCopyContext fastCopyContext,
                                    String sort,
                                    String pkMappingTableName,
                                    boolean addToBundle)
       throws PersistenceException
       {
          String createMapping = getPkMappingSql(srcTableName,
                                                 dstTableName,
                                                 srcImplicitSqlIndexName,
                                                 null,
                                                 sort,
                                                 pkMappingTableName);

          if (addToBundle)
          {
             Function<FastCopyContext, Object[]> generateArgs = (context) ->
             {
                Object[] arguments = new Object[]
                {
                   context.dstBuf.getMultiplexID(),
                   context.srcBuf.getMultiplexID()
                };
                return arguments;
             };

             fastCopyBundle.addBundleElement(new FastCopyBundleElement(createMapping, generateArgs));
             return;
          }

          Object[] multiplexArg = new Object[]
          {
             fastCopyContext.dstBuf.getMultiplexID(),
             fastCopyContext.srcBuf.getMultiplexID()
          };
          srcPersistence.executeSQL(createMapping, Persistence.TEMP_CTX, multiplexArg);
       }

       /**
        * A generator method which provides a suitable SQL to insert all records from an extent table
        * to another based on an in-database primary key mapping. This will work only if the pk mapping
        * was already created.
        *
        * @param   srcExtent
        *          The name of the source extent temp-table.
        * @param   dstExtent
        *          The name of the destination extent temp-table.
        * @param   extColumnListSrc
        *          A string containing the columns from which the copy should be done.
        * @param   extColumnListDst
        *          A string containing the columns to which the copy should be done.
        * @param   pkMappingTableName
        *          The name of the table which stores the primary key mapping.
        *
        * @return  The SQL which copies the records from one source extent table to a destination one.
        */
       private String getExtentInsertSql(String srcExtent,
                                         String dstExtent,
                                         String extColumnListSrc,
                                         String extColumnListDst,
                                         String pkMappingTableName)
       {
          StringBuilder sql = new StringBuilder();
          sql.append("insert into ").append(dstExtent)
             .append(" (").append(extColumnListDst)
             .append(") direct sorted select ").append(extColumnListSrc)
             .append(" from ").append(srcExtent)
             .append(" join ").append(pkMappingTableName)
             .append(" on ").append(srcExtent)
             .append(".").append(ReservedProperty.PARENT__ID.column)
             .append(" = ").append(pkMappingTableName)
             .append(".").append(SRC__PK)
             .append(" order by ").append(srcExtent)
             .append(".").append(ReservedProperty.PARENT__ID.column)
             .append(", ").append(ReservedProperty.LIST__INDEX.column);

          return sql.toString();
       }

       /**
        * Special generator method which provides a suitable SQL to insert into a destination table
        * only default values. This is based on an in-databse primary key mapping in order to correctly
        * set the parent id. The list index will be locally generated and the values are set to default.
        * This should be used when a source table exists, but an separate extent table for the source doesn't.
        *
        * @param   extent
        *          The size of the extent for which the insert sql should be generated.
        * @param   dstExtent
        *          The name of the destination extent table.
        * @param   extColumnListSrc
        *          A list of fields from which the copy should be done. This should be formed only by ?,
        *          which will be set on the default values.
        * @param   extColumnListDst
        *          A list of the names of the extent field properties in the destination.
        * @param   pkMappingTableName
        *          The name of the in database pk mapping from which the parent-id will be extracted.
        * @param   uid
        *          Value used to generate unique names for intermediate tables.
        *
        * @return  The SQL which creates new default records in the destination table.
        */
       private String getSimpleExtentInsertSql(int extent,
                                               String dstExtent,
                                               String extColumnListSrc,
                                               String extColumnListDst,
                                               String pkMappingTableName,
                                               long uid)
       {
          StringBuilder source = new StringBuilder();
          source.append("(VALUES (0)");
          for (int i = 1; i < extent; i++)
          {
             source.append(", (").append(i).append(")");
          }

          if (ALLOW_UID)
          {
             source.append(") as ").append(LIST__INDEX__TABLE).append("_").append(uid);
          }
          else
          {
             source.append(") as ").append(LIST__INDEX__TABLE);
          }

          source.append("(").append(ReservedProperty.LIST__INDEX.column).append(")");

          StringBuilder sql = new StringBuilder();
          sql.append("insert into ").append(dstExtent)
             .append(" (").append(extColumnListDst)
             .append(") direct sorted select ").append(extColumnListSrc)
             .append(" from ").append(source.toString())
             .append(" join ").append(pkMappingTableName)
             .append(" order by ").append(pkMappingTableName)
             .append(".").append(DST__PK)
             .append(", ").append(ReservedProperty.LIST__INDEX.column);

          return sql.toString();
       }

       /**
        * A generator method which will create a database-level primary key mapping which can be used
        * (through join) in order to speed up the copy process.
        *
        * @param   srcTable
        *          The name of the source table.
        * @param   dstTable
        *          The name of the destination table.
        * @param   sqlIndexName
        *          The SQL name of the index used in the main table copy process
        * @param   extraWhereClauses
        *          A list of extra where clauses that need to be appended.
        * @param   sort
        *          The sort clause used in the main table copy process.
        * @param   pkMappingTableName
        *          The name which should be used for the generated table.
        *
        * @return  The SQL which creates a database level primary key mapping.
        */
       private String getPkMappingSql(String srcTable,
                                      String dstTable,
                                      String sqlIndexName,
                                      List<String> extraWhereClauses,
                                      String sort,
                                      String pkMappingTableName)
       {
          StringBuilder sql = new StringBuilder();
          sql.append("insert into ").append(pkMappingTableName).append(" direct sorted ")
             .append("select next pk for (").append(dstTable).append(", ?)")
             .append(" as \"").append(DST__PK).append("\",").append(Session.PK).append(" as \"").append(SRC__PK)
             .append("\",").append(TempRecord._PEER_ROWID).append(" from ").append(srcTable).append(" ")
             .append("use index (").append(sqlIndexName).append(") ")
             .append("where ").append(MULTIPLEX_FIELD_NAME).append(" = ? ");

          if (extraWhereClauses != null)
          {
             for (String clause : extraWhereClauses)
             {
                sql.append(" and ").append(clause);
             }
          }

          sql.append(" order by ").append(sort);
          return sql.toString();
       }

       /**
        * A generator method which will copy the records from a source table into a destination table
        * based on an already existing database-level pk mapping.
        *
        * @param   srcTable
        *          The name of the source table.
        * @param   dstTable
        *          the name of the destination table.
        * @param   columnListSrc
        *          A string containing the columns from which the copy should be done.
        * @param   columnListDst
        *          A string containing the columns to which the copy should be done.
        * @param   sort
        *          The sort clause
        * @param   pkMappingTableName
        *          The table which contains the pk mapping.
        *
        * @return  The SQL which copies records from the source to the destination based on the provided
        *          pk mapping.
        */
       private String getShadowCopySql(String srcTable,
                                       String dstTable,
                                       String columnListSrc,
                                       String columnListDst,
                                       String sort,
                                       String pkMappingTableName)
       {
          StringBuilder sql = new StringBuilder();
          sql.append("insert into ").append(dstTable);

          if (columnListDst.length() > 0)
          {
             sql.append(" ( ").append(columnListDst).append(") ");
          }
          else
          {
             sql.append(columnListDst);
          }

          sql.append(" direct sorted select ").append(columnListSrc)
             .append(" from ").append(srcTable)
             .append(" join ").append(pkMappingTableName)
             .append(" on ").append(srcTable)
             .append(".").append(Session.PK)
             .append(" = ").append(pkMappingTableName)
             .append(".").append(SRC__PK)
             .append(" order by ").append(sort);

          return sql.toString();
       }

       /**
        * A generator method which will copy the records from a source table into a destination table.
        *
        * @param   srcTable
        *          The name of the source table.
        * @param   dstTable
        *          the name of the destination table.
        * @param   columnListSrc
        *          A string containing the columns from which the copy should be done.
        * @param   columnListDst
        *          A string containing the columns to which the copy should be done.
        * @param   sqlIndexName
        *          The name of the index which should be used when copying records.
        * @param   sort
        *          The sort clause.
        *
        * @return  The SQL which copies records from the source to the destination
        */
       private String getFastTableCopySql(String srcTable,
                                          String dstTable,
                                          String columnListSrc,
                                          String columnListDst,
                                          String sqlIndexName,
                                          String sort)
       {
          StringBuilder sql = new StringBuilder();
          sql.append("insert into ").append(dstTable);

          if (columnListDst.length() > 0)
          {
             sql.append(" ( ").append(columnListDst).append(") ");
          }
          else
          {
             sql.append(columnListDst);
          }

          sql.append(" direct sorted select ").append(columnListSrc)
             .append(" from ").append(srcTable)
             .append(" use index (").append(sqlIndexName)
             .append(") where ").append(MULTIPLEX_FIELD_NAME)
             .append(" = ? order by ").append(sort);

          return sql.toString();
       }

       /**
        * A generator method which will copy the records from a source table into a destination table
        * with an additional condition on the recid of the source table.
        *
        * @param   srcTable
        *          The name of the source table.
        * @param   dstTable
        *          the name of the destination table.
        * @param   columnListSrc
        *          A string containing the columns from which the copy should be done.
        * @param   columnListDst
        *          A string containing the columns to which the copy should be done.
        * @param   sqlIndexName
        *          The name of the index which should be used when copying records.
        * @param   extraWhereClauses
        *          Other where clauses to be included. Used to implement append-mode.
        * @param   sort
        *          The sort clause.
        *
        * @return  The SQL which copies records from the source to the destination
        */
       private String getSafeTableCopySqlNoMapping(String srcTable,
                                                   String dstTable,
                                                   String columnListSrc,
                                                   String columnListDst,
                                                   String sqlIndexName,
                                                   List<String> extraWhereClauses,
                                                   String sort)
       {
          StringBuilder sql = new StringBuilder();
          sql.append("insert into ").append(dstTable);

          if (columnListDst.length() > 0)
          {
             sql.append(" ( ").append(columnListDst).append(") ");
          }
          else
          {
             sql.append(columnListDst);
          }

          sql.append(" direct sorted select ").append(columnListSrc)
             .append(" from ").append(srcTable)
             .append(" use index (").append(sqlIndexName)
             .append(") where ").append(MULTIPLEX_FIELD_NAME)
             .append(" = ?");
    //         .append("and ").append(ReservedProperty.ID.column)
    //         .append(" < ?");

          if (extraWhereClauses != null)
          {
             for (String whereClause : extraWhereClauses)
             {
                sql.append(" and ").append(whereClause);
             }
          }

          sql.append(" order by ").append(sort);

          return sql.toString();
       }

       /**
        * This builds the columns from which the records should be copied. Note that the first columns
        * are given through parameters: this should stand for the pk and _multiplex. However, when we will
        * insert into the destination, the pk is assigned automatically by the database, while the _multiplex is
        * fixed.
        *
        * @param   dstTableName
        *          The name of the destination table. This is used to use the NEXT PK functionality.
        *
        * @return  A string builder containing the columns which should be copied in the right order.
        *          This is a string builder as it is expected that this column list will be completed
        *          with other names.
        */
       private StringBuilder getSrcTempColumns(String dstTableName)
       {
          return new StringBuilder("next pk for (").append(dstTableName).append(", ?)")
                                                   .append(",?,")
                                                   .append(ReservedProperty._ERRORFLAG.column)
                                                   .append(",")
                                                   .append(ReservedProperty._ORIGINROWID.column)
                                                   .append(",")
                                                   .append(ReservedProperty._DATASOURCEROWID.column)
                                                   .append(",")
                                                   .append(ReservedProperty._ERRORSTRING.column)
                                                   .append(",")
                                                   .append(ReservedProperty._PEERROWID.column)
                                                   .append(",")
                                                   .append(ReservedProperty._ROWSTATE.column);
       }

       /**
        * This builds a wildcard for the given table instead of the columns. The first columns are given
        * through parameters: this should stand for the pk (recid) and _multiplex. When inserting into
        * the destination, the pk is assigned automatically by the database and the multiplex is
        * a prepared value. The pk and _multiplex need to be excepted from the wildcard selection,
        * this is done using EXCEPT.
        *
        * @param   dstTableName
        *          The name of the destination table. This is used to use the NEXT PK functionality.
        * @param   sourcetableName
        *          The name of the source table.
        *
        * @return  A string builder containing the columns which should be copied in the right order.
        *          This is a string builder as it is expected that this column list will be completed
        *          with other names.
        */
       private StringBuilder getSrcTempWildcard(String dstTableName, String sourcetableName)
       {
          return new StringBuilder("next pk for (").append(dstTableName).append(", ?)")
                                                   .append(",?,")
                                                   .append(sourcetableName).append(".* EXCEPT (")
                                                   .append(sourcetableName).append(".")
                                                   .append(ReservedProperty.ID.column).append(",")
                                                   .append(sourcetableName).append(".")
                                                   .append(MULTIPLEX_FIELD_NAME).append(")");
       }

       /**
        * A particular method which builds the columns from which the records should be copied in a context
        * where an intermediate database-level pk mapping is used. For this case, the destination pk won't be
        * generated automatically by the table using NEXT PK, but extracted from the provided pk mapping.
        *
        * @param   pkMappingTableName
        *          The database-level pk mapping which is used when doing the copy.
        *
        * @return  A string builder containing the columns which should be copied in the right order.
        *          This is a string builder as it is expected that this column list will be completed
        *          with other names.
        */
       private StringBuilder getSrcMappedColumns(String pkMappingTableName)
       {
          return new StringBuilder(pkMappingTableName)
                            .append(".").append(DST__PK).append(",?,")
                            .append(ReservedProperty._ERRORFLAG.column).append(",")
                            .append(ReservedProperty._ORIGINROWID.column).append(",")
                            .append(ReservedProperty._DATASOURCEROWID.column).append(",")
                            .append(ReservedProperty._ERRORSTRING.column).append(",")
                            // we need to qualify _peerrowid, as this field is both in the pkMappingTable
                            // and source.
                            // The qualifier doesn't matter, they should be equal anyways
                            .append(pkMappingTableName).append(".")
                            .append(ReservedProperty._PEERROWID.column).append(",")
                            .append(ReservedProperty._ROWSTATE.column);
       }

       /**
        * A particular method which builds a wildcard from which the records should be copied in a context
        * where an intermediate database-level pk mapping is used. For this case, the destination pk won't be
        * generated automatically by the table using NEXT PK, but extracted from the provided pk mapping.
        * Note that pk and _multiplex need to be excepted from the wildcard selection of the source table,
        * this is done using EXCEPT.
        *
        * @param   pkMappingTableName
        *          The database-level pk mapping which is used when doing the copy.
        * @param   sourceTableName
        *          The name of the source table.
        *
        * @return  A string builder containing a wildcard for the source table which should be copied
        *          in the right order.
        */
       private StringBuilder getSrcMappedWildcard(String pkMappingTableName,
                                                  String sourceTableName)
       {
          return new StringBuilder().append(pkMappingTableName).append(".").append(DST__PK)
                                    .append(",?,")
                                    .append(sourceTableName).append(".* EXCEPT (")
                                    .append(sourceTableName).append(".")
                                    .append(ReservedProperty.ID.column).append(",")
                                    .append(sourceTableName).append(".")
                                    .append(MULTIPLEX_FIELD_NAME).append(")");
       }

       /**
        * This builds the columns to which the records should be copied. Note that all columns are fixed.
        *
        * @return  A string builder containing the target columns in the right order.
        *          This is a string builder as it is expected that this column list will be completed
        *          with other names.
        */
       private StringBuilder getDstTempColumns()
       {
          return new StringBuilder(Session.PK).append(",")
                            .append(MULTIPLEX_FIELD_NAME).append(",")
                            .append(ReservedProperty._ERRORFLAG.column).append(",")
                            .append(ReservedProperty._ORIGINROWID.column).append(",")
                            .append(ReservedProperty._DATASOURCEROWID.column).append(",")
                            .append(ReservedProperty._ERRORSTRING.column).append(",")
                            .append(ReservedProperty._PEERROWID.column).append(",")
                            .append(ReservedProperty._ROWSTATE.column);
       }

       /**
        * This extracts information from the source and destination tables. The most important
        * part is the ordering of the properties - which can be done through a specified
        * property mapping. Also, this will identify the extent fields (and the mapping of them
        * between the source and destination).
        *
        * @return   {@code true} if the processing didn't required loose-mode, so that the order of
        *           the properties in the source matches the one in the destination.
        */
       private boolean processProperties()
       {
          srcSimpleProps = new ArrayList<>();
          dstSimpleProps = new ArrayList<>();
          extentFields = new HashMap<>();

          if (!simpleCopy)
          {
             // maybe the order is preserved, so there is no need for complex management here
             Map<Property, Property> propMapping = new HashMap<>();
             for (Map.Entry<DatumAccess, DatumAccess> prop : propsMap.entrySet())
             {
                Property srcProp = srcMeta.getFieldInfo(prop.getKey().getPropertyName());
                Property dstProp = dstMeta.getFieldInfo(prop.getValue().getPropertyName());

                propMapping.put(dstProp, srcProp);

                if (srcProp.extent == 0)
                {
                   srcSimpleProps.add(srcProp);
                   dstSimpleProps.add(dstProp);
                }
                else
                {
                   List<Property[]> extents =
                         extentFields.computeIfAbsent(srcProp.extent, k -> new ArrayList<>());
                   extents.add(new Property[] { srcProp, dstProp });
                }
             }

             // we need to make sure we fill all the destination properties accordingly
             // if they are not in the property mapping, we need to set them to a default value
             Iterator<Property> iterSrc = srcMeta.getFields(false);
             Iterator<Property> iterDst = dstMeta.getFields(false);
             boolean orderPreserved = true;
             while (iterDst.hasNext())
             {
                Property propDst = iterDst.next();
                if (orderPreserved)
                {
                   Property propSrc = iterSrc.hasNext() ? iterSrc.next() : null;
                   if (propSrc == null || propMapping.get(propDst) != propSrc)
                   {
                      orderPreserved = false;
                   }
                }

                if (propMapping.get(propDst) != null || getInitialValue(dstMeta, propDst) == null)
                {
                   // this destination property was already resolved or
                   // it allows a null default, so no need to enforce special default value
                   continue;
                }

                if (propDst.extent == 0)
                {
                   // mark the lack of a source property with null - this will be replaced with
                   // the default value when generating the SQLs
                   srcSimpleProps.add(null);
                   dstSimpleProps.add(propDst);
                }
                else
                {
                   List<Property[]> extents =
                         extentFields.computeIfAbsent(propDst.extent, k -> new ArrayList<>());
                   extents.add(new Property[] { null, propDst });
                }
             }

             orderPreserved = orderPreserved && !iterSrc.hasNext();
             return orderPreserved;
          }
          else
          {
             Iterator<Property> iter = dstMeta.getFields(false);
             while (iter.hasNext())
             {
                Property prop = iter.next();
                if (prop.extent == 0)
                {
                   srcSimpleProps.add(prop);
                   dstSimpleProps.add(prop);
                }
                else
                {
                   List<Property[]> extents =
                         extentFields.computeIfAbsent(prop.extent, k -> new ArrayList<>());
                   extents.add(new Property[] { prop, prop });
                }
             }

             return true;
          }
       }

       /**
        * A helper method which will append a source and destination property name to correct string builders:
        * comma separated and having support for datetime-tz data type.
        * Note that this should not be used for the first column (as it will have a leading comma).
        * This also handles the cases when the source property is null, so a default value should be
        * added to the source fields.
        *
        * @param   srcMeta
        *          The dmo meta of the source.
        * @param   dstMeta
        *          The dmo meta of the destination. Used to retrieve eventual initial values.
        * @param   srcProp
        *          The source property which should be appended to the list of source fields.
        * @param   dstProp
        *          The destination property which should be appended to the list of source fields.
        * @param   srcSb
        *          The string builder which shall hold the information about the source fields.
        * @param   dstSb
        *          The string builder which shall hold the information about the destination fields.
        * @param   args
        *          The list of parameters which will be used in the SQL. Here the default will be appended
        *          if needed.
        */
       private void appendColumnName(DmoMeta srcMeta,
                                     DmoMeta dstMeta,
                                     Property srcProp,
                                     Property dstProp,
                                     StringBuilder srcSb,
                                     StringBuilder dstSb,
                                     List<Object> args)
       {
          if (srcProp == null)
          {
             // this should be a default value
             args.add(getInitialValue(dstMeta, dstProp));
             srcSb.append(",").append("?");
             dstSb.append(",").append(dstProp.column);
             //TODO: add default for the extra DTZ__OFFSET field
          }
          else
          {
             srcSb.append(",").append(srcProp.column);
             dstSb.append(",").append(dstProp.column);
             if (srcProp._isDatetimeTz)
             {
                srcSb.append(",").append(srcProp.column).append(DDLGeneratorWorker.DTZ_OFFSET);
                dstSb.append(",").append(dstProp.column).append(DDLGeneratorWorker.DTZ_OFFSET);
             }
          }
       }

       /**
        * This will return the sort clause which should be used (in case am explicit index name is
        * provided). If no index name is provided, the sort clause will be a simple combination between
        * the multiplex and pk fields.
        *
        * @param   indexName
        *          The index which should be taken in consideration when computing the sort clause.
        *          This can be null, resulting in a default sort clause.
        *
        * @return  A sort clause suitable for the provided buffer and index name.
        */
       private String getSortClause(String indexName)
       throws PersistenceException
       {
          StringBuilder sort = null;
          if (indexName != null)
          {
             Iterator<P2JIndex> iter = srcMeta.getDatabaseIndexes();
             while (iter.hasNext())
             {
                P2JIndex idx = iter.next();
                if (idx.getName().equals(indexName))
                {
                   sort = new StringBuilder(MULTIPLEX_FIELD_NAME);

                   ArrayList<P2JIndexComponent> comps = idx.components();
                   for (int i = 0; i < comps.size(); i++)
                   {
                      P2JIndexComponent idxComp = comps.get(i);
                      String field = idxComp.getColumnName();
                      if (idxComp.isCharType() && srcDialect.needsComputedColumns())
                      {
                         field = srcDialect.getComputedColumnPrefix(!idxComp.isIgnoreCase()) + field;
                      }
                      sort.append(",");
                      srcDialect.orderByNulls(sort, field, null, true, false, idxComp.isMandatory());
                   }
                   if (!idx.isUnique())
                   {
                      sort.append(",").append(Session.PK);
                   }
                   break;
                }
             }

             if (sort == null)
             {
                throw new PersistenceException("Could not find index for " + indexName);
             }
          }

          if (sort == null)
          {
             sort = new StringBuilder(MULTIPLEX_FIELD_NAME).append(",").append(Session.PK);
          }

          return sort.toString();
       }

       /**
        * This will retrieve the database-level pk mapping into a local mapping. This should be used only
        * when the database-level pk mapping can't be used in the desired scenario.
        *
        * @param   persistence
        *          A persistence instance which should be used in order to retrieve the database-level
        *          pk mapping.
        * @param   pkMappingTableName
        *          The name of the table which stored the database-level pk mapping.
        *
        * @return  A map between the source pk and a group formed by the destination pk and source peer id
        */
       private Map<Long, Long[]> getMapping(Persistence persistence, String pkMappingTableName)
       throws PersistenceException
       {
          Map<Long, Long[]> srcToDstPks = new HashMap<>();
          ScrollableResults<Long> pkMapping =
                persistence.executeSQLQuery("select * from " + pkMappingTableName, Persistence.TEMP_CTX, null);

          while (pkMapping.next())
          {
             Long dstPk = pkMapping.get(0,  Long.class);
             Long srcPk = pkMapping.get(1,  Long.class);
             Long srcPeerId = pkMapping.get(2,  Long.class);
             srcToDstPks.put(srcPk, new Long[] {dstPk, srcPeerId});
          }

          return srcToDstPks;
       }

       /**
        * Retrieve the initial value of a certain property. The meta is used in order to retrieve
        * the property metas which can be checked against the provided property and find the initial value.
        *
        * @param   meta
        *          The meta of the dmo for the provided property.
        * @param   prop
        *          The property for which the initial value should be retrieved.
        *
        * @return  The initial value of the provided property.
        */
       private Object getInitialValue(DmoMeta meta, Property prop)
       {
          RecordMeta recordMeta = meta.getRecordMeta();
          PropertyMeta[] propMetas = recordMeta.getPropertyMeta(false);
          for (int i = 0; i < propMetas.length; i++)
          {
             PropertyMeta propMeta = propMetas[i];
             if (propMeta.getId() == prop.id)
             {
                return propMeta.getInitialValue();
             }
          }

          return null;
       }

       /**
        * This method will call for {@link #copyTable} with table specific parameters. Also this
        * will trigger {@link #processProperties} which will analyze the tables and compute
        * a list of their properties (and extents) in a copy suitable order.
        *
        * @param   fastCopyContext
        *          The context that has the information required.
        * @param   loose
        *          If the copy is in loose mode.
        */
       private void copyTable(FastCopyContext fastCopyContext,
                              boolean loose)
       throws PersistenceException
       {
          FastCopyBundleElement bundleElement;

          // force a mapping if required or if in append mode
          if (appendMode || this.forceMapping || !extentFields.isEmpty())
          {
             if (!this.forceMapping && extentFields.isEmpty())
             {
                bundleElement = safeTableCopyNoMapping(fastCopyContext,
                                                       srcSimpleProps,
                                                       dstSimpleProps,
                                                       getSortClause(),
                                                       loose);
             }
             else
             {
                bundleElement = safeTableCopy(fastCopyContext,
                                              srcSimpleProps,
                                              dstSimpleProps,
                                              getSortClause(),
                                              assembleUidMapping(pkMappingName),
                                              loose);
                generatesPkMapping = true;
             }
          }
          else
          {
             bundleElement = copyTable(fastCopyContext,
                                       srcSimpleProps,
                                       dstSimpleProps,
                                       getSortClause(),
                                       loose);
          }
          fastCopyBundle.addBundleElement(bundleElement);
       }

       /**
        * This method will call for {@link #getSortClause} with table specific parameters. This will
        * also cache the sort clause.
        *
        * @return  The sort clause which should be used in the table copy.
        */
       private String getSortClause()
       throws PersistenceException
       {
          if (sort == null)
          {
             sort = getSortClause(srcImplicitIndexName);
          }

          return sort;
       }

       /**
        * Compute the destination table update SQL.
        *
        * @return  String
        *          The {@code dstTableSql} field.
        */
       private String getDstTableSql()
       {
          String dstTableSql = "update " + dstMeta.getSqlTableName() +
                               " set " + TempRecord._PEER_ROWID + " = ? " +
                               " where " + Session.PK + " = ?";
          return dstTableSql;
       }

       /**
        * This method will call for {@link #copyExtentFields} with table specific parameters. This will
        * also try to create a database-level primary key mapping if it doesn't already exist.
        *
        * @param   fastCopyContext
        *          The context that has the required information.
        */
       private void copyExtentFields(FastCopyContext fastCopyContext)
       throws PersistenceException
       {
          createPkMapping(fastCopyContext);
          copyExtentFields(extentFields,
                           assembleUidMapping(pkMappingName),
                           uid);
       }

       /**
        * This method will call for {@link #createPkMapping} with table specific parameters. This will
        * take care that a single pk mapping will be online at once.
        *
        * @param   fastCopyContext
        *          The context that has the required information.
        */
       private void createPkMapping(FastCopyContext fastCopyContext)
       throws PersistenceException
       {
          if (!generatesPkMapping)
          {
             createPkMapping(fastCopyContext,
                             getSortClause(),
                             assembleUidMapping(pkMappingName),
                             true);
             generatesPkMapping = true;
          }
       }

       /**
        * This method will call for {@link #getMapping} with table specific parameters. This will
        * cache eventually the mapping. Also, this may trigger a database-level pk mapping if it
        * doesn't already exist.
        *
        * @param   fastCopyContext
        *          The context that has the required information.
        *
        * @return  Initializes the cache for the primary key after the copying of tables or
        *          returns the cache if it was already created.
        */
       private Map<Long, Long[]> getMapping(FastCopyContext fastCopyContext)
       throws PersistenceException
       {
          if (!fastCopyContext.hasMapping)
          {
             throw new IllegalStateException("Can't provide a PK mapping after the copy was done!");
          }
          return getMapping(srcPersistence, assembleUidMapping(pkMappingName));
       }

       /**
        * Append the <code>uid</code> value to properties in order to generate
        * unique names for the temporary and intermediary tables used in operations
        * or storing indexes,
        *
        * @param   property
        *          The property to which <code>uid</code> will be attached.
        *
        * @return  A string representing a unique name.
        */
       private String assembleUidMapping(String property)
       {
          if (!ALLOW_UID)
          {
             return property;
          }

          return property + "_" + uid;
       }

       /**
        * Builder/Context for a {@code FastCopyHelper}.
        */
       static class FastCopyContext
       {
          /** Is this context designated for copying before tables? */
          boolean isBefore = false;

          /** Flag identifying if the source and destination tables have the same structure. */
          boolean simpleCopy = false;

          /** Flag that indicates if the copy should happen with APPEND. */
          boolean appendMode = false;

          /** Flag that indicates if this is a loose copy process. */
          boolean looseCopyMode = false;

          /** Flag that indicates if the mapping is mandatory to be generated at the end. */
          boolean forceMapping = false;

          /** The source buffer. */
          TemporaryBuffer srcBuf = null;

          /** The destination buffer. */
          TemporaryBuffer dstBuf = null;

          /** The property mapping. */
          Map<DatumAccess, DatumAccess> propsMap = null;

          /** {@code true} if at the current state of the execution, the mapping is in the database */
          boolean hasMapping = false;

          /**
           * Default constructor.
           */
          public FastCopyContext()
          {

          }

          /**
           * Setter for the {@code isBefore} field.
           *
           * @param   isBefore
           *          The new value.
           *
           * @return  FastCopyContext
           *          This instance.
           */
          public FastCopyContext setIsBefore(boolean isBefore)
          {
             this.isBefore = isBefore;
             return this;
          }

          /**
           * Setter for the {@code simpleCopy} field.
           *
           * @param   simpleCopy
           *          The new value.
           *
           * @return  FastCopyContext
           *          This instance.
           */
          public FastCopyContext setSimpleCopy(boolean simpleCopy)
          {
             this.simpleCopy = simpleCopy;
             return this;
          }

          /**
           * Setter for the {@code appendMode} field.
           *
           * @param   appendMode
           *          The new value.
           *
           * @return  FastCopyContext
           *          This instance.
           */
          public FastCopyContext setAppendMode(boolean appendMode)
          {
             this.appendMode = appendMode;
             return this;
          }

          /**
           * Setter for the {@code looseCopyMode} field.
           *
           * @param   looseCopyMode
           *          The new value.
           *
           * @return  FastCopyContext
           *          This instance.
           */
          public FastCopyContext setLooseCopyMode(boolean looseCopyMode)
          {
             this.looseCopyMode = looseCopyMode;
             return this;
          }

          /**
           * Setter for the {@code srcBuf} field.
           *
           * @param   srcBuf
           *          The new value.
           *
           * @return  FastCopyContext
           *          This instance.
           */
          public FastCopyContext setSourceBuffer(TemporaryBuffer srcBuf)
          {
             this.srcBuf = srcBuf;
             return this;
          }

          /**
           * Setter for the {@code dstBuf} field.
           *
           * @param   dstBuf
           *          The new value.
           *
           * @return  FastCopyContext
           *          This instance.
           */
          public FastCopyContext setDestinationBuffer(TemporaryBuffer dstBuf)
          {
             this.dstBuf = dstBuf;
             return this;
          }

          /**
           * Setter for the {@code propsMap} field.
           *
           * @param   propsMap
           *          The new value.
           *
           * @return  FastCopyContext
           *          This instance.
           */
          public FastCopyContext setPropsMap(Map<DatumAccess, DatumAccess> propsMap)
          {
             this.propsMap = propsMap;
             return this;
          }

          /**
           * Force the intermediate pk-mapping when copying. This is used when there is a
           * normalized extent table copy or before table copy, so the mapping of the
           * source-destination pk is needed.
           *
           * @param   forceMapping
           *          The new value.
           *
           * @return  FastCopyContext
           *          This instance.
           */
          public FastCopyContext setForceMapping(boolean forceMapping)
          {
             this.forceMapping = forceMapping;
             return this;
          }

          /**
           * Function to build a {@code FastCopyHelper} based on the information stored in this context.
           *
           * @return  FastCopyHelper
           *          The created instance based on this context.
           */
          public FastCopyHelper build()
          {
             return new FastCopyHelper(this);
          }
       }

       /**
        * Class that acts as the key for the {@code fastCopyCache}.
        */
       static class FastCopyKey
       {
          /** The class of the source buffer. */
          final Class<? extends Record> sourceBufferClass;

          /** The class of the destination buffer. */
          final Class<? extends Record> destinationBufferClass;

          /** A bitmask that indicates which copying mode is being used. */
          final byte copyMode;

          /**
           * Basic constructor.
           *
           * @param   fastCopyContext
           *          The context with the required information.
           */
          FastCopyKey(FastCopyContext fastCopyContext)
          {
             sourceBufferClass =      fastCopyContext.srcBuf   != null ?
                                      fastCopyContext.srcBuf.getDMOImplementationClass()   : null;
             destinationBufferClass = fastCopyContext.dstBuf   != null ?
                                      fastCopyContext.dstBuf.getDMOImplementationClass()   : null;
             copyMode = (byte) ((fastCopyContext.appendMode    ? 1 << 0 : 0) +
                                (fastCopyContext.looseCopyMode ? 1 << 1 : 0) +
                                (fastCopyContext.forceMapping  ? 1 << 2 : 0));
          }

          /**
           * Function that computes the hash.
           */
          @Override
          public int hashCode()
          {
             int hash = 17;
             hash = 37 * hash + (sourceBufferClass      != null ? sourceBufferClass.hashCode() : 0);
             hash = 37 * hash + (destinationBufferClass != null ? destinationBufferClass.hashCode() : 0);
             hash = 37 * hash + copyMode;
             return hash;
          }

          /**
           * Function that checks the equality between two {@code FastCopyKey}.
           */
          @Override
          public boolean equals(Object o)
          {
             if (!(o instanceof FastCopyKey))
             {
                return false;
             }

             FastCopyKey that = (FastCopyKey) o;

             return this.sourceBufferClass == that.sourceBufferClass                       &&
                    this.destinationBufferClass == that.destinationBufferClass             &&
                    this.copyMode == that.copyMode;
          }
       }

       /**
        * Class that stores a bundle of SQL that will need to be executed in order to perform the copy process.
        */
       static class FastCopyBundle
       {
          /** List of elements used in the copy process. */
          final List<FastCopyBundleElement> bundle;

          /**
           * Default constructor.
           */
          FastCopyBundle()
          {
             bundle = new ArrayList<>();
          }

          /**
           * Check if the bundle has already been computed. If so, one can start the execution process directly.
           *
           * @return  boolean
           *          {@code true} if the bundle has already been computed, {@code false} otherwise.
           */
          boolean isBundleComputed()
          {
             return bundle.size() > 0;
          }

          /**
           * Add an element to the bundle.
           *
           * @param   element
           *          The element that will be added to the bundle.
           */
          void addBundleElement(FastCopyBundleElement element)
          {
             bundle.add(element);
          }

          /**
           * Method that executes all the elements in the bundle for the given context.
           * In case an error occurs, the session will rollback to a savepoint defined before the
           * execution began.
           *
           * @param   context
           *          The context with the required information.
           */
          void executeSqls(FastCopyContext context)
          throws PersistenceException
          {
             Session session = context.srcBuf.getSession();
             Savepoint sp = session.setSavepoint();
             for (int i = 0; i < bundle.size(); i++)
             {
                FastCopyBundleElement element = bundle.get(i);
                try
                {
                   element.execute(context);
                }
                catch (PersistenceException exc)
                {
                   session.rollbackSavepoint(sp);
                   throw exc;
                }
             }
          }
       }

       /**
        * Class that denotes an element of the bundle.
        * This can be extended depending on the type of the element.
        */
       class FastCopyBundleElement
       {
          /** The SQL that will be executed. */
          final String sql;

          /** A {@code Function} that will generate the arguments for the SQL, depending on the given context. */
          final Function<FastCopyContext, Object[]> generateArgs;

          /**
           * Default constructor with two parameters.
           *
           * @param   sql
           *          The SQL that will be used.
           * @param   generateArgs
           *          The {@code Function} used to generate the arguments.
           */
          FastCopyBundleElement(String sql, Function<FastCopyContext, Object[]> generateArgs)
          {
             this.sql = sql;
             this.generateArgs = generateArgs;
          }

          /**
           * Method that executes the SQL of this element, with the computed arguments.
           *
           * @param   context
           *          The context used to generate arguments.
           */
          void execute(FastCopyContext context)
          throws PersistenceException
          {
             srcPersistence.executeSQL(sql, Persistence.TEMP_CTX, generateArgs.apply(context));
          }
       }
    }