# Validation.java

    /*
    ** Module   : Validation.java
    ** Abstract : Database record validation and persistence operation.
    **
    ** Copyright (c) 2004-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 ECF 20200207 First revision. Validates non-nullable fields and unique constraints.
    **     OM  20200214 Added maximum index size validation implementation.
    **     CA  20200604 Track if during validation a newly created record was saved.
    **     ECF 20200909 Add StaleRecordException to those methods which can throw it.
    ** 002 ECF 20200925 Create session if needed.
    **         20200929 Performance: UNION ALL multiple unique index validation SQL statements together to
    **                  reduce number of queries prepared/sent.
    **     OM  20201001 Improved DMO manipulation performance by caching slow Property annotation access.
    **     AIL 20201020 Closed unclosed prepared statement.
    **     ECF 20201201 Fixed FFC invalidation for flush update case when record was validated previously.
    **         20210126 Ensure all unvalidated unique indices are validated at flush time.
    **     ECF 20210117 Do not update DMO state when explicitly validating.
    **     OM  20210419 After a record is successfully inserted in database table, the buffer storing it is
    **                  removed from early inserts list of dirty context.
    **     ECF 20210711 Confirm a unique constraint violation detected by query, in case unflushed changes to the
    **                  suspect record clear the violation.
    **     ECF 20210713 Improved efficiency of unique constraint check by query.
    **     OM  20210927 FFC invalidates the records whose changed fields are used in query predicates.
    **     ECF 20211005 Reworked FFC invalidation for individual records for performance.
    **         20211008 Slightly more aggressive FFC invalidation for non-indexed property updates.
    **     OM  20220701 In case of flush(), save the data in a micro-transaction before setting up the savepoint
    **                  for the new innermost block.
    **     OM  20221012 The TriggerTracker are bound to DMO, not to the buffer they are contained into.
    **     TJD 20220504 Upgrade do Java 11 minor changes
    **     RAA 20221221 Modified the execution of sql's. They now pass through the SQLStatementLogger
    **                  in case logging is intended.
    **     OM  20221117 Some properties (datetime-tz) may take a different number of positional parameters when
    **                  used in insert/update or where predicate.
    **     CA  20230110 getDirtyIndices and getUnvalidatedIndices now return null instead of an empty BitSet, when
    **                  no indices are found.
    ** 003 RAA 20230518 Replaced lambdas with method referencing when using SQLStatementLogger.
    ** 004 RAA 20230607 SQLs are now executed through SQLExecutor instead of SQLStatementLogger.
    ** 005 ECF 20230608 The write trigger should have a dedicated savepoint scoped to it.
    ** 006 RAA 20230615 Added SQL as a parameter when using SQLExecutor.
    ** 007 DDF 20230705 Replaced static field initialization with a method called at server bootstrap.
    ** 008 DDF 20231127 Used a dialect specific method to handle unique constraint violation.
    ** 009 RAA 20230724 Replaced FunctionWithException with QueryStatement.
    ** 010 RAA 20230802 Added support for VALIDATE keyword.
    **     RAA 20230802 Added support for H2 unique index validation.
    **     RAA 20230802 Split flush function into two for more straight-forward use when dealing with VALIDATE.
    **     RAA 20230802 In case a flush with VALIDATE fails, but it gets cured, a second flush is now attempted.
    **     RAA 20231127 Field allowDBUniqueCheck is now directly assigned instead of being passed as a parameter.
    **     RAA 20240316 Rebase fix: Added condition to skip the server-side index validation.
    ** 011 CA  20240320 When flush is not active and inside a FULL tx (thus the temp-table buffers are not
    **                  auto-commit, use 'validateUniqueByQuery' for temp-tables.
    ** 012 AL2 20240329 Set allowDbUniqueCheck using a bracket pattern (set and reset in finally).
    **                  Fixed case where the validation cure was not having an opened transaction.
    **                  Disable allowDbUniqueCheck when doing a cure.
    ** 013 AI  20240418 Allow invalidate of reverse lookup even if dirty prop is indexed.
    ** 014 CA  20240507 Unique index validation must validate only dirty indexes, even for NEW records, if we are
    **                  not in flush mode.
    ** 015 BS  20230511 Shared cache instance per persistent database.
    ** 016 CA  20240827 Notify that a record has been flushed - for non-temp records, this will allow invalidation
    **                  of the FastFindCache for that DMO.
    ** 017 OM  20240909 FFCAche does not require knowledge of current tenant.
    ** 018 TJD 20221125 Update DirtyShareContext on every index update
    **     TJD 20230210 Mark DMO validated before trying to put it into nursery
    **     TJD 20230428 Null check for unique and non-unique dirty indices
    **     TJD 20240110 Fixes for DirtyShare cross session.
    **     TJD 20240308 Allow RecordNursery to work over records modified in another transaction
    ** 019 AL2 20241022 Reduce logging level when validation is cured.
    ** 020 AS  20241004 Added support for validating a subset of unique indexes, used in batch assigns.
    ** 021 AL2 20240829 Use different endpoints into the record nursery for transient and updated records.
    **     AL2 20240903 Send dirty props before flush to record nursery to be aware of the actual changes.
    **     AL2 20241112 Adapt to new record nursery remove signature.
    ** 022 AL2 20241217 Don't notify shadow non-transient record index updates to the record nursery.
    ** 023 AS  20250220 Refactored validateMaybeFlush to not return the getActiveUpdateDiffs.
    */

    /*
    ** 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.orm;

    import java.math.*;
    import java.sql.*;
    import java.util.*;
    import java.util.logging.Level;

    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.dialect.*;
    import com.goldencode.p2j.persist.orm.SQLExecutor.QueryStatement;
    import com.goldencode.p2j.persist.orm.types.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.CentralLogger;

    /**
     * An object which performs validation of the state of a DMO, and optionally flushes the DMO's
     * data to the database, if the validation passes successfully. Before validation is performed,
     * this object determines whether validation and flushing is actually necessary, based on the
     * state of the DMO. This determination can be overridden to force validation under certain
     * conditions.
     * <p>
     * Validation consists of an optional (configurable) maximum index size check, a non-null field
     * check, and a unique constraint violation check.
     * <p>
     * Instances of this class are meant for a single use.
     */
    public class Validation
    implements DmoState
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(Validation.class);

       /** Record buffer requesting validation */
       private final RecordBuffer buffer;

       /** Record whose data is to be validated and optionally flushed to the database */
       private final BaseRecord dmo;

       /** Incubator for newly created records, until they are first flushed to the database */
       private final RecordNursery nursery;

       /** The name of the buffer. It is used when printing the error message, if any. */
       private final String bufferName;

       /** Tracks unique constraints in uncommitted transactions */
       private final UniqueTracker uniqueTracker;

       /** Context-local API helper for unique constraint tracking */
       private final UniqueTracker.Context uniqueTrackerCtx;

       /** Whether to flush the record; {@code false} to validate only */
       private final boolean flush;

       /** Whether to update the DMO state as a side effect of validation */
       private final boolean updateState;

       /**
        * The optional multiplex value, in case of validation of temp-table, otherwise ignored. It is only used
        * when invalidating the FastFind cache for the current table.
        */
       private final Integer multiplex;

       /** Flag indicating the record was flushed to the database (either newly inserted or updated) */
       private boolean flushed = false;

       /**
        * The maximum permitted size for all combined fields of an index. By default this
        * is equal to Progress version 10.x limit of 1971 bytes.
        * <p>
        * Set it to 0 to disable P4GL index-key limit check. The dialect specific limit is still
        * enforced by each dialect because otherwise irrecoverable errors may occur when persisting
        * records with wider key length.
        */
       private static int P4GL_MAX_INDEX_SIZE = DBUtils.DEFAULT_MAX_INDEX_SIZE;

       /**
        * Constructor.
        *
        * @param   buffer
        *          Record buffer which requested validation be performed.
        * @param   dmo
        *          Record whose data is to be validated and optionally flushed to the database.
        * @param   multiplex
        *          The optional multiplex value, in case of validation of temp-table, otherwise ignored. It is
        *          used only when invalidating the FastFind cache for the current table. Must be {@code null} for
        *          permanent tables. Must be positive for temp-tables.
        * @param   bufferName
        *          The name of the buffer. It is used when printing the error message, if any.
        * @param   nursery
        *          Incubator for newly created records, until they are first flushed to the database.
        * @param   uniqueTracker
        *          Tracks unique constraints in uncommitted transactions.
        * @param   uniqueTrackerCtx
        *          Context-local API helper for unique constraint tracking.
        * @param   flush
        *          {@code true} to validate and persist DMO's data; {@code false} to validate only.
        * @param   updateState
        *          Update the DMO's index and validation state as a side effect of validation. This should be set
        *          to {@code true} for implicit/organic validation; {@code false} for explicit validation.
        */
       public Validation(RecordBuffer buffer,
                         BaseRecord dmo,
                         Integer multiplex,
                         String bufferName,
                         RecordNursery nursery,
                         UniqueTracker uniqueTracker,
                         UniqueTracker.Context uniqueTrackerCtx,
                         boolean flush,
                         boolean updateState)
       {
          this.buffer = buffer;
          this.dmo = dmo;
          this.multiplex = multiplex;
          this.bufferName = bufferName;
          this.nursery = nursery;
          this.uniqueTracker = uniqueTracker;
          this.uniqueTrackerCtx = uniqueTrackerCtx;
          this.flush = flush;
          this.updateState = updateState;
       }

       /**
        * Constructs and returns the message that is printed with error 132 when the attempt to save (update or
        * insert) a record will lead to a collision in a unique index.
        *
        * @param   uIndex
        *          The index that failed validation, as a {@code BitSet} of its components.
        * @param   bufferName
        *          The legacy buffer name that failed the validation.
        * @param   dmo
        *          The record currently stored in buffer that caused the validation failure.
        *
        * @return  The message to be displayed when a unique index fails validation (error 132).
        */
       public static String getFailUniqueIndexMessage(BitSet uIndex, String bufferName, BaseRecord dmo)
       {
          if (uIndex == null)
          {
             return "unknown";
          }

          PropertyMeta[] props = dmo._recordMeta().getPropertyMeta(false);
          StringBuilder sb = new StringBuilder();
          sb.append(bufferName).append(" already exists with");

          for (int i = uIndex.nextSetBit(0); i >= 0; i = uIndex.nextSetBit(i + 1))
          {
             Object datum = dmo.data[i];
             if (props[i].getType() == recid.class)
             {
                // RECID values are not displayed here
                continue;
             }
             else
             {
                if (props[i].getType() == character.class)
                {
                   datum = "\"" + datum + "\"";
                }
                else
                {
                   BaseDataType asBDT = props[i].getDataHandler().convert(datum);
                   datum = asBDT.toStringMessage();
                }
             }
             sb.append(' ').append(props[i].getLegacyName()).append(' ').append(datum);
          }

          return sb.toString();
       }

       /**
        * This method will check whether the record is in a state which requires validation and
        * flushing, and will perform both if it is. The flush is an optional operation, and is only
        * performed if this object was constructed with the {@code persist} option set to {@code
        * true}.
        * <p>
        * The successful completion of this method indicates that either validation and flushing
        * were determined to not yet be appropriate, or that they were, and the operation(s)
        * completed successfully. Callers should check the state of the record after this method
        * returns to determine which case it was.
        * <p>
        * If validation is determined to be appropriate, but it fails, an exception is thrown.
        *
        * @throws  PersistenceException
        *          if there was a database error during validation or data persistence. This
        *          generally will not be recoverable.
        * @throws  ValidationException
        *          if there was a validation error. This represents an application level data
        *          validation failure and is recoverable.
        * @throws  StaleRecordException
        *          if the record being validated/flushed is stale. A stale record should not get this
        *          far under normal circumstances.
        */
       public void validateMaybeFlush()
       throws PersistenceException,
              ValidationException,
              StaleRecordException
       {
          try
          {
             boolean isNew = dmo.checkState(NEW);

             // get the offset of the property updated
             BitSet offsets = dmo.getActiveOffsets();

             // validate mandatory constraint, if any, on the updated field
             if (multiplex == null)
             {
                // mandatory constraints are only enforced for persistent tables
                checkNotNull(offsets);
                if (isNew)
                {
                   // if we're flushing, check all non-nullable properties; otherwise, only check dirty properties
                   int failingOffset = checkNotNull(!flush);
                   if (failingOffset >= 0)
                   {
                      failNotNull(failingOffset);
                   }
                }
             }

             // TODO: this check needs to be split apart to only check those indices needing it, like we do with
             // unique constraint validation
             checkMaxIndexSize();

             // validate fully updated unique indices, if any, or all indices if flushing or not updating index
             // state
             BitSet valUnique = dmo.getUnvalidatedIndices(offsets, flush || !updateState);
             if (valUnique != null && !valUnique.isEmpty())
             {
                // validate unique indices (and flush, if flush was requested)
                validateUniqueIndices(valUnique);
             }
             else if (flush || (buffer.isAutoCommit() && !dmo.needsValidation()))
             {
                // we need to flush to the database if:
                // * we've been explicitly told to do so, but there were no remaining unique indices to validate;
                // * we are auto-commit and there are no remaining indices to be updated
                flush();
             }

             if (!flushed && updateState)
             {
                if (isNew)
                {
                   // update the nursery if we did not already flush and:
                   // * the table has no remaining index needing update; or
                   // * any indices for a record were updated
                   boolean indexUpdated = false;
                   BitSet unique = dmo.getDirtyIndices(offsets, true, flush);
                   if (unique != null && !unique.isEmpty())
                   {
                      indexUpdated(unique, true);
                      indexUpdated = true;
                   }

                   BitSet nonunique = dmo.getDirtyIndices(offsets, false, flush);
                   if (nonunique != null && !nonunique.isEmpty())
                   {
                      indexUpdated(nonunique, false);
                      indexUpdated = true;
                   }
                   // update at least primary index
                   if (!indexUpdated)
                   {
                      nursery.indexUpdated(buffer, null, true, dmo);
                   }
                }
                else if (dmo.checkState(CHANGED))
                {
                   nursery.indexUpdated(buffer, dmo);
                }

                dmo.markValidated(offsets);
             }
          }
          catch (ValidationException exc)
          {
             nursery.remove(buffer, dmo);

             throw exc;
          }
       }

       /**
        * Method called at server bootstrap that initializes values from the directory
        * configuration. Until this method is called, default values are used.
        */
       public static void bootstrap()
       {
          DirectoryService ds = DirectoryService.getInstance();
          if (ds != null)
          {
             if (!ds.bind())
             {
                throw new RuntimeException("Directory bind failed");
             }

             try
             {
                P4GL_MAX_INDEX_SIZE = Utils.getDirectoryNodeInt(ds,
                                                                "persistence/max-index-size",
                                                                DBUtils.DEFAULT_MAX_INDEX_SIZE,
                                                                false);
             }
             finally
             {
                ds.unbind();
             }
          }
       }

       /**
        * Validate the unique indices represented by the given index bit set. If the validation was configured
        * to flush the record, it will be persisted (inserted or updated) if validation passes.
        *
        * @param   check
        *          A bit set whose set bits represent the 0-based positions of the unique indices to be
        *          validated in the target record's unique index array, as stored in the DMO's {@link RecordMeta}
        *          structure.
        *
        * @throws  PersistenceException
        *          if there was a database error during validation or data persistence. This
        *          generally will not be recoverable.
        * @throws  ValidationException
        *          if there was a validation error. This represents an application level data
        *          validation failure and is recoverable.
        * @throws  StaleRecordException
        *          if the record being validated/flushed is stale. A stale record should not get this
        *          far under normal circumstances.
        */
       private void validateUniqueIndices(BitSet check)
       throws PersistenceException,
              ValidationException,
              StaleRecordException
       {
          // unique constraint checking takes place in two phases:
          // 1. we check whether any uncommitted transaction has made a matching update to a unique
          //    index;
          // 2. we attempt to insert/update the DMO into the database, which will fail if there is a
          //    unique constraint violation (for validation-only, we roll back the change).

          UniqueTracker.Token token = null;
          Session session = buffer.getSession(true);

          try
          {
             // validate against uncommitted transaction data; no-op for temp-table
             token = uniqueTrackerCtx.lockAndChange(uniqueTracker, dmo, bufferName, flush);

             boolean allowDBUniqueCheck;
             boolean isCheckEmpty = check != null && check.isEmpty();
             if (isCheckEmpty)
             {
                allowDBUniqueCheck = false;
             }
             else
             {
                allowDBUniqueCheck = canSkipServerSideUniqueCheck() && (flush || buffer.isAutoCommit());
             }

             if ((multiplex != null || !flush) && !isCheckEmpty && !allowDBUniqueCheck)
             {
                // validate by executing unique index queries
                validateUniqueByQuery(check);
             }

             if (flush || buffer.isAutoCommit())
             {
                // validation will be done by the database as part of the flush (for persistent tables)
                boolean oldValue = session.getPersister().setAllowDBUniqueCheck(allowDBUniqueCheck);
                try
                {
                   flush();
                }
                finally
                {
                   // reset the persister allowDBUniqueCheck state
                   session.getPersister().setAllowDBUniqueCheck(oldValue);
                }
             }

             // validation was performed and succeeded; changes may have been persisted to database;
             // the new/dirty/changed state of the record will have have been updated by the save

             // mark record as having passed validation (whether or not data was persisted)
             dmo.updateState(session, INVALID, false);
          }
          catch (ValidationException exc)
          {
             if (token != null)
             {
                // token will be non-null when the UniqueTracker update was successful, but the
                // database update was not; roll back tracker changes; no-op for temp-table
                uniqueTrackerCtx.rollbackChange(uniqueTracker, token);
             }

             if (updateState)
             {
                // mark record as having failed validation; it will stay this way until it passes
                dmo.updateState(session, INVALID, true);
             }

             throw exc;
          }
          finally
          {
             // unlock tracker's resources locked above, no matter how we exit; no-op for temp-tables
             // and for no-unique-constraint case
             uniqueTrackerCtx.unlock(uniqueTracker, token);
          }
       }

       /**
        * One or more indices were updated as the result of the record's properties being touched. Notify the
        * record to update its meta state, and update the newly created record nursery.
        *
        * @param   indices
        *          Bit set representing those indices that were updated.
        * @param   unique
        *          {@code true} if {@code indices} refers to unique indices; else {@code false}.
        */
       private void indexUpdated(BitSet indices, boolean unique)
       throws PersistenceException
       {
          for (int i = indices.nextSetBit(0); i >= 0; i = indices.nextSetBit(i + 1))
          {
             int idxUid = (i + 1) * (unique ? 1 : -1);
             dmo.indexUpdated(idxUid);
          }
          nursery.indexUpdated(buffer, indices, unique, dmo);
       }

       /**
        * Flush the record being validated to the database.
        *
        * @throws  PersistenceException
        *          if there is any non-validation database error.
        * @throws  ValidationException
        *          if there is an error validating a unique or not-null constraint.
        * @throws  StaleRecordException
        *          if the record being flushed is stale. A stale record should not get this far under normal
        *          circumstances.
        */
       private void flush()
       throws PersistenceException,
              ValidationException,
              StaleRecordException
       {
          // notify the FastFindCache about the results which are about to be invalidated. In the event of
          // an error persisting the record, we will have invalidated the cache unnecessarily, but it's better
          // to lose the optimization than to get incorrect results in a parallel thread if it was accessing
          // the cache between save and invalidate operations
          RecordMeta recMeta = dmo._recordMeta();
          FastFindCache ffc = FastFindCache.getInstance(recMeta.isTemporary(), buffer.getDatabase());
          boolean wasNew = dmo.checkState(NEW);
          if (wasNew)
          {
             // invalidate cached results for all indexes on insert
             ffc.invalidate(recMeta.getDmoMeta().getId(), multiplex);
          }
          else
          {
             BitSet dirtyOffsets = dmo.getActiveOffsets();
             BitSet dirtyUnique = dmo.getDirtyIndices(dirtyOffsets, true, false);
             BitSet dirtyNonunique = dmo.getDirtyIndices(dirtyOffsets, false, false);
             if ((dirtyUnique != null && !dirtyUnique.isEmpty()) ||
                 (dirtyNonunique != null && !dirtyNonunique.isEmpty()))
             {
                // one or more index was touched; invalidate cached results across entire indices
                ffc.invalidate(recMeta.getDmoMeta().getId(), multiplex, dirtyUnique, dirtyNonunique);
             }

             BitSet dirtyProps = (BitSet) dmo.getDirtyProps().clone();
             // if any property was touched, invalidate cached result(s) for the touched record
             if (!dirtyProps.isEmpty())
             {
                ffc.invalidate(dmo.getClass().getName(), dmo.primaryKey(), dirtyProps);
             }
          }

          Session session = buffer.getSession(true);
          SavepointManager.PersistenceCode lastBlockSetup = null;
          if (!dmo.checkState(NOUNDO))
          {
             // preemptively set lazy savepoints in the savepoint manager before we create a "micro" savepoint
             // here, because otherwise, the save here will trigger the savepoint manager to lazily create them,
             // and we do not want those savepoints nested within this one
             SavepointManager savepointManager = session.getSavepointManager();
             if (savepointManager != null)
             {
                // this is a no-op if we are not in a transaction or we are in a full transaction block
                lastBlockSetup = savepointManager.lazilySetSavepoints(true);
             }
          }

          // if not already fired, do it now. When the trigger is fired the oldDMO is reset, preventing double
          // firing
          buffer.maybeFireWriteTrigger();

          BitSet dirtyProps = (BitSet) dmo.getDirtyProps().clone();
          try
          {
             flushImpl(session, wasNew);
          }
          finally
          {
             nursery.remove(buffer, dmo, dirtyProps);
          }

          flushed = true;

          if (lastBlockSetup != null)
          {
             lastBlockSetup.run();
          }
       }

       /**
        * The implementation of the flushing process to the database.
        * In case a flush with VALIDATE fails, the intention is to see if it can be "cured". If that is the case,
        * a normal flush will be attempted.
        *
        * @param   session
        *          The session in use.
        * @param   wasNew
        *          Is the DMO marked with NEW?
        *
        * @throws  PersistenceException
        *          if there is any non-validation database error.
        * @throws  ValidationException
        *          if there is an error validating a unique or not-null constraint.
        * @throws  StaleRecordException
        *          if the record being flushed is stale. A stale record should not get this far under normal
        *          circumstances.
        */
       private void flushImpl(Session session,
                              boolean wasNew)
       throws PersistenceException,
              ValidationException,
              StaleRecordException
       {
          // create a "micro" savepoint, so we can roll back the save on failure
          Savepoint sp = session.setSavepoint();
          boolean fullTx = false;

          if (sp == null)
          {
             // we are not currently within a transaction, so create a short-lived transaction to persist
             // the change
             fullTx = session.beginTransaction(null);
          }

          boolean commit = true;
          try
          {
             // this will perform the appropriate insert or update, based on the state of the DMO
             session.save(dmo, true);
             if (wasNew)
             {
                buffer.recordInserted();
             }
          }
          catch (PersistenceException exc)
          {
             commit = false;

             // rollback to the savepoint or rollback the micro-transaction
             if (sp != null)
             {
                session.rollbackSavepoint(sp);
             }
             else if (fullTx)
             {
                session.rollback();
             }

             // go up the exception chain and look for a SQL exception:
             Throwable cause = exc.getCause();
             boolean isAllowDBUniqueCheck = session.getPersister().getAllowDBUniqueCheck();
             while (cause != null                   &&
                   !(cause instanceof SQLException) &&
                   cause != cause.getCause())
             {
                cause = cause.getCause();
             }

             while (cause instanceof SQLException)
             {
                SQLException sqle = (SQLException) cause;
                // UNIQUE VIOLATION - unique_violation
                if (buffer.getDialect().isUniqueConstraintViolation(sqle))
                {
                   // if the right index is found, this method will not return normally, instead it will
                   // throw a specific exception
                   validateUniqueByQuery(null);
                   if (isAllowDBUniqueCheck)
                   {
                      // We were not able to execute the query with VALIDATE, but a "cure" occurred,
                      // meaning that we can try executing the query without validation.
                      if (LOG.isLoggable(Level.FINE))
                      {
                         LOG.fine("Flush with VALIDATE failed due to " + sqle);
                      }
                      boolean oldAllowDBUniqueCheck = session.getPersister().setAllowDBUniqueCheck(false);
                      try
                      {
                         flushImpl(session, wasNew);
                      }
                      finally
                      {
                         session.getPersister().setAllowDBUniqueCheck(oldAllowDBUniqueCheck);
                      }
                      return;
                   }
                   break;
                }

                // if this exception comes from H2, then we need to move to the inner exception (a.k.a.
                // JdbcSQLIntegrityConstraintViolationException), because the outer one (SQLException) does not
                // have the SQLState set accordingly.
                cause = sqle.getCause();
             }

             // if the exception could not be identified as a SQL UNIQUE VIOLATION exception or the
             // generateUniqueIndexesCondition() method could not identify the right index to compose the
             // P4GL error message, we let the caller to handle the exception
             throw new ValidationException(exc.getMessage(), exc);
          }
          finally
          {
             if (commit)
             {
                buffer.recordFlushed();

                // release the savepoint or commit/roll-back the micro-transaction
                if (sp != null)
                {
                   session.releaseSavepoint(sp);
                }
                else if (fullTx)
                {
                   session.commit();
                }
             }
          }
       }

       /**
        * Get the state of the {@link #flushed} flag.
        *
        * @return   See above.
        */
       public boolean wasFlushed()
       {
          return flushed;
       }

       /**
        * This method checks whether the record fits the maximum allowable index size for 4GL and the
        * current configured dialect. If there is at least one index that will pass this limit when
        * the change is applied a {@code ValidationException} is thrown. Otherwise the method returns
        * peacefully.
        */
       private void checkMaxIndexSize()
       throws ValidationException
       {
          Dialect dialect = buffer.getDialect();
          int dialectMaxIndexSize = dialect.getIndexLengthLimit();
          if (P4GL_MAX_INDEX_SIZE == 0 && dialectMaxIndexSize == 0)
          {
             // quick-out for index-key size validation is disabled
             return;
          }

          RecordMeta recordMeta = dmo._recordMeta();
          PropertyMeta[] properties = recordMeta.getPropertyMeta(false);
          BitSet[] uIndexes = recordMeta.getUniqueIndices();
          int[] uiSizes = new int[uIndexes.length];
          int[] duiSizes = new int[uIndexes.length];
          BitSet[] nIndexes = recordMeta.getNonuniqueIndices();
          int[] niSizes = new int[nIndexes.length];
          int[] dniSizes = new int[nIndexes.length];

          BitSet idxProps = recordMeta.getAllIndexedProperties();
          // walk changed bitset and update the uiSizes and niSizes with the space used
          for (int i = idxProps.nextSetBit(0); i >= 0; i = idxProps.nextSetBit(i + 1))
          {
             // compute the size for an index property
             PropertyMeta crtProp = properties[i];
             int size = crtProp.getDataHandler().getFieldSizeInIndex(dmo.data[i]);

             // search the unique indexes where it is used and add the size there. If an index is
             // breaking one of the constraints, stop with error and skip additional computations
             for (int uComp = 0, uIndexesLength = uIndexes.length; uComp < uIndexesLength; uComp++)
             {
                if (uIndexes[uComp].get(i))
                {
                   uiSizes[uComp] += size;
                   duiSizes[uComp] += dialect.computeIndexKeySize(crtProp.getType(), dmo.data[i]);
                   checkIndexSize(uiSizes[uComp], duiSizes[uComp], dialectMaxIndexSize, uComp, true);
                }
             }

             // do the same for non-unique indexes
             for (int nComp = 0, nIndexesLength = nIndexes.length; nComp < nIndexesLength; nComp++)
             {
                if (nIndexes[nComp].get(i))
                {
                   niSizes[nComp] += size;
                   dniSizes[nComp] += dialect.computeIndexKeySize(crtProp.getType(), dmo.data[i]);
                   checkIndexSize(niSizes[nComp], dniSizes[nComp], dialectMaxIndexSize, nComp, false);
                }
             }
          }

          // if we reach this point then we are good to go, no index constraints were broken
       }

       /**
        * Validates an intermediary index size against the maximum P4GL and dialect specific
        * constraints.
        *
        * @param   p4glSize
        *          The current calculated size of the space occupied by {@code dmo} in P4GL. May be
        *          incomplete.
        * @param   sqlSize
        *          The current calculated size of the space occupied by {@code dmo} in SQL. May be
        *          incomplete.
        * @param   dialectMaxSize
        *          The maximum supported index size by the dialect used in FWD.
        * @param   indexPos
        *          The position of the index.
        * @param   unique
        *          {@code true} only if the current index is unique.
        *
        * @throws  ValidationException
        *          When the {@code p4glSize} already overflows the index size constraint.
        */
       private void checkIndexSize(int p4glSize,
                                   int sqlSize,
                                   int dialectMaxSize,
                                   int indexPos,
                                   boolean unique)
       throws ValidationException
       {
          int x = (P4GL_MAX_INDEX_SIZE != 0 && p4glSize > P4GL_MAX_INDEX_SIZE) ? 1 :
                  (dialectMaxSize != 0      && sqlSize > dialectMaxSize) ? 2 : 0;
          if (x == 0)
          {
             return;
          }

          String legacyTableName = (dmo instanceof TempRecord ? "(TEMP-TABLE) " : "") + bufferName;

          String message;
          if (x == 1)
          {
             // Note that, when displayed in 4GL, the (129) represents the 'old' index limitation of
             //      OpenEdge <= v10.1B, not an error code. They increased the limitation in newer
             //      versions, but the error ocode remained the same.
             message = "The total length of the fields in an index exceeds max key size. " +
                       "Index " + dmo._recordMeta().getIndexLegacyName(indexPos, unique) +
                       " of table " + legacyTableName + " (129)";
          }
          else
          {
             // this record is stored successfully on P4GL but the current dialect cannot handle
             // it so a SQL error will be emitted if we continue with the flush to database
             message = "The total length of the fields in an index exceeds " +
                       "maximum key size supported by this dialect. " +
                       "Index " + dmo._recordMeta().getIndexLegacyName(indexPos, unique) +
                       " of table " + legacyTableName;
          }
          ErrorManager.displayError(11353, message, false);
          // the message is usually displayed multiple times with temp-tables
          throw new StopConditionException(message, 11353);
       }

       /**
        * Check whether properties are both mandatory and are currently {@code null}. If so,
        * throw an exception.
        *
        * @param   offsets
        *          Offsets of the properties to be checked in the DMO.
        *
        * @throws  ValidationException
        *          if the property is mandatory and is currently {@code null}.
        */
       private void checkNotNull(BitSet offsets)
       throws ValidationException
       {
          BitSet nonNullable = dmo._recordMeta().nonNullableProps;
          for (int i = offsets.nextSetBit(0); i != -1; i = offsets.nextSetBit(i + 1))
          {
             if (nonNullable.get(i) && dmo.data[i] == null)
             {
                failNotNull(i);  // err: 110
             }
          }
       }

       /**
        * Check whether any property is both mandatory and is currently {@code null}. If so, throw an exception.
        * This method should be invoked only for transient records which have the initial/default value set to
        * unknown (?). Normally the field assignment is tested for null/unknown values at the assign moment.
        *
        * @return  Negative value if the check succeeded, else the non-negative offset value of the first field
        *          in the DMO's data array to fail the non-null test.
        */
       private int checkNotNull(boolean onlyCheckDirty)
       throws ValidationException
       {
          BitSet nonNullable = dmo._recordMeta().nonNullableProps;
          BitSet currentlyNull = dmo.getNullProps();
          BitSet check = currentlyNull;
          if (onlyCheckDirty)
          {
             check = (BitSet) check.clone();
             check.and(dmo.getDirtyProps());
          }

          if (nonNullable.intersects(check))
          {
             BitSet copy = (BitSet) nonNullable.clone();
             copy.and(check);
             int offset = copy.nextSetBit(0);

             return offset;
          }

          return -1;
       }

       /**
        * The property at the given offset is mandatory but is currently {@code null}. Throw an
        * exception to signal the error.
        *
        * @param   offset
        *          Offset of the property in the DMO.
        *
        * @throws  ValidationException
        *          always, to indicate the mandatory property error.
        */
       private void failNotNull(int offset)
       throws ValidationException
       {
          DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmo.getClass());
          Property fi = dmoInfo.getProperty(offset);

          String msg = bufferName + "." + fi.legacy + " is mandatory, but has unknown (?) value";

          if (updateState)
          {
             dmo.updateState(buffer.getSession(false), INVALID, true);
          }

          throw new ValidationException(msg, 110);
       }

       /**
        * Validate the DMO with respect to the unique constraints, if any, of its table. This occurs
        * in two steps. The first checks whether any uncommitted transactions across sessions contain
        * any record with which this record would conflict, in terms of a unique constraint. This
        * step is a no-op for temp-tables, which are scoped to a single user session. The second step
        * attempts to insert (for a newly created record) or update (for a previously persisted
        * record) the record in its database. If this operation is meant to validate only, this
        * insert/update is rolled back. If either operation fails, an exception is thrown.
        * <p>
        * If this method returns normally, the record is considered valid from the standpoint of
        * unique constraints.
        *
        * @return  {@code true} if the record was saved (or updated) and this was not a validation-only
        *          operation.
        *
        * @throws  PersistenceException
        *          if there was a database error during validation or data persistence. This
        *          generally will not be recoverable.
        * @throws  ValidationException
        *          if there was a validation error. This represents an application level data
        *          validation failure and is recoverable.
        */
       /*
       private boolean checkUniqueConstraints()
       throws PersistenceException,
              ValidationException
       {
          boolean queryCheckMethod = false;
          int dirtyOffset = dmo.getActiveOffset();
          BitSet checkUniqIndices = null;
          RecordMeta recordMeta = dmo._recordMeta();

    //      if (!force)
    //      {
             // determine whether unique constraint check and flush is warranted based on current state
             if (dmo.checkState(NEW))
             {
                // for a transient (newly created) record, at least one index needs to be fully dirty
                // (even if not changed) to trigger validation and flush OR the table must have no indices
                if (dmo.isIndexed() && !dmo.isAnyIndexFullyDirty())
                {
                   // validation was not performed; do not update any record/property state
                   return false;
                }

                // if we are in a single property validation for a transient record, we need to check
                // whether ANY mandatory property is null; if so, this error may mask any unique constraint
                // violation when we try to persist the DMO (depending on the database implementation);
                // also use this method if 2 or fewer unique indices are fully dirty in connection with
                // the most recent change
                checkUniqIndices = dmo.getFullyDirtyUniqueIndices(dirtyOffset);
                queryCheckMethod = singleProp &&
                                   hasUniqueConstraint &&
                                   rollback &&
                                   (checkNotNull() >= 0 || checkUniqIndices.cardinality() <= 2);
             }
             else
             {
                // for an existing, persisted record, any update that touches an index component
                // triggers validation and possibly flush OR the table must have no indices
                if (dmo.isIndexed() && !dmo.getDirtyProps().intersects(recordMeta.allIndexedProps))
                {
                   // validation was not performed; do not update any record/property state
                   return false;
                }

    //            checkUniqIndices = dmo.getDirtyUniqueIndices(dirtyOffset);
    //            queryCheckMethod = singleProp &&
    //                               hasUniqueConstraint &&
    //                               rollback &&
    //                               checkUniqIndices.cardinality() <= 2;
             }
    //      }

          // unique constraint checking takes place in two phases:
          // 1. we check whether any uncommitted transaction has made a matching update to a unique
          //    index;
          // 2. we attempt to insert/update the DMO into the database, which will fail if there is a
          //    unique constraint violation (for validation-only, we roll back the change).

          UniqueTracker.Token token = null;
          boolean saved = false;

          try
          {
             if (hasUniqueConstraint)
             {
                // validate against uncommitted transaction data; no-op for temp-table
                token = uniqueTrackerCtx.lockAndChange(uniqueTracker, dmo, bufferName);
             }

             if (queryCheckMethod)
             {
                // use the query method of checking unique constraints instead of the persist & rollback
                // method; this will not get hung up on a null mandatory field in a transient DMO
                validateUniqueByQuery(checkUniqIndices);
             }
             else if (hasUniqueConstraint || !rollback)
             {
                // notify the FastFindCache about the indices which are about to be invalidated. In the event of
                // an error persisting the record ([saved] set to false below), we will have invalidated the cache
                // unnecessarily, but it's better to lose the optimization than to get incorrect results in a
                // parallel thread if it was accessing the cache between save and invalidate operations
                FastFindCache ffc = FastFindCache.getInstance(recordMeta.isTemporary());
                if (dmo.checkState(NEW))
                {
                   // invalidate all indexes on insert
                   ffc.invalidate(recordMeta.getDmoMeta().getId(), multiplex);
                }
                else
                {
                   // invalidate only affected indexes on update
                   BitSet dirtyUnique = dmo.getDirtyIndices(recordMeta.getUniqueIndices(), dirtyOffset);
                   BitSet dirtyNonunique = dmo.getDirtyIndices(recordMeta.getNonuniqueIndices(), dirtyOffset);
                   ffc.invalidate(recordMeta.getDmoMeta().getId(), multiplex, dirtyUnique, dirtyNonunique);
                }

                // validate against data already committed in the database; this has to be done for the validate
                // and persist case, even if we have no unique constraint
                saved = validateUniqueCommitted();
             }

             // validation was performed and succeeded; changes may have been persisted to database;
             // the new/dirty/changed state of the record will have have been updated by the save

             // mark record as having passed validation (whether or not data was persisted)
             dmo.updateState(INVALID, false);

             // log the validation event for possible rollback
             dmo.logValidation(session);
          }
          catch (ValidationException exc)
          {
             if (token != null)
             {
                // token will be non-null when the UniqueTracker update was successful, but the
                // database update was not; roll back tracker changes; no-op for temp-table
                uniqueTrackerCtx.rollbackChange(uniqueTracker, token);
             }

             // mark record as having failed validation; it will stay this way until it passes
             dmo.updateState(INVALID, true);

             throw exc;
          }
          finally
          {
             // unlock tracker's resources locked above, no matter how we exit; no-op for temp-tables
             // and for no-unique-constraint case
             uniqueTrackerCtx.unlock(uniqueTracker, token);

             // if full validation was done and we persisted the record, clear NEEDS_VALIDATION flag
             if (!singleProp && saved)
             {
                dmo.updateState(NEEDS_VALIDATION, false);
             }
          }

          return saved;
       }
       */

       /**
        * Iterate the specified unique indices and decide whether a record already exists with the unique
        * combination of index components in the DMO being validated. In this case, emit a specific validation
        * error. This method makes sure the proper index is identified to match the 4GL error message. For this,
        * the order in which the unique indexes are tested is essential.
        *
        * @param   check
        *          A bit set whose set bits represent the positions of the unique indices in the DMO's array
        *          of unique indices to be checked. If {@code null}, check all the unique indices.
        *
        * @throws  ValidationException
        *          when the method detects a collision of current DMO with another record stored in the database.
        * @throws  PersistenceException
        *          if an error occurred while accessing the database.
        */
       private void validateUniqueByQuery(BitSet check)
       throws PersistenceException,
              ValidationException
       {
          if (check != null && check.isEmpty())
          {
             return;
          }

          Session session = buffer.getSession(true);
          RecordMeta recordMeta = dmo._recordMeta();
          PropertyMeta[] props = recordMeta.getPropertyMeta(false);
          BitSet[] uniqueIndices = recordMeta.uniqueIndices;
          String[] uniqueSQLs = recordMeta.getUniqueIndicesValidationSQLs(
                Dialect.getDialect(session.getDatabase()));
          int len = uniqueIndices.length;

          if (check == null)
          {
             // check all unique indices
             check = new BitSet();
             check.set(0, len);
          }

          String query;
          if (check.cardinality() > 1)
          {
             // check multiple indices using UNION ALL
             StringBuilder buf = new StringBuilder();
             for (int k = check.nextSetBit(0), count = 0; k >= 0; k = check.nextSetBit(k + 1), count++)
             {
                if (count > 0)
                {
                   buf.append(" union all ");
                }

                buf.append(uniqueSQLs[k]);
             }
             query = buf.toString();
          }
          else
          {
             query = uniqueSQLs[check.nextSetBit(0)];
          }

          try (PreparedStatement ps = session.getConnection().prepareStatement(query))
          {
             int psOffset = 0;

             for (int k = check.nextSetBit(0); k >= 0; k = check.nextSetBit(k + 1))
             {
                psOffset = setUniqueQueryParameters(ps, uniqueIndices[k], recordMeta, props, psOffset);
             }

             QueryStatement f = PreparedStatement::executeQuery;
             ResultSet rs = SQLExecutor.getInstance().execute(session.getDatabase(), query, f, ps);

             while (rs.next())
             {
                // violated indices are detected in P4GL legacy order; for each one, confirm there is no
                // modified DMO in memory which cures the violation
                int i = rs.getInt(1);
                BitSet uIndex = uniqueIndices[i];

                if (confirmUniqueConstraintViolation(session, i, uIndex, uniqueSQLs[i], recordMeta, props))
                {
                   throw new ValidationException(getFailUniqueIndexMessage(uIndex, bufferName, dmo), 132);
                }

                // if we exit the loop normally, all violations are cured by in-memory changes (which may yet
                // have to be validated and flushed themselves)
             }
          }
          catch (SQLException sqlEx)
          {
             throw new PersistenceException("Failed to test uniqueness", sqlEx);
          }
       }

       /**
        * Set the substitution parameters for a prepared statement used to check uniqueness against the records
        * in a database.
        *
        * @param   ps
        *          Prepared statement.
        * @param   uIndex
        *          A bit set describing which data elements participate in the unique index being checked by the
        *          query.
        * @param   recordMeta
        *          Metadata for the DMOs associated with the table being queried.
        * @param   props
        *          Array of DMO metadata objects, each describing a property of the DMO interface associated with
        *          the table being queried.
        * @param   psOffset
        *          Zero-based offset of the first parameter to be substituted in the prepared statement by this
        *          method (this method can be called multiple times for the same prepared statement, if multiple
        *          individual statements are UNION'd together for a unique constraint check).
        *
        * @return  The one-based offset of the last parameter substituted by the current invocation.
        *
        * @throws  SQLException
        *          if there is a SQL error substituting parameters.
        */
       private int setUniqueQueryParameters(PreparedStatement ps,
                                            BitSet uIndex,
                                            RecordMeta recordMeta,
                                            PropertyMeta[] props,
                                            int psOffset)
       throws SQLException
       {
          TypeManager.getTypeHandler(Long.class).setParameter(ps, ++psOffset, dmo.primaryKey(), false);

          if (recordMeta.isTemporary())
          {
             TypeManager.getTypeHandler(Integer.class)
                        .setParameter(ps, ++psOffset, ((TempRecord) dmo)._multiplex(), false);
          }

          for (int i = uIndex.nextSetBit(0); i >= 0; i = uIndex.nextSetBit(i + 1))
          {
             Object datum = dmo.data[i];
             if (datum != null)
             {
                if (props[i].getType() == character.class)
                {
                   datum = TextOps.trim((String) datum, " ", false, true);
                   if (!props[i].isCaseSensitive())
                   {
                      datum = ((String) datum).toUpperCase();
                   }
                }
                else if (props[i].getType() == decimal.class)
                {
                   // must round [datum] to field's decimals
                   datum = ((BigDecimal) datum).setScale(props[i].getDecimals(), RoundingMode.HALF_UP);
                }
             }
             props[i].getDataHandler().setParameter(ps, ++psOffset, datum, false);
          }

          return psOffset;
       }

       /**
        * Function that checks if VALIDATE keyword can be used with this statement.
        *
        * @return  boolean
        *          {@code true} if there exists support for the VALIDATE keyword.
        */
       private boolean canSkipServerSideUniqueCheck()
       {
          return buffer != null && buffer.getPersistence().allowDBUniqueCheck();
       }

       /**
        * Verify a unique constraint violation detected by querying the database, by confirming that the record
        * found to be causing the constraint violation does not have unflushed changes associated with it in
        * memory, awaiting its own validation and flush operation. If a modified, unflushed copy of the suspect
        * record is found in the current session cache, its data elements which participate in the violated
        * constraint are compared with those causing the violation. If any element compares differently, the
        * constraint violation is rejected.
        *
        * @param   session
        *          Current database session.
        * @param   pos
        *          The position of the unique index being tested in the array of the target DMO's unique indices.
        * @param   index
        *          A bit set describing which data elements participate in the unique index being tested.
        * @param   sql
        *          The original SQL statement associated with the unique constraint check for the unique index
        *          being tested. It is modified to return the primary key of the violating record.
        * @param   recordMeta
        *          Metadata for the DMOs associated with the table being queried.
        * @param   props
        *          Array of DMO metadata objects, each describing a property of the DMO interface associated with
        *          the table being queried.
        *
        * @return  {@code true} if the unique constraint violation was confirmed; {@code false} if it was
        *          rejected based on unflushed, in-memory changes.
        *
        * @throws  PersistenceException
        *          if there is an error getting a JDBC connection or accessing the session cache.
        * @throws  SQLException
        *          if there is an error preparing or executing the query.
        */
       private boolean confirmUniqueConstraintViolation(Session session,
                                                        int pos,
                                                        BitSet index,
                                                        String sql,
                                                        RecordMeta recordMeta,
                                                        PropertyMeta[] props)
       throws PersistenceException,
              SQLException
       {
          // modify SELECT statement to select primary key instead of the position of the violated index
          int len = ("select " + pos).length();
          String sqlPK = "select " + Session.PK + sql.substring(len);

          try (PreparedStatement ps = session.getConnection().prepareStatement(sqlPK))
          {
             setUniqueQueryParameters(ps, index, recordMeta, props, 0);
             ResultSet rs = ps.executeQuery();
             if (!rs.next())
             {
                // if there is no violation at this point, another session may have cleared it asynchronously
                // (should not happen for a temp-table)
                return false;
             }

             long id = rs.getLong(1);
             BaseRecord cached = session.getCached(dmo.getClass(), id);

             if (cached != null && cached.checkState(DmoState.CHANGED))
             {
                // if any data element in the changed record does not match the corresponding unique check datum,
                // the violation is cleared; note that the changed record may still fail its own validation later
                for (int i = index.nextSetBit(0); i >= 0; i = index.nextSetBit(i + 1))
                {
                   Object uniqueDatum = dmo.data[i];
                   Object cachedDatum = cached.data[i];
                   if (!Objects.equals(uniqueDatum, cachedDatum))
                   {
                      return false;
                   }
                }
             }
          }

          // no unflushed record clears the violation, so it is confirmed
          return true;
       }

       /**
        * Use the database to validate and optionally persist all current changes in the record.
        * This method sets a savepoint and attempts to insert/update the record. If it succeeds,
        * the data is considered valid from a unique constraint perspective.
        * <p>
        * If this object was constructed to not persist the data, the current transaction is rolled
        * back to this savepoint. Otherwise, the transaction is allowed to continue.
        * <p>
        * It is possible that this method is called when there is no active transaction within which
        * to set a savepoint. This can occur when inserting or updating a temp table record outside
        * an application transaction. In this case, we create a database transaction instead of a
        * savepoint (unless this operation is validation only). The rollback semantics are the same
        * whether using a savepoint or a transaction.
        *
        * @return  {@code true} if the record was saved (or updated) and we are not in a validation-only
        *          check.
        *
        * @throws  PersistenceException
        *          if there was a database error during validation or data persistence. This
        *          generally will not be recoverable.
        * @throws  ValidationException
        *          if there was a validation error. This represents an application level data
        *          validation failure and is recoverable.
        */
       /*
       private boolean validateUniqueCommitted()
       throws PersistenceException,
              ValidationException
       {
          // if there is at least one unique index to validate against, create a savepoint, so we can roll
          // back the save on failure
          Savepoint sp = session.setSavepoint();
          boolean fullTx = false;

          if (sp == null)
          {
             // we are not currently within a transaction, so create a short-lived transaction to persist
             // the change
             fullTx = session.beginTransaction(null);
          }

          try
          {
             // this will perform the appropriate insert or update, based on the state of the DMO
             session.save(dmo, !rollback);
          }
          catch (PersistenceException exc)
          {
             // validation failed, or we had a different error; roll back to savepoint
             // TODO: is this always safe (or even possible) in the event of a non-unique constraint
             //       related database error?
             if (sp != null)
             {
                session.rollbackSavepoint(sp);
             }
             else if (fullTx)
             {
                session.rollback();
             }

             // go up the exception chain and look for a SQL exception:
             Throwable cause = exc.getCause();
             while (cause != null                   &&
                   !(cause instanceof SQLException) &&
                   cause != cause.getCause())
             {
                cause = cause.getCause();
             }
             if (cause instanceof SQLException)
             {
                SQLException sqle = (SQLException) cause;
                if ("23505".equals(sqle.getSQLState())) // UNIQUE VIOLATION - unique_violation
                {
                   // if the right index is found, this method will not return normally, instead it will
                   // throw a specific exception
                   validateUniqueByQuery(null);
                }
             }

             // if the exception could not be identified as a SQL UNIQUE VIOLATION exception or the
             // generateUniqueIndexesCondition() method could not identify the right index to compose the
             // P4GL error message, we let the caller to handle the exception
             throw new ValidationException(exc.getMessage(), exc);
          }

          // if only validating and not persisting, roll back; else, commit
          if (rollback)
          {
             if (sp != null)
             {
                session.rollbackSavepoint(sp);
             }
             else if (fullTx)
             {
                session.rollback();
             }
          }
          else
          {
             if (sp != null)
             {
                session.releaseSavepoint(sp);
             }
             else if (fullTx)
             {
                session.commit();
             }
          }

          return !rollback;
       }
       */
    }