# 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();
             }
          }
       }
    }