AdaptiveQuery.java

/*
** Module   : AdaptiveQuery.java
** Abstract : Query which adapts its retrieval type from preselect to dynamic
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20060721   @28133 Created initial version. Query which adapts
**                           its retrieval type from preselect to dynamic,
**                           based upon DMO state change notifications.
** 002 ECF 20060725   @28186 Integrated ProgressiveResults. Implementation
**                           provides better performance for use cases
**                           which require access to a small number of
**                           result records while retaining most of the
**                           preselect performance advantage for larger
**                           queries.
** 003 ECF 20060730   @28262 Fixed order by clause defect. Order by clause
**                           was being omitted.
** 004 ECF 20060801   @28277 Fixed NPE in order by processing. Added
**                           safety code to assembleOrderByClause().
** 005 ECF 20060802   @28347 Enabled transition from dynamic to preselect
**                           mode. Once invalidated, an AdaptiveQuery will
**                           now transition back to preselect mode as soon
**                           as possible.
** 006 ECF 20060803   @28387 Integrated ResultsAdapter and made this class
**                           more easily extended. Refactored much of the
**                           behavior into polymorphic methods.
** 007 ECF 20060808   @28573 Refined ResultsAdapter to better handle
**                           FilteredResults. Integrated ResultsProvider
**                           interface with new inner class ResultsSource.
** 008 ECF 20060830   @28992 Fixed join behavior. For a single table query
**                           and for the first table of a multi-table
**                           query, a natural join's inverse DMO is
**                           assumed to be provided as a query parameter,
**                           and a DynamicJoin is created. For all other
**                           natural joins, the ServerJoin implementation
**                           is used.
** 009 GES 20060830   @29018 Fixed null ptr exception in tracing code.
** 010 ECF 20060831   @29082 Added support for sorting by an indexed
**                           property. Specifically, support sorting by
**                           a property of a DMO's associated list of
**                           composite properties.
** 011 ECF 20060901   @29154 Fixed NPE. Cannot safely use invalidBuffer to
**                           extract DMO proxy in createSimpleQuery().
** 012 ECF 20060911   @29459 Fixed defect in transition from dynamic to
**                           preselect mode. A state change event during
**                           this transition must cancel the transition.
** 013 ECF 20061006   @30195 Implemented scrolling mode. When set to
**                           scrolling mode, non-linear scrolling is
**                           possible in preselect mode. Previously, only
**                           linear scrolling from first to last was
**                           possible. This is necessitated by the fact
**                           that AdaptiveQuery may back a browse widget.
** 014 ECF 20061019   @30524 Additional fixes to support scrolling mode.
**                           These were made primarily to support the use
**                           of this query with a Browse widget, and
**                           possibly as a component within a scrolling
**                           CompoundQuery.
** 015 ECF 20061027   @30748 Replaced Object with DataModelObject for DMO
**                           arguments to methods/constructors. Required
**                           for compile-time type safety.
** 016 ECF 20061119   @31323 Refined invalidation strategy. Do not switch
**                           from preselect mode on session closing events
**                           if a reposition listener is registered to the
**                           query (indicates a browse association). Upon
**                           record change events, only invalidate if
**                           changed property is one which would affect
**                           content or sorting of result set. Also
**                           improved dynamic mode reposition fetching.
** 017 ECF 20061121   @31343 Added safety code to prevent NPE. During
**                           setupInvalidationProperties(), query
**                           component may return null map of restriction
**                           property names if where clause was null.
** 018 ECF 20061129   @31477 Additional change related to #016 (@31323).
**                           If associated with a browse on session close,
**                           discard current high level results object.
** 019 ECF 20061203   @31573 Further changes to sessionClosing() and
**                           invalidate(RecordBuffer) methods. We now
**                           invalidate results in the event of an error
**                           on session close. We also use the explicit
**                           browse check (isBrowsed()) to detect whether
**                           the query is associated with a browse when
**                           invalidating results. Also, this test was
**                           moved from sessionClosing() to invalidate(),
**                           to ensure that a browse association was being
**                           considered in all invalidation scenarios, not
**                           just session close.
** 020 ECF 20061207   @31617 Support insert RecordChangeEvent. Modified
**                           stateChanged() method.
** 021 ECF 20070223   @32226 Do not switch to dynamic mode if the record
**                           currently being visited is deleted. This
**                           supports a common idiom of deleting records
**                           in a loop without thrashing back and forth
**                           between modes.
** 022 ECF 20070227   @32242 Fixed #021 (@32226). The implementation of
**                           this optimization was incorrect.
** 023 ECF 20070330   @32242 Fixed NPE in stateChanged() method.
** 024 ECF 20070412   @32974 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.
** 025 ECF 20070416   @33017 Integrated user interrupt handling.
** 026 ECF 20070509   @33452 Modified sessionClosing() behavior to behave
**                           as superclass if scrolling. Temporarily
**                           rolled back #021 (@32226), which causes
**                           ProgressiveResults to lose records. Need a
**                           better solution for this problem which
**                           preserves the optimization in #021.
** 027 EVL 20070609   @34005 Adding explicit import of the class
**                           org.hibernate.type.Type to eliminate conflict
**                           with the same class from java.lang.reflect
**                           package to be able to compile for Java 6.
** 028 ECF 20070706   @34400 Minor optimization. Replaced StringBuffer
**                           with StringBuilder.
** 029 ECF 20070709   @34421 Modified sessionClosing(). Always preserve
**                           preselected results rather than invalidating
**                           the query. This significantly improves
**                           performance in certain use patterns where the
**                           session is closed regularly before the query
**                           has iterated all of its results (e.g., a
**                           commit which occurs within a loop at a lower
**                           nested scope).
** 030 ECF 20070711   @34445 Further improved sessionClosing(). Event
**                           handling on session close is now delegated to
**                           the results adapter.
** 031 ECF 20070720   @34905 Added toString() method for debugging.
** 032 ECF 20070827   @34998 Handle temp table multiplex ID when used in
**                           query's sort criteria. Certain logic had an
**                           improper dependency on all SortCriterion
**                           instances having a getter method, which is
**                           not the case for multiplex IDs.
** 033 ECF 20071022   @35557 Prevent thrashing between modes. Added crude
**                           safety mechanism which disables transitioning
**                           from dynamic to preselect mode more than 3
**                           times.
** 034 ECF 20071130   @36118 Integrated generics.
** 035 ECF 20071206   @36270 Removed sort field from AdaptiveComponent.
**                           This field was not being used. Constructor
**                           now sets SortIndex in superclass.
** 036 ECF 20080122   @36888 Fixed handling of state change events in
**                           dynamic mode. It is necessary to reset the
**                           dynamic query's cursor to avoid retrieving
**                           stale data.
** 037 SVL 20071230   @36222 When ProgressiveResults reports that changes
**                           in results detected while getting them, the
**                           mode is switched from preselect to dynamic.
** 038 SVL 20080418   @38040 Query registers as ChangeBroker listener in
**                           the constructor.
** 039 ECF 20080502   @28133 Removed isRepositionOnOpen(). Method is
**                           obsolete.
** 040 SVL 20080526   @38312 isPrelockUsed() added.
** 041 SVL 20080604   @38535 currentRowImpl() is used instead of
**                           currentRow().
** 042 CA  20080617   @38867 If there are components with no inverse
**                           record to join on, then the query will not
**                           be executed and no results will be returned.
** 043 ECF 20080701   @39033 Optimized stateChanged(). Avoid invalidating
**                           preselected results in certain delete and
**                           update cases when we can detect invalidation
**                           is unnecessary.
** 044 ECF 20080708   @39081 Refined #043 (@39033) optimization. A delete
**                           of the first record found by the query will
**                           trigger an immediate query/caching of all
**                           remaining results. Tightened conditions where
**                           double-checking of invalidation is done.
** 045 ECF 20080709   @39085 Fixed sessionClosing() and stateChanged(). We
**                           now prevent using a dead connection after the
**                           session closes and avoid caching results on a
**                           delete event in dynamic mode, respectively.
** 046 ECF 20080710   @39091 Modified sessionClosing(). cacheResults()
**                           no longer throws IllegalStateException.
** 047 ECF 20080723   @39174 Added retry handling. Implement Finalizable
**                           and prepare for retry in retry() method by
**                           setting state as if a reposition to the
**                           current record has occurred.
** 048 CA  20080729   @39219 Removed retry handling. Retry was added back 
**                           to PreselectQuery.
** 049 SVL 20080730   @39223 Support API change in IndexHelper.
** 050 SVL 20080815   @39400 Into provideNewResults() before we skip the
**                           first result got after switching to the
**                           preselect mode, we check whether it equals
**                           the last result retrieved in dynamic mode.
** 051 ECF 20080813   @39553 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.
** 052 CA  20080815   @39449 Support API change in DBUtils.
** 053 ECF 20081014   @40088 Added support for inlining query substitution
**                           parameters. Integrated HQLPreprocessor API
**                           changes;  modified AdaptiveComponent to use
**                           appropriate HQL, based on preselect vs.
**                           dynamic mode.
** 054 ECF 20081030   @40305 Fixed regression introduced by #053 (@40088).
**                           Refactored AdaptiveComponent inner class and
**                           code in top-level class which uses it.
** 055 ECF 20081105   @40309 Overrode parent's open() method. Registers
**                           with the ChangeBroker at the appropriate
**                           scope.
** 056 CA  20081210   @40836 Do not cleanup if the query was explicitly
**                           closed.
** 057 CA  20081211   @40858 Fixed regression introduced by H056: on
**                           sessionClosing event, if the query is closed,
**                           return true.
** 058 ECF 20081216   @40916 Removed flushBuffers() invocation. This work
**                           is now done when a component is added to the
**                           query. The previous invocation, during record
**                           retrieval, was too late.
** 059 SVL 20090118   @41169 getOriginalWhere() is used together with
**                           getRawArgs() during dynamic query creation.
** 060 ECF 20090202   @41247 Rolled back #040 (@38312). Prelock mechanism
**                           has been disabled.
** 061 ECF 20090220   @41350 Override parent's getRow() method. Needed in
**                           cases where row data is required during a
**                           transition from preselect to dynamic mode,
**                           before a new Results object is available.
** 062 ECF 20090630   @42986 Fixed reposition support in conjunction with
**                           changes to the base class.
** 063 ECF 20090701   @43008 Fixed NPE in setupInvalidationProperties(). It is
**                           possible for a component to have null sort
**                           criteria, if the query's only sort is by primary
**                           key.
** 064 ECF 20090702   @43033 Added isNativelyScrolling(). Overrides parent's
**                           implementation to report false.
** 065 ECF 20090703   @43053 Reimplemented load() method. This method now sets
**                           the backing buffer to unknown mode and throws
**                           MissingRecordException if a target record cannot
**                           be loaded.
** 066 ECF 20090707   @43102 Fixed regression in #062. When storing results in
**                           the ResultsAdapter in createResults(), set the
**                           betweenRows flag to false to ensure the next call
**                           to next() must advance the query.
** 067 ECF 20090716   @43212 Added late initialization of backing record
**                           buffer.
** 068 ECF 20090724   @43414 Added implicit transaction support. Added
**                           push/pop implicit transaction calls to
**                           DynamicResults.
** 069 CA  20090731   @43464 Register the query for off-end listener 
**                           registration after block initialization (done
**                           only for block types FOR and FOR EACH).
** 070 ECF 20090810   @43585 Changed to accommodate SessionListener API
**                           change. sessionClosing() method was replaced with
**                           sessionEvent().
** 071 ECF 20090811   @43597 Fixed resource cleanup. Made changes to execute()
**                           to make sure a resource cleaner is registered in
**                           dynamic mode. Added call to cleanup adapter when
**                           switching modes.
** 072 SVL 20091030   @44303 Added isNativelyPreselect() function.
** 073 SVL 20120828          Make dynamic results non-ID-only if there is a
**                           client-side where clause.
** 074 ECF 20131028          Import change.
** 075 SVL 20140210          Use HQLExpression instead of HQL string.
** 076 SVL 20140604          Added support for INDEX-INFORMATION.
** 077 SVL 20140812          Fixed PREV mistakenly treated as LAST.
** 078 SVL 20140909          Added cursor for proper support of scrolling queries.
**                           Implemented deleteResultListEntry.
** 079 SVL 20141209          Fixed case of full and empty cursor set.
** 080 SVL 20150212          Use normal invalidation for browsed queries. Removed duplicate
**                           functionality in repositionByID (it is handled by Cursor).
** 081 SVL 20150331          Added deleteResultListEntry(boolean).
** 082 OM  20150507          Removed listener from global scope of ChangeBroker.
** 083 ECF 20150801          Added support for multiple table joins, in the context of an
**                           optimized compound query. Refactored inner class AdaptiveQuery into
**                           a top-level class.
** 084 ECF 20150929          Deregister as a SessionListener if a query has been closed.
** 085 EVL 20160217          Clean up comments from symbols invalid for Solaris to compile.
** 086 SVL 20160314          Reference row was added for scrolling queries.
** 087 ECF 20160321          Minor doc fixes.
** 088 ECF 20160515          Doc and format cleanup.
** 089 ECF 20160610          Enable ProgressiveResults to work more effectively with the
**                           Hibernate query cache when processing a projection query.
** 090 SVL 20160608          The query can handle null IDs (for scrolling compound queries
**                           with OUTER join).
** 091 OM  20160603          Fixed rtrimming to space character only.
** 092 SVL 20160629          Reset cursor on query (re)open.
** 093 ECF 20160614          Rolled back #089. Track entities associated with query to enable
**                           smarter flushing strategy for query.
** 094 SVL 20160721          Proper reset on query reopen.
** 095 OM  20160720          Method getPersistence().popImplicitTransaction() changed signature.
** 096 GES 20160804          Switched from WhereExpression to lambda usage.
**         20160919          All initialization is now done with a default constructor followed
**                           by a call to initialize(). This enables query instance members in
**                           converted code to be local variables that are effectively final.
**                           This is needed to support lambdas without moving queries to be
**                           instance members. Queries as instance members breaks recursion
**                           use cases.
** 097 CA  20161012          Fixed scope processing when the query is from a persistent 
**                           procedure, and is part of a QUERY resource.
** 098 OM  20161215          Kept breakValue after switching back to preselect mode.
** 099 OM  20170209          Added return value for repositioning methods.
** 100 ECF 20180201          RandomAccessQuery.initializeBuffer has been removed.
** 101 ECF 20180418          Set external buffers into dynamic query for single-component
**                           adaptive query when switching to dynamic mode.
** 102 ECF 20180709          Inform ProgressiveResults if this is a projection query, to enable
**                           downstream optimizations.
** 103 OM  20180625          Added support for dynamic sorting criteria.
** 104 ECF 20190302          Renamed setExternalBuffers to addExternalBuffers.
** 105 OM  20190330          Ignore close request when query not fully initialized.
** 106 SVL 20190320          Added createResultListEntry.
** 107 ECF 20190427          Fixed results caching.
** 108 CA  20190811          A dynamic query can be closed and then re-opened.
**     EVL 20190822          The closed query should return Unknown value for size request, not 0.
**     HC  20190823          Fixed an NPE condition.
**     EVL 20190823          Clean up from debug comments.
** 109 SVL 20191002          Misc fixes for the cases exposed during implementation of
**                           create/delete-reslut-list-entry
** 110 CA  20191005          A query must know if it is dynamic, when closing.
** 111 CA  20191009          Added where and sort clause translation in case of bound buffers.
** 112 SVL 20191218          Handled case when REPOSITION TO ROWID fails.
** 113 ECF 20200215          New ORM implementation.
** 114 CA  20200924          Replaced DMO property access from reflection to property's data handler.
**     OM  20201001          Dropped redundant ORDER BY elements in multi-table queries.
**     CA  20201005          Fixed assembleOrderByClause - emit the _multiplex column, too, so that H2 will 
**                           match the WHERE and ORDER BY indexes (if possible).
**     ECF 20201011          Do not use ProgressiveResults if the sort phrase does not match a legacy index,
**                           to forego the performance penalty of re-sorting the results for multiple
**                           brackets.
**         20201019          Modified sort phrase index comparison to only check outermost query component.
**     OM  20210226          Fixed navigation of resultsets which include OUTER-JOIN-ed tables.
**     CA  20210310          A dynamic predicate must set the default lock to NONE, instead of SHARE.
**     SVL 20210413          Added NPE protection in add/clearDynamicSortCriterion.
**     ECF 20210723          Changed signature of query close worker method.
**     AL2 20210909          Added support for indexed reposition.
**     CA  20220117          Fixed close for dynamic PreselectQuery's, the close(boolean) needs to be 
**                           implemented.
**     OM  20220125          If the first row is deleted, the cursor is reset and first row is refetched.
**     RV  20220526          Improved reset method so it clears breakValue and foundFirst properties.
**     OM  20220701          Dropped overriding methods which were duplicating code from super class.
**     OM  20220707          Shared meta-knowledge about fields and properties of a record was collected in
**                           DmoMeta to avoid duplicating their collection and storage.
**     OM  20220726          Added recordAlreadyFound flag for block next/prev iterations on unique
**                           find-by-rowid queries.
**     CA  20220901          Refactored scope notification support: ScopeableFactory was removed, and the 
**                           registration is now specific to each type of scopeable.  For each case, the block
**                           will be registered for scope support (for that particular scopeable) only when
**                           the scopeable is 'active' (i.e. unnamed streams or accumulators are used).  This
**                           allows a lazy registration of scopeables, to avoid the unnecessary overhead of
**                           processing all the scopeables for each and every block.
**     CA  20220531          Ensure the sortTranslated flag is set when the sort is translated with the bound 
**                           buffers.
**     OM  20221103          New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
**     CA  20221130          Refactored the WHERE clause translation (when the bound and definition buffers
**                           are not the same), to be aware of the external buffers (i.e. added for CAN-FIND
**                           sub-select clauses).  The translate will be performed before the query is being
**                           executed.
**     SVL 20230108          Improved performance by replacing some "for-each" loops with indexed "for" loops.
**     SVL 20230109          Reflected PreselectQuery.components() now providing direct access to the
**                           components array in order to improve performance.
** 115 RAA 20230213          Added the possibility to execute a query in a lazy manner. Also, when a result
**                           set is created, ProgressiveResults can no longer be the type returned.
**     RAA 20230221          If the query will be executed lazy, then it is no longer registered as a 
**                           listener.
** 116 AL2 20230315          Make dynamic results "auto fetch".
** 117 IAS 20230405          Fixed FILL support for recursive DATA-RELATION
** 118 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 119 AL2 20230526          Fixed isLazyMode to handle only single-table queries.
** 120 SVL 20230516          Support for more granular configuration of dirty sharing functionality.
** 121 DDF 20230627          Replaced static variables of DirtyShareSupport with static method calls.
** 122 AB  20230803          Modified assembleWhereClause() to match the new list of arguments, return type
**                           and to avoid double adding "where" or "and".
**     AB  20230822          Modified getCurrentIds(boolean nullable) to take into consideration if
**                           the component is outer.
**     AB  20230830          Modified assembleWhereClause() to append correctly "and" and "where" in 
**                           case of "breakValue".
** 123 SAT 20231002          Added the case when last() is called and the query is attached to a browse.
** 124 AD  20231108          RecordChangeListener code is now executed in PreselectQuery.
**     AD  20231113          Added back isLazyMode() check in registerRecordChangeListeners() and
**                           the unregisterOnCleanup check in cleanup() method.
**     AD  20231115          Overridden initialize(String) to fix the registering to record change listener.
**     AL2 20231127          Move registerRecordChangeListeners in PreselectQuery.
** 125 ES  20231203          Changed first, last, next, previous, current, unique methods to return boolean,
**                           instead of void. These methods now returns true if operation was succesful, and false 
**                           not and lenientOffEnd is set on true, otherwise it thrown an QQE exception. 
** 126 DDF 20240207          Only cache the results once when TRANSACTION_COMMITTING event is notified.
**     DDF 20240208          Only non-scrolling queries will cache results.
**     DDF 20240209          Do not set the results when the old and cached results match and if the 
**                           the results are already cached, mark them as such.
** 127 AL2 20240307          Mark the query to retrieve the entire result set (useful for FILL operations).
** 128 AL2 20240318          Invalidate query if there are dirty changes made by this session on a component.
**     AL2 20240319          Made the invalidation due to dirty component only at initialization. 
** 129 AL2 20240321          Made earlyInvalidate protected.
** 130 CA  20240320          Avoid BDTs within internal FWD runtime.  Replaced iterators with Java 'for',  
**                           where it applies.
** 131 CA  20240323          Fixed double-offend registration in 'AdaptiveQuery.initialize()'.
**                           Query's 'hintFullResults' can not work with forward-only queries.
**     CA  20240331          Use ArrayList and Java for loops in query assembler.
** 132 CA  20240402          Translate the where clause before creating the query in createSimpleQuery or
**                           createCompoundQuery.
** 133 CA  20240422          Allow the query to be reset to its initialization state, so the same instance can
**                           be used to execute child buffer FILL operations.
** 134 AL2 20240424          Let earlyInvalidate return true if it actually invalidated or false otherwise.
**                           Let local invalidation for multi-component. Early invalidate when resetting.
** 135 AL2 20240508          Set lenient-off-end to queries that back the dynamic results.
** 136 AL2 20240514          Set OFF-END to FRONT when early invalidating.
** 137 TJD 20240412          Renamed isEnabledCrossSection to isEnabledCrossSession
**     TJD 20240730          Allow offEnd.FRONT to be treated like offEnd.BACK for prev/last queries
** 138 AL2 20240906          Implemented get(forceOnlyId). Cached results avoid spoiling the session cache.
***    AL2 20241004          Fixed results get to properly honor the forceOnlyId flag.
** 139 DDF 20230712          Added an additional condition when creating ScrollableResults, the query
**                           must not have forward-only enabled.
**     DDF 20230717          Only allow scrolling when forwardOnly is false.
**     DDF 20230721          Added setForwardOnly() method.
**     DDF 20230825          Refactor TRANSACTION_COMMITTING event in sessionEvent() method.
**     DDF 20240130          Set forwardOnly flag when created a RAQ. Do not create a new cursor
**                           when forwardOnly is set.
** 140 AP  20241009          Reset sortIndex when a query component is invalidated.
** 141 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
**                           based on [sharedDb] parameter.
**                           Implementation of [setMultiTenantAlternative] method.
** 142 DDF 20241121          Added delegateChanged() for enabling results caching when the ProgressiveResults
**                           closes it's delegate.
** 143 AOG 20250109          Added afterReposition calls after forward/backward operation is done.
** 144 AL2 20250129          Added quickRevalidation to avoid useless invalidations that may trigger infinite
**                           dynamic mode for no gain.
**     AL2 20250130          Don't quickly revalidate if the invalid component is different from the one that
**                           already triggered dynamic mode. Reset invalidComponent if quickly revalidated.
** 145 AL2 20250203          Avoided NPE in quickRevalidation caused by SESSION_CLOSING_WITH_ERROR.
** 146 ES  20250210          Don't quickly revalidate if the query is in dynmaic mode.
** 147 AL2 20250219          Extended use of cacheAll on invalidation when we already iterated some records.
**     AL2 20240220          Fixed bug when deleting the last record and the cacheAll operation would have
**                           still set the betweenRows flag, although the query ran off end.
** 148 ES  20250218          Added available parameter to the coreFetch method call.
** 149 AL2 20250226          The extended use of cacheAll should work only with shouldExecuteForward.
** 150 AL2 20250403          Caching results may cause invalidation. In that case, stop caching.
** 151 AL2 20250415          Make reposition if indexed-reposition smarter to reset query if in dynamic mode.
** 152 ES  20250424          Handle StopConditionException through ErrorManager.
** 153 LS  20250502          When creating a new RAQ in 'createSimpleQuery' set the included/excluded fields.
*/

/*
** 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.lang.reflect.*;
import java.sql.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;

import com.goldencode.p2j.persist.dirty.*;
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.ErrorManager;
import com.goldencode.p2j.util.logging.*;

/**
 * An adaptive, iterating query which first attempts to retrieve its records
 * in preselect mode, but changes to dynamic record retrieval as necessary.
 * <code>AdaptiveQuery</code> balances the high performance of a preselect
 * query implementation with the flexibility of a dynamic query
 * implementation.
 * <p>
 * The design point behind this implementation is that in many if not most
 * cases, a preselect query plan is the highest performing strategy.  It
 * leverages the strength of the database server to do the heavy lifting of
 * set-oriented data record retrieval.  In most use cases, this is the most
 * appropriate retrieval strategy.  However, to emulate Progress' semantic,
 * it is necessary to reflect table updates in a query which has already
 * begun iterating through its result set.  This dynamic approach is much
 * slower in the P2J architecture, since it requires at minimum one, and
 * sometimes many more round trips to the server for each record retrieved.
 * Therefore it is desirable to avoid dynamic retrieval mode whenever
 * possible.
 * <p>
 * The one performance weakness of the preselect approach is the case where
 * client code does not need the full result set acquired by the query.  To
 * deal with this, <code>AdaptiveQuery</code> uses an instance of {@link
 * ProgressiveResults} to fetch progressively larger brackets of result set
 * data while in preselect mode.  Thus, early results are returned quickly,
 * and if more results are needed, they are fetched in increasingly larger
 * batches.  This amortizes the access cost most effectively for both ends of
 * the spectrum:  use cases which require only one or a handful of results
 * never incur the cost of larger fetches and use cases which require many
 * results acquire them with far fewer calls than in dynamic mode.
 * <p>
 * <code>AdaptiveQuery</code> takes an optimistic approach by operating in
 * preselect mode immediately and for as long as possible.  It registers as a
 * {@link ChangeBroker} listener.  If during its preselect mode operation it
 * receives notification of a DMO state change which would invalidate its
 * current result set, it automatically switches over to dynamic retrieval
 * mode.  It will remain in dynamic mode for as long as necessary, but will
 * switch back into preselect mode at the earliest opportunity.
 * <p>
 * Once switching from preselect to dynamic mode, the query will operate in
 * this mode until the underlying <code>DynamicResults</code> object detects
 * that it is possible to switch back to preselect mode.  This occurs when
 * the <code>DynamicQuery</code> underlying the <code>DynamicResults</code>
 * object crosses a <i>sort band</i> boundary.  A <i>sort band</i> is the
 * grouping defined by the coarsest sort criterion of the query.  A boundary
 * is crossed when the DMO property defining the sort band changes from one
 * retrieved record to the next.  The value of this property in the last
 * record before the boundary is crossed is termed the <i>break value</i>.
 * When the break value for the underlying dynamic query is non-null, this is
 * an indication that the sort band boundary has been crossed.  At this point,
 * it is possible to construct a new preselect mode query which uses all the
 * original query criteria, plus the break value, to ensure the query results
 * pick up where the dynamically retrieved result set ended.
 * <p>
 * TODO:  Make invalidation check more robust;  currently it is too conservative.
 * <p>
 * TODO:  Make revalidation failsafe more sophisticated;  currently it locks into dynamic mode
 * after <code>MAX_REVALIDATIONS</code> revalidations.
 * <p>
 * TODO:  Make global invalidation setting configurable from the directory.
 */
public class AdaptiveQuery
extends PreselectQuery
implements VirtualResultsListener
{
   /** Maximum number of times query can switch from dynamic to preselect mode */
   private static final int MAX_REVALIDATIONS = 3;
   
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(AdaptiveQuery.class.getName());
   
   /** Force invalidation checks across all sessions */
   private static final boolean forceGlobalInvalidation = false;
   
   /** Results adapter which can connect to different low-level delegates */
   private ResultsAdapter adapter;
   
   /** Implement invalidation checks across all sessions */
   private boolean globalInvalidation = forceGlobalInvalidation;
   
   /** Force dynamic mode from the beginning if there is a dirty component */
   private boolean localInvalidation = true;
   
   /** High level results object for parent query */
   private Results highLevelResults = null;
   
   /** Query component which triggered invalidation */
   private AdaptiveComponent invalidComponent = null;
   
   /** Properties whose changes trigger invalidation, keyed by DMO entity */
   private Map<String, Set<String>> invalidationProperties = null;
   
   /** Property value which indicates a new sort band in dynamic mode */
   private Object breakValue = null;
   
   /** Number of times this query has transitioned back to preselect mode */
   private int revalCount = 0;
   
   /** Are results cached due to a previously committed transaction? */
   private boolean resultsCached = false;
   
   /** Preselected results should all be cached on next revalidation */
   private boolean cacheOnReval = false;
   
   /** Reflects whether an error was detected while getting results */
   protected boolean errorCondition = false;
   
   /** Represents cache of the dynamic scrolling query. */
   protected Cursor cursor = null;
   
   /** Determines if we have fetched a result row. Only for non-scrolling queries. */
   protected boolean foundFirst = false;
   
   /** Flag to check the ability to fetch the records in a lazy manner, without server-side invalidation */
   private Boolean lazyMode = null;
   
   /** Flag indicating the entire result set will be retrieved (useful for FILL operations). */
   private boolean forceFullResults = false;
   
   /** Flag to indicate that this query should always operated in dynamic mode, once invalidate */
   private boolean avoidRevalidation = false;
   
   /**
    * Default constructor. Initialization is deferred until <code>initialize()</code> is
    * called.
    */
   public AdaptiveQuery()
   {
   }
   
   /**
    * Initialization logic which is typically used for a multi-table query. Details
    * specific to each table component of the query are added via one of the
    * <code>addComponent</code> method variants.
    * <p>
    * Each component will provide its own sort criteria;  they are combined
    * when the query is first executed in preselect mode.
    * <p>
    * This code is intentionally separated from the constructor. The purpose of this separation
    * is to allow construction to occur in the prior scope while initialization can still occur
    * within a method or lambda that is inside the next block's scope.
    *             
    * @return   This query instance.
    */
   public AdaptiveQuery initialize()
   {
      super.initialize(null);
      
      adapter = new ResultsAdapter(new ResultsSource());
      
      TransactionManager.registerOffEndQuery(this);
      
      return this;
   }
   
   /**
    * Initializer logic which is typically used for a multi-table query. Details
    * specific to each table component of the query are added via one of the
    * <code>addComponent</code> method variants.
    * <p>
    * This code is intentionally separated from the constructor. The purpose of this separation
    * is to allow construction to occur in the prior scope while initialization can still occur
    * within a method or lambda that is inside the next block's scope.
    *
    * @param    sort
    *           FQL order by clause to be applied across all components of the query.
    *
    * @return   This query instance.
    */ 
   public AdaptiveQuery initialize(String sort)
   {
      this.sort = sort;
      components = new ArrayList<>(3);
    
      TransactionManager.registerOffEndQuery(this);
      
      earlyInvalidate();

      return this;
   }
   
   /**
    * Initialization logic which is typically used for a multi-table query. Details
    * specific to each table component of the query are added via one of the
    * <code>addComponent</code> method variants.
    * <p>
    * This variant is used by code which is converted from an OPEN QUERY
    * statement, which permits advancing off the end of a result set without
    * raising a <code>QueryOffEndException</code>.
    * <p>
    * This code is intentionally separated from the constructor. The purpose of this separation
    * is to allow construction to occur in the prior scope while initialization can still occur
    * within a method or lambda that is inside the next block's scope.
    *
    * @param    lenientOffEnd
    *           <code>true</code> if advancing past either end of the result
    *           set should NOT raise a <code>QueryOffEndException</code>;
    *           <code>false</code> if doing so should raise the exception.
    *             
    * @return   This query instance.
    */
   public AdaptiveQuery initialize(boolean lenientOffEnd)
   {
      super.initialize(lenientOffEnd, null);
      
      adapter = new ResultsAdapter(new ResultsSource());
      
      TransactionManager.registerOffEndQuery(this);
      
      earlyInvalidate();
      
      return this;
   }
   
   /**
    * Initialization logic designed for a single-table adaptive query.
    * <p>
    * This code is intentionally separated from the constructor. The purpose of this separation
    * is to allow construction to occur in the prior scope while initialization can still occur
    * within a method or lambda that is inside the next block's scope.
    *
    * @param    dmo
    *           DMO proxy which defines buffer and record type.
    * @param    where
    *           FQL where clause.  May be <code>null</code>.
    * @param    whereExpr
    *           Client-side where clause expression.  May be <code>null</code>.
    * @param    sort
    *           FQL order by clause.
    * @param    indexInfo
    *           Index information string as it is returned by INDEX-INFORMATION.
    *           May be <code>null</code> if it is not an OPEN QUERY case or if
    *           you don't need debug information about selected indexes.
    * @param    inverse
    *           DMO to which this query should join via a foreign relation.
    *           May be <code>null</code>.
    * @param    args
    *           Substitution parameters to be inserted into placeholders within
    *           the where clause.
    * @param    lockType
    *           Lock type to apply to records retrieved by this query. If {@code null}, the default lock level
    *           is assumed.
    *             
    * @return   This query instance.
    */
   public AdaptiveQuery initialize(DataModelObject   dmo,
                                   String            where,
                                   Supplier<logical> whereExpr,
                                   String            sort,
                                   String            indexInfo,
                                   DataModelObject   inverse,
                                   Object[]          args,
                                   LockType          lockType)
   {
      if (lockType == null)
      {
         lockType = getDefaultLock();
      }
      
      String translatedSort = sort;
      RecordBuffer defBuffer = ((BufferReference) dmo).definition();
      RecordBuffer buffer = ((BufferReference) dmo).buffer();
      
      if (!buffersMatch(buffer, defBuffer))
      {
         translatedSort = translateSort(buffer, defBuffer, sort);
         sortTranslated = true;
      }
      
      initialize(translatedSort);
      
      addComponent(dmo, where, sort, indexInfo, inverse, args, lockType, NEXT, false);
      
      adapter = new ResultsAdapter(new ResultsSource());
      
      if (whereExpr != null)
      {
         addWhereExpression(whereExpr);
      }
      
      registerRecordChangeListeners(null);
      
      earlyInvalidate();
      
      return this;
   }
   
   /**
    * Add a component to the query.  The following defaults are used:
    * <ul>
    *   <li>no substitution parameters
    *   <li>share lock
    *   <li>inner join
    *   <li><code>NEXT</code> iteration type
    *   <li>no inverse join DMO
    * </ul>
    *
    * @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.
    */
   public void addComponent(DataModelObject dmo, String where, String sort)
   {
      addComponent(dmo, where, sort, null, null, getDefaultLock(), NEXT, false);
   }
   
   /**
    * Add a component to the query.  The following defaults are used:
    * <ul>
    *   <li>no substitution parameters
    *   <li>share lock
    *   <li>inner join
    *   <li><code>NEXT</code> iteration type
    * </ul>
    *
    * @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   inverse
    *          DMO to which this query should join via a foreign relation.
    *          May be <code>null</code>.
    */
   public void addComponent(DataModelObject dmo,
                            String where,
                            String sort,
                            DataModelObject inverse)
   {
      addComponent(dmo, where, sort, inverse, null, getDefaultLock(), NEXT, false);
   }
   
   /**
    * Add a component to the query.  The following defaults are used:
    * <ul>
    *   <li>share lock
    *   <li>inner join
    *   <li><code>NEXT</code> iteration type
    *   <li>no inverse join DMO
    * </ul>
    *
    * @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   args
    *          Substitution parameters to be inserted into placeholders within
    *          the where clause.
    */
   public void addComponent(DataModelObject dmo,
                            String where,
                            String sort,
                            Object[] args)
   {
      addComponent(dmo, where, sort, null, args, getDefaultLock(), NEXT, false);
   }
   
   /**
    * Add a component to the query.  The following defaults are used:
    * <ul>
    *   <li>share lock
    *   <li>inner join
    *   <li><code>NEXT</code> iteration type
    * </ul>
    *
    * @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   inverse
    *          DMO to which this query should join via a foreign relation.
    *          May be <code>null</code>.
    * @param   args
    *          Substitution parameters to be inserted into placeholders within
    *          the where clause.
    */
   public void addComponent(DataModelObject dmo,
                            String where,
                            String sort,
                            DataModelObject inverse,
                            Object[] args)
   {
      addComponent(dmo, where, sort, inverse, args, getDefaultLock(), NEXT, false);
   }
   
   /**
    * Add a component to the query.  The following defaults are used:
    * <ul>
    *   <li>no substitution parameters
    *   <li>inner join
    *   <li><code>NEXT</code> iteration type
    *   <li>no inverse join DMO
    * </ul>
    *
    * @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 component of
    *          the query.
    */
   public void addComponent(DataModelObject dmo,
                            String where,
                            String sort,
                            LockType lockType)
   {
      addComponent(dmo, where, sort, null, null, lockType, NEXT, false);
   }
   
   /**
    * Add a component to the query.  The following defaults are used:
    * <ul>
    *   <li>no substitution parameters
    *   <li>inner join
    *   <li><code>NEXT</code> iteration type
    * </ul>
    *
    * @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   inverse
    *          DMO to which this query should join via a foreign relation.
    *          May be <code>null</code>.
    * @param   lockType
    *          Lock type to apply to records retrieved by this component of
    *          the query.
    */
   public void addComponent(DataModelObject dmo,
                            String where,
                            String sort,
                            DataModelObject inverse,
                            LockType lockType)
   {
      addComponent(dmo, where, sort, inverse, null, lockType, NEXT, false);
   }
   
   /**
    * Add a component to the query.  The following defaults are used:
    * <ul>
    *   <li>inner join
    *   <li><code>NEXT</code> iteration type
    *   <li>no inverse join DMO
    * </ul>
    *
    * @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   args
    *          Substitution parameters to be inserted into placeholders within
    *          the where clause.
    * @param   lockType
    *          Lock type to apply to records retrieved by this component of
    *          the query.
    */
   public void addComponent(DataModelObject dmo,
                            String where,
                            String sort,
                            Object[] args,
                            LockType lockType)
   {
      addComponent(dmo, where, sort, null, args, lockType, NEXT, false);
   }
   
   /**
    * Add a component to the query.  The following defaults are used:
    * <ul>
    *   <li>inner join
    *   <li><code>NEXT</code> iteration type
    * </ul>
    *
    * @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   inverse
    *          DMO to which this query should join via a foreign relation.
    *          May be <code>null</code>.
    * @param   args
    *          Substitution parameters to be inserted into placeholders within
    *          the where clause.
    * @param   lockType
    *          Lock type to apply to records retrieved by this component of
    *          the query.
    */
   public void addComponent(DataModelObject dmo,
                            String where,
                            String sort,
                            DataModelObject inverse,
                            Object[] args,
                            LockType lockType)
   {
      addComponent(dmo, where, sort, inverse, args, lockType, NEXT, false);
   }
   
   /**
    * Add a component to the query.  The following defaults are used:
    * <ul>
    *   <li>no inverse join DMO
    * </ul>
    *
    * @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   args
    *          Substitution parameters to be inserted into placeholders within
    *          the where clause.
    * @param   lockType
    *          Lock type to apply to records retrieved by this component of
    *          the query.
    * @param   iteration
    *          Iteration/retrieval mode for this component of the query.
    *          Only <code>NEXT</code> is supported.
    * @param   outer
    *          <code>true</code> if an outer join is required.  At this time,
    *          only inner joins are supported.  Future support for outer
    *          joins is expected.
    *
    * @throws  UnsupportedOperationException
    *          if <code>outer</code> is <code>true</code> (temporary);
    *          if <code>iteration</code> is not <code>NEXT</code> (temporary);
    *          if <code>dmo</code>'s associated database is different than
    *          that of a previously added component's database (cross-database
    *          preselect joins are not possible).
    */
   public void addComponent(DataModelObject dmo,
                            String where,
                            String sort,
                            Object[] args,
                            LockType lockType,
                            int iteration,
                            boolean outer)
   {
      addComponent(dmo, where, sort, null, args, lockType, iteration, outer);
   }
   
   /**
    * Add a component to the query.
    *
    * @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   inverse
    *          DMO to which this query should join via a foreign relation.
    *          May be <code>null</code>.
    * @param   args
    *          Substitution parameters to be inserted into placeholders within
    *          the where clause.
    * @param   lockType
    *          Lock type to apply to records retrieved by this component of
    *          the query.
    * @param   iteration
    *          Iteration/retrieval mode for this component of the query.
    *          Only <code>NEXT</code> is supported.
    * @param   outer
    *          <code>true</code> if an outer join is required.  At this time,
    *          only inner joins are supported.  Future support for outer
    *          joins is expected.
    *
    * @throws  UnsupportedOperationException
    *          if <code>outer</code> is <code>true</code> (temporary);
    *          if <code>iteration</code> is not <code>NEXT</code> (temporary);
    *          if <code>dmo</code>'s associated database is different than
    *          that of a previously added component's database (cross-database
    *          preselect joins are not possible).
    */
   public void addComponent(DataModelObject dmo,
                            String where,
                            String sort,
                            DataModelObject inverse,
                            Object[] args,
                            LockType lockType,
                            int iteration,
                            boolean outer)
   {
      addComponent(dmo, where, sort, null, inverse, args, lockType, iteration, outer);
   }
   
   /**
    * Add a component to the query.
    *
    * @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   indexInfo
    *          Index information string as it is returned by INDEX-INFORMATION.
    *          May be <code>null</code> if it is not an OPEN QUERY case or if
    *          you don't need debug information about selected indexes.
    * @param   inverse
    *          DMO to which this query should join via a foreign relation.
    *          May be <code>null</code>.
    * @param   args
    *          Substitution parameters to be inserted into placeholders within
    *          the where clause.
    * @param   lockType
    *          Lock type to apply to records retrieved by this component of
    *          the query.
    * @param   iteration
    *          Iteration/retrieval mode for this component of the query.
    *          Only <code>NEXT</code> is supported.
    * @param   outer
    *          <code>true</code> if an outer join is required.  At this time,
    *          only inner joins are supported.  Future support for outer
    *          joins is expected.
    *
    * @throws  UnsupportedOperationException
    *          if <code>outer</code> is <code>true</code> (temporary);
    *          if <code>iteration</code> is not <code>NEXT</code> (temporary);
    *          if <code>dmo</code>'s associated database is different than
    *          that of a previously added component's database (cross-database
    *          preselect joins are not possible).
    */
   public void addComponent(DataModelObject dmo,
                            String where,
                            String sort,
                            String indexInfo,
                            DataModelObject inverse,
                            Object[] args,
                            LockType lockType,
                            int iteration,
                            boolean outer)
   {
      if (getTableCount() > 0)
      {
         throw new UnsupportedOperationException(
            "Multi-table adaptive queries are not supported in this release");
      }
      
      if (iteration != NEXT)
      {
         throw new UnsupportedOperationException(
            "Adaptive query only supports the NEXT iteration type");
      }
      
      BufferReference proxy = (BufferReference) dmo;
      BufferReference inverseProxy = (BufferReference) inverse;
      AbstractJoin join = processJoin(proxy, inverseProxy);
      
      RecordBuffer buffer = proxy.buffer();
      RecordBuffer defBuffer = proxy.definition();
      Supplier<String> translateWhere = null;
      if (!buffersMatch(buffer, defBuffer) || anyBoundBufferInComponent())
      {
         sort = translateSort(buffer, defBuffer, sort);
         sortTranslated = true;
         
         translateWhere = where == null ? null : () ->
         {
            List<RecordBuffer> binding = new ArrayList<>();
            List<RecordBuffer> definition = new ArrayList<>();
            binding.add(buffer);
            definition.add(defBuffer);
            for (int i = 0; i < components.size(); i++)
            {
               QueryComponent qc = components.get(i);
               if (qc.getBuffer() != qc.getDefinitionBuffer())
               {
                  binding.add(qc.getBuffer());
                  definition.add(qc.getDefinitionBuffer());
               }
            }
            return translateWhere(binding, definition, where);
         };
      }
      
      AdaptiveComponent comp = new AdaptiveComponent(this,
                                                     proxy,
                                                     join,
                                                     where,
                                                     translateWhere,
                                                     sort,
                                                     indexInfo,
                                                     lockType,
                                                     args,
                                                     iteration,
                                                     outer,
                                                     inverseProxy);
      addComponent(comp);
      
      // Temporary tables are local by nature, so we don't care what
      // happens in other sessions.
      if (globalInvalidation && comp.getBuffer().isTemporary())
      {
         globalInvalidation = false;
      }
   }
   
   /**
    * Open the query, first registering with the <code>ChangeBroker</code> at
    * the appropriate scope.
    */
   public void open()
   {
      closed = false;

      if (cursor != null)
      {
         cursor.reset();
      }
      invalidComponent = null;
      revalCount = 0;
      foundFirst = false;
      avoidRevalidation = false;
      
      earlyInvalidate();
      
      super.open();
   }
   
   /**
    * Check if this type of query should register record change listeners. Use this 
    * to avoid computing any scope for registration and end up with a no registration
    * behavior. 
    * 
    * @return   {@code true} if we should invest time into computing the scope in order
    *           to call {@link #registerRecordChangeListeners} or {@code false} if we
    *           can skip doing any work to honor {@link #registerRecordChangeListeners}.
    */
   @Override
   public boolean shouldRegisterRecordChangeListeners()
   {
      return !isLazyMode();
   }
   
   /**
    * Retrieve the first composite row of results for the query.  The
    * underlying buffer for each query component is updated with the
    * appropriate record.
    * <p>
    * If the query involves outer joins, some of the underlying buffers may
    * be empty when this method returns.
    *
    * @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 first(LockType lockType)
   {
      if (isScrolling())
      {
         boolean available = false;
         if (cursor.isFoundFirst())
         {
            Object[] data = cursor.getFirst();
            
            if (data == null && cursor.isFullSet())
            {
               // The set is full and empty.
               loadByValues(null, lockType);
               offEnd = OffEnd.BACK;
            }
            else if (data != null)
            {
               available = true;
               if (!loadByValues(data, lockType))
               {
                  // this can happen when the first record was deleted externally, by other query or directly
                  // by buffer and this query/cursor is not aware of the situation
                  cursor.reset();
                  
                  try
                  {
                     available = super.first(lockType);
                  }
                  catch (QueryOffEndException e)
                  {
                     cursor.addResultFirst(null);
                     throw e;
                  }
                  
                  cursor.addResultFirst(getCurrentIds());
               }
            }
            else
            {
               if (cursor.isFirstRowDeleted() && cursor.isForwardScroll())
               {
                  return next(lockType);
               }
               else
               {
                  try
                  {
                     available = super.first(lockType);
                  }
                  catch (QueryOffEndException e)
                  {
                     cursor.addResultFirst(null);
                     throw e;
                  }
               }
               
               cursor.addResultFirst(getCurrentIds());
            }
         }
         else
         {
            try
            {
               available = super.first(lockType);
            }
            catch (QueryOffEndException e)
            {
               cursor.addResultFirst(null);
               throw e;
            }
         
            cursor.addResultFirst(getCurrentIds());
         }
         
         
         return available;
      }
      else
      {
         return super.first(lockType);
      }
   }
   
   /**
    * Retrieve the last composite row of results for the query.  The
    * underlying buffer for each query component is updated with the
    * appropriate record.
    * <p>
    * If the query involves outer joins, some of the underlying buffers may
    * be empty when this method returns.
    *
    * @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 last(LockType lockType)
   {
      if (isScrolling())
      {
         if (isBrowsed() && !cursor.isFullSet())
         {
            Results results = getResults();

            while (results.next())
            {
               try
               {
                  fetch(true, lockType, false);
               }
               catch (MissingRecordException e)
               {
                  continue;
               }

               cursor.addResultNext(getCurrentIds());
            }
            // Mark that the set is full.
            cursor.addResultNext(null);
            // current row is null after results().next and we need to go back to the last row
            results.previous();

            return true;
         }

         boolean avail = false;
         if (cursor.isFoundFirst())
         {
            Object[] data = cursor.getLast();
            
            if (data == null && cursor.isFullSet())
            {
               // The set is full and empty.
               loadByValues(null, lockType);
               offEnd = OffEnd.BACK;
               return false;
            }
            else if (data != null)
            {
               avail = true;
               if (!loadByValues(data, lockType))
               {
                  avail = previous(lockType);
               }
               
               return avail;
            }
         }

         try
         {
            avail = super.last(lockType);
         }
         catch (QueryOffEndException e)
         {
            cursor.addResultLast(null);
            throw e;
         }
         
         cursor.addResultLast(getCurrentIds());
         
         return avail;
      }
      else
      {
         return super.last(lockType);
      }
   }
   
   /**
    * Get the 1-based index of the current row in the result set.
    *
    * @return  Current row or the unknown value if the query has not yet
    *          executed.
    */
   public integer currentRowImpl()
   {
      if (isScrolling())
      {
         return cursor.currentRow();
      }
      else
      {
         return super.currentRowImpl();
      }
   }
   
   /**
    * Progress-compatible function that returns the 1-based index of the
    * current row in the result set.
    *
    * @return  Unknown value if the query is not open or result set is empty.
    *          1 if the query is positioned before the first record.
    *          A value 1 greater than the number of rows in the query result
    *          set if the query is positioned beyond the last record.
    *          Otherwise returns the number of rows in the query result set.
    *
    */
   public integer currentRow()
   {
      if (isScrolling())
      {
         return cursor.currentRow();
      }
      else
      {
         return super.currentRow();
      }
   }
   
   /**
    * Reload the current composite row of results for the query, overriding
    * the lock type to apply to each record.
    * <p>
    * If the query involves outer joins, some of the underlying buffers may
    * be empty when this method returns.
    *
    * @param   lockType
    *          Lock type which should override that of any query component.
    * 
    * @return  {@code true} if the current operation actually reloaded the
    *          current composite row of results for the query.
    *
    * @throws  ErrorConditionException
    *          if the reload triggers an error.
    * @throws  IllegalStateException
    *          if no query component has been added to this query.
    */
   public boolean current(LockType lockType)
   {
      if (currentRowDeleted)
      {
         if (isScrolling() || _isOffEnd())
         {
            ErrorManager.displayError(4114, "No query record is available", false);
         }
         
         return false;
      }
      
      return super.current(lockType);
   }
   
   /**
    * Reposition the query to the specified row in the result set.  The row
    * indicates the position between two results, such that a call to
    * retrieve the next record will get the record after the new position,
    * and a call to retrieve the previous record will get the record before
    * the new position.
    *
    * @param   row
    *          1-based index of the target position.
    *
    * @throws  ErrorConditionException
    *          if there is an error repositioning the cursor.
    */
   public void reposition(int row)
   {
      if (components == null)
      {
         return;
      }

      if (isScrolling())
      {
         prepareReposition();
         
         cursor.reposition(row);
         OffEnd res = cursor.getOffEnd();
         if (!OffEnd.NONE.equals(res))
         {
            offEnd = res;
            betweenRows = false;
         }
         else
         {
            betweenRows = true;
         }
         
         afterReposition(true);
      }
      else
      {
         super.reposition(row);
      }
   }
   
   /**
    * Advance the current cursor position forward by the specified number of
    * rows.  This will always leave the cursor between two results.  For
    * example, given a request to move 1 row forward:
    * <ul>
    *   <li>if the cursor currently is positioned <i>directly on a result</i>,
    *       this would move it ahead <i>one and a half positions</i>, to just
    *       beyond the following result;
    *   <li>if the cursor currently is positioned <i>between two results</i>,
    *       this would move it ahead <i>one position</i>, to just beyond the
    *       following result.
    * </ul>
    *
    * @param   rows
    *          Number of rows to scroll the cursor forward.
    *
    * @return  {@code true} if operation was successful and the cursor was moved the exact amount
    *          of rows as specified by the parameter. If {@code false} is returned then it is
    *          possible that cursor's position was moved but not with {@code rows} rows.
    *
    * @throws  ErrorConditionException
    *          if there is an error repositioning the cursor.
    */
   public boolean forward(int rows)
   {
      if (isScrolling())
      {
         if (rows == 0)
         {
            return true;
         }
         
         if (rows < 0)
         {
            return backward(-rows);
         }
         
         prepareReposition();
         
         boolean moved = cursor.forward(rows);
         offEnd = cursor.getOffEnd();
         afterReposition(true);
         
         return moved;
      }
      else
      {
         return super.forward(rows);
      }
   }
   
   /**
    * Reposition the cursor such that a request to retrieve the next result
    * will retrieve the result which matches the specified array of primary
    * key IDs.  A request to retrieve the previous result will retrieve the
    * result immediately previous to this, if any, in the results list.
    *
    * @param   ids
    *          Array of primary key IDs, arranged from left to right to
    *          coincide with the records being joined by the underlying
    *          query.
    * @param   forceFetch
    *          <code>true</code> to override the current fetch-on-reposition
    *          setting to <code>true</code>, in case the underlying
    *          <code>Results</code> implementation attempts to fetch the
    *          record during repositioning;  <code>false</code> to honor the
    *          current setting.
    *
    * @return  <code>true</code> if reposition was successful;
    *          <code>false</code> if unsuccessful.
    *
    * @throws  ErrorConditionException
    *          if there is an error repositioning the cursor.
    */
   protected boolean repositionByID(Long[] ids, boolean forceFetch)
   {
      return repositionByID(ids, forceFetch, true);
   }
   
   /**
    * Reposition the cursor such that a request to retrieve the next result
    * will retrieve the result which matches the specified array of primary
    * key IDs.  A request to retrieve the previous result will retrieve the
    * result immediately previous to this, if any, in the results list.
    *
    * @param   ids
    *          Array of primary key IDs, arranged from left to right to
    *          coincide with the records being joined by the underlying
    *          query.
    * @param   forceFetch
    *          <code>true</code> to override the current fetch-on-reposition
    *          setting to <code>true</code>, in case the underlying
    *          <code>Results</code> implementation attempts to fetch the
    *          record during repositioning;  <code>false</code> to honor the
    *          current setting.
    * @param   scan
    *          For scrolling queries: <code>true</code> to scan all results in
    *          the event a match is not found in the cache;  <code>false</code>
    *          to check the cache only.
    *
    * @return  <code>true</code> if reposition was successful;
    *          <code>false</code> if unsuccessful.
    *
    * @throws  ErrorConditionException
    *          if there is an error repositioning the cursor.
    */
   protected boolean repositionByID(Long[] ids, boolean forceFetch, boolean scan)
   {
      if (isScrolling())
      {
         prepareReposition();
         integer oldCurrentRow = scan ? currentRow() : null;
         boolean done = false;
         try
         {
            try
            { 
               if (!scan)
               {
                  done = cursor.reposition(ids, false);
               }
               else if (!indexedReposition)
               {
                  done = cursor.reposition(ids, true);
               }
               else
               {
                  // indexed-reposition case
                  if (this.isPreselect())
                  {
                     // preselect mode
                     try
                     {
                        done = cursor.reposition(ids, true);
                     }
                     catch (QueryOffEndException offEndExc)
                     {
                        // no-op
                     }
                     
                     if (!done)
                     {
                        reset(true, false);
                        done = cursor.reposition(ids, true);
                     }
                  }
                  else
                  {
                     // dynamic mode
                     if (cursor.reposition(ids, false))
                     {
                        // look-up in cache first
                        done = true;
                     }
                     else
                     {
                        // the query is in dynamic mode
                        reset(true, false);
                        done = cursor.reposition(ids, true);
                     }
                  }
               }
            }
            catch (QueryOffEndException offEndExc)
            {
               throw offEndExc;
            }
         }
         finally
         {
            offEnd = cursor.getOffEnd();
            if (scan)
            {
               afterReposition(done, oldCurrentRow);
            }
            else
            {
               afterReposition(done);
            }
         }
         
         return done;
      }
      else
      {
         return super.repositionByID(ids, forceFetch);
      }
   }
   
   /**
    * Return the number of results found for this query.
    * <p>
    * Note that the first time this method is invoked, the database server
    * may have to scroll to the end of the result set, which may be a
    * time-consuming process for a large result set.
    *
    * @return  Size of result set or value if the query is not open.
    */
   public integer size()
   {
      if (isScrolling())
      {
         return closed ? new integer() : cursor.size();
      }
      else
      {
         return super.size();
      }
   }
   
   /**
    * Retrieve the next composite row of results for the query.  The
    * underlying buffer for each query component 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)
   {
      if (isScrolling() && cursor.isFoundFirst())
      {
         Object[] data;
         
         do
         {
            data = cursor.getNext();
            
            if (data == null && (cursor.isFullSet() || !cursor.isForwardScroll()))
            {
               offEnd = OffEnd.BACK;
               
               try
               {
                  fetch(false, lockType, false);
               }
               catch (MissingRecordException e)
               {
                  // cannot happen, we are not loading a record
               }
               
               return false;
            }
            
            if (data != null)
            {
            
               if (loadByValues(data, lockType))
               {
                  return true;
               }
            }
            
         } while (data != null);
      }
      
      boolean connected = (adapter != null && adapter.isConnected());
      invalidateIfReferenceRowExists();
      Results results = getResults();
      
      boolean missing;
      boolean avail = false;
      
      do
      {
         missing = false;
         try
         {
            avail = false;
            if (betweenRows && connected && !isScrolling())
            {
               avail = true;
            }
            else
            {
               errorCondition = false;
               if (!results.next())
               {
                  if (errorCondition)
                  {
                     // if an error condition was detected (and handled) then
                     // we should retry next() action
                     return next(lockType);
                  }
                  else
                  {
                     avail = false;
                  }
               }
               else
               {
                  avail = true;
               }
            }
            
            offEnd = (avail ? OffEnd.NONE : OffEnd.BACK);
            fetch(avail, lockType, false);
            
            if (isScrolling())
            {
               if (avail)
               {
                  cursor.addResultNext(getCurrentIds());
               }
               else
               {
                  cursor.addResultNext(null);
               }
            }
         }
         catch (QueryOffEndException exc)
         {
            if (isScrolling())
            {
               cursor.addResultNext(null);
            }
            
            offEnd = OffEnd.BACK;
            if (!isLenientOffEnd())
            {
               exc.fillInStackTrace();
               throw exc;
            }
         }
         catch (MissingRecordException exc)
         {
            missing = true;
         }
         finally
         {
            betweenRows = false;
         }
      } while (missing);
      
      return avail;
   }
   
   /**
    * Fetch the array of primary key IDs associated with the next result in
    * this query's result list.
    *
    * @return  Array of record IDs reflecting the next query result, or
    *          <code>null</code> if there is no such result.
    */
   public Object[] peekNext()
   {
      Object[] res = super.peekNext();
      if (isScrolling())
      {
         cursor.addResultNext(res);
      }
      return res;
   }
   
   /**
    * Fetch the array of primary key IDs associated with the previous result
    * in this query's result list.
    *
    * @return  Array of record IDs reflecting the previous query result, or
    *          <code>null</code> if there is no such result.
    */
   public Object[] peekPrevious()
   {
      Object[] res = super.peekPrevious();
      if (isScrolling())
      {
         cursor.addResultPrevious(res);
      }
      return res;
   }
   
   /**
    * Retrieve the previous composite row of results for the query.  The
    * underlying buffer for each query component 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 previous(LockType lockType)
   {
      if (isScrolling() && cursor.isFoundFirst())
      {
         Object[] data;
         
         do
         {
            data = cursor.getPrevious();
            
            if (data == null && (cursor.isFullSet() || cursor.isForwardScroll()))
            {
               offEnd = OffEnd.FRONT;
               
               try
               {
                  fetch(false, lockType, false);
               }
               catch (MissingRecordException e)
               {
                  // cannot happen, we are not loading a record
               }
               
               return false;
            }
            
            if (data != null)
            {
               if (loadByValues(data, lockType))
               {
                  return true;
               }
            }
            
         } while (data != null);
      }
      
      boolean setToLast = treatPreviousAsLast();
      boolean avail = false;
      try
      {
         // When moving backward, all revalidation bets are off.
         breakValue = null;
         
         avail = super.previous(lockType);
         
         if (isScrolling())
         {
            if (!isOffEnd().booleanValue())
            {
               cursor.addResultPrevious(getCurrentIds());
            }
            else
            {
               cursor.addResultPrevious(null);
            }
         }
         
         return avail;
      }
      catch (UnsupportedOperationException exc)
      {
         // We tried to do something the underlying Results implementation
         // could not do;  invalidate and try again.
         if (isPreselect())
         {
            invalidate();
            if (!setToLast)
            {
               execute();
            }
            
            avail = super.previous(lockType);
            
            if (isScrolling())
            {
               if (!isOffEnd().booleanValue())
               {
                  cursor.addResultPrevious(getCurrentIds());
               }
               else
               {
                  cursor.addResultPrevious(null);
               }
            }

            return avail;
         }
         else
         {
            throw exc;
         }
      }
   }
   
   /**
    * Move the current cursor position backward by the specified number of
    * rows.  This will always leave the cursor between two results.  For
    * example, given a request to move 1 row backward:
    * <ul>
    *   <li>if the cursor currently is positioned <i>directly on a result</i>,
    *       this would move it back <i>one half position</i>, to just
    *       before the current result;
    *   <li>if the cursor currently is positioned <i>between two results</i>,
    *       this would move it back <i>one position</i>, to just before the
    *       current result.
    * </ul>
    *
    * @param   rows
    *          Number of rows to scroll the cursor backward.
    *
    * @return  {@code true} if operation was successful and the cursor was moved the exact amount
    *          of rows as specified by the parameter. If {@code false} is returned then it is
    *          possible that cursor's position was moved but not with {@code rows} rows.
    *
    * @throws  ErrorConditionException
    *          if there is an error repositioning the cursor.
    */
   public boolean backward(int rows)
   {
      if (isScrolling())
      {
         if (rows == 0)
         {
            return true;
         }
         
         if (rows < 0)
         {
            return forward(-rows);
         }
         
         prepareReposition();
         
         boolean moved = cursor.backward(rows);
         offEnd = cursor.getOffEnd();
         afterReposition(true);
         
         return moved;
      }
      else
      {
         if (rows != 0)
         {
            // When moving backward, all revalidation bets are off.
            breakValue = null;
         }
         
         return super.backward(rows);
      }
   }
   
   /**
    * Reset the query by clearing its state to a known default.  This is
    * used to ensure that subsequent requests for FIRST and LAST record
    * retrievals operate on a known query/buffer state.
    * <p>
    * This implementation nulls out the current result set, so that the next
    * record retrieval request triggers a new server-side query.  Optionally,
    * it also resets resolvable substitution arguments and triggers where
    * expression arguments to be resolved, if a client-side where cause
    * expression is defined.
    * <p>
    * For scrolling queries cache is reset.
    *
    * @param   resolveArgs
    *          <code>true</code> if query substitution arguments should be
    *          resolved/reset as part of the reset;  <code>false</code> if
    *          existing argument values should be used for the next query
    *          execution.
    * @param   forceOrigArgs
    *          Flag indicating that the original arguments used at the construction of the query must be
    *          re-calculated.
    */
   @Override
   public void reset(boolean resolveArgs, boolean forceOrigArgs)
   {
      super.reset(resolveArgs, forceOrigArgs);
      
      if (isScrolling())
      {
         cursor.reset();
      }
      
      invalidComponent = null;
      breakValue = null;
      foundFirst = false;
      
      earlyInvalidate();
   }
   
   /**
    * Load one or more records into their associated buffers, given an array
    * of primary key ID values, or the actual DMOs themselves.  The IDs/DMOs
    * are arranged to coincide with the tables underlying the query, from
    * left-most to right-most (in the sense of how they are joined by the
    * query).
    * <p>
    * In the event an expected value cannot be loaded (e.g., the record has
    * been deleted or is otherwise no longer available), this method throws
    * {@link MissingRecordException}, after setting the associated buffer(s)
    * into {@link RecordBuffer#setUnknownMode() unknown mode}. Note that a
    * record being locked by another session does not constitute a "missing"
    * record. Note also that this exception is not thrown until all the
    * elements of <code>data</code> have been processed to the extent
    * possible.
    *
    * @param   data
    *          Array of primary key IDs or DMOs, one per table involved in the
    *          query.
    * @param   lockType
    *          Lock type which should by used.  Set to <code>null</code> to
    *          allow query to use its current lock type.
    * @param   silentIfNullId
    *          If <code>true</code>, do not raise {@link MissingRecordException}
    *          if some of the provided IDs are <code>null</code>. <code>null</code>
    *          IDs are valid for queries with OUTER join.
    * 
    * @throws  MissingRecordException
    *          if any record cannot be loaded, because it is no longer
    *          available (e.g., deleted, etc.).
    * @throws  PersistenceException
    *          if there is an error loading data.
    */
   public void load(Object[] data, LockType lockType, boolean silentIfNullId)
   throws MissingRecordException,
          PersistenceException
   {
      if (isScrolling())
      {
         Long[] arr = new Long[data.length];
         if (data.length > 0 && data[0] instanceof Long)
         {
            System.arraycopy(data, 0, arr, 0, data.length);
         }
         else
         {
            for (int i = 0; i < data.length; i++)
            {
               arr[i] = (data[i] == null) ? null : ((Record) data[i]).primaryKey();
            }
         }
         
         if (repositionByID(arr, true, false))
         {
            cursor.next();
            offEnd = OffEnd.NONE;
         }
      }
      else if (isPreselect())
      {
         super.load(data, lockType, silentIfNullId);
      }
      
      try
      {
         coreFetch(data, lockType, data != null, false, silentIfNullId);
      }
      catch (MissingRecordException exc)
      {
         setEmptyBuffersUnknown();
         
         throw exc;
      }
   }
   
   /**
    * Assemble an array of primary key IDs for the current record(s) in the
    * buffers underlying this query, from left-most to right-most (in the
    * sense of how the query joins the associated tables).  This method
    * essentially takes a lightweight snapshot of the currently loaded
    * record(s), so that this may be restored later.
    * <p>
    * This method overrides the parent's implementation to handle the case
    * where we have just transitioned from preselect to dynamic mode, but we
    * do not yet have a <code>Results</code> object from which to gather data.
    * We generate a dynamic <code>Results</code> object and position to the
    * first result, then use the parent's implementation to gather the data.
    *
    * @return  Array of primary key IDs or DMOs (?), one per table involved in the
    *          query.
    *
    * @see     #load
    */
   public Object[] getRow()
   {
      Object[] srcData = super.getRow();
      if (srcData == null && !isPreselect())
      {
         Results results = getResults();
         results.first();
         srcData = super.getRow();
      }
      
      return srcData;
   }
   
   /**
    * Assemble an array of primary key IDs for the current record(s) in the buffers underlying
    * this query, from left-most to right-most (in the sense of how the query joins the associated
    * tables). This method essentially takes a lightweight snapshot of the currently loaded
    * record(s), so that this may be restored later.<p>
    * For scrolling queries it gets the row from the cache instead of interrogating
    * {@code results}. For non-scrolling queries it works in the same way as {@link #getRow()}.
    *
    * @return  Array of primary key IDs, one per table involved in the query.
    *
    * @see     #load(Object[], LockType, boolean)
    */
   public Object[] getCachedRow()
   {
      if (isScrolling())
      {
         return cursor.getRow();
      }
      else
      {
         return getRow();
      }
   }
   
   /**
    * Respond to a dirty (i.e., error-driven) session closing event by
    * invalidating any current, preselected results.  This will cause a
    * switch to dynamic mode, if the query is not associated with a browse.
    * If we are in dynamic mode already for a clean session close, this
    * method is a no-op.
    * <p>
    * On a clean (i.e., no error) session closing event, delegate the event
    * to the results adapter.
    *
    * @param   event
    *          Session event type.
    *
    * @return  Under normal conditions, <code>false</code>, indicating this
    *          query should not be deregistered as a session listener after
    *          this method returns.  Under an error condition, return
    *          <code>true</code>.
    *
    * @throws  PersistenceException
    *          not thrown (required by interface).
    */
   public boolean sessionEvent(SessionListener.Event event)
   throws PersistenceException
   {
      if (closed)
      {
         return true;
      }
      
      boolean deregister = SessionListener.Event.SESSION_CLOSING_WITH_ERROR.equals(event);
      
      switch (event)
      {
         case TRANSACTION_COMMITTING:
            if (resultsCached || isScrolling())
            {
               break;
            }
            
            if (cacheOnReval || isPreselect())
            {
               if (adapter.isResultSetCached())
               {
                  resultsCached = true;
                  break;
               }
               
               if (adapter.isConnected())
               {
                  Results old = adapter.getResults();
                  errorCondition = false;
                  Results cached = cacheResults(old, true);
                  if (errorCondition)
                  {
                     // caching the results made the query go into dynamic mode
                     break;
                  } 
                  else if (old != cached)
                  {
                     old.sessionClosing();
                     adapter.setResults(cached);
                  }
               }
            }
            else
            {
               adapter.sessionClosing();
            }
            
            resultsCached = true;
            
            break;
            
         case SESSION_CLOSING_NORMALLY:
            if (!resultsCached)
            {
               resetResults();
            }
            break;
            
         case SESSION_CLOSING_WITH_ERROR:
            invalidate(invalidComponent);
            break;
            
         default:
            return false;
      }
      
      return deregister;
   }
   
   /**
    * Create a string representation of this object's state, primarily for debug purposes.
    * 
    * @return  String representation of this object.
    */
   public String toString()
   {
      return "[" + (isPreselect() ? "PRE" : "DYN") + "] " + super.toString();
   }
   
   /**
    * Indicate whether this query preselects all of its results.  This
    * implementation initially preselects its results;  however, the current
    * result set may be invalidated, at which point the query switches to a
    * dynamic retrieval mode.
    *
    * @return  <code>true</code> if the query currently is in preselect mode,
    *          <code>false</code> if its result set has been invalidated.
    */
   public boolean isPreselect()
   {
      return (invalidComponent == null);
   }
   
   /**
    * Respond to a record change event by invalidating the current preselect
    * results, if currently in preselect mode and the state change impacts
    * the validity of the current results.
    *
    * @param   event
    *          Event which describes the DMO state change.
    *
    * @throws  PersistenceException
    *          if any error occurs while processing the state change event.
    */
   public void stateChanged(RecordChangeEvent event)
   throws PersistenceException
   {
      boolean preselect = isPreselect();
      boolean invalidate = true;
      boolean cacheAll = false;
      int jumpToRowNumber = -1;
      
      if (adapter.isConnected())
      {
         storeReferenceRow(event);
         setupInvalidationProperties();
         
         if (event.isDelete())
         {
            // TODO:  make this check more specific and more restrictive.
            // Specifically, we only want to invalidate if we know the
            // deleted record is included in our preselected results (not
            // including the record currently being visited).
            
            // Optimization:  if the source of the event is a buffer used by
            // this query, do not go into dynamic mode.  Do not perform this
            // optimization for scrolling queries, which can navigate back to
            // the deleted record.
            if (!isScrolling())
            {
               ArrayList<QueryComponent> components = components();
               for (int i = 0; invalidate && i < components.size(); i++)
               {
                  QueryComponent comp = components.get(i);
                  RecordBuffer buffer = comp.getBuffer();
                  
                  if (event instanceof GlobalChangeEvent)
                  {
                     GlobalChangeEvent gce = (GlobalChangeEvent) event;
                     if (gce.getEntity().equals(buffer.getEntityName()))
                     {
                        Record bDMO = buffer.getCurrentRecord();
                        Long pk = gce.getPrimaryKey();
                        if (bDMO != null && !bDMO.primaryKey().equals(pk))
                        {
                           continue;
                        }
                     }
                     else
                     {
                        continue;
                     }
                  }
                  else if (buffer != event.getSource())
                  {
                     continue;
                  }
                  
                  if (cacheOnReval) 
                  {
                     invalidate = false;
                  }
                  
                  cacheOnReval = isPreselect();
                  
                  // The cacheAll optimization is meant to handle the case of
                  // a delete loop.  If the first record we visited in a
                  // progressive result set is deleted by our backing buffer,
                  // we assume a primary purpose of the current loop is to
                  // delete the records found by the query.  This is not
                  // guaranteed, but it is the common case for the
                  // circumstances we test for below.  The idea is to let the
                  // invalidation proceed, but then immediately to re-find all
                  // of the remaining records by re-executing the query.
                  int rowNumber = adapter.getRowNumber();
                  if (invalidate &&
                      cacheOnReval &&
                      (adapter.getResults() instanceof ProgressiveResults) &&
                      (rowNumber == 0 || 
                       this.getTableCount() == 1 && revalCount == 0 && shouldExecuteForward()))
                  {
                     cacheAll = true;
                     jumpToRowNumber = rowNumber;
                  }
               }
            }
         }
         else if (event.isInsert())
         {
            if (preselect || breakValue != null)
            {
               invalidate = doubleCheckInvalidation(event);
            }
            if (invalidate)
            {
               cacheOnReval = false;
            }
         }
         else
         {
            if (preselect || breakValue != null)
            {
               // Quick out:  AdaptiveQuery sorts on an index only, which
               // cannot contain extent fields.  If the event is about extent
               // field changes only, we do not need to invalidate.
               if (event.isExtentFieldChangeOnly())
               {
                  invalidate = false;
               }
               else
               {
                  // This is the update case.  If any property changed is one
                  // of the properties used by this query to determine the
                  // content or sort order of the result set, we invalidate.
                  // Otherwise, do nothing.
                  String entity = event.getEntity();
                  Set<String> indexProps = invalidationProperties.get(entity);
                  Set<String> changedProps = event.getProperties().keySet();
                  
                  // Sanity check.
                  if (indexProps == null || changedProps == null)
                  {
                     return;
                  }
                  
                  // Test each changed property to see if it is a member of
                  // the set of index properties for the modified DMO type.
                  invalidate = false;
                  Iterator<String> iter = changedProps.iterator();
                  while (iter.hasNext())
                  {
                     if (indexProps.contains(iter.next()))
                     {
                        invalidate = doubleCheckInvalidation(event);
                        
                        break;
                     }
                  }
               }
            }
            
            if (invalidate)
            {
               cacheOnReval = false;
            }
         }
         
         if (invalidate)
         {
            if (preselect)
            {
               invalidate(event);
            }
            else
            {
               resetDynamicScrolling();
            }
         }
         
         boolean debug = LOG.isLoggable(Level.FINE);
         
         if (debug)
         {
            if (invalidate)
            {
               LOG.log(Level.FINE, "ALLOWED invalidation " + this);
            }
            else
            {
               LOG.log(Level.FINE, "AVOIDED invalidation " + this);
            }
         }
         
         // The first record found by this query was deleted.  Switch back
         // into preselect mode immediately, and re-find all (remaining)
         // matching records with a scrolling query that will not be subject
         // to the "dropped" records problem encountered when using
         // ProgressiveResults within a delete loop.
         if (cacheAll)
         {
            if (debug)
            {
               LOG.log(Level.FINE, "Forcing full cache of results after delete event " + this);
            }
            
            // Allows execute() to determine that we now are operating in preselect mode.
            invalidComponent = null;
            
            // Allows super.execute() to determine that we need a new preselect result set.
            setResults(null);
            
            // Retrieve preselect result set.  Adapter will be reconnected to
            // new delegate results.
            execute();
            
            if (adapter.isConnected())
            {
               // restore the old state of the results position
               if (jumpToRowNumber > 0 && adapter.setRowNumber(jumpToRowNumber))
               {
                  // the query might have run off-end in the process
                  this.betweenRows = true;
               }
            }
            else
            {
               LOG.log(Level.WARNING, "Can't restore state of query after caching.");
            }
         }
      }
      
      // Prevent a transition from dynamic to preselect mode.
      if (invalidate && !preselect && breakValue != null)
      {
         breakValue = null;
         
         if (LOG.isLoggable(Level.FINEST))
         {
            LOG.log(Level.FINEST, "Cancelling transition from dynamic to preselect mode");
         }
      }
   }
   
   /**
    * This function is called when ProgressiveResults has detected changes in retrieved results.
    * It switches from preselect to dynamic mode.
    */
   public void resultsChanged()
   {
      errorCondition = true;
      invalidate();
   }
   
   /**
    * This function is called when ProgressiveResults has detected that the delegate results
    * are closed, this invalidates any caching done on the results.
    */
   public void delegateChanged()
   {
      resultsCached = false;
   }
   
   /**
    * Explicitly close the prepared query.
    * 
    * @param   inResource
    *          Flag indicating this query is used in a QUERY legacy resource.
    */
   @Override
   public void close(boolean inResource)
   {
      if (inResource && components == null)
      {
         // nothing to close, the query was not initialized yet.
         // This happens only for dynamic queries, when the query was explicitly closed or
         // re-prepared before having the chance to be fully initialized with query-open
         return;
      }
      
      super.close(inResource);
   }
   
   /**
    * Clean up query resources when this query's scope ends.  This
    * implementation deregisters for global change notifications, if
    * necessary, then delegates to the parent's implementation.
    * 
    * @see  PreselectQuery#cleanup()
    */
   public void cleanup()
   {
      // if it was added in the global scope of ChangeBroker allow the listener(s) to be
      // removed from the, otherwise the ChangeBroker will drop it automatically when the
      // respective scope is finished
      if (unregisterOnCleanup)
      {
         ChangeBroker broker = ChangeBroker.get();
         broker.removeFromGlobal(this);
         
         unregisterOnCleanup = false;
      }
      
      if (closed)
      {
         return;
      }
      
      if (components != null)
      {
         ArrayList<QueryComponent> components = components();
         for (int i = 0; i < components.size(); i++)
         {
            AdaptiveComponent next = (AdaptiveComponent) components.get(i);
            next.cleanup();
         }
      }
      
      super.cleanup();
      
      highLevelResults = null;
   }
   
   /**
    * Makes this query scrolling. This method must be invoked before any results are retrieved.
    */
   public void setScrolling()
   {
      super.setScrolling();
      cursor = new Cursor(this);
   }
   
   
   /**
    * Set the forward-only attribute to the given value and resets the <code>cursor</code>
    * if necessary.
    * 
    * @param   forwardOnly
    *          <code>true</code> to enable forward-only, <code>false</code> to disable it.
    */
   @Override
   public void setForwardOnly(boolean forwardOnly)
   {
      super.setForwardOnly(forwardOnly);
      if (forwardOnly)
      {
         cursor = null;
      }
   }
   
   /**
    * Indicate whether this query is a preselect query by its nature.
    *
    * @return  <code>false</code>.
    */
   public boolean isNativelyPreselect()
   {
      return false;
   }
   
   /**
    * Deletes the current row of a query's result list. Implements the 4GL
    * DELETE-RESULT-LIST-ENTRY() method.
    *
    * @param  allowBetweenRows
    *         If <code>true</code> then the cursor can be positioned between rows (the next
    *         row will be deleted). <code>false</code> for conventional DELETE-RESULT-LIST-ENTRY()
    *         mode where the cursor should be positioned on a row (otherwise <code>false</code> is
    *         returned).
    *
    * @return <code>true</code> on success.
    */
   public logical deleteResultListEntry(boolean allowBetweenRows)
   {
      if (isScrolling())
      {
         boolean res = cursor.deleteResultListEntry(allowBetweenRows);
         if (res)
         {
            currentRowDeleted = true;
         }
         
         return new logical(res);
      }
      else if (!currentRowDeleted && foundFirst)
      {
         currentRowDeleted = true;
         
         return new logical(true);
      }
      
      return new logical(false);
   }

   /**
    * Creates an entry in the result list for the current row. Implements the 4GL
    * CREATE-RESULT-LIST-ENTRY() method. Created entry contains id(s) of the record(s) currently
    * located in the backing buffer(s).
    *
    * @return <code>true</code> on success.
    */
   @Override
   public logical createResultListEntry()
   {
      cursor.addResultAtCurrentPosition(getCurrentIds(false));
      return new logical(true);
   }
   
   /**
    * Force this query to operate in dynamic retrieval mode, if it supports this mode.
    */
   @Override
   public void forceDynamicOperation()
   {
      avoidRevalidation = true;
      invalidate();
   }
   
   /**
    * Indicate whether this query type is scrolling by its nature.
    * 
    * @return  <code>false</code>.
    */
   protected boolean isNativelyScrolling()
   {
      return false;
   }
   
   /**
    * 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> to require global invalidation checks;
    *          <code>false</code> to only check locally for changes which may
    *          invalidate results.
    */
   protected boolean isGlobalInvalidation()
   {
      return globalInvalidation;
   }
   
   /**
    * Report whether this query must operate in a one-by-one manner to honor the dirty 
    * changes in the same session.
    * 
    * @return  <code>true</code> to require force dynamic mode right from the start.
    * 
    */
   protected boolean isLocalInvalidation()
   {
      return localInvalidation;
   }
   
   /**
    * Adds a new sorting criterion as the strongest criterion for sorting the result of this
    * query. The other criteria remain queued, but with lower priority. 
    * <p>
    * Use {@link #clearDynamicSortCriteria} to clean all dynamically added criteria and restore the
    * sorting order of the query to its original form.
    *
    * @param   fr
    *          A reference to field used as new strongest criterion. The current criteria will
    *          be kept, but with lower priority.
    * @param   asc
    *          The direction of sorting for this criterion. Use {@code true} for ascending sort
    *          and {@code false} for descending sorting.
    */
   @Override
   void addDynamicSortCriterion(FieldReference fr, boolean asc)
   {
      if (fr == null || components == null)
      {
         return; // no-op
      }

      ArrayList<QueryComponent> components = components();
      for (int i = 0; i < components.size(); i++)
      {
         QueryComponent comp = components.get(i);
         if (fr.getParentBuffer() == comp.getBuffer() && comp instanceof AdaptiveComponent)
         {
            ((AdaptiveComponent) comp).addDynamicSortCriterion(fr, asc);
            return;
         }
      }
   }
   
   /**
    * Clears any dynamically sort criteria added at runtime. The query sort order will be restored
    * to its form from generated at conversion time.
    */
   void clearDynamicSortCriteria()
   {
      if (components == null)
      {
         return;
      }

      ArrayList<QueryComponent> components = components();
      for (int i = 0; i < components.size(); i++)
      {
         QueryComponent comp = components.get(i);
         if (comp instanceof AdaptiveComponent)
         {
            ((AdaptiveComponent) comp).clearDynamicSortCriteria();
         }
      }
   }
   
   /**
    * Assemble the restriction (i.e., where) clause for an FQL query. If more
    * than one component contains a where clause, they are combined with a
    * logical "and". Form of the clause is:
    * <pre>
    *    where ({component0_clause})
    *     [and ({component1_clause})
    *     [...]]
    * </pre>
    * <p>
    *
    * @param   sortCriteria
    *          List of <code>SortCriterion</code> objects which describe the
    *          sort behavior for this query.
    * @param   indices
    *          int which represents the position of the component we generate
    *          the where clause for.
    * @param   whereClause
    *          FQLExpression which represents the variable where the "where" clause is
    *          going to be saved.
    * @param   onClause
    *          FQLExpression which represents the variable where the "on" clause is
    *          going to be saved.
    * @param   isOn
    *          boolean that will help to distinguish between "where" and "and" and "on".
    *
    * @throws  PersistenceException
    *          if there is an error creating the <code>FQLPreprocessor</code>
    *          from which the preprocessed where clause is retrieved for each
    *          query component.
    *
    */
   @Override
   protected void assembleWhereClause(ArrayList<SortCriterion> sortCriteria, 
                                      int indices,
                                      FQLExpression whereClause, 
                                      FQLExpression onClause,
                                      boolean isOn)
   throws PersistenceException
   {
      super.assembleWhereClause(sortCriteria, indices, whereClause, onClause, isOn);
      
      // If this query was last in dynamic mode, we need to use the break
      // value provided by the DynamicResults object as a reference point at
      // which to begin our results.  Put the comparison operation in the
      // where clause with a placeholder which will be substituted with the
      // break value at execute time.
      if (breakValue != null)
      {
         if (whereClause.isEmpty())
         {
            whereClause.append(" where (");
         }
         else
         {
            whereClause.append(" and (");
         }
         
         SortCriterion crit = sortCriteria.get(0);
         if (TemporaryBuffer.MULTIPLEX_FIELD_NAME.equals(crit.getUnqualifiedName()))
         {
            // skip the _multiplex component as this appears first for temp-tables
            crit = sortCriteria.get(1);
         }
                
         whereClause.append(crit.toWhereExpression(true, false));
         whereClause.append(") ");
      }
      
   }
   
   /**
    * Assemble the {@code order by} clause for an FQL query by combining the sort criteria from each query
    * component sequentially.
    * Any text-based sort criteria are augmented with the necessary {@code upper()} and {@code rtrim()}
    * functions.
    *
    * @return  List of {@code SortCriterion} objects which describe the sort behavior for this query.
    *
    * @throws  PersistenceException
    *          if the sort clause of any query component contains any unqualified property names;
    *          if a sort clause contains a reference to an unrecognized record buffer;
    *          if there is an error creating the {@code HQLPreprocessor} from which setup data is required.
    */
   @Override
   protected ArrayList<SortCriterion> assembleOrderByClause()
   {
      ArrayList<SortCriterion> all = new ArrayList<>();
      ArrayList<QueryComponent> components = components();
      for (int i = 0; i < components.size(); i++)
      {
         AdaptiveComponent next = (AdaptiveComponent) components.get(i);
         
         switch (next.getIteration())
         {
            case FIRST:
            case LAST:
               if (i > 0)
               {
                  // sort clause for FIRST/LAST is only used for the outermost component
                  continue;
               }
               break;
            default:
               break;
         }
         
         List<SortCriterion> sortCriteria = next.getSortCriteria();
         sortCriteria = assembleOrderByClause(sortCriteria);
         all.addAll(sortCriteria);
      }
      
      return all;
   }
   
   /**
    * Prepare query substitution parameters for this query.  In addition to the basic parameters
    * handled by the superclass, this method will add the query's {@code breakValue} as a
    * parameter if it is non-null. A non-{@code null} {@code breakValue} indicates that the query
    * is shifting from dynamic mode to preselect mode.  All records retrieved from this point
    * forward must not include those already retrieved, and the {@code breakValue} demarcates
    * this boundary. The where clause will have been modified to include a comparison operation
    * with the {@code breakValue} in this case.
    *
    * @param   args
    *          Argument list to be populated.
    */
   @Override
   protected void prepareParameters(List<Object> args)
   throws PersistenceException
   {
      super.prepareParameters(args);
      
      if (breakValue == null && invalidComponent != null)
      {
         RecordBuffer buffer = invalidComponent.getBuffer();
         Record dmo = buffer.getSnapshot();
         if (dmo != null)
         {
            AdaptiveComponent comp = (AdaptiveComponent) components().get(0);
            List<SortCriterion> sortCriteria = comp.getSortCriteria();
            int len = sortCriteria.size();
            Method getter = null;
            for (int i = 0; i < len && getter == null; i++)
            {
               SortCriterion crit = sortCriteria.get(i);
               getter = crit.getMethod();
            }
            
            if (getter != null)
            {
               try
               {
                  PropertyMeta meta = buffer.getDmoInfo().getPropsByGetterMap().get(getter);
                  breakValue = meta.getDataHandler().getField(dmo, meta.getOffset());
               }
               catch (Exception exc)
               {
                  DBUtils.handleException(buffer.getDatabase(), exc);
                  LOG.log(Level.SEVERE, exc.getMessage(), exc);
               }
            }
         }
      }
      
      if (breakValue != null)
      {
         args.add(breakValue);
      }
   }
   
   /**
    * Overrides the parent's pure preselect implementation to use a different
    * retrieval techniques depending upon the current mode of the query.  As
    * long as the query remains in preselect mode, the superclass' approach
    * is used.  If, however, the preselect result set has been invalidated by
    * a DMO state change, this method creates engages the "backup" dynamic
    * retrieval mechanism.
    * <p>
    * In this case, a logical <code>DynamicResults</code> object is created
    * the first time through this method after invalidation.  Thereafter,
    * this object is simply used as a regular {@link Results} object, in that
    * parent's {@link PreselectQuery#fetch} method uses the new results
    * object without knowing or caring that it has been substituted with a
    * dynamic implementation.
    * <p>
    * In addition, if this query is set to check for global DMO change events,
    * such events are collected and processed, which will possibly invalidate
    * the query's current results, or cause a change to dynamic mode.
    * 
    * @return  <code>true</code> if the underlying preselect result set was
    *          created during this invocation;  <code>false</code> if query
    *          was executed previously and we already have a preselect result
    *          set.
    *
    * @throws  ErrorConditionException
    *          if an error occurs executing the preselect query.
    * @throws  IllegalStateException
    *          if no query component has been added to this query.
    */
   protected boolean execute()
   {
      // Check if all components with a non-null JOIN have a valid inverse
      // record on which to perform the join
      if (!verifyJoins())
      {
         return true;
      }
      
      // Possibly invalidate if another session has made a change.
      if (DirtyShareSupport.isEnabledCrossSession() &&
          DirtyShareSupport.isGlobalNotificationEnabled() &&
          isGlobalInvalidation())
      {
         ArrayList<QueryComponent> components = components();
         for (int i = 0; i < components.size(); i++)
         {
            AdaptiveComponent comp = (AdaptiveComponent) components.get(i);
            comp.processGlobalEvents();
         }
      }
      
      if (isPreselect())
      {
         try
         {
            ArrayList<QueryComponent> components = components();
            for (int i = 0; i < components.size(); i++)
            {
               AdaptiveComponent comp = (AdaptiveComponent) components.get(i);
               comp.initialize();
            }
            
            return super.execute();
         }
         catch (PersistenceException exc)
         {
            ErrorManager.handleStopException(new StopConditionException(exc));
         }
      }
      
      if (adapter.isConnected())
      {
         return false;
      }
      
      // If we call super.execute() above, a cleaner is registered for us,
      // but if not, we have to do it here.
      registerCleaner();
      
      try
      {
         // Create a dynamic results object which will drive record retrieval.
         DynamicQuery query = null;
         int tc = getTableCount();
         if (tc > 1)
         {
            query = createCompoundQuery(invalidComponent);
         }
         else
         {
            query = createSimpleQuery(invalidComponent);
         }
         
         boolean[] idOnly = new boolean[tc];
         ArrayList<QueryComponent> components = components();
         for (int i = 0; i < components.size(); i++)
         {
            QueryComponent comp = components.get(i);
            idOnly[i] = !isClientWhere() && !isFullRecords() && comp.isIdOnly();
         }
         DynamicResults dr = new DynamicResults(query, idOnly);
         Results hlr = createResults(dr);
         setResults(hlr);
      }
      catch (PersistenceException exc)
      {
         ErrorManager.recordOrThrowError(exc);
      }
      
      return false;
   }
   
   /**
    * Respond to the condition that a component with a non-null JOIN has a
    * missing inverse on which to perform the join.
    * <p>
    * This implementation is a placeholder which currently defers to the
    * parent's implementation.
    */
   protected void handleMissingJoinRecord()
   {
      /*
      if (!isPreselect() || revalCount > 0)
      {
         throw new QueryOffEndException("Missing join record");
      }
      */
      
      super.handleMissingJoinRecord();
   }
   
   /**
    * Indicate whether a call to move to the previous record in the result list should be treated
    * as a call to move to the last record in the list. This will be the cases listed in the super
    * function {@link PreselectQuery#treatPreviousAsLast}, but only if the query is in preselect
    * mode. That fixes previous mistakenly treated as last after query invalidation.
    *
    * @return  <code>true</code> to treat a previous request as a request to move to the last
    *          record in the list;  else <code>false</code>.
    */
   protected boolean treatPreviousAsLast()
   {
      return isPreselect() && super.treatPreviousAsLast();
   }
   
   /**
    * Execute the query to create a first level result set.
    * <p>
    * This case is for dialects that do not support lazy queries:
    * If we are executing the query after a re-validation, or if the query's sort phrase does not match
    * any legacy index, fall back to the superclass' implementation. Otherwise, this implementation creates
    * an instance of {@code ProgressiveResults}, which defers actually executing the query until
    * the first request to fetch a result.
    * 
    * This case is for dialects that support lazy queries:
    * This implementation can create either a {@code ScrollingResults} or a type of {@code Results}
    * determined by the execution of the super function.
    *
    * @param   persistence
    *          Object which is used to execute the query.
    * @param   fql
    *          FQL query string.
    * @param   args
    *          Substitution parameters to be inserted into placeholders within the FQL query
    *          string.
    *
    * @return  An instance of {@code ProgressiveResults} which fetches progressively larger
    *          brackets of query results as the results are accessed (for dialects that do not support lazy
    *          queries),
    *          Or
    *          An instance of {@code ScrollingResults}, possibly fetched in a lazy manner (for dialects that
    *          do support lazy queries).
    *
    * @throws  PersistenceException
    *          not thrown by this implementation.
    */
   @Override
   protected Results executeQuery(Persistence persistence, String fql, Object[] args)
   throws PersistenceException
   {
      Results results = null;
      if (isLazyMode())
      {
         Long templateRowid = getTemplateQueryRowid(args);
         if (templateRowid != null)
         {
            results = super.executeQuery(persistence, fql, args);
            return results;
         }
         
         return new ScrollingResults(persistence, executeScroll(persistence, fql, args, true));
      }
      
      if (forceFullResults || cacheOnReval || probablyRequiresResort())
      {
         results = super.executeQuery(persistence, fql, args);
      }
      else
      {
         // determine whether this is a (primary key only) projection query
         boolean projection = !isClientWhere() && !isFullRecords();
         ArrayList<QueryComponent> components = components();
         for (int i = 0; projection && i < components.size(); i++)
         {
            QueryComponent comp = components.get(i);
            projection = comp.isIdOnly();
         }
         
         // create the progressive results
         ProgressiveResults pr = new ProgressiveResults(persistence,
                                                        fql,
                                                        args,
                                                        isScrolling(),
                                                        isForwardOnly(),
                                                        getEntities(),
                                                        projection,
                                                        getDataRelation());
         pr.addResultsChangeListener(this);
         results = pr;
      }
      
      return results;
   }
   
   /**
    * Function responsible with returning a list of {@code ScrollableResults}.
    * It converts the FQL into SQL and executes the query against the SQL server.
    * If the query is intended for temporary tables, then the execution could be done in a lazy manner.
    * 
    * @param   <T>
    *          The type of the rows returned in the list.
    * @param   persistence
    *          Object which is used to execute the query.
    * @param   fql
    *          FQL query string.
    * @param   args
    *          Substitution parameters to be inserted into placeholders within the FQL query
    *          string.
    * @param   lazyMode
    *          Flag that indicates whether the query should be executed in a lazy manner.
    *          Not all queries can be executed with this option, but the checks for that are not done here.
    *          
    * @return  A scrollable list of rows of type {@code <T>} from SQL server, as specified by the
    *          {@code fql} query.
    *          
    * @throws  PersistenceException
    *          when an error occurred while performing the requested operations.
    */
   protected <T> ScrollableResults<T> executeScroll(Persistence persistence,
                                                    String fql,
                                                    Object[] args,
                                                    boolean lazyMode)
   throws PersistenceException
   {
      int scrollMode = isScrolling() || lazyMode ? ResultSet.TYPE_SCROLL_INSENSITIVE
                                                 : ResultSet.TYPE_FORWARD_ONLY;

      return persistence.scroll(getEntities(), fql, args, 0, 0, getDataRelation(), scrollMode, lazyMode);
   }
   
   /**
    * Override the parent's implementation to intercept result creation, and
    * always return our adapter, after setting the <code>results</code>
    * parameter as the adapter's delegate.
    *
    * @param   results
    *          An object which manipulates the database result set returned
    *          from execution of the query.
    *
    * @return  <code>Results</code> object.
    *
    * @throws  PersistenceException
    *          if there is any error creating results.
    */
   @Override
   protected Results createResults(Results results)
   throws PersistenceException
   {
      adapter.setResults(results);
      
      betweenRows = false;
      
      if (highLevelResults == null)
      {
         highLevelResults = super.createResults(adapter);
      }
      
      return highLevelResults;
   }
   
   /**
    * Create a single-table, dynamic query to be used as the worker object
    * for a <code>DynamicResults</code> object when this query switches from
    * preselect to dynamic mode.  Creates a modified implementation of
    * <code>RandomAccessQuery</code>, which does not reset the record buffer
    * upon instantiation.
    *
    * @param   invalidComp
    *          Query component which caused this query to be invalidated.
    *
    * @return  A <code>RandomAccessQuery</code> based upon this query's single
    *          component.
    * 
    * @throws  PersistenceException
    *          if there is an error creating the <code>HQLPreprocessor</code>
    *          from which setup data is required.
    */
   protected DynamicQuery createSimpleQuery(AdaptiveComponent invalidComp)
   throws PersistenceException
   {
      invalidComp.translateWhere();
      
      // Always do the initial fetch with lock type of NONE.  It will be
      // overridden as necessary in PreselectQuery.fetch().
      LockType lockType = LockType.NONE;
      
      AdaptiveComponent comp = (AdaptiveComponent) components().get(0);
      comp.translateWhere();
      
      Object[] args = comp.getRawArgs();
      if (parameterFilter != null)
      {
         args = parameterFilter.filterParameters(args);
      }
      
      RandomAccessQuery query = new RandomAccessQuery();
      query.initialize(comp.getBuffer().getDMOProxy(),
                       comp.getOriginalWhere(),
                       null,
                       comp.getOriginalSort(),
                       comp.getInverse(),
                       args,
                       lockType);
      query.setForwardOnly(isForwardOnly());
      query.overrideIncludeFields(included);
      query.overrideExcludeFields(excluded);

      if (this.foundFirst)
      {
         query.setRecordAlreadyFound(true);
      }
      
      RecordBuffer[] ext = getExternalBuffers();
      if (ext != null)
      {
         query.addExternalBuffers(ext);
      }
      
      setWorkerRefererenceRecord(query);
      
      return query;
   }
   
   /**
    * Create a multi-table, compound query to be used as the worker object
    * for a <code>DynamicResults</code> object when this query switches from
    * preselect to dynamic mode.  Uses the parent query's components to
    * define a <code>Joinable</code> instance for each of the compound
    * query's own components.
    * <p>
    * Single-table <code>AdaptiveQuery</code> instances are used for every
    * joinable component, except the one corresponding to the buffer which
    * caused invalidation of the preselect result set.  For the component
    * corresponding with this buffer, an instance of
    * <code>RandomAccessQuery</code> is used instead.
    *
    * @param   invalidComp
    *          Query component which caused this query to be invalidated.
    *
    * @return  A <code>CompoundQuery</code> based upon this query's multiple
    *          components.
    * 
    * @throws  PersistenceException
    *          if there is an error creating the <code>HQLPreprocessor</code>
    *          from which setup data is required.
    */
   protected DynamicQuery createCompoundQuery(AdaptiveComponent invalidComp)
   throws PersistenceException
   {
      invalidComp.translateWhere();
      
      CompoundQuery compoundQuery = new CompoundQuery()
      {
         /**
          * Don't allow this compound query, which was de-optimized, to be re-optimized, in
          * order to prevent thrashing back and forth between the two modes.
          */
         @Override
         protected boolean canOptimize()
         {
            return false;
         }
      };
      
      compoundQuery.initialize(false, false);
      
      // iterate the adaptive components and add their fallback compound query components into
      // the new query, forcing each into dynamic mode
      ArrayList<QueryComponent> components = components();
      for (int i = 0; i < components.size(); i++)
      {
         AdaptiveComponent comp = (AdaptiveComponent) components.get(i);
         comp.translateWhere();
         
         CompoundComponent fallback = comp.getFallback();
         fallback.getQuery().forceDynamicOperation();
         compoundQuery.addCompoundComponent(fallback);
      }
      
      return compoundQuery;
   }
   
   /**
    * Reset the state of the results adapter to its initial default state and
    * invoke the superclass' overridden method.
    */
   protected void resetResults()
   {
      resultsCached = false;
      
      super.resetResults();
      
      if (adapter != null)
      {
         adapter.cleanup();
         adapter.setResults(null);
      }
   }
   
   /**
    * Get array of the primary keys of the records contained in the backing buffers.
    *
    * @return  array of the primary keys of the records contained in the backing buffers or
    *          <code>null</code> if any of the buffers doesn't contain a record.
    */
   protected Object[] getCurrentIds()
   {
      return getCurrentIds(true);
   }
   
   /**
    * Get array of the primary keys of the records contained in the backing buffers.
    *
    * @param   nullable
    *          If a buffer doesn't contain a record, then depending on the value of this
    *          parameter, <code>null</code> is returned (if <code>nullable</code> is
    *          <code>true</code>) or <code>null</code> element is added to the resulting array (if
    *          <code>nullable</code> is <code>false</code>).
    *
    * @return  array of the primary keys of the records contained in the backing buffers or
    *          <code>null</code> if any of the buffers doesn't contain a record and
    *          <code>nullable</code> is <code>true</code>.
    */
   protected Object[] getCurrentIds(boolean nullable)
   {
      List<Long> data = new ArrayList<>();

      ArrayList<QueryComponent> components = components();
      for (int i = 0; i < components.size(); i++)
      {
         QueryComponent comp = components.get(i);
         
         RecordBuffer buffer = comp.getBuffer();
         if (!buffer.isAvailable())
         {
            if (nullable && !comp.isOuter())
            {
               return null;
            }
            else
            {
               data.add(null);
            }
         }
         else
         {
            data.add(buffer._rowID());
         }
      }
      
      return data.toArray();
   }
   
   /**
    * Load all records for a given result row into their backing buffers.
    *
    * @param  data
    *         An array of primary key IDs or DMOs representing records to be fetched for the
    *         composite row.
    * @param  lockType
    *         Overriding lock type to apply to fetched records. If null, default lock types are
    *         applied.
    *
    * @return <code>true</code> if the record(s) were successfully loaded.
    */
   protected boolean loadByValues(Object[] data, LockType lockType)
   {
      currentRowDeleted = false;
      
      try
      {
         if (data == null)
         {
            coreFetch(null, lockType, false, false, true);
            return false;
         }
         
         offEnd = OffEnd.NONE; // required for PreselectQuery to fetch properly
         boolean fetched = coreFetch(data, lockType, true, false, true);
         accumulate();
         
         return fetched;
      }
      catch (MissingRecordException e)
      {
         return false;
      }
      catch (PersistenceException e)
      {
         ErrorManager.recordOrThrowError(e);
         return false;
      }
   }
   
   /**
    * Fetch all records for a given result row into their backing buffers.
    * Each result row contains one column per query component.  Each column
    * contains the unique ID of the record to be fetched or the record itself.
    * If the latter, the record is refetched in case it has changed since the
    * record was first retrieved.  If the scrollable cursor backing this query
    * currently is at a position where no retrieval is possible, the backing
    * buffers will have their current records released.
    *
    * @param   available
    *          <code>true</code> if the most recent scrollable cursor move
    *          left the cursor at an available result row;  <code>false</code>
    *          if the cursor cannot read a result at its current position.
    * @param   lockType
    *          Overriding lock type to apply to fetched records.  If
    *          <code>null</code>, default lock types are applied.
    * @param   errorIfNull
    *          <code>true</code> if failed retrievals should raise error.
    *
    * @throws  MissingRecordException
    *          if the record was expected to be available, but wasn't.  This
    *          typically indicates the record was deleted between the time it
    *          initially was found by the query and the time it was to be
    *          fetched to store in the record buffer.
    * @throws  ErrorConditionException
    *          if 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.
    */
   protected void fetch(boolean available, LockType lockType, boolean errorIfNull)
   throws MissingRecordException
   {
      super.fetch(available, lockType, errorIfNull);
      
      if (!isScrolling() && !foundFirst && available && offEnd == OffEnd.NONE)
      {
         foundFirst = true;
      }
   }
   
   /**
    * Store the last fetched row as the reference row for future iterations.
    *
    * @param event
    *        Event caused by update of a record of specific type.
    */
   protected void storeReferenceRow(RecordChangeEvent event)
   {
      if (cursor != null)
      {
         cursor.storeReferenceRow(event);
      }
   }
   
   /**
    * Invalidate the query if reference row exists.
    */
   protected void invalidateIfReferenceRowExists()
   {
      if (cursor != null)
      {
         Record[] row = cursor.getReferenceRow();
         if (row != null)
         {
            invalidate();
         }
      }
   }
   
   /**
    * For single-table query: set the first component of the reference row as the reference record
    * for the worker query.
    *
    * @param query
    *        Worker query.
    */
   protected void setWorkerRefererenceRecord(RandomAccessQuery query)
   {
      if (cursor != null)
      {
         Record[] persistable = cursor.getReferenceRow();
         if (persistable != null)
         {
            query.setReferenceRecord(persistable[0]);
         }
      }
   }
   
   /**
    * Check if this query should operate in dynamic mode right from the start.
    * 
    * @return   {@code true} if this was invalidated early (before even being executed).
    */
   protected boolean earlyInvalidate()
   {
      if (DirtyShareSupport.isEnabledIntraSession() && isLocalInvalidation())
      {
         // invalidate if this session has some dirty changes on a component.
         ArrayList<QueryComponent> components = components();
         for (int i = 0; i < components.size(); i++)
         {
            AdaptiveComponent comp = (AdaptiveComponent) components.get(i);
            if (comp.isDirty(true))
            {
               forceDynamicOperation();
               comp.getBuffer().allowReversibleOffEnd();
               comp.getBuffer().resetSortIndex();
               return true;
            }
         }
      }
      return false;
   }
   
   /**
    * Invalidate the preselected result set of this query, which effectively
    * switches this query from preselect mode to dynamic mode.
    *
    * @param   invalidComponent
    *          Query component which is triggering invalidation.
    */
   void invalidate(AdaptiveComponent invalidComponent)
   {
      if (!isPreselect())
      {
         // If we are already in dynamic mode, invalidation means clearing the
         // dynamic query's cursor cache.  This prevents us from getting a
         // potentially stale value for first() and last() navigation.
         resetDynamicScrolling();
      }
     
      if (isPreselect() && quickRevalidate(invalidComponent)) 
      {
         this.invalidComponent = null;
      }
      else 
      {
         this.invalidComponent = invalidComponent;
      }
      
      if (adapter.isConnected())
      {
         resetResults();
      }
      else
      {
         clearRepoCache();
      }
      
      adapter.prepareDelegateChange();
   }
   
   /**
    * Check if we can quickly invalidate and revalidate back. The original intent is to invalidate, but this
    * routine can signal if the revalidation can be done straight after. This happens when there is no actual
    * reference row and the query would operate in dynamic mode from the very start. In this case, prefer to
    * revalidate on spot.
    * <p>
    * The usual case for this quick revalidate is a dynamic query that was just opened. Its behavior is to 
    * execute the query, even if there was not actual NEXT/PREV/etc. Thus, there are many queries that are
    * opened and not yet used. Such case leads to useless invalidations, that may even get stuck in dynamic
    * mode.
    * 
    * @param   invalidComponent
    *          Query component which is triggering invalidation.
    *          
    * @return  {@code true} if a quick revalidation can happen. This means dismissing the invalidation.
    *          {@code false} if the invalidation is required to happen. Note that even if this returns true,
    *          the results should be reset and a new delegate requested. Returning true doesn't mean that 
    *          the current results are good!
    */
   private boolean quickRevalidate(AdaptiveComponent invalidComponent)
   {
      if (this.avoidRevalidation || 
         (this.invalidComponent != null && this.invalidComponent != invalidComponent) ||
         invalidComponent == null)
      {
         return false;
      }
      
      if (invalidComponent.getBuffer().getSnapshot() == null &&  // there is no buffer snapshot
          (this.cursor == null || this.cursor.getReferenceRow() == null)) // and there is no reference row
      {
         // there is no actual reference row from which the RAQ can continue, so the invalidated query
         // won't ever find a suitable break value: attempt a quick revalidation now
         if (++revalCount > MAX_REVALIDATIONS)
         {
            if (revalCount == 4 && LOG.isLoggable(Level.FINE))
            {
               LOG.log(Level.FINE, "AdaptiveQuery permanently forced to dynamic mode:  " + this);
            }
            
            return false;
         }
         
         // if the execution reaches this point, it means that we did a quick 
         // invalidate -> revalidate
         return true;
      }
      else
      {
         return false;
      }
   }
   
   /**
    * Helper method to understand if this query is going to use lazy query results mode.
    * Such mode automatically solves invalidation at the database side, so there is no
    * need to invalidate in the server-side.
    *  
    * @return  {@code true} if this is a lazy mode query with implicit invalidation handling
    */
   private boolean isLazyMode()
   {
      if (lazyMode == null)
      {
         ArrayList<QueryComponent> components = components();
         if (components == null || components.size() != 1)
         {
            return false;
         }
         RecordBuffer buffer = components.get(0).getBuffer();
         lazyMode = buffer != null    &&
                    buffer.isActive() &&
                    buffer.getPersistence().supportsLazyQueryResultsMode();
      }
      
      return lazyMode;
   }
   
   /**
    * Indicate whether the query results as initially fetched are likely to require a re-sort before they
    * are returned. This decision is based on whether the sort phrase exists and does not match any legacy
    * index of the first query component table.
    * <p>
    * This method makes an assumption (which will not be correct in all cases) that if an index on the first
    * table exactly matches the sort phrase, it will be used to fetch the results. However, the database
    * query planner may well select a different index, based on the WHERE clause. In that case, this method
    * will report incorrectly that a re-sort is not required, which may lead to &quot;chunking&quot; the
    * result set with {@code ProgressiveResults} when this is not the most efficient approach. However, it
    * may help detect some cases where a re-sort will take place, avoiding this anti-pattern.
    * 
    * @return  {@code true} if there is no sort phrase, or if the sort phrase does not match any legacy
    *          index of the first query component table.
    */
   private boolean probablyRequiresResort()
   {
      String sort = getSortPhrase();
      if (sort == null)
      {
         // no specific sorting is required, so the result set can be returned in its initial order
         // TODO: is this a valid state, or do we always have a sort phrase?
         return false;
      }
      
      boolean match = false;
      
      if (!components.isEmpty())
      {
         try
         {
            QueryComponent comp = components.get(0);
            RecordBuffer buffer = comp.getBuffer();
            IndexHelper ih = buffer.getIndexHelper();
            if (ih.getIndexForSort(buffer, sort) != null)
            {
               match = true;
            }
         }
         catch (PersistenceException exc)
         {
            match = false;
         }
      }
      
      return !match;
   }
   
   /**
    * Given a record change event for which this query is listening, determine
    * whether invalidation may be avoided.  For an update, assume it already
    * has been determined that a relevant property (one which can affect the
    * changed DMO's inclusion in preselected results) has been modified.
    * <p>
    * This additional check is about detecting whether it is possible that a
    * modified or newly inserted record can possibly appear later in the
    * preselected result set than the record we currently are visiting.
    * 
    * @param   event
    *          Event which describes the DMO state change.
    * 
    * @return  <code>true</code> if invalidation is confirmed;
    *          <code>false</code> to abandon invalidation.
    */
   private boolean doubleCheckInvalidation(RecordChangeEvent event)
   {
      if (isScrolling())
      {
         return true;
      }
      
      // TODO:  make this check more specific and more restrictive.
      // Specifically, we only want to invalidate if we know the inserted or
      // updated record would have been included in our preselected results.
      
      boolean invalidate = true;
      boolean isInsert = event.isInsert();
      RecordBuffer sourceBuf = event.getSource();
      Record targetDMO = event.getDMO();

      ArrayList<QueryComponent> components = components();
      for (int i = 0; invalidate && i < components.size(); i++)
      {
         AdaptiveComponent c = (AdaptiveComponent) components.get(i);
         RecordBuffer buffer = c.getBuffer();
         
         if (isInsert)
         {
            if (buffer != sourceBuf)
            {
               continue;
            }
         }
         else
         {
            Record current = buffer.getCurrentRecord();
            if (current != targetDMO)
            {
               continue;
            }
         }
         
         Record snapshot = buffer.getSnapshot();
         DMOSorter sorter = c.getDMOSorter();
         if (sorter == null)
         {
            // Assume we are using default index (by primary key).  In this
            // case, an insert causes invalidation, an update does not.
            invalidate = isInsert;
         }
         else
         {
            // Use sorter to determine whether changed record *could* appear
            // further on in the results.  If not, do not invalidate now.
            int comp = sorter.compare(targetDMO, snapshot);
            if (comp <= 0)
            {
               invalidate = false;
            }
         }
      }
      
      return invalidate;
   }
   
   /**
    * If in dynamic mode, reset the cursor underlying the dynamic query in
    * order to prevent stale data.
    */
   private void resetDynamicScrolling()
   {
      if (adapter.isConnected())
      {
         Results res = adapter.getResults();
         if (res instanceof DynamicResults)
         {
            ((DynamicResults) res).query.resetScrolling();
         }
      }
   }
   
   /**
    * Set up the map of DMO implementation classes to sets of properties
    * which, if changed, would invalidate the current, preselected result set
    * for this query.  These will be properties which are used either to
    * restrict (i.e., where clause) or sort (i.e., order by clause) the
    * contents of the result set.
    * 
    * @throws  PersistenceException
    *          if there is an error creating the <code>HQLPreprocessor</code>
    *          from which setup data is required.
    */
   private void setupInvalidationProperties()
   throws PersistenceException
   {
      if (invalidationProperties != null)
      {
         return;
      }
      
      invalidationProperties = new HashMap<>();

      ArrayList<QueryComponent> components = components();
      for (int i = 0; i < components.size(); i++)
      {
         AdaptiveComponent comp = (AdaptiveComponent) components.get(i);

         // Collect property names from where clause.
         Map<String, Set<String>> whereProps = comp.getRestrictionProperties();
         if (whereProps != null)
         {
            Iterator<Map.Entry<String, Set<String>>> iter2 = whereProps.entrySet().iterator();
            while (iter2.hasNext())
            {
               Map.Entry<String, Set<String>> next = iter2.next();
               String entity = next.getKey();
               Set<String> props = next.getValue();
               Set<String> indexProps = invalidationProperties.get(entity);
               if (indexProps == null)
               {
                  indexProps = new HashSet<>();
                  invalidationProperties.put(entity, indexProps);
               }
               indexProps.addAll(props);
            }
         }
         
         // Collect property names from sort criteria.
         List<SortCriterion> sortCriteria = comp.getSortCriteria();
         if (sortCriteria == null || sortCriteria.isEmpty())
         {
            continue;
         }
         
         Iterator<SortCriterion> iter3 = sortCriteria.iterator();
         while (iter3.hasNext())
         {
            SortCriterion next = iter3.next();
            Class<? extends Record> dmoClass = next.getDMOClass();
            String entity = dmoClass.getName();
            Set<String> indexProps = invalidationProperties.get(entity);
            if (indexProps == null)
            {
               indexProps = new HashSet<>();
               invalidationProperties.put(entity, indexProps);
            }
            indexProps.add(next.getUnqualifiedName());
         }
      }
   }
   
   /**
    * Invalidate the preselected result set of this query, which effectively
    * switches this query from preselect mode to dynamic mode.
    *
    * @param   event
    *          Object which holds information about the DMO state change
    *          which occurred.
    */
   private void invalidate(RecordChangeEvent event)
   {
      String entity = event.getEntity();
      AdaptiveComponent comp = null;
      ListIterator<QueryComponent> iter = components.listIterator(components.size());
      while (iter.hasPrevious() && comp == null)
      {
         AdaptiveComponent next = (AdaptiveComponent) iter.previous();
         if (next.getBuffer().getEntityName().equals(entity))
         {
            comp = next;
         }
      }
      
      assert comp != null;
      
      invalidate(comp);
      
      if (LOG.isLoggable(Level.FINEST))
      {
         Exception exc = new Exception("Details (not an error):  [" + getHQL() + "]");
         
         LOG.log(Level.FINEST,
                 "Adaptive query's result set invalidated by:  " +
                 invalidComponent.getBuffer().toString(event.getDMO()) +
                 (event.isDelete()
                  ? "Record deleted"
                  : "Affected properties:  " + event.getProperties()),
                 exc);
      }
   }
   
   /**
    * Invalidate the query by using the last query component as the invalid component. For a
    * multi-table query, this will force all components above this to start in dynamic mode.
    */
   private void invalidate()
   {
      ListIterator<QueryComponent> iter = components.listIterator(getTableCount());
      if (iter.hasPrevious())
      {
         AdaptiveComponent comp = (AdaptiveComponent) iter.previous();
         invalidate(comp);
      }
   }
   
   /**
    * Begin the transition of this query from dynamic to preselect mode.  The
    * remaining work of this transition is performed in the {@link #fetch}
    * and {@link #execute} methods.
    *
    * @param   breakValue
    *          The sort band break value which indicates that the current,
    *          dynamic query has crossed a sort band boundary.
    */
   private void revalidate(Object breakValue)
   {
      if (avoidRevalidation)
      {
         return;
      }
   
      // Prevent thrashing between modes too often.  If we are attempting to
      // revalidate more than 3 times, we probably are in a use case which is
      // better off staying in dynamic mode.
      // TODO:  make this heuristic more intelligent, possibly by taking into
      // account the frequency of mode changes and the overall number of
      // records traversed.
      if (++revalCount > MAX_REVALIDATIONS)
      {
         if (revalCount == 4 && LOG.isLoggable(Level.FINE))
         {
            LOG.log(Level.FINE, "AdaptiveQuery permanently forced to dynamic mode:  " + this);
         }
         
         return;
      }
      
      this.breakValue = breakValue;
      adapter.prepareDelegateChange();
      
      if (LOG.isLoggable(Level.FINEST))
      {
         Exception exc = new Exception("Details (not an error):  [" + getHQL() + "]");
         
         LOG.log(Level.FINEST,
                 "Adaptive query reset to preselect mode at record:  " +
                 invalidComponent.getBuffer().getDMOInterface().getName() +
                 "#" +
                 adapter.getID(0) +
                 ";  breakValue = " +
                 breakValue,
                 exc);
      }
   }

   /**
    * Mark the query to retrieve the entire result set (useful for FILL operations).
    */
   public void hintFullResults()
   {
      if (isScrolling())
      {
         // cannot move to first if we are not scrolling
         this.forceFullResults = true;
      }
   }
   
   /**
    * An implementation of the {@link Results} interface which uses a single
    * table or multi-table <code>DynamicQuery</code> as a delegate.  An
    * instance of this method replaces the enclosing query's preselected
    * results when a DMO state change invalidates those results.  If a single
    * table must be accessed, a modified <code>RandomAccessQuery</code> is
    * used as a delegate.  For multiple tables, a <code>CompoundQuery</code>
    * is used.
    * <p>
    * The adaptive query's original query components are interrogated for
    * their properties in order to create the dynamic query (and its
    * components in the multi-table case).
    */
   private class DynamicResults
   implements Results
   {
      /** Underlying query which manages the navigation of results */
      private final DynamicQuery query;
      
      /** All buffers being updated by the dynamic query */
      private final RecordBuffer[] buffers = getRecordBuffers();
      
      /** ID-only flags, ordered by column */
      private final boolean[] idOnly;
      
      /** Flag indicating this results object has been closed */
      private boolean closed = false;
      
      /**
       * Constructor which accepts a backing, dynamic query and turns
       * scrolling on for that query.
       * 
       * @param   query
       *          Dynamic query which will back this results object.
       * @param   idOnly
       *          Array of ID only flags, ordered by column.
       * 
       * @throws  PersistenceException
       *          if there is an error beginning a new transaction.
       */
      DynamicResults(DynamicQuery query, boolean[] idOnly)
      throws PersistenceException
      {
         this.query = query;
         this.idOnly = idOnly;
         query.setScrolling();
         query.setLenientOffEnd(true);
      }
      
      /**
       * Move cursor to the first results row.
       *
       * @return  <code>true</code> if there are any results.
       */
      public boolean first()
      {
         query.first();
         
         return postFetch(false);
      }
      
      /**
       * Move cursor to the last results row.
       *
       * @return  <code>true</code> if there are any results.
       */
      public boolean last()
      {
         query.last();
         
         return postFetch(true);
      }
      
      /**
       * Move cursor to the next results row.
       *
       * @return  <code>true</code> if there is a result under the cursor after the move.
       */
      public boolean next()
      {
         query.next();
         
         return postFetch(true);
      }
      
      /**
       * Move cursor to the previous results row.
       *
       * @return  <code>true</code> if there is a result under the cursor after the move.
       */
      public boolean previous()
      {
         query.previous();
         
         return postFetch(false);
      }
      
      /**
       * Is the cursor on the first row in the results set?
       *
       * @return  <code>true</code> if the cursor is on the first row.
       */
      public boolean isFirst()
      {
         return (getRowNumber() == 0);
      }
      
      /**
       * Is the cursor on the last row in the results set?
       *
       * @return  <code>true</code> if the cursor is on the last row.
       */
      public boolean isLast()
      {
         throw new UnsupportedOperationException();
      }

      /**
       * Is the result delegate responsible for fetching? This is usually true when using
       * query delegates which handle the fetching process already.
       * 
       * @return   {@code true} for complex results which also do the fetching
       */
      @Override
      public boolean isAutoFetch()
      {
         return true;
      }
      
      /**
       * Returns the row at the current position. It can be either only an array of PK or
       * an array of DMOs. 
       * 
       * @param   forceOnlyId
       *          Indicates whether it's only the primary keys that must be returned, 
       *          or original data found is returned (either PK or DMO).
       *
       * @return  Object array or {@code null}.
       */
      public Object[] get(boolean forceOnlyId)
      {
         int len = buffers.length;
         Object[] row = new Object[len];
         for (int i = 0; i < len; i++)
         {
            row[i] = get(i);
            
            if (forceOnlyId && (row[i] instanceof Record))
            {
               row[i] = ((Record) row[i]).primaryKey();
            }
         }
         
         return row;
      }
      
      /**
       * Get the object at the current result row, at the specified column.
       *
       * @param   column
       *          Zero-based index column of the desired object.
       *
       * @return  Object at <code>column</code> or <code>null</code>.
       */
      public Object get(int column)
      {
         Record dmo = buffers[column].getCurrentRecord();
         
         return ((dmo != null && idOnly[column]) ? dmo.primaryKey() : dmo);
      }
      
      /**
       * Get the primary key ID at the current result row, at the specified column.
       *
       * @param   column
       *          Zero-based index column of the desired ID.
       *
       * @return  ID of the record or <code>null</code>.
       */
      public Long getID(int column)
      {
         Object datum = get(column);
         
         return (datum != null && idOnly[column]) ? (Long) datum : ((Record) datum).primaryKey();
      }
      
      /**
       * Get the row number currently under the cursor.
       *
       * @return  Zero-based index of the current row, or <code>0</code> by
       *          default if the cursor's position is not currently known.
       */
      public int getRowNumber()
      {
         integer rowNum = query.currentRowImpl();
         
         return (rowNum.isUnknown() ? 0 : rowNum.intValue());
      }
      
      /**
       * Set the row number currently under the cursor.
       *
       * @param   row
       *          Zero-based index of the row to be set as the current row.
       *
       * @return  <code>true</code> if there is a row at the specified row
       *          number;  else <code>false</code>.
       */
      public boolean setRowNumber(int row)
      {
         reposition(row);
         
         return postFetch(false);
      }
      
      /**
       * Scroll the cursor ahead by the specified number of rows.  If the
       * number is negative, the cursor is moved backward.  This may put the
       * cursor off the end (either end) of the query's results.
       *
       * @param   rows
       *          Number of rows to jump ahead or back.
       *
       * @return  <code>true</code> if there is a row at the new location;
       *          else <code>false</code>.
       */
      public boolean scroll(int rows)
      {
         // Set the delegate query's fetchOnReposition strategy to match the
         // enclosing AdaptiveQuery's current strategy.
         query.setFetchOnReposition(isFetchOnReposition());
         
         if (rows < 0)
         {
            query.backward(-rows);
         }
         else
         {
            query.forward(rows);
         }
         
         return postFetch(rows > 0);
      }
      
      /**
       * Reset the cursor to its natural starting position, before the first result row.
       */
      public void reset()
      {
         reposition(0);
      }
      
      /**
       * Invoked when the current Hibernate session is about to close.  Gives
       * the implementation an opportunity to perform appropriate processing in
       * response to this event.
       */
      public void sessionClosing()
      {
      }
      
      /**
       * Clean up and release any resources which this object is holding, such
       * as open result sets.
       */
      public void cleanup()
      {
         if (!closed)
         {
            closed = true;
         }
      }
      
      /**
       * Reposition the backing query to the given row.  This method sets the
       * fetch-on-reposition strategy of the backing query to that of the
       * enclosing query before attempting the reposition.
       *
       * @param   row
       *          Zero-based index of the target row.
       */
      private void reposition(int row)
      {
         // Set the delegate query's fetchOnReposition strategy to match the
         // enclosing AdaptiveQuery's current strategy.
         query.setFetchOnReposition(isFetchOnReposition());
         
         query.reposition(row + 1);    // reposition() is 1-based
      }
      
      /**
       * Optionally switch the enclosing back to preselect mode if possible
       * and provide a return value which indicates whether the results object
       * currently is positioned on a row.
       * 
       * @param   revalidate
       *          <code>true</code> if query should attempt to switch back to
       *          preselect mode, else <code>false</code>.  The switch should
       *          only be made if the last operation moved forward through the
       *          query's results.
       *
       * @return  <code>true</code> if positioned on a row, else
       *          <code>false</code> if off-end.
       */
      private boolean postFetch(boolean revalidate)
      {
         if (revalidate)
         {
            Object breakValue = query.getBreakValue();
            if (breakValue != null)
            {
               revalidate(breakValue);
            }
         }
         
         return !query._isOffEnd();
      }
   }
   
   /**
    * A {@link ResultsProvider} implementation which is invoked when
    * transitioning between preselect and dynamic modes (both directions).
    * Provides a callback for {@link ResultsAdapter} when a new delegate
    * {@link Results delegate worker} is needed by the adapter.
    */
   private class ResultsSource
   implements ResultsProvider
   {
      /**
       * Invoked by a {@link ResultsAdapter} when a new <code>Results</code>
       * delegate worker is needed.  Determines whether we need a preselect
       * or dynamic results delegate and installs the appropriate delegate in
       * the results adapter.
       *
       * @param   adapter
       *          <code>ResultsAdapter</code> which is the source of the
       *          request.
       */
      public void provideNewResults(ResultsAdapter adapter)
      {
         // Dynamic --> Preselect transition.
         if (breakValue != null)
         {
            // Allows execute() to determine that we now are operating in preselect mode.
            invalidComponent = null;
            
            // Allows super.execute() to determine that we need a new
            // preselect result set.
            adapter.cleanup();
            setResults(null);
            
            // Retrieve preselect result set.  Adapter will be reconnected to
            // new delegate results.
            execute();
            
            // Store the last record(s) retrieved in dynamic mode.
            int i = 0;
            boolean recordsPresent = true;
            Long[] ids = new Long[getTableCount()];
            
            if (cursor != null)
            {
               Object[] row = cursor.peekLastLoadedResult();
               if (row != null)
               {
                  for (int j = 0; j < row.length; j++)
                  {
                     Long id = row[j] instanceof Record
                               ? ((Record) row[j]).primaryKey()
                               : (Long) row[j];
                     ids[j] = id;
                  }
               }
               else
               {
                  recordsPresent = false;
               }
            }
            else
            {
               ArrayList<QueryComponent> components = components();
               for (int j = 0; j < components.size(); j++)
               {
                  QueryComponent comp = components.get(j);
                  RecordBuffer buf = comp.getBuffer();
                  if (buf.getCurrentRecord() != null)
                  {
                     ids[i++] = buf.getCurrentRecord().primaryKey();
                  }
                  else
                  {
                     recordsPresent = false;
                     break;
                  }
               }
            }
            
            // Get the first result. It could be already visited into the
            // dynamic mode and we should skip it.
            if (recordsPresent && adapter.first())
            {
               Object[] res = adapter.get();
               for (int j = 0; j < res.length; j++)
               {
                  Long id = res[j] instanceof Record
                            ? ((Record) res[j]).primaryKey()
                            : (Long) res[j];
                  
                  if (!ids[j].equals(id))
                  {
                     // We didn't visit this result, reset to the original position.
                     adapter.reset();
                     break;
                  }
               }
            }
         }
         // Preselect --> Dynamic transition.
         else
         {
            // Allows execute() to determine that we need a new DynamicResults object.
            adapter.setResults(null);
            
            // Retrieve dynamic results.  Adapter will be reconnected to new delegate results.
            execute();
         }
      }
   }
}