# DirtyShareContext.java

    /*
    ** Module   : DirtyShareContext.java
    ** Abstract : The implementation of DirtyShareContext
    **
    ** Copyright (c) 2004-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 ECF 20080420   @38626 Created initial version. Client context's
    **                           primary bridge to the DirtyShareManager.
    **                           Manages dirty state specific to the current
    **                           context.
    ** 002 ECF 20080610   @38682 Added safety logic to prevent NPEs.
    ** 003 ECF 20080612   @38719 Fixed getDirtyInfo(). If an uncommitted
    **                           delete was made before the backing table in
    **                           the dirty database was created, we could
    **                           attempt a query nonetheless and bomb.
    ** 004 ECF 20080627   @39073 Minor optimization. Avoid collection empty
    **                           check is some cases.
    ** 005 CA  20080716   @39122 Renamed method lockUniqueIndexes to
    **                           lockAllIndexes - this will lock all the
    **                           indexes for a certain entity. Modified the
    **                           delete(...) and rollbackInsert(...) methods
    **                           so the index locking can be disabled.
    ** 006 ECF 20080815   @39542 Added support for global change events. These
    **                           are events that represent index-changing
    **                           inserts, updates, or deletes, which are
    **                           processed by sessions other than the session
    **                           in which the event originated.
    ** 007 ECF 20081211   @40869 Fixed delete()/rollbackInsert() defect. The
    **                           deletion of a record which was inserted in
    **                           the same transaction rolls back the insert,
    **                           but does not track the record as deleted.
    **                           This caused a problem when the insert itself
    **                           was later rolled back. Such records are now
    **                           tracked via the deadInserts set.
    ** 008 ECF 20090428   @42129 Added isLocalInsert(). Reports whether a record
    **                           is being tracked as newly inserted by this
    **                           context.
    ** 009 ECF 20090513   @42173 Modified update() method. Need to pass new lock
    **                           parameter to modified DirtyShareManager update()
    **                           method.
    ** 010 ECF 20090601   @42569 Added support for Progress isolation leak quirk.
    **                           Added new methods.
    ** 011 ECF 20090610   @42639 Modified updateSnapshots() method. Now returns
    **                           list of DMOs affected and tracks entities whose
    **                           uncommitted changes have been leaked. The list of
    **                           such entities is now used during cleanup.
    ** 012 ECF 20090611   @42661 Optimized updateSnapshots(). In a long-running
    **                           transaction with many updates, it was doing much
    **                           unnecessary work. We now track which DMOs need to
    **                           have their snapshots updated and refresh only
    **                           these.
    ** 013 CA  20090721   @43285 Fixed rollback of dirty records, when a record is
    **                           in dead insert mode - the changes map should not
    **                           be checked in this case.
    ** 014 ECF 20090721   @43307 Modified delete(). If the record being deleted is
    **                           already being tracked for changes, it must be
    **                           removed from the changes and dirtySnapshots
    **                           collections.
    ** 015 ECF 20090819   @43692 Added new update() variant. Used when it is known
    **                           that the target DMO already is being tracked by
    **                           the dirty share manager.
    ** 016 ECF 20110405          Fixed dirty change leak. Remove record identifier
    **                           from deadInserts set upon insert to avoid error
    **                           during rollbackInsert() for same record later.
    **                           Otherwise, a record is leaked in the dirty share
    **                           manager for the life of the server process. Also
    **                           fixed (separate) minor bug in rollbackInsert()
    **                           which was not removing dirtySnapshot identifiers
    **                           when their corresponding records were removed
    **                           from dirty share tracking.
    ** 017 ECF 20131028          Import change.
    ** 018 ECF 20140821          Changed insert API to allow insertion of unvalidated, newly created
    **                           records.
    ** 019 OM  20160307          Added support for EXTENTs in update() methods. Updated signature of
    **                           getDirtyInfo().
    ** 020 ECF 20160608          Removed unnecessary intern calls.
    ** 021 ECF 20160917          Added ignoreLocal parameter to isDirtyDelete.
    ** 022 ECF 20171217          Prevent error trying to merge null DMOs in updateSnapshots.
    **         20171219          Avoid using incorrect DMO in update during new record creation.
    ** 023 ECF 20180719          Fixed defect in optimization in getDirtyInfo.
    ** 024 ECF 20200901          New ORM implementation.
    ** 025 OM  20200929          Reset the temporary NEW flag after record is saved to dirty database.
    **     OM  20210419          After a record is successfully inserted in database table, the buffer storing it
    **                           is removed from early inserts list of dirty context.
    **     OM  20221103          New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
    ** 026 AL2 20230822          Removed readOnly parameter.
    ** 027 AL2 20240319          Extended isEntityDirty to allow checking only the current context.
    ** 028 TJD 20240123          Java 17 compatibility updates
    ** 029 TJD 20230421          Allow reuse of DELETED and then UPDATED dirty share records
    **     TJD 20230508          Updates needed to support dirty records leaks to other sessions on FIND
    **     TJD 20230724          Migrate earlyPublishEntities from List to Set to improve performance
    **     TJD 20240109          Fixes and updates to cross section DirtyShare
    **     TJD 20240308          DirtyShare optimizations and simplifications
    **     TJD 20240318          Support for cases with released records
    **     TJD 20240604          Support for cases with records modified after initial flushing
    **     TJD 20240619          Fix intra session earlyInserts leaks
    **     AL2 20240903          Added dirtyData as a marker on the data that was actually changed.
    ** 030 AL2 20241021          Adapt getDirtyInfo for CAN-FIND cases in which the buffer is not loaded with the
    **                           actual DMO as a result of the query execution.
    ** 031 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
    **                           based on [sharedDb] parameter.
    ** 032 AL2 20250110          Use entity and DMO instead of buffer when updating.
    ** 033 ES  20250426          Handle StopConditionException through ErrorManager.
    */

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

    import java.util.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.Record;
    import com.goldencode.p2j.persist.event.*;
    import com.goldencode.p2j.persist.lock.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;

    /**
     * Default implementation of a client context's primary interface to store
     * uncommitted index updates for sharing with other contexts, and to read
     * uncommitted index updates made by other contexts.  This is intended to
     * support compatibility with Progress' peculiar transaction isolation
     * behavior of immediately sharing updates to indexed fields across user
     * sessions, but deferring the sharing of updates to non-indexed fields until
     * the enclosing transaction is committed.
     * <p>
     * This implementation maintains state specific to the current context, in
     * order both to filter information appropriately, and to be able to clean up
     * shared data when the current context's transactions are completed.
     *
     * @see  DirtyShareManager
     * @see  DefaultDirtyShareManager
     */
    public final class DirtyShareContext
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(DirtyShareContext.class.getName());

       /** Object which manages dirty reads for the database */
       private final DirtyShareManager manager;

       /** Identifiers of records inserted by this context */
       private Set<RecordIdentifier<String>> inserts = null;

       /** Identifiers of records leaked by this context */
       private Set<RecordIdentifier<String>> leaks = null;

       /** Identifiers of records inserted by this context, which deletes have rolled back */
       private Set<RecordIdentifier<String>> deadInserts = null;

       /** Buffers holding newly created records, which have not yet had all their indices updated */
       private Map<RecordIdentifier<String>, RecordBuffer> earlyInserts = null;

       /** Map of entity names to sets of IDs inserted/updated by this context */
       private Map<String, Set<Long>> changes = null;

       /** Map of entity names to sets of IDs deleted by this context */
       private Map<String, Set<Long>> deletes = null;

       /** Set of IDs of DMOs changed since last updateSnapshots() call */
       private Set<RecordIdentifier<String>> dirtySnapshots = null;

       /**
        * Constructor which stores a reference to the dirty share manager for the
        * associated database.
        *
        * @param   manager
        *          Dirty share manager for the associated database.
        */
       DirtyShareContext(DirtyShareManager manager)
       {
          this.manager = manager;
       }

       /**
        * Register with a global event clearinghouse an interest in insert,
        * delete, and update events which affect the given DMO entity.
        *
        * @param   entity
        *          DMO entity for which the caller is interested in tracking
        *          change.
        *
        * @return  A unique identifier for the registrant, assigned by the event
        *          clearinghouse, used later to collect events and deregister.
        *
        * @see     #getGlobalEvents(long, long)
        * @see     #deregisterForGlobalEvents(long)
        */
       public Long registerForGlobalEvents(String entity)
       {
          return manager.registerForGlobalEvents(entity);
       }

       /**
        * Deregister with a global event clearinghouse an interest in insert,
        * delete, and update events which affect a particular DMO type.
        *
        * @param   registerID
        *          A unique identifier for the registrant being deregistered.
        *
        * @see     #getGlobalEvents(long, long)
        * @see     #registerForGlobalEvents(String)
        */
       public void deregisterForGlobalEvents(long registerID)
       {
          manager.deregisterForGlobalEvents(registerID);
       }

       /**
        * Retrieve any new events that have been collected by a global event
        * clearinghouse, representing inserts, deletes, and updates made to
        * record types for which the caller previously has registered interest.
        *
        * @param   registerID
        *          A unique identifier for an object which has registered interest
        *          in record-changing events.
        * @param   lastID
        *          A unique identifier representing the last event retrieved by
        *          the caller.  If less than 0, it is assumed no events have yet
        *          been retrieved since the caller registered interest in these
        *          events.
        *
        * @return  Array of applicable, new events, or <code>null</code> if no
        *          such events have been collected.
        *
        * @throws  EventRegistrationException
        *          if <code>registerID</code> is invalid or has expired.
        */
       public GlobalChangeEvent[] getGlobalEvents(long registerID, long lastID)
       throws EventRegistrationException
       {
          return manager.getGlobalEvents(registerID, lastID);
       }

       /**
        * Lock or unlock all indexes associated with the given DMO entity.
        *
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation
        *          class associated with a table being tracked for uncommitted
        *          changes.
        * @param   lockType
        *          Type of lock to acquire, or <code>NONE</code> to release
        *          existing locks.  <code>NO_WAIT</code> variants will be treated
        *          as regular, blocking variants.
        *
        * @throws  PersistenceException
        *          if there is an error querying JDBC metadata while retrieving
        *          index information.
        */
       public void lockAllIndexes(String entity, LockType lockType)
       throws PersistenceException
       {
          manager.lockAllIndexes(entity, lockType);
       }

       /**
        * Begin tracking a newly inserted record in the dirty database.
        * <p>
        * This record is removed when the adding context's current transaction is committed or rolled back.
        *
        * @param   buffer
        *          Record buffer responsible for the insert.
        * @param   dmo
        *          Record to be tracked.
        * @param   lock
        *          {@code true} to acquire write locks to insert the record;
        *          {@code false} to assume write locks are acquired by calling code.
        * @param   dirtyData
        *          The data that was changed and caused this update. This is explicitly sent
        *          because sometimes we can't infer the dirty props from the DMO anymore.
        *
        * @throws  PersistenceException
        *          if there is any database error.
        */
       public void insert(RecordBuffer buffer, Record dmo, boolean lock, DirtyData dirtyData)
       throws PersistenceException
       {
          if (dmo == null)
          {
             // no current buffer, no-op
             return;
          }
          String entity = buffer.getEntityName();
          Long id = dmo.primaryKey();
          RecordIdentifier<String> ident = new RecordIdentifier<>(entity, id);

          // make a copy of the DMO to share with other contexts
          Record copy = dmo.snapshot();

          // if record comes from record nursery and needs validation mark it as such
          if (dmo.needsValidation())
          {
             if (earlyInserts == null)
             {
                earlyInserts = new HashMap<>();
             }

             earlyInserts.putIfAbsent(ident, buffer);
          }

          if (deadInserts != null)
          {
             // Remove this record from the dead inserts set if we were tracking
             // it as such;  this will prevent any problems if we later have to
             // roll back this insert.
             deadInserts.remove(ident);
          }

          // store in database
          manager.insert(entity, copy, dmo.getIndexState(), lock, dirtyData);

          // Track insert in local context.
          if (inserts == null)
          {
             inserts = new HashSet<>();
          }
          inserts.add(ident);
          trackChange(entity, id);

          // the NEW flag was added above to instruct the [Persister] that this is a new record for this database
          // and it needs to be inserted, not updated, but it was not cleared because the second parameter of
          // [session.save(copy, false);] prevented the flag to be unset. Doing this now:
          copy.updateState(null, DmoState.NEW, false);
       }

       /**
        * Indicate whether the specified record is being tracked as newly inserted
        * by this context.
        *
        * @param   ident
        *          Record identifier of the record to be checked.
        *
        * @return  <code>true</code> if this record is newly inserted by this
        *          context, else <code>false</code>.
        */
       public boolean isTracked(RecordIdentifier<String> ident)
       {
          if (changes != null && changes.containsKey(ident.getTable()))
          {
             if (changes.get(ident.getTable()).contains(ident.getRecordID()))
             {
                return true;
             }
          }

          return manager.isTracked(ident.getTable(), ident.getRecordID());
       }

       /**
        * If the specified record is an unvalidated insert created by a buffer local to this context,
        * get the buffer which created it.
        *
        * @param   ident
        *          Record identifier of the record to be checked.
        *
        * @return  Creating buffer, or <code>null</code> if the record is not a locally created,
        *          unvalidated insert.
        */
       public RecordBuffer getCreatingBuffer(RecordIdentifier<String> ident)
       {
          return earlyInserts != null ? earlyInserts.get(ident) : null;
       }

       /**
        * Rollback the insert of a record previously introduced with the {@link
        * #insert(RecordBuffer, Record, boolean, DirtyData)} method.
        *
        * @param   ident
        *          Identifier for the target record, which encapsulates its entity
        *          name and primary key.
        * @param   lock
        *          <code>true</code> to acquire write locks;
        *          <code>false</code> to assume write locks are acquired by
        *          calling code.
        *
        * @throws  IllegalStateException
        *          if the record was not inserted in this context during the
        *          current transaction
        * @throws  PersistenceException
        *          if there is any database error.
        */
       public void rollbackInsert(RecordIdentifier<String> ident, boolean lock)
       throws PersistenceException
       {
          // if the target record was already deleted from the dirty context, then
          // the changes map check is not relevant (it may be empty if only one
          // record is in the dirty context and this is a dead record)
          if ((deadInserts == null || !deadInserts.contains(ident)) &&
              (changes == null || changes.isEmpty() ||
               inserts == null || !inserts.contains(ident)))
          {
             throw new IllegalStateException(
                "No dirty entry for '" + ident + "';  cannot roll back insert");
          }

          try
          {
             if (deadInserts != null && deadInserts.remove(ident))
             {
                // This insert occurred within the existing transaction, and was
                // rolled back by a subsequent delete within the transaction, so
                // it already has been removed from the dirty share manager.  We
                // cannot remove it again as this will cause an error.

                if (deadInserts.isEmpty())
                {
                   deadInserts = null;
                }

                return;
             }

             // Remove from local context insert tracking.
             inserts.remove(ident);
             if (earlyInserts != null)
             {
                earlyInserts.remove(ident);
             }

             // Remove from database.
             manager.rollbackInsert(ident, lock);
          }
          finally
          {
             // Remove from local context change tracking.
             String entity = ident.getTable();
             Long id = ident.getRecordID();
             Set<Long> ids = changes.get(entity);
             if (ids != null && ids.remove(id))
             {
                if (ids.isEmpty())
                {
                   changes.remove(entity);
                }
                dirtySnapshots.remove(ident);
             }
          }
       }

       /**
        * Notify this object that a record was inserted into database and should not be tracked any more in
        * {@code earlyInserts}.
        *
        * @param   id
        *          The identifier of the record.
        */
       public void recordInserted(RecordIdentifier<String> id)
       {
          if (earlyInserts != null && !earlyInserts.isEmpty())
          {
             earlyInserts.remove(id);
          }
       }

       /**
        * Track an update to one or more indexed, DMO properties, as indicated by the
        * {@code properties}, {@code extIndexes} and {@code values} arguments.
        * <p>
        * Any record added to the dirty database as a result of this method is removed when the adding
        * context's current transaction is committed or rolled back.
        * <p>
        * Assumes appropriate indexes have been locked by the caller.
        *
        * @param   entity
        *          The entity table for which the DMO is provided
        * @param   dmo
        *          The DMO that should be registered as updated to the dirty-share mechanism.
        * @param   transientIndexValidated
        *          {@code true} if the update was caused by an index validation of a transient record.
        * @param   dirtyData
        *          The data that was changed and caused this update. This is explicitly sent
        *          because sometimes we can't infer the dirty props from the DMO anymore.
        *
        * @throws  PersistenceException
        *          if there is any database error.
        */
       public void update(String entity,
                          Record dmo,
                          boolean transientIndexValidated,
                          DirtyData dirtyData)
       throws PersistenceException
       {
          if (dmo == null)
          {
        	  // no-op
        	  return;
          }

          Long id = dmo.primaryKey();

          // determine whether the dirty share manager is already tracking changes to this record from
          // a previous insert or update.  If not, begin tracking the record, then update it. This set
          // of calls is assumed to be "atomic", since we must have an exclusive lock on the record.
          if (manager.isTracked(entity, id))
          {
             manager.update(entity,
                            id,
                            transientIndexValidated,
                            false,
                            dirtyData);
          }
          else
          {
             // get a snapshot of a current record if not yet tracked and start tracking it
             // not sure if this ever will be called as most cases are handled by insert now.
             Record copy = dmo.snapshot();
             copy.updateState(null, DmoState.NEW, true); // the record is new relative to DIRTY database

             manager.update(entity,
                            copy,
                            false,
                            dirtyData);
             copy.updateState(null, DmoState.NEW, false);
          }

          // Track update in local context.
          trackChange(entity, id);
       }

       /**
        * Mark a record as having been deleted in an uncommitted transaction, or
        * unmark a record which previously was so marked.
        * <p>
        * If marking a record as deleted, that was earlier inserted by the current
        * context, rollback the insert, but do not mark the record as deleted.
        * <p>
        * A record is unmarked when the adding context's current transaction is
        * committed or rolled back.
        *
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation
        *          class associated with a table being tracked for uncommitted
        *          changes.
        * @param   id
        *          Primary key of deleted record.
        * @param   rollback
        *          If <code>false</code>, the target record is marked as deleted;
        *          if <code>true</code>, it is unmarked.
        * @param   lock
        *          <code>true</code> to acquire write locks to delete the record;
        *          <code>false</code> to assume write locks are acquired by
        *          calling code.
        *
        * @throws  PersistenceException
        *          if a database error occurs rolling back an earlier insert.
        */
       public void delete(String entity, Long id, boolean rollback, boolean lock)
       throws PersistenceException
       {
          RecordIdentifier<String> ident = new RecordIdentifier<>(entity, id);

          if (!rollback)
          {
             // Special case 1:  if we are deleting a record we previously
             // inserted in the same transaction, remove the record, but do not
             // update the shared deleted records state.
             if (inserts != null && inserts.contains(ident))
             {
                rollbackInsert(ident, lock);
                if (deadInserts == null)
                {
                   deadInserts = new HashSet<>(8);
                }
                deadInserts.add(ident);

                return;
             }

             // Special case 2:  if we are deleting a record we previously
             // updated in the same transaction, remove the record, but still
             // update the shared deleted records state.
             if (changes != null)
             {
                Set<Long> changed = changes.get(entity);
                if (changed != null && changed.remove(id))
                {
                   if (changed.isEmpty())
                   {
                      changes.remove(entity);
                   }
                }
             }
             if (dirtySnapshots != null)
             {
                dirtySnapshots.remove(ident);
             }
          }

          // Update shared state.
          try
          {
             manager.delete(entity, id, rollback, lock);
          }
          finally
          {
             // Track state locally, so we can clean up at end of transaction.
             if (rollback)
             {
                if (deletes != null)
                {
                   Set<Long> ids = deletes.get(entity);
                   if (ids != null)
                   {
                      if (ids.remove(id) && ids.isEmpty())
                      {
                         deletes.remove(entity);
                      }
                   }
                }
             }
             else
             {
                Set<Long> ids = null;
                if (deletes == null)
                {
                   deletes = new HashMap<>();
                }
                else
                {
                   ids = deletes.get(entity);
                }
                if (ids == null)
                {
                   ids = new HashSet<>();
                   deletes.put(entity, ids);
                }
                ids.add(id);
             }
          }
       }

       /**
        * Indicate whether there are currently any uncommitted changes (inserts,
        * updates, or deletes) in the table represented by the given DMO entity
        * name.
        *
        * @param   entity
        *          Fully qualified name of the DMO entity to be checked.
        *
        * @return  <code>true</code> if there any pending, uncommitted changes in
        *          the backing table, else <code>false</code>.
        */
       public boolean isEntityDirty(String entity)
       {
          return isEntityDirty(entity, false);
       }

       /**
        * Indicate whether there are currently any uncommitted changes (inserts,
        * updates, or deletes) in the table represented by the given DMO entity
        * name.
        *
        * @param   entity
        *          Fully qualified name of the DMO entity to be checked.
        * @param   localOnly
        *          Check for the dirty status only in the current context.
        *
        * @return  <code>true</code> if there any pending, uncommitted changes in
        *          the backing table, else <code>false</code>.
        */
       public boolean isEntityDirty(String entity, boolean localOnly)
       {
          if (localOnly)
          {
             if (changes != null && changes.containsKey(entity))
             {
                return true;
             }
             else if (deletes != null && deletes.containsKey(entity))
             {
                return true;
             }

             return false;
          }
          else
          {
             int dirtyState = manager.getEntityDirtyState(entity);
             return (dirtyState != DirtyInfo.NONE);
          }
       }

       /**
        * Indicate whether the record specified by the given entity and primary key has
        * been deleted within an uncommitted transaction within a context, optionally
        * ignoring deletes in the current context.
        *
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation
        *          class associated with a table being tracked for uncommitted
        *          changes.
        * @param   id
        *          Primary key of the record to be checked.
        * @param   ignoreLocal
        *          Ignore a delete if it occurred in the current context and only report
        *          whether a delete of the given record occurred in a different context.
        *
        * @return  {@code true} if the specified record has been deleted, else {@code false}.
        */
       public boolean isDirtyDelete(String entity, Long id, boolean ignoreLocal)
       {
          if (!manager.isDeleted(entity, id))
          {
             return false;
          }

          if (ignoreLocal)
          {
             // It is marked deleted in the shared space, but if this context is the
             // one that deleted it, return false.
             Set<Long> deleted = null;
             if (deletes != null)
             {
                deleted = deletes.get(entity);
             }

             return (deleted == null || !deleted.contains(id));
          }

          return true;
       }

       /**
        * Retrieve from the dirty database the record specified by the given
        * entity and primary key, if it exists, and if it was not put there by the
        * current context.
        *
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation
        *          class associated with a table being tracked for uncommitted
        *          changes.
        * @param   id
        *          Primary key of the record to be retrieved.
        *
        * @return  Target record if it exists in the dirty database, and was not
        *          put there by the current context, else <code>null</code>.
        *
        * @throws  PersistenceException
        *          if there is any database error.
        */
       public Record getDirtyDMO(String entity, Long id)
       throws PersistenceException
       {
          // If we're the context making the change, we don't need the dirty
          // copy, since it's the same as the one we have in our current session.
          if (changes != null)
          {
             Set<Long> changed = changes.get(entity);
             if (changed != null && changed.contains(id))
             {
                return null;
             }
          }

          return manager.getDirtyDMO(entity, id);
       }

       /**
        * Execute an HQL query and return the results as a list.  If the query returns multiple
        * results per row, each element in the list will be an array of {@code Object}s.
        * <p>
        * No locking is attempted.
        *
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation class associated
        *          with a table being tracked for uncommitted changes. Should be {@code null}
        *          for a multi-table query.
        * @param   fql
        *          FQL query statement. This method makes no assumptions as to the types of object(s) returned.
        * @param   params
        *          Substitution values for the query. If none, this should be an empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list. If this value is
        *          non-positive, no upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this value is non-positive,
        *          an offset of 0 is used by default.
        *
        * @return  A list of results, or {@code null} if no result was found.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> List<T> list(String entity,
                               String fql,
                               Object[] params,
                               int maxResults,
                               int startOffset)
       throws PersistenceException
       {
          // if a specific entity is provided, check its dirty state to provide a quick out for the
          // common case
          if (entity != null)
          {
             int dirtyState = manager.getEntityDirtyState(entity);
             if ((dirtyState & DirtyInfo.CHANGE) == DirtyInfo.NONE)
             {
                return null;
             }
          }

          // query the dirty share manager
          return manager.list(fql, params, maxResults, startOffset);
       }

       /**
        * Given certain search criteria and possibly a potential DMO match in the primary database,
        * report any significant information currently being tracked for uncommitted transactions,
        * which might override data (or the lack thereof) found in the primary database.
        * <p>
        * If a {@link DirtyInfo} object is returned by this method, it will contain overriding
        * information, such as a dirty DMO which matched the search criteria, whether that record was
        * newly inserted, or the fact that the candidate record found in the primary search has been
        * deleted or modified in an uncommitted transaction.  A {@code null} return indicates no
        * overriding information was available.
        *
        * @param   buffer
        *          Record buffer associated with the current query.
        * @param   index
        *          UID of index which governs the ordering of data in the current query.
        * @param   queryType
        *          Type of query.
        * @param   bundle
        *          An object containing one or more FQL statements to be executed against the dirty
        *          database to find a record which matches the current query criteria.  The list is in
        *          order of most specific to least specific search criteria.
        * @param   params
        *          Query substitution parameters which match placeholders in the most specific FQL
        *          statement.
        * @param   found
        *          DMO, if any, found in the primary database, which matches the current query criteria.
        * @param   byRowid
        *          The predicate of the query does a lookup for ROWID/RECID of the table. In case of
        *          transient records they are accessible without the need of having a full matched
        *          index.
        * @param   bufferLoadedByQuery
        *          The buffer associated with the current query is going to be loaded by the query.
        *          This doesn't happen for CAN-FIND, that uses the buffer only to reference the entity, not
        *          for actual loading.
        *
        * @return  An object which summarizes the findings, or {@code null} if no uncommitted change
        *          information was available for the given arguments.
        *
        * @throws  PersistenceException
        *          if there is any database error.
        */
       public DirtyInfo getDirtyInfo(RecordBuffer buffer,
                                     int index,
                                     int queryType,
                                     FQLBundle bundle,
                                     Object[] params,
                                     Record found,
                                     boolean byRowid,
                                     boolean bufferLoadedByQuery)
       throws PersistenceException
       {
          String entity = buffer.getEntityName();
          int dirtyState = manager.getEntityDirtyState(entity);

          // quick out (hopefully the common case): if there are no changes pending for the target
          // entity, we can bypass the check altogether
          if (dirtyState == DirtyInfo.NONE)
          {
             return null;
          }

          Long foundID = (found != null ? found.primaryKey() : null);

          // if we found a record in the primary database with a unique find, and we already are
          // tracking that record for changes/insert in this context (and it is validated), the
          // primary database in this session already reflects reality, and we don't need to check
          // the dirty database
          if (foundID != null && queryType == QueryConstants.UNIQUE && changes != null)
          {
             Set<Long> ids = changes.get(entity);
             if (ids != null && ids.contains(foundID) &&
                 (earlyInserts == null || !earlyInserts.containsKey(new RecordIdentifier<>(entity, foundID))))
             {
                return null;
             }
          }

          // has the record found in primary database been deleted?
          // if record was deleted and changed afterwards we should continue here
          if ((dirtyState & DirtyInfo.DELETE) == DirtyInfo.DELETE  &&
             ((dirtyState & DirtyInfo.CHANGE) != DirtyInfo.CHANGE) &&
              foundID != null                                      &&
              isDirtyDelete(entity, foundID, false))
          {
             DirtyInfo info = new DirtyInfo();
             info.setDeleted();

             return info;
          }

          DirtyInfo info = null;

          if ((dirtyState & DirtyInfo.CHANGE) == DirtyInfo.CHANGE)
          {
             List<String> statements = bundle.getDirtyStatements();

             info = manager.getDirtyInfo(entity, foundID, index, statements, params, byRowid);

             if (info == null)
             {
                return null;
             }

             Record dirtyDMO = info.getDirtyDMO();
             if (dirtyDMO != null && changes != null)
             {
                // ignore validated changes that are driven by this context; they are already
                // reflected in the primary database
                Long id = dirtyDMO.primaryKey();
                RecordIdentifier<String> ident = new RecordIdentifier<>(entity, id);
                Set<Long> changed = changes.get(entity);
                // if dmo is changed in the current context
                if (changed != null &&
                    changed.contains(id))
                {
                   // first check whether the dirty DMO represents an unvalidated record being managed by this
                   // context; if so, replace the copy generated by the dirty database with the actual
                   // record being managed for the primary database.
                   if (earlyInserts != null && earlyInserts.containsKey(ident))
                   {
                      RecordBuffer creatingBuffer = earlyInserts.get(ident);
                      // if DMO was created by a different buffer replace DirtyInfo with local version
                      if ((!bufferLoadedByQuery || creatingBuffer != buffer) && creatingBuffer.isTransient())
                      {
                         Record localDMO = creatingBuffer.getCurrentRecord();
                         info.setInserted();
                         info.setLocal();
                         info.setDirtyDMOs(localDMO, localDMO);
                      }
                      else
                      {
                         // ignore DirtyInfo result in other case
                         return null;
                      }
                   }
                   // otherwise if we have a candidate and it exists in DirtyShare database ignore DirtyInfo,
                   // our candidate shall prevail
                   else if (foundID != null && info.isModified())
                   {
                      return null;
                   }

                }
             }
          }

          return info;
       }

       /**
        * Clean up all information contributed to the dirty share manager by this context for the current
        * transaction.  This method is called when a full database transaction has been committed or rolled back
        * against the primary database.  It is also invoked when the client context ends.  It removes all records
        * it added to the dirty database for new inserts, updates, leaks and deletes.
        *
        * @throws  StopConditionException
        *          if a critical database error occurs that would leave the dirty database in an improper state.
        */
       public void cleanup()
       {
          try
          {
             if (changes != null)
             {
                if (leaks != null && inserts != null)
                {
                   inserts.addAll(leaks);
                }
                manager.cleanupChanges(changes, inserts);
             }
             if (deletes != null)
             {
                manager.cleanupDeletes(deletes);
             }
          }
          catch (PersistenceException exc)
          {
             ErrorManager.handleStopException(new StopConditionException(exc));
          }
          finally
          {
             leaks = null;
             changes = null;
             inserts = null;
             deadInserts = null;
             earlyInserts = null;
             deletes = null;
             dirtySnapshots = null;
          }
       }

       /**
        * Remember that the record represented by the given entity and primary key
        * ID has been modified in the current context.  This is necessary for
        * proper cleanup at the end of the current transaction.
        *
        * @param   entity
        *          Name of an entity, which is the name of the DMO implementation
        *          class associated with a table being tracked for uncommitted
        *          changes.
        * @param   id
        *          Primary key ID of the modified record.
        */
       private void trackChange(String entity, Long id)
       {
          if (changes == null)
          {
             changes = new HashMap<>();
          }

          if (dirtySnapshots == null)
          {
             dirtySnapshots = new HashSet<>();
          }

          Set<Long> ids = changes.computeIfAbsent(entity, k -> new HashSet<>());
          ids.add(id);

          RecordIdentifier<String> ident = new RecordIdentifier<String>(entity, id);
          dirtySnapshots.add(ident);
       }

       /**
        * Overwrite the state of the records in the dirty database which match the
        * IDs of all DMOs for the given entities, which have been modified by the
        * current context.
        * <p>
        * This is necessary to support a quirk/bug in Progress which compromises
        * transaction isolation by leaking uncommitted changes (even non-indexed
        * data) under certain conditions.
        *
        * @param   entities
        *          List of entities, which are the names of the DMO implementation
        *          classes associated with tables being tracked for uncommitted
        *          changes.  Only those records which match these entities will
        *          have their current state leaked.
        * @param   persistence
        *          Persistence service object to be used to retrieved the latest
        *          version of the target records in the current context.
        *
        * @return  List of all DMOs whose snapshots were updated with the dirty
        *          share manager.  The list may be empty, but will not be
        *          <code>null</code>.
        *
        * @throws  PersistenceException
        *          if there is any database error.
        */
       public List<Record> updateSnapshots(Set<String> entities, Persistence persistence)
       throws PersistenceException
       {

          List<Record> allDMOs = null;
          if (changes == null || dirtySnapshots == null)
          {
             // Nothing to update.
             return Collections.emptyList();
          }

          for (String entity : entities)
          {
             Set<Long> changed = changes.get(entity);
             if (changed == null || changed.isEmpty())
             {
                continue;
             }

             if (allDMOs == null)
             {
                allDMOs = new ArrayList<>();
             }
             Set<Record> entityDMOs = new HashSet<>();
             Iterator<Long> iter = changed.iterator();
             while(iter.hasNext())
             {
                Long id = iter.next();
                RecordIdentifier<String> ident = new RecordIdentifier<>(entity, id);
                Record dmo = null;

                if ((earlyInserts == null || !earlyInserts.containsKey(ident)) &&
                     dirtySnapshots.remove(ident))
                {
                   // get record from local cache if stored or made visible
                   dmo = persistence.quickLoad(ident, Persistence.SHARED_CTX);
                }
                else if (dirtySnapshots.remove(ident))
                {
                   // get record from earlyInserts
                   RecordBuffer buffer = earlyInserts.get(ident);
                   dmo = buffer.getCurrentRecord();
                   if (dmo == null)
                   {
                      // early inserts record is released and cant do anything about it, try next one
                      continue;
                   }
                   IndexState is = dmo.getIndexState();
                   // but if given record do not have all indices complete do not dirty leak it
                   if (!is.isEmpty())
                   {
                      dmo = null;
                   }
                }
                if (dmo != null)
                {
                   if (leaks == null)
                   {
                      leaks = new HashSet<>();
                   }
                   // leak that dirty DMO
                   leaks.add(ident);
                   entityDMOs.add(dmo);
                   // continue updating list of DMOs to be potentially evicted
                   allDMOs.add(dmo);
                }
             }
             if (!entityDMOs.isEmpty())
             {
                manager.updateSnapshots(entity, entityDMOs);
             }
          }
          if (allDMOs == null)
          {
             allDMOs = Collections.emptyList();
          }

          return allDMOs;
       }
    }