AdaptiveFind.java

/*
** Module   : AdaptiveFind.java
** Abstract : Query which adapts its retrieval type from dynamic to preselect
**
** Copyright (c) 2004-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- --------------------------------Description-------------------------------------
** 001 ECF 20060801   @28343 Created initial version. Query which adapts its
**                           retrieval type from dynamic to preselect, and
**                           which starts its search from the current buffer
**                           record(s).
** 002 ECF 20060803   @28386 Fixed multiple flaws. Set lenientOffEnd to true
**                           in constructor and use a FindQuery as the dynamic
**                           replacement query delegate. Improved result set
**                           invalidation strategy.
** 003 ECF 20060901   @29153 Fixed NPE. Cannot safely use invalidBuffer to
**                           extract DMO proxy in createSimpleQuery().
** 004 ECF 20060925   @29961 Check for a pending error before retrieving a
**                           record. Retrieval is not attempted if an error is
**                           pending in the ErrorManager.
** 005 ECF 20061003   @30151 Call prepareFetch() instead of obsolete method
**                           checkPendingError(). Implemented releaseBuffers()
**                           to override do-nothing AbstractQuery
**                           implementation.
** 006 ECF 20061020   @30564 Simplified constructors. Removed constructors
**                           which contained client-side where expression,
**                           inverse DMO, or query substitution parameters
**                           arguments, since none of these are supported by
**                           the adaptive find model.
** 007 ECF 20061027   @30747 Replaced Object with DataModelObject for DMO
**                           arguments to methods/constructors. Required for
**                           compile-time type safety.
** 008 ECF 20061115   @31207 Override ancestor's implementation of
**                           isResetSnapshot() method. A converted FIND must
**                           clear the placeholder snapshot when a record is
**                           not found.
** 009 ECF 20070225   @32233 Added isGlobalCleanup() method. Used by
**                           PreselectQuery to determine that this object
**                           registers its results resources cleaner to the
**                           global (i.e., external proc) scope.
** 010 ECF 20070308   @32333 Fixed behavior for first(). Upon failure to find
**                           a record, if not in looping mode, this method
**                           must report a 'FIND FIRST/LAST failed...'
**                           message. Note that this covers the FIND
**                           LAST/PREVIOUS case, because AdaptiveFind inverts
**                           the sort direction for these queries and always
**                           invokes first().
** 011 ECF 20070412   @32973 Fixed createSimpleQuery(). Always use lock type
**                           of NONE for the dynamic query. This will be
**                           overridden by PreselectQuery.fetch(), if a
**                           stricter lock is required.
** 012 ECF 20070705   @34399 Fixed maybeInvalidate(). Invalidation is required
**                           if record buffer releases a record AdaptiveFind
**                           has loaded.
** 013 ECF 20071130   @36117 Removed isRefreshSnapshot() method. The
**                           RecordBuffer placeholder snapshot is no longer
**                           cleared by any query type when the query does not
**                           find a record to place in the buffer.
** 014 SVL 20080418   @38042 Added new convenience constructor.
** 015 ECF 20080813   @39552 Support invalidation of results based on DMO
**                           change events in other sessions. Currently, this
**                           support is limited to changes which affect an
**                           index on the backing table.
** 016 ECF 20080826   @39572 Fixed isGlobalValidation(). Can only return true
**                           for non-temp tables.
** 017 ECF 20081031   @40304 Modified methods due to changes in parent class.
** 018 ECF 20090603   @42582 Made maybeInvalidate() invalidation approach more
**                           conservative. We now invalidate if there is any
**                           uncommitted change to the backing table, in any
**                           session.
** 019 ECF 20090716   @43211 Implemented lazy initialization. Backing buffer
**                           cannot be initialized and flushed until the first
**                           attempt to find a record, once we are sure the
**                           buffer's scope has been opened.
** 020 CA  20090731   @43463 Removed lenientOffEnd activation in c'tor. The 
**                           query is allowed to throw QueryOffEndException's
**                           (to be consistent with FIND NEXT/PREV behavior).
** 021 CA  20090806   @43535 Fixed regression related to H020 (@43463) - a
**                           FIND NEXT/PREV with a NO-ERROR clause must not
**                           end the block (a QOEE must not be thrown, so
**                           lenientOffEnd must be true if we are in silent
**                           error mode).
** 022 ECF 20131028          Import change.
** 023 SVL 20140604          Resolved ambiguity with the newly added constructor of the
**                           superclass.
** 024 ECF 20140814          Replaced RecordBuffer.flushAll with flush in initialize method.
** 025 ECF 20150701          Changed addComponent to accommodate query component refactoring.
** 026 IAS 20160701          Fixed legacy name in the [565] error message.
** 027 ECF 20160728          Added optional, external buffers to constructors.
** 028 GES 20160919          Changes to match parent class constructor/initialization changes.
** 029 GES 20170831          Shifting to a deferred initialize() approach that is the same
**                           as the parent class.
** 030 ECF 20170913          Ensure initialization only occurs once.
** 031 GES 20171220          Changes to match new silent mode implementation.
** 032 OM  20171129          releaseBuffers() is overwritten from P2JQuery.
** 033 ECF 20190302          Renamed setExternalBuffers to addExternalBuffers.
** 034 ECF 20190813          Change to RecordBuffer.release API.
** 035 OM  20200822          Fixed query invalidation.
** 036 GES 20200731          Eliminated context-local lookups by using ErrorHelper.
**     CA  20210310          A dynamic predicate must set the default lock to NONE, instead of SHARE.
**     SVL 20230109          Reflected PreselectQuery.components() now providing direct access to the
**                           components array in order to improve performance.
** 037 SVL 20230516          Support for more granular configuration of dirty sharing functionality.
** 038 DDF 20230627          Replaced static variables of DirtyShareSupport with static method calls.
** 039 ES  20231203          Changed first, next methods to return boolean,instead of void. These methods now 
**                           returns true if operation was successful, and false not and lenientOffEnd is set 
**                           on true, otherwise it thrown an QQE exception. 
** 040 AL2 20240321          Suppressed earlyInvalidte from AdaptiveQuery.
** 041 CA  20240402          Refactored so the query navigation is delegated to a wrapped query, which can be
**                           re-built if the buffers used at the AdaptiveFind's creation have been switched.
** 042 AL2 20240424          Match the new signature for earlyInvalidte in AdaptiveQuery.
** 043 CA  20240802          The query wrapper must register for finalizable support in the block where it gets
**                           initialized.
** 044 DDF 20240814          Added silent method equivalents to first, next to avoid using silent mode
**                           lambdas.
**     DDF 20240821          lenientOffEnd flag should be set for each silent mode method because the silent 
**                           flag retrieved through the ErrorHelper is set after initialization.
**     DDF 20240822          Delegate lenientOffEnd to the silent mode methods in AdaptiveFindQuery.
**                           Made ErrorHelper instance a class member for performance improvements.
**     DDF 20240828          Removed silent mode methods that are accessible through AbstractQuery and
**                           moved the errorHelper property to AbstractQuery.
** 045 TJD 20240412          Renamed isEnabledCrossSection to isEnabledCrossSession 
**     TJD 20240912          DirtyShareSupport flags logic dependency update 
*/

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

package com.goldencode.p2j.persist;

import java.util.*;
import java.util.function.*;

import com.goldencode.p2j.persist.dirty.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager.ErrorHelper;

/**
 * This instance is a wrapper to an implementation of {@link AdaptiveQuery} which may begin in dynamic record 
 * retrieval mode.  If so, it switches to preselect mode as soon as possible.  It is intended as the backing 
 * implementation for converted FIND statements which can be executed in succession without requiring a new
 * query to be executed for each record retrieved.  Candidates are combinations of FIND FIRST/NEXT and 
 * FIND LAST/PREV, or instances of FIND NEXT and FIND PREV which operate in loops.
 * <p>
 * This type of query by its nature operates only on a single table and therefore on a single record buffer.
 * If the record buffer already is in use, its latest record is used as a reference point for the next record
 * to be retrieved and the query is initialized in dynamic mode.  Otherwise, the query is initialized in 
 * preselect mode.
 * <p>
 * The wrapped {@link #query} will always be {@link #createQuery() re-created} if the query's buffers (saved
 * at the {@link AdaptiveFind}'s instantiation) have been bound to other buffers (i.e. the referenced buffer
 * has switched).
 */
public class AdaptiveFind
{
   /** The real wrapped query. */
   private AdaptiveFindQuery query = null;
   
   /**
    * The list of query initialization actions which will be applied to {@link #query}, when it will be 
    * {@link #createQuery() created}.
    */
   private List<Consumer<AdaptiveFindQuery>> init = new LinkedList<>();
   
   /** Flag indicating whether this object has been initialized */
   private boolean initialized = false;

   /** The original DMO reference used to create this instance. */
   private DataModelObject dmo = null;

   /** The original external DMOs references used for initialization. */
   private DataModelObject[] externalDMOs = null;
   
   /**
    * Default constructor. Initialization is deferred until <code>initialize()</code> is called.
    */
   public AdaptiveFind()
   {
      super();
   }
   
   /**
    * Convenience deferred initializer designed for a single-table adaptive query.  The where clause must 
    * expect no substitution parameters.  A share lock will be applied to retrieved records.
    *
    * @param   dmo
    *          DMO proxy which defines buffer and record type.
    * @param   where
    *          FQL where clause.  May be <code>null</code>.
    * @param   sort
    *          FQL order by clause.
    * @param   dmos
    *          Zero or more external buffers.
    *
    * @return  The current instance (<code>this</code>) so that chaining can be used. 
    */
   public AdaptiveFind initialize(DataModelObject    dmo,
                                  String             where,
                                  String             sort,
                                  DataModelObject... dmos)
   {
      return initialize(dmo, where, sort, LockType.SHARE, dmos);
   }

   /**
    * Convenience deferred initializer designed for a single-table adaptive query.  The where clause must 
    * expect no substitution parameters.
    *
    * @param   dmo
    *          DMO proxy which defines buffer and record type.
    * @param   where
    *          FQL where clause.  May be <code>null</code>.
    * @param   sort
    *          FQL order by clause.
    * @param   lockType
    *          Lock type to apply to records retrieved by this query.
    * @param   dmos
    *          Zero or more external buffers.
    *
    * @return  The current instance (<code>this</code>) so that chaining can be used. 
    */
   public AdaptiveFind initialize(DataModelObject    dmo,
                                  String             where,
                                  String             sort,
                                  LockType           lockType,
                                  DataModelObject... dmos)
   {
      if (initialized)
      {
         return this;
      }
      
      // save the 'ReferenceProxy' for all DMOs
      this.dmo = dmo;
      this.externalDMOs = dmos;
      
      init.add((query) -> { query.initialize(dmo, where, sort, lockType, dmos); });
      
      initialized = true;
      
      TransactionManager.registerFinalizable(new Finalizable()
      {
         @Override
         public void retry() { }
         
         @Override
         public void iterate() { }
         
         @Override
         public void finished() { }
         
         @Override
         public void deleted()
         {
            query = null;
            initialized = false;
            init.clear();
            AdaptiveFind.this.dmo = null;
            externalDMOs = null;
         }
      }, false);
      
      return this;
   }

   /**
    * Convenience deferred initializer designed for a single-table adaptive query.  The where clause must 
    * expect no substitution parameters.  A share lock will be applied to retrieved records.
    *
    * @param   dmo
    *          DMO proxy which defines buffer and record type.
    * @param   where
    *          FQL where clause.  May be <code>null</code>.
    * @param   sort
    *          FQL order by clause.
    * @param   inverseSorting
    *          Whether this query has the inverse sorting direction.
    * @param   dmos
    *          Zero or more external buffers.
    *
    * @return  The current instance (<code>this</code>) so that chaining can be used. 
    */
   public AdaptiveFind initialize(DataModelObject    dmo,
                                  String             where,
                                  String             sort,
                                  boolean            inverseSorting,
                                  DataModelObject... dmos)
   {
      init.add((query) -> 
      {
         query.inverseSorting = inverseSorting;
      });
      
      return initialize(dmo, where, sort, LockType.SHARE, dmos);
   }

   /**
    * Retrieve the next composite row of results for the query.  The underlying buffer is updated with the 
    * appropriate record.
    *
    * @return  <code>true</code> if query could retrive the next record successfully;
    *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
    *
    * @throws  ErrorConditionException
    *          if the query or the fetch of any record fails, or if no records are available and the query is 
    *          set to fail in this case.
    * @throws  QueryOffEndException
    *          if no records are available and the query is set not to fail in this case.
    * @throws  IllegalStateException
    *          if no query component has been added to this query.
    */
   public boolean next()
   {
      return next(null);
   }
   
   /**
    * Retrieve the next composite row of results for the query.  The underlying buffer is updated with the 
    * appropriate record.
    *
    * @param   lockType
    *          Lock type to apply to records retrieved (overrides default lock type set for each query 
    *          component).
    *
    * @return  <code>true</code> if query could retrive the next record successfully;
    *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
    *
    * @throws  ErrorConditionException
    *          if the query or the fetch of any record fails, or if no records are available and the query is 
    *          set to fail in this case.
    * @throws  QueryOffEndException
    *          if no records are available and the query is set not to fail in this case.
    * @throws  IllegalStateException
    *          if no query component has been added to this query.
    */
   public boolean next(LockType lockType)
   {
      createQuery();

      return query.next(lockType);
   }
   
   /**
    * Retrieve the next composite row of results for the query.  The underlying buffer is updated with the 
    * appropriate record.
    * <p>
    * This operation is executed in silent mode.
    *
    * @return  <code>true</code> if query could retrive the next record successfully;
    *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
    *
    * @throws  ErrorConditionException
    *          if the query or the fetch of any record fails, or if no records are available and the query is 
    *          set to fail in this case.
    * @throws  QueryOffEndException
    *          if no records are available and the query is set not to fail in this case.
    * @throws  IllegalStateException
    *          if no query component has been added to this query.
    */
   public boolean silentNext()
   {
      return silentNext(null);
   }
   
   /**
    * Retrieve the next composite row of results for the query.  The underlying buffer is updated with the 
    * appropriate record.
    * <p>
    * This operation is executed in silent mode.
    *
    * @param   lockType
    *          Lock type to apply to records retrieved (overrides default lock type set for each query 
    *          component).
    *
    * @return  <code>true</code> if query could retrive the next record successfully;
    *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
    *
    * @throws  ErrorConditionException
    *          if the query or the fetch of any record fails, or if no records are available and the query is 
    *          set to fail in this case.
    * @throws  QueryOffEndException
    *          if no records are available and the query is set not to fail in this case.
    * @throws  IllegalStateException
    *          if no query component has been added to this query.
    */
   public boolean silentNext(LockType lockType)
   {
      createQuery();
      
      return query.silentNext(lockType);
   }
   
   /**
    * Retrieve the first composite row of results for the query.  The underlying buffer is updated with the 
    * appropriate record.
    *
    * @return  <code>true</code> if query could retrive the first record successfully;
    *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
    *
    * @throws  ErrorConditionException
    *          if the query or the fetch of any record fails, or if no records are available and the query is 
    *          set to fail in this case.
    * @throws  QueryOffEndException
    *          if no records are available and the query is set not to fail in this case.
    * @throws  IllegalStateException
    *          if no query component has been added to this query.
    */
   public boolean first()
   {
      return first(null);
   }
   
   /**
    * Retrieve the first composite row of results for the query.  The underlying buffer is updated with the 
    * appropriate record.
    *
    * @param   lockType
    *          Lock type to apply to records retrieved (overrides default lock type set for each query 
    *          component).
    *
    * @return  <code>true</code> if query could retrive the first record successfully;
    *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
    *
    * @throws  ErrorConditionException
    *          if the query or the fetch of any record fails, or if no records are available and the query is 
    *          set to fail in this case.
    * @throws  QueryOffEndException
    *          if no records are available and the query is set not to fail in this case.
    * @throws  IllegalStateException
    *          if no query component has been added to this query.
    */
   public boolean first(LockType lockType)
   {
      createQuery();
      
      return query.first(lockType);
   }
   
   /**
    * Retrieve the first composite row of results for the query.  The underlying buffer is updated with the 
    * appropriate record.
    * <p>
    * This operation is executed in silent mode.
    *
    * @return  <code>true</code> if query could retrive the first record successfully;
    *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
    *
    * @throws  ErrorConditionException
    *          if the query or the fetch of any record fails, or if no records are available and the query is 
    *          set to fail in this case.
    * @throws  QueryOffEndException
    *          if no records are available and the query is set not to fail in this case.
    * @throws  IllegalStateException
    *          if no query component has been added to this query.
    */
   public boolean silentFirst()
   {
      return silentFirst(null);
   }
   
   /**
    * Retrieve the first composite row of results for the query.  The underlying buffer is updated with the 
    * appropriate record.
    * <p>
    * This operation is executed in silent mode.
    * 
    * @param   lockType
    *          Lock type to apply to records retrieved (overrides default lock type set for each query 
    *          component).
    *
    * @return  <code>true</code> if query could retrive the first record successfully;
    *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
    *
    * @throws  ErrorConditionException
    *          if the query or the fetch of any record fails, or if no records are available and the query is 
    *          set to fail in this case.
    * @throws  QueryOffEndException
    *          if no records are available and the query is set not to fail in this case.
    * @throws  IllegalStateException
    *          if no query component has been added to this query.
    */
   public boolean silentFirst(LockType lockType)
   {
      createQuery();
      
      return query.silentFirst(lockType);
   }
   
   /**
    * Create the {@link #query} - this will apply all recorded {@link #init initialization steps}.
    */
   private void createQuery()
   {
      checkQuery();
      
      if (query == null)
      {
         query = new AdaptiveFindQuery();
         for (Consumer<AdaptiveFindQuery> part : init)
         {
            part.accept(query);
         }
      }
   }
   
   /**
    * Check the query if the referenced buffers have changed their bound buffers.  If so, close it, so 
    * {@link #createQuery()} will be initialized.
    */
   private void checkQuery()
   {
      if (query == null)
      {
         return;
      }
      
      boolean buffersChanged = false;
      // compare the query main buffer with the current buffer
      if (query.buffer != ((BufferReference) dmo).buffer())
      {
         buffersChanged = true;
      }
      else if (externalDMOs != null && externalDMOs.length > 0)
      {
         for (int i = 0; i < externalDMOs.length; i++)
         {
            if (query.externalBuffers[i] != ((BufferReference) externalDMOs[i]).buffer())
            {
               buffersChanged = true;
               break;
            }
         }
      }
      
      if (buffersChanged)
      {
         // close the existing query - it will be re-created
         query.close();
         query = null;
      }
   }

   /**
    * The wrapped query to which the query navigation calls are delegated.
    */
   private class AdaptiveFindQuery
   extends AdaptiveQuery
   {
      /** Buffer updated by this query */
      private RecordBuffer buffer = null;
      
      /** Record most recently found by this query */
      private Record lastFound = null;
      
      /** The resolved external buffers when the query was initialized. */
      private RecordBuffer[] externalBuffers = null;

      /**
       * Convenience deferred initializer designed for a single-table adaptive query.  The where clause must 
       * expect no substitution parameters.
       *
       * @param   dmo
       *          DMO proxy which defines buffer and record type.
       * @param   where
       *          FQL where clause.  May be <code>null</code>.
       * @param   sort
       *          FQL order by clause.
       * @param   lockType
       *          Lock type to apply to records retrieved by this query.
       * @param   dmos
       *          Zero or more external buffers.
       */
      public void initialize(DataModelObject    dmo,
                             String             where,
                             String             sort,
                             LockType           lockType,
                             DataModelObject... dmos)
      {
         initialize(dmo, where, null, sort, (DataModelObject) null, null, lockType);
         
         buffer = ((BufferReference) dmo).buffer();
         
         // if the NO-ERROR clause is specified, then a QOEE must not be thrown
         setLenientOffEnd(buffer.txHelper.errHlp.isSilent());
         
         // set external buffers, if any
         if (dmos != null && dmos.length > 0)
         {
            addExternalBuffers(dmos);
            
            externalBuffers = new RecordBuffer[dmos.length];
            for (int i = 0; i < dmos.length; i++)
            {
               externalBuffers[i] = ((BufferReference) dmos[i]).buffer();
            }
         }
         
         // start off in dynamic mode if the buffer already is active
         if (buffer.isActive() && buffer.getSnapshot() != null)
         {
            AdaptiveComponent comp = (AdaptiveComponent) components().get(0);
            invalidate(comp);
         }
      }

      /**
       * Retrieve the first composite row of results for the query.  The
       * underlying buffer is updated with the appropriate record.
       *
       * @param   lockType
       *          Lock type to apply to records retrieved (overrides default
       *          lock type set for each query component).
       *
       * @return  <code>true</code> if query could retrive the first record succefully;
       *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
       *
       * @throws  ErrorConditionException
       *          if the query or the fetch of any record fails, or if no records
       *          are available and the query is set to fail in this case.
       * @throws  QueryOffEndException
       *          if no records are available and the query is set not to fail
       *          in this case.
       * @throws  IllegalStateException
       *          if no query component has been added to this query.
       */
      public boolean first(LockType lockType)
      {
         init();
         
         if (!prepareFetch())
         {
            return false;
         }
         
         maybeInvalidate();
         
         boolean available = false;
         try
         {
            available = super.first(lockType);
         }
         catch (QueryOffEndException exc)
         {
            ErrorManager.recordOrThrowError(
               new PersistenceException(
                  "FIND FIRST/LAST failed for table " + buffer.getLegacyName(),
                  565));
         }
         
         if (available)
         {         
            lastFound = buffer.getSnapshot();
         }
         
         return available;
      }
      
      /**
       * Retrieve the next composite row of results for the query.  The
       * underlying buffer is updated with the appropriate record.
       *
       * @param   lockType
       *          Lock type to apply to records retrieved (overrides default
       *          lock type set for each query component).
       *
       * @return  <code>true</code> if query could retrive the next record succefully;
       *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
       *
       * @throws  ErrorConditionException
       *          if the query or the fetch of any record fails, or if no records
       *          are available and the query is set to fail in this case.
       * @throws  QueryOffEndException
       *          if no records are available and the query is set not to fail
       *          in this case.
       * @throws  IllegalStateException
       *          if no query component has been added to this query.
       */
      public boolean next(LockType lockType)
      {
         init();
         
         if (!prepareFetch())
         {
            return false;
         }
         
         maybeInvalidate();
         
         boolean available = super.next(lockType);
         if (available)
         {         
            lastFound = buffer.getSnapshot();
         }
         
         return available;
      }
      
      /**
       * If in silent error mode, this method will always return <code>true</code>.
       * Else, it will let the {@link PreselectQuery#isLenientOffEnd()} decide 
       * the mode.
       * 
       * @return  <code>true</code> if an exception will be thrown, else
       *          <code>false</code>.
       */
      public boolean isLenientOffEnd()
      {
         return buffer.txHelper.errHlp.isSilent() || super.isLenientOffEnd();
      }

      /**
       * Report whether this query must check for changes across all sessions and
       * possibly invalidate its current results if any are found.  If not, this
       * query will only honor local changes to invalidate results.
       * 
       * @return  <code>true</code> if query is against a permanent table;
       *          <code>false</code> if query is against a temporary table.
       */
      protected boolean isGlobalInvalidation()
      {
         return !buffer.isTemporary();
      }

      /**
       * Add a component to the query.  This implementation overrides the parent
       * method only to disallow multiple tables.  Because it is called from a
       * superclass constructor, instance variables must not be initialized in
       * this method.
       *
       * @param   comp
       *          Query component to be added.
       *
       * @throws  UnsupportedOperationException
       *          if invoked more than once per instance;  also:
       *          if attempting an outer join (temporary);
       *          if iteration type is not <code>NEXT</code> (temporary);
       */
      protected void addComponent(QueryComponent comp)
      {
         if (getTableCount() > 0)
         {
            throw new UnsupportedOperationException(
               "Multi-table adaptive finds are not supported");
         }
         
         super.addComponent(comp);
      }

      /**
       * Release the current record, if any, from the buffer which is used by this query.
       *
       * @throws  QueryOffEndException
       *          if the buffer contained a record and it was released.
       */
      public void releaseBuffers()
      {
         // although this is implemented as an iterating query, it maps back to a FIND statement,
         // so we allow a write trigger to fire
         buffer.release(true);
      }

      /**
       * Indicate whether results resources should be cleaned up upon exiting the current program's top-level 
       * scope or upon exiting the nearest enclosing scope in which the results were created.
       *
       * @return  <code>true</code>.
       */
      protected boolean isTopLevelCleanup()
      {
         return true;
      }

      /**
       * Check if this query should operate in dynamic mode right from the start.
       * 
       * This kind of query is invalidated by cross-session dirty context anyway, no need for extra handling.
       * 
       * @return   Always return {@code false} as this isn't ever going to be invalidated early.
       */
      @Override
      protected boolean earlyInvalidate()
      {
         return false;
      }

      /**
       * Initialize and flush the buffer.
       */
      private void init()
      {
         buffer.initialize();
         
         try
         {
            buffer.flush();
         }
         catch (ValidationException exc)
         {
            // a validation error can only result from flushing an unrelated record currently in
            // the buffer in preparation for a query result; since this will not be within the
            // context of any silent error mode under which this query is running, we instruct
            // the error manager to not suppress the message
            ErrorManager.throwError(exc, false);
         }
      }

      /**
       * Invalidate the current result set associated with this query if the record buffer's contents have
       * changed since the last time this query updated the buffer.  If in preselect mode, this will switch to
       * dynamic mode.  If already in dynamic mode, a new dynamic query will be created to reflect the new 
       * buffer contents.  This method will also take a conservative approach and invalidate if there is any 
       * uncommitted change in the backing table.
       */
      private void maybeInvalidate()
      {
         Record snapshot = buffer.getSnapshot();
         String entity = buffer.getEntityName();
         if ((snapshot == null && lastFound != null) ||
             (snapshot != null &&
              (lastFound == null || !lastFound.primaryKey().equals(snapshot.primaryKey()))))
         {
            AdaptiveComponent comp = (AdaptiveComponent) components().get(0);
            invalidate(comp);
            return;
         }
      
         if (DirtyShareSupport.isEnabledCrossSession() || DirtyShareSupport.isEnabledIntraSession())
         {
            DirtyShareContext dirtyContext = buffer.getDirtyContext();
            if (dirtyContext == null)
            {
               return;
            }
      
            if (dirtyContext.isEntityDirty(entity))
            {
               AdaptiveComponent comp = (AdaptiveComponent) components().get(0);
               invalidate(comp);
            }
         }
      }
      
   }
}