# RandomAccessQuery.java

    /*
    ** Module   : RandomAccessQuery.java
    ** Abstract : Backing query for converted queries which require dynamic record retrieval
    **
    ** Copyright (c) 2004-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 ECF 20051007   @23259 Created initial version. Backing query for
    **                           converted queries which require a dynamic
    **                           record retrieval semantic.
    ** 002 ECF 20051111   @23386 Adapted to use with CompoundQuery. Implements
    **                           Joinable, which is used by CompoundQuery when
    **                           adding query components.
    ** 003 ECF 20051129   @23574 Added lenient-off-end mode. When in this
    **                           mode, QueryOffEndExceptions thrown by the
    **                           underlying buffer during next and previous
    **                           operations are suppressed.
    ** 004 ECF 20051216   @23747 Added scrolling support. Now extends the new
    **                           DynamicQuery base class, which provides
    **                           cursoring support services. Modified
    **                           retrieval logic to allow for repositioning.
    ** 005 ECF 20060104   @23828 Added accumulation support. Record retrieval
    **                           methods first, last, next, and previous now
    **                           update any registered accumulators after
    **                           successfully retrieving a record.
    ** 006 GES 20060202   @24226 Removed main().
    ** 007 ECF 20060202   @24355 Added temp table support. Add buffer's
    **                           multiplex ID as a query substitution argument
    **                           for temp table queries.
    ** 008 ECF 20060208   @24365 Changed arguments processing in constructor.
    **                           Store duplicates of BaseDataType arguments
    **                           rather than references to the original
    **                           objects.
    ** 009 ECF 20060215   @24723 Added support for client-side where clause
    **                           processing. Major changes to execute() were
    **                           necessary to support this. All constructors
    **                           now accept an optional WhereExpression
    **                           parameter. Also improved handling of query
    **                           substitution parameter resolution to be more
    **                           correct.
    ** 010 ECF 20060302   @25023 Add support for foreign key joins. Added new
    **                           constructor variants which mirror existing
    **                           constructors, but accept an additional,
    **                           inverse DMO parameter. Modified query
    **                           execution to integrate natural joins.
    ** 011 ECF 20060317   @25171 Fixed defect related to extent fields. Use
    **                           mapping of query substitution parameters,
    **                           which may have been reordered during HQL
    **                           where clause preprocessing.
    ** 012 ECF 20060330   @25256 Removed final modifier from class. FindQuery
    **                           extends this class.
    ** 013 ECF 20060424   @25696 Added new constructor variants. Each existing
    **                           variant now has a counterpart which does not
    **                           expect a WhereExpression parameter.
    ** 014 ECF 20060424   @25700 Roll back previous change.
    ** 015 ECF 20060529   @26645 Added initializeBuffer method. Resets buffer
    **                           during query construction.
    ** 016 ECF 20060531   @26851 Replaced EndConditionException with
    **                           QueryOffEndException. The latter is a more
    **                           specific version of the former, used to
    **                           indicate a query has run off the end of its
    **                           result set.
    ** 017 ECF 20060610   @27102 Fixed lock processing defect. A record must
    **                           not be stored in the buffer after a NO-WAIT
    **                           record lock attempt fails.
    ** 018 ECF 20060621   @27506 Logging improvements.
    ** 019 ECF 20060713   @28046 Use new DBUtils class.
    ** 020 ECF 20060720   @28118 Minor changes driven by RecordBuffer naming
    **                           changes. RecordBuffer.getLatestRecord() was
    **                           changed to getSnapshot().
    ** 021 ECF 20060728   @28272 Minor change to accommodate RecordBuffer
    **                           changes. Enclose RecordBuffer.setTempRecord()
    **                           call in a try-catch block.
    ** 022 ECF 20060802   @28353 Added break value support. Implemented new
    **                           getBreakValue() method.
    ** 023 ECF 20060803   @28404 Added updateBuffer() method. Allows
    **                           subclasses to override setting current record
    **                           into buffer.
    ** 024 ECF 20060925   @29964 Check for a pending error before retrieving a
    **                           record. Retrieval is not attempted if an
    **                           error is pending in the ErrorManager.
    ** 025 ECF 20061003   @30155 Call prepareFetch() instead of obsolete
    **                           method checkPendingError(). Implemented
    **                           releaseBuffers() to override do-nothing
    **                           AbstractQuery implementation.
    ** 026 ECF 20061019   @30530 Changed reset() method. If the query is in
    **                           scrolling mode, the cursor is reset as part
    **                           of this method's processing.
    ** 027 ECF 20061023   @30609 Moved errorIfNull logic to AbstractQuery base
    **                           class. Method setErrorIfNull() is now
    **                           required by the P2JQuery interface and is
    **                           implemented in AbstractQuery instead of here.
    ** 028 ECF 20061027   @30755 Replaced Object with DataModelObject for DMO
    **                           arguments to methods/constructors. Required
    **                           for compile-time type safety.
    ** 029 ECF 20061115   @31210 Changed call to set current buffer record.
    **                           RecordBuffer API has changed to permit proper
    **                           management of placeholder snapshot record.
    **                           Added isResetSnapshot() method to allow
    **                           subclasses to override this behavior.
    ** 030 ECF 20070216   @32165 Explicitly set errorIfNull to false. This is
    **                           required to override the default setting of
    **                           true made by AbstractQuery.
    ** 031 ECF 20070228   @32248 Optimized no-lock and temp table fetches. In
    **                           these cases, it is not necessary to fetch the
    **                           record in two steps. The no-lock case makes
    **                           no guarantee that the record in memory has
    **                           not been updated by other sessions. The temp
    **                           table case guarantees that no other session
    **                           can make an update to the record in memory,
    **                           because a temp table record is inherently
    **                           scoped to a session.
    ** 032 ECF 20070308   @32335 Fixed behavior for first/last. Upon failure
    **                           to find a record, if not in looping mode,
    **                           these methods must report a 'FIND FIRST/LAST
    **                           failed...' message.
    ** 033 ECF 20070412   @32985 Retrofit to use RecordLockContext for record
    **                           locking. Accommodate method name change in
    **                           RecordBuffer: setCurrentRecord --> setRecord.
    ** 034 ECF 20070416   @33028 Integrated user interrupt handling.
    ** 035 ECF 20070504   @33410 Retrofitted load() method to use modified
    **                           signature specified by Joinable.
    ** 036 ECF 20070508   @33460 Fixed executeImpl() to properly handle a
    **                           validation exception. New requirement based
    **                           on a change to RecordBuffer.flush().
    ** 037 ECF 20070518   @33684 Added getRecordBuffers() and recordBuffers().
    **                           The former is required by the Joinable
    **                           interface. The latter is required by the
    **                           RecordChangeListener interface.
    ** 038 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.
    ** 039 ECF 20070718   @34571 Fixed findNext()/findPrevious() and their
    **                           callers. These methods were not properly
    **                           updating locks when necessary.
    ** 040 ECF 20070713   @34858 Moved all CAN-FIND specific processing to the
    **                           FindQuery subclass. Exposed some private
    **                           methods as protected and refactored class to
    **                           support this.
    ** 041 ECF 20070827   @35002 Minor optimization for temp table queries.
    **                           Omit multiplex ID from query substitution
    **                           parameters. HQLHelper now hard-codes the ID
    **                           into the where clause, since it is known at
    **                           the time the where clause is generated.
    ** 042 ECF 20070926   @35246 Fix placeholder record defect. In cases where
    **                           the referenceRecord DMO is modified, we need
    **                           to null it out and use the buffer's current
    **                           snapshot as our placeholder record instead.
    ** 043 ECF 20071130   @36123 Removed isRefreshSnapshot() method. The
    **                           RecordBuffer placeholder snapshot is no
    **                           longer cleared by any query type when the
    **                           query does not find a record to place in the
    **                           buffer. Integrated generics.
    ** 044 ECF 20071204   @36266 Fixed off-end tracking. Replaced boolean flag
    **                           with OffEnd enum to discern between off-end
    **                           directions (i.e., off back vs off front of
    **                           result set). Current off-end status and
    **                           active sort index are reported to the record
    **                           buffer when fetching a record. Off-end status
    **                           is checked when searching for a new record.
    ** 045 ECF 20080112   @37006 Removed restriction of dealing only with IDs.
    **                           This query can now handle both Persistables
    **                           and primary key IDs when retrieving data.
    **                           This is necessary when the full record is
    **                           needed immediately, not just an ID, such as
    **                           in presort cases.
    ** 046 CA  20080311   @37430 Removed the listener code from c'tor and
    **                           added it to the new registerChangeListener()
    **                           method.
    ** 047 ECF 20080310   @37482 Made changes necessary to support embedded,
    **                           H2 database.
    ** 048 SVL 20080418   @38041 Added support for the inverse sorting.
    ** 049 SVL 20080421   @38082 Added support for legacy key join helper.
    ** 050 ECF 20080508   @38245 Override base lock type for temporary tables.
    **                           This allows some downstream optimizations.
    ** 051 ECF 20080510   @38610 Implemented support for dirty database
    **                           checking. Looks into a shared database of
    **                           uncommitted updates and possibly overrides
    **                           data found in primary database.
    ** 052 ECF 20080609   @38652 Removed overly aggressive optimization in
    **                           executeImpl(). Queries with non-NO-LOCK types
    **                           must also search among uncommitted updates,
    **                           and locks must be acquired on dirty records.
    ** 053 ECF 20080610   @38690 Fixed dirty check processing in executeImpl()
    **                           method. We were not correctly handling the
    **                           case of finding a dirty version of the same
    **                           DMO found in the primary database.
    ** 054 ECF 20080611   @38712 Fixed processResults(). When acquiring a lock
    **                           for a dirty record, the record should be
    **                           reloaded, in case it disappeared while we
    **                           were waiting on the lock.
    ** 055 CA  20080618   @38868 When the JOINed foreign buffer does not refer
    **                           any record, the query must return no result
    **                           as it can not be executed.
    ** 056 SVL 20080630   @38985 Fixed executeImpl() for the case when a dirty
    **                           record is inserted and modified.
    ** 057 SVL 20080730   @39222 Added dmoSorter instance variable.
    ** 058 CA  20080815   @39465 Support API change in DBUtils.
    ** 060 ECF 20081009   @40082 Integrated new support for inlining query
    **                           substitution parameters. Queries supported by
    **                           this class don't actually have their
    **                           parameters inlined, but changes were needed
    **                           to integrate HQLPreprocessor API changes and
    **                           the ParameterIndices class.
    ** 061 ECF 20081105   @40312 Implemented registerRecordChangeListeners().
    **                           Required by the Joinable interface.
    ** 062 ECF 20081215   @40917 Modified executeImpl(). A validation error
    **                           ignores silent error mode, since the flushing
    **                           of the previous record in the buffer is not
    **                           masked by silent error mode.
    ** 063 ECF 20090303   @41599 Fixed to better support being a Joinable
    **                           component of a CompoundQuery. Implemented
    **                           RecordChangeListener, added getOffEnd()
    **                           method. Replaced activeBundleKey Object with
    **                           an int to support changes in HQLHelper.
    ** 064 ECF 20090317   @41627 Removed early calls to getHelper(). This
    **                           method must not be invoked until the query is
    **                           actually being executed, because of possible
    **                           dependencies in HQLHelper.obtain() on the
    **                           state of the query's substitution parameters.
    **                           Specifically, a FieldReference parameter may
    **                           not be backed by a record if this method is
    **                           invoked arbitrarily early.
    ** 065 ECF 20090512   @42154 Modified prepareBuffer(). Now invokes flushAll()
    **                           instead of flush() on backing buffer. This
    **                           ensures all like buffers are allowed to flush
    **                           their transient records properly.
    ** 066 ECF 20090603   @42596 Added support for Progress isolation leak quirk.
    **                           Modified executeImpl() and processResults() to
    **                           use new features in HQLPreprocessor and in
    **                           DirtyShareContext to leak uncommitted changes to
    **                           other sessions and to select appropriate record.
    ** 067 ECF 20090609   @42635 Fixed executeImpl(). We now evict, if possible,
    **                           those DMOs read into the Hibernate Session, but
    **                           not loaded into the record buffer. This includes
    **                           DMOs whose uncommitted changes are leaked to
    **                           other sessions early, and DMOs which are found in
    **                           the primary database, but overridden by records
    **                           found by the dirty share manager.
    ** 068 ECF 20090623   @42941 Fixed executeImpl(). Handle the case of a unique
    **                           search finding information in the dirty database.
    **                           Also added back multiplex ID as a substitution
    **                           parameter for temp table queries.
    ** 069 ECF 20090703   @43058 Reimplemented load() method. This method now sets
    **                           the backing buffer to unknown mode and throws
    **                           MissingRecordException if a target record cannot
    **                           be loaded.
    ** 070 ECF 20090716   @43221 Enabled lazy record buffer initialization. Added
    **                           RecordBuffer.initialize() call in c'tor.
    ** 071 ECF 20090724   @43421 Added implicit transaction support. In various
    **                           methods which execute queries, we push/pop an
    **                           implicit transaction.
    ** 072 CA  20090731   @43470 Register the query for off-end listener
    **                           registration after block initialization (done
    **                           only for block types FOR and FOR EACH). Added
    **                           getOffEndListeners, which returns a list of all
    **                           off-end listeners for all used buffers.
    ** 073 SVL 20091030   @44293 getOffEnd() function was made public in order to
    **                           support API change into P2JQuery.
    ** 074 OM  20130829          Added change detection to buffer on current() methods.
    **                           Updated calls to match the new Persistence.load() signatures.
    ** 075 ECF 20131028          Import change.
    ** 076 OM  20140107          Added TODO for missing fatal error message.
    ** 077 SVL 20140604          Added support for index information field.
    ** 078 ECF 20140814          Replaced RecordBuffer.flushAll with flush in prepareBuffer method.
    **                           Fixed dirty share logic to work with new flush/validate algorithm.
    **                           Replaced Apache commons logging with J2SE logging.
    ** 079 OM  20150507          Removed listener from global scope of ChangeBroker.
    ** 080 ECF 20150801          Implemented new Joinable methods necessary for compound query
    **                           optimization. Error message fix. Enabled database statistics.
    ** 081 ECF 20150906          Reimplemented silent error mode processing of query substitution
    **                           parameters. Fixed NPE related to outer joins.
    ** 082 SVL 20151112          Continue normally if reposition failed in load().
    ** 083 EVL 20160223          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 084 SVL 20160314          Added setter for referenceRecord.
    ** 085 OM  20160316          Signature of DirtyShareContext.getDirtyInfo() has changed.
    **     ECF 20160504          Pass new parameter to Persistence.load. Minor change to isIdOnly.
    ** 086 SVL 20160608          The query can handle null IDs (for scrolling compound queries
    **                           with OUTER join).
    ** 087 ECF 20160615          Bias toward fetching full records when possible.
    ** 088 IAS 20160701          Fixed legacy name in the [565] error message.
    ** 089 IAS 20160714          Use legacy name instead of DMO alias.
    ** 090 OM  20160715          Added support for template records.
    **                           Persistence.popImplicitTransaction() changed signature.
    ** 091 ECF 20160725          Fixed buffer lookup during HQL preprocessing.
    ** 092 ECF 20160823          Improved executeImpl to better detect missing records and avoid
    **                           deadlock.
    ** 093 ECF 20160901          DirtyShareContext.isDirtyDelete signature change.
    ** 094 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.
    ** 095 CA  20161012          Fixed scope processing when the query is from a persistent
    **                           procedure, and is part of a QUERY resource.
    ** 096 ECF 20161014          Fixed implementation of current() method in the event there is no
    **                           current record in the buffer to reload, and this is not considered
    **                           an error in the context of the query (e.g., query represents an
    **                           outer join and record can be null).
    ** 097 ECF 20171212          Reworked query substitution parameter error handling and error
    **                           condition handling.
    ** 098 OM  20171129          releaseBuffers() is overwritten from P2JQuery.
    **     ECF 20171217          Added workaround to executeImpl to avoid dirty share manager blowup,
    **                           but this is not a long-term solution.
    ** 099 ECF 20180201          Removed initializeBuffer, which released the current record too
    **                           aggressively.
    ** 100 OM  20180220          Fixed LEFT OUTER JOIN queries.
    ** 101 OM  20180321          Fixed NPE caused by a null substitution array.
    ** 102 ECF 20180418          Fixed sort clause generation in makeAdaptiveServerJoinComponent.
    ** 103 OM  20180515          Added dynamic filtering of results set.
    ** 104 OM  20180901          Improved dynamic filtering.
    ** 105 OM  20180918          Filtering is case insensitive and uses LIKE patterns.
    **     OM  20180924          Filtering on decimal fields uses STRING(f, format) function.
    ** 106 OM  20181023          Added external buffers when making the adaptive server join components.
    ** 107 OM  20190330          Ignore close request when query not fully initialized.
    ** 108 ECF 20190813          Change to RecordBuffer.release and RecordBuffer.setRecord APIs.
    ** 109 SVL 20191002          Log if sort index was not found.
    ** 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 CA  20200110          If the index is missing, assume the primary index (encountered when
    **                           the sort clause uses fields in a WORD index).
    **     EVL 20200206          Adding NPE protection to initialize method.
    ** 113 ECF 20200419          Removed Hibernate dependencies.
    ** 114 OM  20200915          Handled query navigation on fatal error cases.
    **     ECF 20200919          Persistence.load API signature change.
    **     CA  20200924          Replaced Method.invoke with ReflectASM.
    **     OM  20201120          Added javadoc note about a possible incorrect logged error.
    **     CA  20210310          A dynamic predicate must set the default lock to NONE, instead of SHARE.
    **     IAS 20210402          Do not use fast-find cache for meta tables.
    **     ECF 20210723          Changed signature of query close worker method.
    **     ECF 20210806          Fixed query substitution parameter preprocessing, and handle a fatal error
    **                           differently than raising a 565 error.
    **     IAS 20210922          Do not cache the result if the query depends on mutable SESSION attributes.
    **     OM  20210927          FFC invalidates the records whose changed fields are used in query predicates.
    **     OM  20220216          Fine-tuned logs when the query index is not found or an error occurs.
    **     SVL 20220518          next/previous perform first/last if the previous search using the same index has
    **                           failed.
    **     OM  20220623          Added P2JQuery.Parameter in resolveArg().
    **     OM  20220726          Added recordAlreadyFound flag for block next/prev iterations on unique
    **                           find-by-rowid queries. Cleanup setRecordAlreadyFound() method.
    **     OM  20221125          Display error 12378 on attempts to reference uninitialized temp-table.
    **     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.
    ** 115 CA  20230227          Fixed where clause translation when the external buffer's binding and definition
    **                           does not match.
    ** 116 AL2 20230210          Allow direct access for recid/unique index look-ups.
    **         20230227          Added locking if query is find-by-row-id and record is found in cache.
    **         20230307          Honor record nursery before direct access.
    ** 117 DDF 20230314          Added hasContraints() that checks for where clause/arguments/join/external
    **                           buffers involved in the query.
    **     AL2 20230314          Moved find-by-rowid short-circuit in execute to cover more cases.
    **         20230317          Prepare buffer before attempting direct-access.
    ** 118 OM  20230404          The join navigation key may contain null/unknown values.
    ** 119 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 120 RAA 20230522          Added instance of P2JQueryStatistics for storing some statistics.
    **     RAA 20230522          Function executeImpl now goes through P2JQueryLogger
    **                           in case logging is intended.
    ** 121 SVL 20230516          Support for more granular configuration of dirty sharing functionality.
    ** 122 RAA 20230607          P2J Queries are now executed in P2JQueryExecutor instead of P2JQueryLogger.
    ** 123 AL2 20230608          Small refactoring to compute isSimpleQuery only once.
    ** 124 DDF 20230627          Replaced static variables of DirtyShareSupport with static method calls.
    ** 125 IAS 20230706          Runtime changes for CONTAINS with non-constant search expression.
    ** 126 DDF 20230904          Short-circuit when the table is empty, but only after the records are flushed.
    ** 127 AL2 20230926          Make honorRecordNursery package-private.
    ** 128 DDF 20231103          Remove short-circuit for empty table. It is called earlier in FindQuery
    **                           by using buffer.fastHasRecords().
    ** 129 RAA 20231006          Added getBundleFql and replaced parameters used in P2JQueryExecutor.
    **     RAA 20231010          Removed DatabaseStatistics usage.
    **     RAA 20231122          Added activeBundle FQL caching.
    ** 130 RAA 20231220          Surrounded P2JQueryExecutor.execute in a try-catch, in execute function.
    ** 131 ES  20231203          Changed first, last, next, previous, current, unique methods to return boolean,
    **                           instead of void. These methods now returns true if operation was successful, and false
    **                           not and lenientOffEnd is set on true, otherwise it throws an QQE exception.
    **     ES  20231203          Added new setLenientOffEnd method.
    ** 132 OM  20231002          Avoided name collision in execute() method.
    ** 133 AI  20240221          Updated addDynamicFilter to add the processed parameter.
    ** 134 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.
    ** 135 AL2 20240313          Fixed helper reset on .reset() if inlined.
    **                           Check if args are unknown even for non-outer.
    ** 136 ES  20240626          Avoid logging DirectedAccessExceptions in expected cases.
    ** 137 SP  20240614          Check in executeDirectAccess if the query uses contains operator.
    ** 138 CA  20240626          'findNext' and 'findPrev' must not find a record of offend is BACK or FRONT.
    ** 139 AL2 20240626          Don't halt query if AbstractQuery.preprocessSubstitutionArguments fails.
    ** 140 RAA 20240613          Added tenant and database id parameters when calling FastFindCache.getInstance.
    **     RAA 20240617          Rearranged ffCache parameters.
    ** 141 CA  20240809          Avoid iterator usage, for performance improvement.
    ** 142 SP  20240801          Added new getTranslatedWhere method to use for translateWhere.
    ** 143 EAB 20240819          Preventing dirty records from being added to ffCache.
    ** 144 DDF 20240814          Added silent method equivalents to first, next, previous, last, unique, current
    **                           to avoid using ErrorManager.silent() which requires a lambda.
    **     DDF 20240821          lenientOffEnd flag is already available in AbstractQuery and it should be
    **                           set for each silent mode method because the silent flag retrieved through
    **                           the ErrorHelper is set after the FindQuery constructor.
    **     DDF 20240822          Made ErrorHelper instance a class member for performance improvements.
    **     DDF 20240828          Removed silent mode methods that are accessible through AbstractQuery and
    **                           moved the errorHelper property to AbstractQuery.
    ** 145 OM  20240909          FFCAche does not require knowledge of current tenant.
    ** 146 CA  20230924          Further reduce context-local lookup.
    ** 147 CA  20241001          A 'find-by-rowid' (be it via FOR EACH or FIND queries) must always throw a
    **                           QueryOffEndException.  This is the same behavior as before H144, where
    **                           RAQ.lenientOffEnd field was set to 'true' by AdaptiveQuery$DynamicResults, and
    **                           the read was done via AbstractQuery getter, which read the wrong field.
    ** 148 TJD 20221129          NPE fix in processDirtyResults when there is nothing to compare to current DMO
    **     TJD 20230508          Bringing back support for records leaks to other sessions
    **     TJD 20230724          Migrate earlyPublishEntities from List to Set to improve performance
    **     TJD 20230825          Fix NPE for NULL session when checking cache
    **     TJD 20240110          Fixes for DirtyShare database cases
    **     TJD 20240126          Dont NPE when ffCache is null.
    **     TJD 20240308          Optimize dirtyContext.updateSnapshots call, dont cache non-session DMOs
    **     TJD 20240319          Do not try to compare dirty results if DMOSorter is null
    **     TJD 20240705          Support for doubled parameters for NEXT/PREV queries
    **     TJD 20240730          Allow offEnd.FRONT to be treated like offEnd.BACK for prev/last queries
    **     TJD 20240912          DirtyShareSupport flags logic dependency update
    ** 149 AL2 20241021          Allow hinting that this is a CAN-FIND query and the buffer is not going to be
    **                           loaded by the found DMO.
    ** 150 RNC 20241031          Added dirty-intra-share check to honor the answer from
    **                           the dirty database in RAQ.executeImpl().
    ** 151 AL2 20241126          Allow processing dirty result when intra-session dirty-share is enabled.
    ** 152 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
    **                           based on [sharedDb] parameter.
    **                           Implementation of [setMultiTenantAlternative] method.
    ** 153 AL2 20241217          Prepare buffer before honoring the record nursery. This way, the record nursery
    **                           won't push the changes on the buffer to the dirty database *before* flushing it.
    ** 154 ES  20250407          Use timeout parameter in the call to Persistence.load.
    ** 155 LS  20250414          Updated getHelper() to use the included/excluded fields.
    **                           Added setters for included/excluded arrays.
    ** 156 ES  20250508          Synchronize the query buffer if it is AUTO-SYNCHRONIZE attribute is set on true.
    */

    /*
    ** 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.io.*;
    import java.lang.reflect.*;
    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.ErrorManager.ErrorHelper;
    import com.goldencode.p2j.util.logging.CentralLogger;
    import com.goldencode.util.*;

    /**
     * The backing query for converted queries which require a dynamic record
     * retrieval semantic.  This means that a single record is retrieved at a
     * time and the query manages state about that record's position relative
     * to others which also match the query's search criteria.  Furthermore,
     * any change to that record's data may change its position relative to other
     * matching records, and these changes must be supported while client code is
     * using the query to visit result records.  Client code can jump among
     * results randomly.  These moves are specified as either relative requests
     * (next, previous, current), or as absolute requests (first, last, unique).
     * <p>
     * This class slavishly implements the Progress semantic of fully dynamic
     * query iteration.  That is, each record is searched for and retrieved
     * individually, so that any change to an existing record can be reflected
     * immediately in the remaining navigation performed on the query.  This
     * means that an update to the current record may cause it to reappear later
     * in the query results, if the changes made to the record would cause it to
     * be sorted differently than when it was first retrieved (note that this can
     * cause infinite looping, just as in the pre-conversion, Progress code).
     * <p>
     * To implement the single-record retrieval semantic, this high level query
     * may issue multiple, database-level, select statements to the database for
     * each record retrieval attempt.  For details of how this is performed, see
     * {@link FQLHelper} and {@link FQLBundle}.  The result of a record retrieval
     * is a Data Model Object (DMO), which is stored in a {@link RecordBuffer}.
     * A record buffer is associated with this object at construction.
     * <p>
     * Because of this implementation requirement, <code>RandomAccessQuery</code>
     * represents <i>an extremely inefficient mechanism</i> by which to query a
     * database for a set of records.  As such, <b>it should not be used for new
     * development;  it is intended only to support legacy, converted code</b>,
     * insofar as that code may rely on the dynamic nature of the Progress
     * semantic.
     * <p>
     * This class implements the <code>Joinable</code> interface so that it can
     * be added as a component to a {@link CompoundQuery}.  When used in such a
     * capacity, the query should be configured to raise an end condition instead
     * of an error condition, in the case that a request to get the first, last,
     * or a unique record fails.  This will allow the compound query to continue
     * processing normally in this case.  This is accomplished by invoking {@link
     * #setErrorIfNull} with the parameter <code>true</code>.
     * <p>
     * When used to represent a Progress query (as created with DEFINE or OPEN
     * QUERY), {@link #setLenientOffEnd} must be set to <code>true</code>, which
     * suppresses end condition exceptions for <code>next</code> and
     * <code>previous</code> commands which run off the end of the query's
     * results.
     * <p>
     * <strong>Transaction Isolation (Dirty Reads)</strong>
     * <br>
     * This class supports Progress' peculiar form of transaction isolation, where
     * uncommitted updates, inserts, and deletes that affect database indexes are
     * visible across sessions.  This is emulated using a shared database (the
     * "dirty" database) which temporarily stores information about uncommitted
     * changes made by other contexts.  When those other sessions commit or roll
     * back their changes, this transient information is cleared from the dirty
     * database and is no longer visible.
     * <p>
     * The implementation of this feature requires that the results found in the
     * primary database backing a query be treated as provisional. Results found
     * in the dirty database may override, if it is determined that the "dirty"
     * result is the more correct result, given the query criteria.
     */
    public class RandomAccessQuery
    extends DynamicQuery
    implements Joinable,
               QueryConstants,
               RecordChangeListener
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(RandomAccessQuery.class.getName());

       /** Record buffer which backs this query */
       private RecordBuffer buffer;

       /** Helper object for dynamic join via a foreign key relation */
       private AbstractJoin join;

       /** Client-side where clause expression, if any */
       private Supplier<logical> whereExpr;

       /** The original arguments (un-evaluated) used at the query initialization. */
       private Object[] origArgs;

       /** Base substitution parameters for FQL queries before applying any dynamic filter. */
       private Object[] args;

       /** Base substitution parameters for FQL queries, including the dynamic filtering components, if any. */
       private Object[] dfArgs;

       /** Lock type to apply to records retrieved */
       private LockType lockType;

       /**
        * Original where clause (only stored until helper is built). It is only used for building the
        * {@code dfWhere} which also contains the information on eventual dynamic filters.
        */
       private String where = null;

       /**
        * Original where clause (only stored until helper is built), including the dynamic filtering
        * components, if any.
        */
       private String dfWhere = null;

       /** Original sort clause (only stored until helper is built) */
       private String sort = null;

       /** DMO sorter associated with query's sort clause */
       private DMOSorter dmoSorter = null;

       /** The id of the index in use for this query. */
       private int index = 0;

       /** Index information string as it is returned by INDEX-INFORMATION. */
       private String indexInfo = null;

       /** Builds FQL statements for various Hibernate queries. */
       private FQLHelper helper = null;

       /** Cached helper for non-null arguments in left outer join queries. */
       private FQLHelper nonNullArgsHelper = null;

       /** FQL data necessary for the current query operation */
       private FQLBundle activeBundle = null;

       /** Key which indicates active bundle choice */
       private int activeBundleKey = NONE;

       /** The FQL of the whole activeBundle. */
       private String activeBundleFql = null;

       /** Substitution parameter values for the next query execution */
       private Object[] currentArgs = null;

       /** Record whose data defines current "place" in query results */
       private Record referenceRecord = null;

       /** Break value indicating a new sort band if non-<code>null</code> */
       private Object breakValue = null;

       /** Force query to retrieve full records, rather than primary keys only */
       private boolean fullRecords = false;

       /** Was last record found retrieved from the dirty database? */
       private boolean dirtyCopy = false;

       /** RecordChangeListener which nulls out placeholder reference record */
       private RecordChangeListener placeholderCleaner = null;

       /** Determines if this will be unregistered from ChangeBroker on cleanup. */
       private boolean unregisterOnCleanup = false;

       /**
        * The list of arguments for currently applied dynamic filters.
        * Note that its size may be smaller than the number of dynamic filters because the null /
        * unknown values were automatically converted to {@code is null} so they lack the argument.
        */
       private List<Object> dynamicFilterArgs = null;

       /**
        * The list of dynamic filters. These are filters added dynamically, at runtime and can also be
        * removed dynamically.
        */
       private Map<FieldReference, BaseDataType> dynamicFilters = null;

       /**
        * The list of format used by dynamic filters. These are filters added dynamically, at runtime
        * and can also be removed dynamically.
        */
       private Map<FieldReference, String> dynamicFormats = null;

       /** The fast-find cache associated to this query. */
       private FastFindCache ffCache = null;

       /**
        * In case this is a FIND-BY-ROWID unique query, this flag marks whether the target record was found in a
        * previous iteration and allow the OFF-END condition to be thrown.
        */
       private boolean recordAlreadyFound = false;

       /** A function to translate the where clause, just before it gets executed. */
       private Supplier<String> translateWhere = null;

       /**
        * Default constructor. Initialization is deferred until <code>initialize()</code> is called.
        */
       public RandomAccessQuery()
       {
       }

       /**
        * Initialization logic which defaults record lock type to <code>LockType.SHARE</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    dmo
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        *
        * @return   This query instance.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort)
       {
          return initialize(dmo, where, whereExpr, sort, null, null, null, getDefaultLock());
       }

       /**
        * Initialization logic which defaults record lock type to <code>LockType.SHARE</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    dmo
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @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.
        *
        * @return   This query instance.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           String            indexInfo)
       {
          return initialize(dmo, where, whereExpr, sort, indexInfo, null, null, getDefaultLock());
       }

       /**
        * Initialization logic which is used when joining to another table using a foreign
        * key.  Defaults record lock type to <code>LockType.SHARE</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    dmo
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @param    inverse
        *           DMO to which this query should join via a foreign relation.
        *           May be <code>null</code>.
        *
        * @return   This query instance.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           DataModelObject   inverse)
       {
          return initialize(dmo, where, whereExpr, sort, null, inverse, null, getDefaultLock());
       }

       /**
        * Initialization logic which is used when joining to another table using a foreign
        * key.  Defaults record lock type to <code>LockType.SHARE</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    dmo
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @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>.
        *
        * @return   This query instance.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           String            indexInfo,
                                           DataModelObject   inverse)
       {
          return initialize(dmo, where, whereExpr, sort, indexInfo, inverse, null, getDefaultLock());
       }

       /**
        * Initialization logic which defaults record lock type to
        * <code>LockType.SHARE</code> and accepts default substitution parameters.
        * <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
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @param    args
        *           Substitution parameters for FQL queries.  These will be used
        *           if not overridden by a record retrieval method.
        *
        * @return   This query instance.
        *
        * @throws   IllegalArgumentException
        *           if any substitution argument provided is not of a supported
        *           type.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           Object[]          args)
       {
          return initialize(dmo, where, whereExpr, sort, null, null, args, getDefaultLock());
       }

       /**
        * Initialization logic which defaults record lock type to
        * <code>LockType.SHARE</code> and accepts default substitution parameters.
        * <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
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @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    args
        *           Substitution parameters for FQL queries.  These will be used
        *           if not overridden by a record retrieval method.
        *
        * @return   This query instance.
        *
        * @throws   IllegalArgumentException
        *           if any substitution argument provided is not of a supported
        *           type.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           String            indexInfo,
                                           Object[]          args)
       {
          return initialize(dmo, where, whereExpr, sort, indexInfo, null, args, getDefaultLock());
       }

       /**
        * Initialization logic which is used when joining to another table using a foreign
        * key.  Defaults record lock type to <code>LockType.SHARE</code> and
        * accepts default substitution parameters.
        * <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
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @param    inverse
        *           DMO to which this query should join via a foreign relation.
        *           May be <code>null</code>.
        * @param    args
        *           Substitution parameters for FQL queries.  These will be used
        *           if not overridden by a record retrieval method.
        *
        * @return   This query instance.
        *
        * @throws   IllegalArgumentException
        *           if any substitution argument provided is not of a supported
        *           type.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           DataModelObject   inverse,
                                           Object[]          args)
       {
          return initialize(dmo, where, whereExpr, sort, null, inverse, args, getDefaultLock());
       }

       /**
        * Initialization logic which is used when joining to another table using a foreign
        * key.  Defaults record lock type to <code>LockType.SHARE</code> and
        * accepts default substitution parameters.
        * <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
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @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 for FQL queries.  These will be used
        *           if not overridden by a record retrieval method.
        *
        * @return   This query instance.
        *
        * @throws   IllegalArgumentException
        *           if any substitution argument provided is not of a supported
        *           type.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           String            indexInfo,
                                           DataModelObject   inverse,
                                           Object[]          args)
       {
          return initialize(dmo, where, whereExpr, sort, indexInfo, inverse, args, getDefaultLock());
       }

       /**
        * Initialization logic which sets an explicit, default, record lock type.
        * <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
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @param    lockType
        *           Lock type to apply to records retrieved, if not overridden by
        *           a record retrieval method.
        *
        * @return   This query instance.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           LockType          lockType)
       {
          return initialize(dmo, where, whereExpr, sort, null, null, null, lockType);
       }

       /**
        * Initialization logic which sets an explicit, default, record lock type.
        * <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
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @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    lockType
        *           Lock type to apply to records retrieved, if not overridden by
        *           a record retrieval method.
        *
        * @return   This query instance.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           String            indexInfo,
                                           LockType          lockType)
       {
          return initialize(dmo, where, whereExpr, sort, indexInfo, null, null, lockType);
       }

       /**
        * Initialization logic which is used when joining to another table using a foreign
        * key.  Sets an explicit, default, record lock type.
        * <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
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @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, if not overridden by
        *           a record retrieval method.
        *
        * @return   This query instance.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           DataModelObject   inverse,
                                           LockType          lockType)
       {
          return initialize(dmo, where, whereExpr, sort, null, inverse, null, lockType);
       }

       /**
        * Initialization logic which is used when joining to another table using a foreign
        * key.  Sets an explicit, default, record lock type.
        * <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
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @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    lockType
        *           Lock type to apply to records retrieved, if not overridden by
        *           a record retrieval method.
        *
        * @return   This query instance.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           String            indexInfo,
                                           DataModelObject   inverse,
                                           LockType          lockType)
       {
          return initialize(dmo, where, whereExpr, sort, indexInfo, inverse, null, lockType);
       }

       /**
        * Initialization logic which sets an explicit, default, record lock type and
        * accepts default substitution parameters.
        * <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
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @param    args
        *           Substitution parameters for FQL queries.  These will be used
        *           if not overridden by a record retrieval method.
        * @param    lockType
        *           Lock type to apply to records retrieved, if not overridden by
        *           a record retrieval method.
        *
        * @return   This query instance.
        *
        * @throws   IllegalArgumentException
        *           if any substitution argument provided is not of a supported
        *           type.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           Object[]          args,
                                           LockType          lockType)
       {
          return initialize(dmo, where, whereExpr, sort, null, null, args, lockType);
       }

       /**
        * Initialization logic which sets an explicit, default, record lock type and
        * accepts default substitution parameters.
        * <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
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @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    args
        *           Substitution parameters for FQL queries.  These will be used
        *           if not overridden by a record retrieval method.
        * @param    lockType
        *           Lock type to apply to records retrieved, if not overridden by
        *           a record retrieval method.
        *
        * @return   This query instance.
        *
        * @throws   IllegalArgumentException
        *           if any substitution argument provided is not of a supported
        *           type.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           String            indexInfo,
                                           Object[]          args,
                                           LockType          lockType)
       {
          return initialize(dmo, where, whereExpr, sort, indexInfo, null, args, lockType);
       }

       /**
        * Initialization logic which is used when joining to another table using a foreign
        * key.  Sets an explicit, default, record lock type and accepts default
        * substitution parameters.
        * <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
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @param    inverse
        *           DMO to which this query should join via a foreign relation.
        *           May be <code>null</code>.
        * @param    args
        *           Substitution parameters for FQL queries.  These will be used
        *           if not overridden by a record retrieval method.
        * @param    lockType
        *           Lock type to apply to records retrieved, if not overridden by
        *           a record retrieval method.
        *
        * @return   This query instance.
        *
        * @throws   IllegalArgumentException
        *           if any substitution argument provided is not of a supported
        *           type.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           DataModelObject   inverse,
                                           Object[]          args,
                                           LockType          lockType)
       {
          return initialize(dmo, where, whereExpr, sort, null, inverse, args, lockType);
       }

       /**
        * Initialization logic which is used when joining to another table using a foreign
        * key.  Sets an explicit, default, record lock type and accepts default
        * substitution parameters.
        * <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
        *           Data model object which determines the record buffer into which
        *           records are retrieved.
        * @param    where
        *           Where clause, using FQL syntax.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Order by clause, using FQL syntax.  May be <code>null</code>
        *           only if query is to be navigated only using one of the
        *           <code>unique</code> method variants.
        * @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 for FQL queries.  These will be used
        *           if not overridden by a record retrieval method.
        * @param    lockType
        *           Lock type to apply to records retrieved, if not overridden by
        *           a record retrieval method.
        *
        * @return   This query instance.
        *
        * @throws   IllegalArgumentException
        *           if any substitution argument provided is not of a supported
        *           type.
        */
       public RandomAccessQuery initialize(DataModelObject   dmo,
                                           String            where,
                                           Supplier<logical> whereExpr,
                                           String            sort,
                                           String            indexInfo,
                                           DataModelObject   inverse,
                                           Object[]          args,
                                           LockType          lockType)
       {
          this.buffer = ((BufferReference) dmo).buffer();
          RecordBuffer defBuffer = ((BufferReference) dmo).definition();

          if (!buffersMatch(buffer, defBuffer))
          {
             sort = translateSort(buffer, defBuffer, sort);

             if (where != null)
             {
                this.translateWhere = () ->
                {
                   ArrayList<RecordBuffer> binding = new ArrayList<>();
                   binding.add(buffer);
                   ArrayList<RecordBuffer> definition = new ArrayList<>();
                   definition.add(defBuffer);

                   return translateWhere(binding, definition, where);
                };
             }
          }

          if (buffer.isReadonly())
          {
             // TODO: readonly buffers cannot be used in a FIND statement but can be used in CAN-FIND functions
          }

          buffer.initialize();
          this.where = where;
          this.dfWhere = where;
          this.sort = sort;
          this.whereExpr = whereExpr;
          if (args != null)
          {
             this.origArgs = new Object[args.length];
             System.arraycopy(args, 0, origArgs, 0, args.length);
          }
          this.args = args;
          this.dfArgs = args;
          this.indexInfo = indexInfo;

          DmoMeta dmoMeta = buffer.getDmoInfo();
          int idxCount = dmoMeta.getIndexCount(true) + dmoMeta.getIndexCount(false);
          String indexName = null;
          boolean argResolved = false;

          if (idxCount > 0)
          {
             // get index, which is needed for dirty checking work and FastFind caching
             try
             {
                IndexHelper indexHelper = buffer.getIndexHelper();
                indexName = indexHelper.getIndexForSort(buffer, sort);

                if (indexName == null)
                {
                   P2JIndex p2jIndex = indexHelper.getPrimaryIndex(buffer.getEntityName());
                   if (p2jIndex != null)
                   {
                      indexName = p2jIndex.getName();
                   }
                }

                if (buffer.getDirtyContext() != null)
                {
                   this.dmoSorter = indexHelper.getSorterForSortPhrase(buffer, sort);
                }

                if (indexName != null)
                {
                   index = buffer.getDmoInfo().getRecordMeta().getIndexIdByName(indexName);
                }

                // in case of find-by-rowid the [index] is not used, anyway
                if (index == 0 && LOG.isLoggable(Level.WARNING))
                {
                   // when accessing the preprocessor, the arguments must be already resolved
                   resolveArgs(false);
                   argResolved = true; // avoid double call to resolveArgs() method
                   if (!getHelper().getFQLPreprocessor().isFindByRowid())
                   {
                      LOG.log(Level.WARNING,
                              "Could not locate index for sort phrase [" + sort + "]. Search may be slow.");
                   }
                }
             }
             catch (PersistenceException exc)
             {
                if (LOG.isLoggable(Level.SEVERE))
                {
                   LOG.log(Level.SEVERE,
                           "Error encountered while locating index for sort phrase [" + sort +
                           "]. Incremental search may be slow.",
                           exc);
                }
             }
          }

          if (inverse != null)
          {
             if (Persistence.isForeignKeysEnabled())
             {
                join = new DynamicJoin(buffer, ((BufferReference) inverse).buffer());
             }
             else
             {
                join = new DynamicLegacyKeyJoin(buffer, ((BufferReference) inverse).buffer());
             }
          }
          else
          {
             join = null;
          }

          if (!dmoMeta.isMeta() && index != 0)
          {
             this.ffCache = FastFindCache.getInstance(buffer.isTemporary(), buffer.getDatabase());
          }

          // override lock type if this is a temporary buffer;  this allows some downstream optimizations
          this.lockType = (buffer.isTemporary() ? LockType.NONE : lockType);

          AbstractQuery.preprocessSubstitutionArguments(buffer.getBufferManager(),
                                                        this::isFindByRowid,
                                                        suppressNestedQueryError(),
                                                        args);

          setErrorIfNull(false);
          if (!argResolved)
          {
             resolveArgs(false);
          }
          registerChangeListener();

          buffer.getTxHelper().registerOffEndQuery(this);

          return this;
       }

       /**
        * Translate the {@link #where} clause, using the {@link #translateWhere} function.
        */
       @Override
       public void translateWhere()
       {
          if (translateWhere == null)
          {
             return;
          }

          this.where = translateWhere.get();
          this.dfWhere = this.where;
          this.translateWhere = null;
       }

       /**
        * Force the query to retrieve full records, rather than primary keys only.
        */
       public void setFullRecords()
       {
          this.fullRecords = true;
       }

       /**
        * Navigate to the first record which meets the query criteria and retrieve
        * it into its record buffer.  Use the default substitution parameters and
        * lock type.
        *
        * @return  <code>true</code> if query could retrieve the first record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if no record
        *          could be found when operating in standalone mode.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean first()
       {
          return first(getCurrentArgs(), lockType);
       }

       /**
        * Navigate to the first record which meets the query criteria and retrieve
        * it into its record buffer.  Use the default lock type, but override the
        * default substitution parameters with those provided.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        *
        * @return  <code>true</code> if query could retrieve the last record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if no record
        *          could be found when operating in standalone mode.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean first(Object[] values)
       {
          return first(values, lockType);
       }

       /**
        * Navigate to the first record which meets the query criteria and retrieve
        * it into its record buffer.  Use the default substitution parameters, but
        * override the default lock type with that provided.
        *
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the last record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if no record
        *          could be found when operating in standalone mode.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean first(LockType lockType)
       {
          return first(getCurrentArgs(), lockType);
       }

       /**
        * Navigate to the first record which meets the query criteria and retrieve
        * it into its record buffer.  Override both the default substitution
        * parameters, and the default lock type with those provided.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the last record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if no record
        *          could be found when operating in standalone mode.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean first(Object[] values, LockType lockType)
       {
          if (!prepareFetch())
          {
             if (throwExceptionOnFatalError())
             {
                // the message is not actually used, but will make things easier when debugging
                String msg = "Query aborted due a fatal error: " + ErrorManager.getErrorText(1).toStringMessage();
                List<QueryOffEndListener> listeners = getOffEndListeners();
                throw new QueryOffEndException(msg, listeners.toArray(new QueryOffEndListener[listeners.size()]));
             }
             else
             {
                errorFindFirstLast();
                return false;
             }
          }

          if (outer)
          {
             // reset now the helper. Another one will be created if any of the arguments are null.
             // Otherwise, the not-null helper will be used if already constructed.
             helper = null;
          }

          if (getHelper().getFQLPreprocessor().isFindByRowid())
          {
             // direct record access using recid/rowid is behaving like unique for all search types
             boolean available = unique(values, lockType);

             if (available)
             {
                recordAlreadyFound = true;
             }
             return available;
          }

          // If a lock type is not specified, assume the query's lock type.
          if (lockType == null)
          {
             lockType = this.lockType;
          }

          Record dmo = null;

          try
          {
             // if query is scrolling, check the cached result list first
             boolean scrolling = (cursor != null);
             if (scrolling)
             {
                dmo = loadByValue(cursor.getFirst(), lockType);
             }

             // if not scrolling or wasn't in the cached result list, perform the query and cache the
             // result (if scrolling)
             if (dmo == null)
             {
                activateFirst();
                dmo = execute(values, lockType, false, true);
                if (scrolling)
                {
                   cursor.addResultFirst(getIDs(dmo));
                }
             }

             finalizeFind(lockType, dmo, this::errorFindFirstLast, false);

             if (dmo == null)
             {
                return false;
             }
          }
          catch (QueryOffEndException exc)
          {
             // rethrow without buffer release; this catch block is here, so we don't hit the
             // ErrorConditionException catch block (QOEE's ancestor) and release the buffer
             throw exc;
          }
          catch (ErrorConditionException exc)
          {
             // if the exception is set to force an override of silent error mode behavior, it is
             // because we encountered a validation error while flushing a record from the buffer
             // to make way for the query result; don't try to release/flush the invalid record
             // again here
             if (!exc.isForce())
             {
                buffer.release(false);
             }

             throw exc;
          }

          return true;
       }

       /**
        * Navigate to the first record which meets the query criteria and retrieve
        * it into its record buffer.  Use the default lock type, but override the
        * default substitution parameters with those provided.
        * <p>
        * This operation is executed in silent mode.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        *
        * @return  <code>true</code> if query could retrieve the last record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if no record
        *          could be found when operating in standalone mode.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean silentFirst(Object[] values)
       {
          return silentFirst(values, lockType);
       }

       /**
        * Navigate to the first record which meets the query criteria and retrieve
        * it into its record buffer.  Override both the default substitution
        * parameters, and the default lock type with those provided.
        * <p>
        * This operation is executed in silent mode.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the last record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if no record
        *          could be found when operating in standalone mode.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean silentFirst(Object[] values, LockType lockType)
       {
          ErrorHelper eh = getErrorHelper();
          boolean wasSilent = eh.isSilent();
          if (!wasSilent)
          {
             eh.setSilent(true);
          }

          boolean wasLenientOffEnd = isLenientOffEnd();
          if (!wasLenientOffEnd)
          {
             setLenientOffEnd(true);
          }

          try
          {
             return first(values, lockType);
          }
          catch (LegacyErrorException lex)
          {
             ErrorManager.processLegacyErrorException(lex, eh);
          }
          catch (ErrorConditionException err)
          {
             // normally we just eat the exception because it is being used to abort processing
             // exactly where the error occurred, which will restart processing after the given
             // code block; however, there is an exception for some deferred error processing use
             // cases where we must bypass silent error mode (throwError(NumberedException, boolean))
             if (err.isForce())
             {
                // don't honor silent mode, no suppression of errors here
                throw err;
             }
          }
          finally
          {
             // disable further silent error processing
             if (!wasSilent)
             {
                eh.setSilent(false);
             }

             if (!wasLenientOffEnd)
             {
                setLenientOffEnd(false);
             }

             eh.handleForwardPending();
          }

          return eh.getErrorStatus();
       }

       /**
        * Navigate to the last record which meets the query criteria and retrieve
        * it into its record buffer.  Use the default substitution parameters and
        * lock type.
        *
        * @return  <code>true</code> if query could retrieve the last record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if no record
        *          could be found when operating in standalone mode.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean last()
       {
          return last(getCurrentArgs(), lockType);
       }

       /**
        * Navigate to the last record which meets the query criteria and retrieve
        * it into its record buffer.  Use the default lock type, but override the
        * default substitution parameters with those provided.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        *
        * @return  <code>true</code> if query could retrieve the last record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if no record
        *          could be found when operating in standalone mode.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean last(Object[] values)
       {
          return last(values, lockType);
       }

       /**
        * Navigate to the last record which meets the query criteria and retrieve
        * it into its record buffer.  Use the default substitution parameters, but
        * override the default lock type with that provided.
        *
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the last record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if no record
        *          could be found when operating in standalone mode.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean last(LockType lockType)
       {
          return last(getCurrentArgs(), lockType);
       }

       /**
        * Navigate to the last record which meets the query criteria and retrieve
        * it into its record buffer.  Override both the default substitution
        * parameters, and the default lock type with those provided.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the last record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if no record
        *          could be found when operating in standalone mode.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean last(Object[] values, LockType lockType)
       {
          if (!prepareFetch())
          {
             if (throwExceptionOnFatalError())
             {
                // the message is not actually used, but will make things easier when debugging
                String msg = "Query aborted due a fatal error: " + ErrorManager.getErrorText(1).toStringMessage();
                List<QueryOffEndListener> listeners = getOffEndListeners();
                throw new QueryOffEndException(msg, listeners.toArray(new QueryOffEndListener[listeners.size()]));
             }
             else
             {
                errorFindFirstLast();
                return false;
             }
          }

          if (outer)
          {
             // reset now the helper. Another one will be created if any of the arguments are null.
             // Otherwise, the not-null helper will be used if already constructed.
             helper = null;
          }

          if (getHelper().getFQLPreprocessor().isFindByRowid())
          {
             // direct record access using recid/rowid is behaving like unique for all search types
             boolean avail = unique(values, lockType);

             if (avail)
             {
                recordAlreadyFound = true;
             }
             return avail;
          }

          // If a lock type is not specified, assume the query's lock type.
          if (lockType == null)
          {
             lockType = this.lockType;
          }

          Record dmo = null;

          try
          {
             // If query is scrolling, check the cached result list first.
             boolean scrolling = (cursor != null);
             if (scrolling)
             {
                dmo = loadByValue(cursor.getLast(), lockType);
             }

             // If not scrolling or wasn't in the cached result list, perform the
             // query and cache the result (if scrolling).
             if (dmo == null)
             {
                activateLast();
                dmo = execute(values, lockType, false, true);
                if (scrolling)
                {
                   cursor.addResultLast(getIDs(dmo));
                }
             }

             // store result in the record buffer, even if null.
             finalizeFind(lockType, dmo, this::errorFindFirstLast, false);

             if (dmo == null)
             {
                return false;
             }
          }
          catch (QueryOffEndException exc)
          {
             // rethrow without buffer release; this catch block is here, so we don't hit the
             // ErrorConditionException catch block (QOEE's ancestor) and release the buffer
             throw exc;
          }
          catch (ErrorConditionException exc)
          {
             // if the exception is set to force an override of silent error mode behavior, it is
             // because we encountered a validation error while flushing a record from the buffer
             // to make way for the query result; don't try to release/flush the invalid record
             // again here
             if (!exc.isForce())
             {
                buffer.release(false);
             }

             throw exc;
          }

          return true;
       }

       /**
        * Navigate to the last record which meets the query criteria and retrieve
        * it into its record buffer.  Use the default lock type, but override the
        * default substitution parameters with those provided.
        * <p>
        * This operation is executed in silent mode.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        *
        * @return  <code>true</code> if query could retrieve the last record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if no record
        *          could be found when operating in standalone mode.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean silentLast(Object[] values)
       {
          return silentLast(values, lockType);
       }

       /**
        * Navigate to the last record which meets the query criteria and retrieve
        * it into its record buffer.  Override both the default substitution
        * parameters, and the default lock type with those provided.
        * <p>
        * This operation is executed in silent mode.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the last record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if no record
        *          could be found when operating in standalone mode.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean silentLast(Object[] values, LockType lockType)
       {
          ErrorHelper eh = getErrorHelper();
          boolean wasSilent = eh.isSilent();
          if (!wasSilent)
          {
             eh.setSilent(true);
          }

          boolean wasLenientOffEnd = isLenientOffEnd();
          if (!wasLenientOffEnd)
          {
             setLenientOffEnd(true);
          }

          try
          {
             return last(values, lockType);
          }
          catch (LegacyErrorException lex)
          {
             ErrorManager.processLegacyErrorException(lex, eh);
          }
          catch (ErrorConditionException err)
          {
             // normally we just eat the exception because it is being used to abort processing
             // exactly where the error occurred, which will restart processing after the given
             // code block; however, there is an exception for some deferred error processing use
             // cases where we must bypass silent error mode (throwError(NumberedException, boolean))
             if (err.isForce())
             {
                // don't honor silent mode, no suppression of errors here
                throw err;
             }
          }
          finally
          {
             // disable further silent error processing
             if (!wasSilent)
             {
                eh.setSilent(false);
             }

             if (!wasLenientOffEnd)
             {
                setLenientOffEnd(false);
             }

             eh.handleForwardPending();
          }

          return eh.getErrorStatus();
       }

       /**
        * Navigate to the next record which meets the query criteria and retrieve
        * it into its record buffer.  Use the default substitution parameters and
        * lock type.
        * <p>
        * Use the record most recently loaded into the backing buffer as the
        * reference point when determining the next record to visit.  If no
        * record has yet been loaded into the buffer, retrieve the first record
        * which meets the query criteria.
        *
        * @return  <code>true</code> if query could retrieve the next record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval.
        * @throws  QueryOffEndException
        *          if no record could be found.
        */
       public boolean next()
       {
          return next(getCurrentArgs(), lockType);
       }

       /**
        * Navigate to the next record which meets the query criteria and retrieve
        * it into its record buffer.  Use the default lock type, but override the
        * default substitution parameters with those provided.
        * <p>
        * Use the record most recently loaded into the backing buffer as the
        * reference point when determining the next record to visit.  If no
        * record has yet been loaded into the buffer, retrieve the first record
        * which meets the query criteria.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        *
        * @return  <code>true</code> if query could retrieve the next record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval.
        * @throws  QueryOffEndException
        *          if no record could be found.
        */
       public boolean next(Object[] values)
       {
          return next(values, lockType);
       }

       /**
        * Navigate to the next record which meets the query criteria and retrieve
        * it into its record buffer.  Use the default substitution parameters, but
        * override the default lock type with that provided.
        * <p>
        * Use the record most recently loaded into the backing buffer as the
        * reference point when determining the next record to visit.  If no
        * record has yet been loaded into the buffer, retrieve the first record
        * which meets the query criteria.
        *
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the next record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval.
        * @throws  QueryOffEndException
        *          if no record could be found.
        */
       public boolean next(LockType lockType)
       {
          return next(getCurrentArgs(), lockType);
       }

       /**
        * Navigate to the next record which meets the query criteria and retrieve
        * it into its record buffer.  Override both the default substitution
        * parameters, and the default lock type with those provided.
        * <p>
        * Use the record most recently loaded into the backing buffer as the
        * reference point when determining the next record to visit.  If no
        * record has yet been loaded into the buffer, retrieve the first record
        * which meets the query criteria.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the next record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval.
        * @throws  QueryOffEndException
        *          if no record could be found and <code>lenientOffEnd</code> is
        *          <code>false</code>.
        */
       public boolean next(Object[] values, LockType lockType)
       {
          if (!prepareFetch())
          {
             return false;
          }

          if (outer)
          {
             // reset now the helper. Another one will be created if any of the arguments are null.
             // Otherwise, the not-null helper will be used if already constructed.
             helper = null;
          }

          if (getHelper().getFQLPreprocessor().isFindByRowid())
          {
             // if the record was found in a previous iteration, raise the OFF-END exception
             if (recordAlreadyFound)
             {
                updateBuffer(null, lockType, false, OffEnd.BACK);
                referenceRecord = null;

                // this is required to always be throw for a 'by-rowid' query, be it a FOR EACH or a FIND.
                // previously AdaptiveQuery$DynamicResults was setting 'lenientOffEnd' flag at the RAQ field,
                // but the read was done via the getter at AbstractQuery, so the wrong value was read, which
                // was always false.
                throw new QueryOffEndException("", null);
             }

             // direct record access using recid/rowid is behaving like unique for all search types
             boolean avail = unique(values, lockType);

             if (avail)
             {
                recordAlreadyFound = true;
             }
             return avail;
          }

          // If a lock type is not specified, assume the query's lock type.
          if (lockType == null)
          {
             lockType = this.lockType;
          }

          Record dmo = null;

          try
          {
             // If query is scrolling, check the cached result list first.
             boolean scrolling = (cursor != null);
             if (scrolling)
             {
                dmo = loadByValue(cursor.getNext(), lockType);
             }

             // If not scrolling or wasn't in the cached result list, perform the
             // query and cache the result (if scrolling).
             if (dmo == null)
             {
                OffEnd offEnd = buffer.getOffEnd(getHelper().getSortIndex(), inverseSorting);
                boolean isFirst;
                if (offEnd != OffEnd.NONE &&  // we have off-end for the *same* database index
                    !buffer.isAvailable())
                {
                   isFirst = true;
                   buffer.resetSnapshot();
                }
                else
                {
                   isFirst = (buffer.getSnapshot() == null &&
                              referenceRecord == null      &&
                              offEnd == OffEnd.FRONT);
                }

                dmo = findNext(values, lockType, true);
                if (scrolling)
                {
                   if (isFirst)
                   {
                      cursor.addResultFirst(getIDs(dmo));
                   }
                   else
                   {
                      cursor.addResultNext(getIDs(dmo));
                   }
                }
             }

             // Store result in the record buffer, even if null.
             OffEnd offEnd = (dmo == null ? OffEnd.BACK : OffEnd.NONE);
             updateBuffer(dmo, lockType, false, offEnd);
             referenceRecord = dmo;

             // Notify accumulators of an iteration.
             accumulate();
             if (dmo == null)
             {
                return false;
             }
          }
          catch (QueryOffEndException exc)
          {
             if (!lenientOffEnd)
             {
                throw exc;
             }

             return false;
          }
          catch (ErrorConditionException exc)
          {
             // if the exception is set to force an override of silent error mode behavior, it is
             // because we encountered a validation error while flushing a record from the buffer
             // to make way for the query result; don't try to release/flush the invalid record
             // again here
             if (!exc.isForce())
             {
                buffer.release(false);
             }

             throw exc;
          }

          return true;
       }

       /**
        * Navigate to the next record which meets the query criteria and retrieve
        * it into its record buffer.  Use the default lock type, but override the
        * default substitution parameters with those provided.
        * <p>
        * Use the record most recently loaded into the backing buffer as the
        * reference point when determining the next record to visit.  If no
        * record has yet been loaded into the buffer, retrieve the first record
        * which meets the query criteria.
        * <p>
        * This operation is executed in silent mode.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        *
        * @return  <code>true</code> if query could retrieve the next record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval.
        * @throws  QueryOffEndException
        *          if no record could be found.
        */
       public boolean silentNext(Object[] values)
       {
          return silentNext(values, lockType);
       }

       /**
        * Navigate to the next record which meets the query criteria and retrieve
        * it into its record buffer.  Override both the default substitution
        * parameters, and the default lock type with those provided.
        * <p>
        * Use the record most recently loaded into the backing buffer as the
        * reference point when determining the next record to visit.  If no
        * record has yet been loaded into the buffer, retrieve the first record
        * which meets the query criteria.
        * <p>
        * This operation is executed in silent mode.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the next record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval.
        * @throws  QueryOffEndException
        *          if no record could be found.
        */
       public boolean silentNext(Object[] values, LockType lockType)
       {
          ErrorHelper eh = getErrorHelper();
          boolean wasSilent = eh.isSilent();
          if (!wasSilent)
          {
             eh.setSilent(true);
          }

          boolean wasLenientOffEnd = isLenientOffEnd();
          if (!wasLenientOffEnd)
          {
             setLenientOffEnd(true);
          }

          try
          {
             return next(values, lockType);
          }
          catch (LegacyErrorException lex)
          {
             ErrorManager.processLegacyErrorException(lex, eh);
          }
          catch (ErrorConditionException err)
          {
             // normally we just eat the exception because it is being used to abort processing
             // exactly where the error occurred, which will restart processing after the given
             // code block; however, there is an exception for some deferred error processing use
             // cases where we must bypass silent error mode (throwError(NumberedException, boolean))
             if (err.isForce())
             {
                // don't honor silent mode, no suppression of errors here
                throw err;
             }
          }
          finally
          {
             // disable further silent error processing
             if (!wasSilent)
             {
                eh.setSilent(false);
             }

             if (!wasLenientOffEnd)
             {
                setLenientOffEnd(false);
             }

             eh.handleForwardPending();
          }

          return eh.getErrorStatus();
       }

       /**
        * Navigate to the previous record which meets the query criteria and
        * retrieve it into its record buffer.  Use the default substitution
        * parameters and lock type.
        * <p>
        * Use the record most recently loaded into the backing buffer as the
        * reference point when determining the previous record to visit.  If no
        * record has yet been loaded into the buffer, retrieve the last record
        * which meets the query criteria.
        *
        * @return  <code>true</code> if query could retrieve the previous record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval.
        * @throws  QueryOffEndException
        *          if no record could be found.
        */
       public boolean previous()
       {
          return previous(getCurrentArgs(), lockType);
       }

       /**
        * Navigate to the previous record which meets the query criteria and
        * retrieve it into its record buffer.  Use the default lock type, but
        * override the default substitution parameters with those provided.
        * <p>
        * Use the record most recently loaded into the backing buffer as the
        * reference point when determining the previous record to visit.  If no
        * record has yet been loaded into the buffer, retrieve the last record
        * which meets the query criteria.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        *
        * @return  <code>true</code> if query could retrieve the previous record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval.
        * @throws  QueryOffEndException
        *          if no record could be found.
        */
       public boolean previous(Object[] values)
       {
          return previous(values, lockType);
       }

       /**
        * Navigate to the previous record which meets the query criteria and
        * retrieve it into its record buffer.  Use the default substitution
        * parameters, but override the default lock type with that provided.
        * <p>
        * Use the record most recently loaded into the backing buffer as the
        * reference point when determining the previous record to visit.  If no
        * record has yet been loaded into the buffer, retrieve the last record
        * which meets the query criteria.
        *
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the previous record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval.
        * @throws  QueryOffEndException
        *          if no record could be found.
        */
       public boolean previous(LockType lockType)
       {
          return previous(getCurrentArgs(), lockType);
       }

       /**
        * Navigate to the previous record which meets the query criteria and
        * retrieve it into its record buffer.  Override both the default
        * substitution parameters, and the default lock type with those provided.
        * <p>
        * Use the record most recently loaded into the backing buffer as the
        * reference point when determining the previous record to visit.  If no
        * record has yet been loaded into the buffer, retrieve the last record
        * which meets the query criteria.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the previous record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval.
        * @throws  QueryOffEndException
        *          if no record could be found and <code>lenientOffEnd</code> is
        *          <code>false</code>.
        */
       public boolean previous(Object[] values, LockType lockType)
       {
          if (!prepareFetch())
          {
             return false;
          }

          if (outer)
          {
             // reset now the helper. Another one will be created if any of the arguments are null.
             // Otherwise, the not-null helper will be used if already constructed.
             helper = null;
          }

          if (getHelper().getFQLPreprocessor().isFindByRowid())
          {
             // if the record was found in a previous iteration, raise the OFF-END exception
             if (recordAlreadyFound)
             {
                updateBuffer(null, lockType, false, OffEnd.FRONT);
                referenceRecord = null;

                // this is required to always be throw for a 'by-rowid' query, be it a FOR EACH or a FIND.
                // previously AdaptiveQuery$DynamicResults was setting 'lenientOffEnd' flag at the RAQ field,
                // but the read was done via the getter at AbstractQuery, so the wrong value was read, which
                // was always false.
                throw new QueryOffEndException("", null);
             }

             // direct record access using recid/rowid is behaving like unique for all search types
             boolean avail = unique(values, lockType);

             if (avail)
             {
                recordAlreadyFound = true;
             }
             return avail;
          }

          // If a lock type is not specified, assume the query's lock type.
          if (lockType == null)
          {
             lockType = this.lockType;
          }

          Record dmo = null;

          try
          {
             // If query is scrolling, check the cached result list first.
             boolean scrolling = (cursor != null);
             if (scrolling)
             {
                dmo = loadByValue(cursor.getPrevious(), lockType);
             }

             // If not scrolling or wasn't in the cached result list, perform the
             // query and cache the result (if scrolling).
             if (dmo == null)
             {
                OffEnd offEnd = buffer.getOffEnd(getHelper().getSortIndex(), inverseSorting || buffer.isOffEndReversible());
                boolean isLast;
                if (offEnd != OffEnd.NONE &&  // we have off-end for the *same* database index
                    !buffer.isAvailable())
                {
                   isLast = true;
                   buffer.resetSnapshot();
                }
                else
                {
                   isLast= offEnd == OffEnd.BACK ||
                           (buffer.getSnapshot() == null &&
                            referenceRecord == null &&
                            offEnd == OffEnd.FRONT);
                }

                dmo = findPrevious(values, lockType, true);
                if (scrolling)
                {
                   if (isLast)
                   {
                      cursor.addResultLast(getIDs(dmo));
                   }
                   else
                   {
                      cursor.addResultPrevious(getIDs(dmo));
                   }
                }
             }

             // Store result in the record buffer, even if null.
             OffEnd offEnd = (dmo == null ? OffEnd.FRONT : OffEnd.NONE);
             updateBuffer(dmo, lockType, false, offEnd);
             referenceRecord = dmo;

             // Notify accumulators of an iteration.
             accumulate();

             if (dmo == null)
             {
                return false;
             }
          }
          catch (QueryOffEndException exc)
          {
             if (!lenientOffEnd)
             {
                throw exc;
             }

             return false;
          }
          catch (ErrorConditionException exc)
          {
             // if the exception is set to force an override of silent error mode behavior, it is
             // because we encountered a validation error while flushing a record from the buffer
             // to make way for the query result; don't try to release/flush the invalid record
             // again here
             if (!exc.isForce())
             {
                buffer.release(false);
             }

             throw exc;
          }
          finally
          {
             // Reset dirty flag.
             dirtyCopy = false;
          }

          return true;
       }

       /**
        * Navigate to the previous record which meets the query criteria and
        * retrieve it into its record buffer.  Use the default lock type, but
        * override the default substitution parameters with those provided.
        * <p>
        * Use the record most recently loaded into the backing buffer as the
        * reference point when determining the previous record to visit.  If no
        * record has yet been loaded into the buffer, retrieve the last record
        * which meets the query criteria.
        * <p>
        * This operation is executed in silent mode.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        *
        * @return  <code>true</code> if query could retrieve the previous record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval.
        * @throws  QueryOffEndException
        *          if no record could be found.
        */
       public boolean silentPrevious(Object[] values)
       {
          return silentPrevious(values, lockType);
       }

       /**
        * Navigate to the previous record which meets the query criteria and
        * retrieve it into its record buffer.  Override both the default
        * substitution parameters, and the default lock type with those provided.
        * <p>
        * Use the record most recently loaded into the backing buffer as the
        * reference point when determining the previous record to visit.  If no
        * record has yet been loaded into the buffer, retrieve the last record
        * which meets the query criteria.
        * <p>
        * This operation is executed in silent mode.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the previous record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval.
        * @throws  QueryOffEndException
        *          if no record could be found.
        */
       public boolean silentPrevious(Object[] values, LockType lockType)
       {
          ErrorHelper eh = getErrorHelper();
          boolean wasSilent = eh.isSilent();
          if (!wasSilent)
          {
             eh.setSilent(true);
          }

          boolean wasLenientOffEnd = isLenientOffEnd();
          if (!wasLenientOffEnd)
          {
             setLenientOffEnd(true);
          }

          try
          {
             return previous(values, lockType);
          }
          catch (LegacyErrorException lex)
          {
             ErrorManager.processLegacyErrorException(lex, eh);
          }
          catch (ErrorConditionException err)
          {
             // normally we just eat the exception because it is being used to abort processing
             // exactly where the error occurred, which will restart processing after the given
             // code block; however, there is an exception for some deferred error processing use
             // cases where we must bypass silent error mode (throwError(NumberedException, boolean))
             if (err.isForce())
             {
                // don't honor silent mode, no suppression of errors here
                throw err;
             }
          }
          finally
          {
             // disable further silent error processing
             if (!wasSilent)
             {
                eh.setSilent(false);
             }

             if (!wasLenientOffEnd)
             {
                setLenientOffEnd(false);
             }

             eh.handleForwardPending();
          }

          return eh.getErrorStatus();
       }

       /**
        * Reload the record most recently loaded into the backing record buffer,
        * if any.  Use the default lock type.
        *
        * @return  <code>true</code> if query could retrieve the current record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record reload.
        */
       public boolean current()
       {
          return current(lockType);
       }

       /**
        * Reload the record most recently loaded into the backing record buffer,
        * if any.  Override the default lock type.
        *
        * @param   lockType
        *          New lock type to apply to the record.
        *
        * @return  <code>true</code> if query could retrieve the current record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record reload, or the lock could
        *          not be transitioned to the requested type.
        */
       public boolean current(LockType lockType)
       {
          if (!prepareFetch())
          {
             return false;
          }

          buffer.armCurrentChanged();
          buffer.reload(lockType, isErrorIfNull());
          buffer.updateCurrentChanged();

          return true;
       }

       /**
        * Navigate to a particular record which meets the query criteria and
        * retrieve it into its record buffer.  No more than one record may match
        * the criteria.  Use the default substitution parameters and lock type.
        *
        * @return  <code>true</code> if query could retrieve the unique record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if more than
        *          one record is found which match the criteria.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean unique()
       {
          return unique(getCurrentArgs(), lockType);
       }

       /**
        * Navigate to a particular record which meets the query criteria and
        * retrieve it into its record buffer.  No more than one record may match
        * the criteria.  Use the default lock type, but override the default
        * substitution parameters with those provided.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        *
        * @return  <code>true</code> if query could retrieve the unique record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if more than
        *          one record is found which match the criteria.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean unique(Object[] values)
       {
          return unique(values, lockType);
       }

       /**
        * Navigate to a particular record which meets the query criteria and
        * retrieve it into its record buffer.  No more than one record may match
        * the criteria.  Use the default substitution parameters, but override
        * the default lock type with that provided.
        *
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the unique record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if more than
        *          one record is found which match the criteria.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean unique(LockType lockType)
       {
          return unique(getCurrentArgs(), lockType);
       }

       /**
        * Navigate to a particular record which meets the query criteria and
        * retrieve it into its record buffer.  No more than one record may match
        * the criteria.  Override both the default substitution parameters, and
        * the default lock type with those provided.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the unique record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if more than
        *          one record is found which match the criteria.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean unique(Object[] values, LockType lockType)
       {
          if (!prepareFetch())
          {
             if (throwExceptionOnFatalError())
             {
                // the message is not actually used, but will make things easier when debugging
                String msg = "Query aborted due a fatal error: " + ErrorManager.getErrorText(1).toStringMessage();
                List<QueryOffEndListener> listeners = getOffEndListeners();
                throw new QueryOffEndException(msg, listeners.toArray(new QueryOffEndListener[listeners.size()]));
             }
             else
             {
                buffer.errorNotOnFile();
                return false;
             }
          }

          if (outer)
          {
             // reset now the helper. Another one will be created if any of the arguments are null.
             // Otherwise, the not-null helper will be used if already constructed.
             helper = null;
          }

          FQLHelper cachedHelper = getHelper();
          FQLPreprocessor fqlPreprocessor = cachedHelper.getFQLPreprocessor();
          if (!buffer.isTemporary() &&
              cachedHelper.isFindTemplate(values, buffer.getDMOImplementationClass()))
          {
             buffer.loadTemplateRecord(fqlPreprocessor.getFindByRowid(values));
             referenceRecord = null; // reset reference record
             dirtyCopy = false;      // reset dirty flag
             return true;
          }
          else if (fqlPreprocessor.isFindByRowid() && fqlPreprocessor.getFindByRowid(values) < 0)
          {
             // according to article P12127, looking for an illegal rowid is equivalent to releasing
             // the buffer. Backstage, error 18 is printed in database log but this is not an effect
             // we need to duplicate in P2J.
             buffer.release(true);
             referenceRecord = null; // reset reference record
             dirtyCopy = false;      // reset dirty flag
             if (isErrorIfNull())
             {
                buffer.errorNotOnFile();
             }
             return false;
          }

          try
          {
             if (lockType == null)
             {
                lockType = this.lockType;
             }

             activateUnique();
             Record dmo = execute(values, lockType, true, true);

             finalizeFind(lockType, dmo, () -> buffer.errorNotOnFile(), true);

             if (dmo == null)
             {
                return false;
             }
          }
          catch (ErrorConditionException exc)
          {
             // if the exception is set to force an override of silent error mode behavior, it is
             // because we encountered a validation error while flushing a record from the buffer
             // to make way for the query result; don't try to release/flush the invalid record again here
             if (!exc.isForce())
             {
                buffer.release(false);
             }

             throw exc;
          }

          return true;
       }

       /**
        * Navigate to a particular record which meets the query criteria and
        * retrieve it into its record buffer.  No more than one record may match
        * the criteria.  Use the default lock type, but override the default
        * substitution parameters with those provided.
        * <p>
        * This operation is executed in silent mode.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        *
        * @return  <code>true</code> if query could retrieve the unique record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if more than
        *          one record is found which match the criteria.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean silentUnique(Object[] values)
       {
          return silentUnique(values, lockType);
       }

       /**
        * Navigate to a particular record which meets the query criteria and
        * retrieve it into its record buffer.  No more than one record may match
        * the criteria.  Override both the default substitution parameters, and
        * the default lock type with those provided.
        * <p>
        * This operation is executed in silent mode.
        *
        * @param   values
        *          Substitution values to use when issuing FQL queries.
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        *
        * @return  <code>true</code> if query could retrieve the unique record successfully;
        *          <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
        *
        * @throws  ErrorConditionException
        *          if an error occurred during record retrieval or if more than
        *          one record is found which match the criteria.
        * @throws  QueryOffEndException
        *          if no record could be found when operating in compound query
        *          mode.
        */
       public boolean silentUnique(Object[] values, LockType lockType)
       {
          ErrorHelper eh = getErrorHelper();
          boolean wasSilent = eh.isSilent();
          if (!wasSilent)
          {
             eh.setSilent(true);
          }

          boolean wasLenientOffEnd = isLenientOffEnd();
          if (!wasLenientOffEnd)
          {
             setLenientOffEnd(true);
          }

          try
          {
             return unique(values, lockType);
          }
          catch (LegacyErrorException lex)
          {
             ErrorManager.processLegacyErrorException(lex, eh);
          }
          catch (ErrorConditionException err)
          {
             // normally we just eat the exception because it is being used to abort processing
             // exactly where the error occurred, which will restart processing after the given
             // code block; however, there is an exception for some deferred error processing use
             // cases where we must bypass silent error mode (throwError(NumberedException, boolean))
             if (err.isForce())
             {
                // don't honor silent mode, no suppression of errors here
                throw err;
             }
          }
          finally
          {
             // disable further silent error processing
             if (!wasSilent)
             {
                eh.setSilent(false);
             }

             if (!wasLenientOffEnd)
             {
                setLenientOffEnd(false);
             }

             eh.handleForwardPending();
          }

          return eh.getErrorStatus();
       }

       /**
        * Set the buffer backing the query to unknown mode.
        *
        * @see     RecordBuffer#setUnknownMode
        */
       public void setUnknownRecord()
       {
          buffer.setUnknownMode();
       }

       /**
        * Clear the current results from the query, so the same query instance can be executed again, without
        * having to {@link #open() re-open} it.
        */
       @Override
       public void clearResults()
       {
          super.clearResults();

          reset(true, true);
       }

       /**
        * 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.
        *
        * @param   resolveArgs
        *          <code>true</code> if query substitution arguments should be
        *          resolved 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)
       {
          if (resolveArgs || forceOrigArgs)
          {
             resolveArgs(forceOrigArgs);
          }
          referenceRecord = null;
          buffer.reset();

          if (cursor != null)
          {
             cursor.reset();
          }

          if (helper != null &&
              helper.getFQLPreprocessor() != null &&
              helper.getFQLPreprocessor().wasInlined())
          {
             helper = null;
             return;
          }
       }

       /**
        * Get the number of tables joined by this query.
        *
        * @return  Always 1.
        */
       public int getTableCount()
       {
          return 1;
       }

       /**
        * Get all the off-end listeners associated with this query.
        *
        * @return  the off-end listeners associated with this query.
        */
       public ArrayList<QueryOffEndListener> getOffEndListeners()
       {
          ArrayList<QueryOffEndListener> listeners = new ArrayList<>(1);
          listeners.add(buffer.getQueryOffEndListener());

          return listeners;
       }

       /**
        * Load a record into its associated buffer, given an array of size one,
        * containing a primary key ID value of the record to be loaded, or the DMO
        * itself.
        * <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
        * into {@link RecordBuffer#setUnknownMode() unknown mode}. Note that a
        * record being locked by another session does not constitute a "missing"
        * record.
        *
        * @param   data
        *          Array containing a single primary key ID or DMO.
        * @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.
        *
        * @see     #getRow()
        */
       public void load(Object[] data, LockType lockType, boolean silentIfNullId)
       throws PersistenceException
       {
          try
          {
             LockType lt = (lockType != null ? lockType : this.lockType);
             verifyScrolling();
             Long[] ids = new Long[1];
             Object datum = data[0];
             ids[0] = datum instanceof Record ? ((Record) datum).primaryKey() : (Long) datum;

             if (cursor.reposition(ids, false))
             {
                cursor.next();
             }

             Record dmo = loadByValue(data, lt, silentIfNullId);
             OffEnd offEnd = (dmo == null ? OffEnd.BACK : OffEnd.NONE);
             updateBuffer(dmo, lt, silentIfNullId, offEnd);

             if (dmo == null)
             {
                buffer.setUnknownMode();

                if (!(datum == null && silentIfNullId))
                {
                   String table = buffer.getTable();
                   RecordIdentifier<String> ident = new RecordIdentifier<>(table, ids[0]);

                   throw new MissingRecordException(ident);
                }
             }
          }
          finally
          {
             // Reset dirty flag.
             dirtyCopy = false;
          }
       }

       /**
        * Set the given results object into this query.  Only works if cursor is not null.
        *
        * @param   results
        *          New result set for this query.
        */
       public void setResults(Results results)
       {
          if (cursor != null)
          {
             cursor.reset();
             results.reset();
             while (results.next())
             {
                cursor.addResultNext(results.get());
             }
             results.reset();
          }
       }

       /**
        * Assemble an array of size one and store in it the primary key ID for
        * the current record in the buffer underlying this query.  This method
        * essentially takes a lightweight snapshot of the currently loaded
        * record, so that this may be restored later.
        *
        * @return  Array containing a single primary key ID.
        *
        * @see     #load
        */
       public Object[] getRow()
       {
          Record dmo = referenceRecord != null ? referenceRecord : buffer.getSnapshot();
          if (dmo == null && !buffer.isUnknownMode())
          {
             throw new NullPointerException("No reference record available");
          }

          return (new Object[] { dmo });
       }

       /**
        * Fetch the array of primary key IDs associated with the first result in
        * this query's result list.
        * <p>
        * This implementation always returns an array with a single element,
        * (or <code>null</code>), since this query type uses a single record
        * buffer.
        *
        * @return  Array of record IDs reflecting the first query result, or
        *          <code>null</code> if there is no such result.
        */
       public Object[] peekFirst()
       {
          if (!prepareFetch())
          {
             return null;
          }

          buffer.release(false);
          activateFirst();

          referenceRecord = execute(getCurrentArgs(), LockType.NONE, false, false);
          if (referenceRecord == null)
          {
             return null;
          }

          return new Object[] { referenceRecord };
       }

       /**
        * Fetch the array of primary key IDs associated with the last result in
        * this query's result list.
        * <p>
        * This implementation always returns an array with a single element,
        * (or <code>null</code>), since this query type uses a single record
        * buffer.
        *
        * @return  Array of record IDs reflecting the last query result, or
        *          <code>null</code> if there is no such result.
        */
       public Object[] peekLast()
       {
          if (!prepareFetch())
          {
             return null;
          }

          buffer.release(false);
          activateLast();

          referenceRecord = execute(getCurrentArgs(), LockType.NONE, false, false);
          if (referenceRecord == null)
          {
             return null;
          }

          return new Object[] { referenceRecord };
       }

       /**
        * Fetch the array of primary key IDs associated with the next result in
        * this query's result list.
        * <p>
        * This implementation always returns an array with a single element,
        * (or <code>null</code>), since this query type uses a single record
        * buffer.
        *
        * @return  Array of record IDs reflecting the next query result, or
        *          <code>null</code> if there is no such result.
        */
       public Object[] peekNext()
       {
          if (!prepareFetch())
          {
             return null;
          }

          buffer.release(false);
          referenceRecord = findNext(getCurrentArgs(), LockType.NONE, false);
          if (referenceRecord == null)
          {
             return null;
          }

          return (new Object[] { referenceRecord });
       }

       /**
        * Fetch the array of primary key IDs associated with the previous result
        * in this query's result list.
        * <p>
        * This implementation always returns an array with a single element,
        * (or <code>null</code>), since this query type uses a single record
        * buffer.
        *
        * @return  Array of record IDs reflecting the previous query result, or
        *          <code>null</code> if there is no such result.
        */
       public Object[] peekPrevious()
       {
          if (!prepareFetch())
          {
             return null;
          }

          buffer.release(false);
          referenceRecord = findPrevious(getCurrentArgs(), LockType.NONE, false);
          if (referenceRecord == null)
          {
             return null;
          }

          return (new Object[] { referenceRecord });
       }

       /**
        * Get an array of all record buffers managed by this query.
        *
        * @return  All record buffers managed by this query.
        */
       @Override
       public RecordBuffer[] getRecordBuffers()
       {
          return (new RecordBuffer[] { buffer });
       }

       /**
        * Get the query substitution arguments associated with this query, if any.
        *
        * @return  Substitution arguments, in order, or <code>null</code> if there are none.
        */
       @Override
       public Object[] getArgs()
       {
          return dfArgs;
       }

       /**
        * Get the legacy natural join, if any, associated with this query.
        *
        * @return  Legacy join object, or <code>null</code> if none.
        */
       @Override
       public AbstractJoin getJoin()
       {
          return join;
       }

       /**
        * Get the original, FQL where clause associated with this query.
        *
        * @return  Original where clause, or <code>null</code> if there is none.
        */
       @Override
       public String getOriginalWhere()
       {
          return dfWhere;
       }

       /**
        * Force this query to operate in dynamic retrieval mode, if it supports this mode.
        * <p>
        * This implementation does nothing, since this query type always is in dynamic retrieval
        * mode.
        */
       @Override
       public void forceDynamicOperation()
       {
          // no-op
       }

       /**
        * Get the external buffers, if any, associated with this query.
        *
        * @return  Array of external buffers or {@code null}.
        */
       @Override
       public RecordBuffer[] getExternalBuffers()
       {
          return super.getExternalBuffers();
       }

       /**
        * Get the sort phrase associated with this query.
        *
        * @return  Sort phrase.
        */
       public String getSortPhrase()
       {
          return sort;
       }

       /**
        * Create an adaptive query component based on the information in this query (which is
        * presumed to be a single-table query), and given a list of adaptive query components which
        * represent outer, nested query loops, which will perform a server-side join to the
        * innermost, nested query component.
        *
        * @param   joinList
        *          List of adaptive components representing nested query loops which will contain
        *          the adaptive query component returned by this method. The last component in the
        *          list represents the nested query loop immediately containing the returned query
        *          component, with which the join will be made.
        * @param   query
        *          Adaptive query which will manage the server-side join of <code>joinList</code> and
        *          the returned query component.
        * @param   iteration
        *          Iteration type: FIRST, LAST, or NEXT.
        * @param   outer
        *          <code>true</code> if the join should be a left outer join. This type of join is
        *          not supported at the time of this writing, so this parameter should always be
        *          <code>false</code>. It is here to support a planned, future enhancement.
        * @param   fallback
        *          A compound query component to use in the event the multi-table adaptive query
        *          switches from preselect to dynamic retrieval mode.
        *
        * @return  An adaptive query component suitable for a server-side join, based on the
        *          information in this query.
        */
       @Override
       public AdaptiveComponent makeAdaptiveServerJoinComponent(List<AdaptiveComponent> joinList,
                                                                AdaptiveQuery query,
                                                                int iteration,
                                                                boolean outer,
                                                                CompoundComponent fallback)
       {
          // NOTE: dynamic components must be already processed (dfWhere and dfArgs configured)
          QueryComponent.ServerJoinData sjd =
                QueryComponent.prepareServerJoinData(joinList, buffer, join, dfArgs);
          BufferReference inverse = (join != null ? join.getInverse() : null);
          AdaptiveComponent comp = new AdaptiveComponent(query,
                                                         buffer,
                                                         sjd.join,
                                                         dfWhere,
                                                         null,
                                                         sort,
                                                         indexInfo,
                                                         lockType,
                                                         sjd.args,
                                                         iteration,
                                                         outer,
                                                         inverse);
          comp.setReferenceSubs(sjd.referenceSubs);

          // gather all related buffers, including [relatedBuffers] from [sjd] and eventual external
          // buffers from original query component
          Set<RecordBuffer> relatedBuffers = new HashSet<>();
          if (sjd.relatedBuffers != null)
          {
             relatedBuffers.addAll(sjd.relatedBuffers);
          }
          RecordBuffer[] externalBuffers = fallback.getQuery().getExternalBuffers();
          if (externalBuffers != null && externalBuffers.length != 0)
          {
             relatedBuffers.addAll(Arrays.asList(externalBuffers));
          }
          if (!relatedBuffers.isEmpty())
          {
             comp.setRelatedBuffers(relatedBuffers);
          }

          comp.setFallback(fallback);
          if (!joinList.isEmpty())
          {
             comp.setNotTop();
          }

          if (iteration == FIRST || iteration == LAST)
          {
             try
             {
                comp.setIteration(iteration, sort);
             }
             catch (PersistenceException exc)
             {
                // should only happen if we have a runtime programming error or conversion error
                // (or with bad, hand-written business logic)
                throw new IllegalArgumentException(exc);
             }
          }

          return comp;
       }

       /**
        * Create a preselect query component based on the information in this query (which is
        * presumed to be a single-table query), and given a list of preselect query components which
        * represent outer, nested query loops, which will perform a server-side join to the
        * innermost, nested query component.
        *
        * @param   joinList
        *          List of preselect components representing nested query loops which will contain
        *          the preselect query component returned by this method. The last component in the
        *          list represents the nested query loop immediately containing the returned query
        *          component, with which the join will be made.
        * @param   iteration
        *          Iteration type: FIRST, LAST, or NEXT.
        * @param   outer
        *          <code>true</code> if the join should be a left outer join. This type of join is
        *          not supported at the time of this writing, so this parameter should always be
        *          <code>false</code>. It is here to support a planned, future enhancement.
        *
        * @return  A preselect query component suitable for a server-side join, based on the
        *          information in this query.
        */
       @Override
       public QueryComponent makePreselectServerJoinComponent(List<QueryComponent> joinList,
                                                              int iteration,
                                                              boolean outer)
       {
          // NOTE: dynamic components must be already processed (dfWhere and dfArgs configured)
          QueryComponent.ServerJoinData sjd =
                QueryComponent.prepareServerJoinData(joinList, buffer, join, dfArgs);
          QueryComponent comp = new QueryComponent(this,
                                                   buffer,
                                                   sjd.join,
                                                   dfWhere,
                                                   null,
                                                   indexInfo,
                                                   lockType,
                                                   sjd.args,
                                                   iteration,
                                                   outer);
          comp.setReferenceSubs(sjd.referenceSubs);
          comp.setRelatedBuffers(sjd.relatedBuffers);

          if (iteration == FIRST || iteration == LAST)
          {
             try
             {
                comp.setIteration(iteration, sort);
             }
             catch (PersistenceException exc)
             {
                // should only happen if we have a runtime programming error or conversion error
                // (or with bad, hand-written business logic)
                throw new IllegalArgumentException(exc);
             }
          }

          return comp;
       }

       /**
        * Indicate whether this query has a client-side where clause expression associated with it.
        *
        * @return  <code>true</code> if there is a where expression; else <code>false</code>.
        */
       @Override
       public boolean hasWhereExpression()
       {
          return (whereExpr != null);
       }

       /**
        * Report the record buffer for whose changes this object is interested
        * in listening.  This listener will be registered with the {@link
        * ChangeBroker} to receive notifications of any change to DMOs whose
        * interface types match that returned by the {@link
        * RecordBuffer#getDMOInterface} method of the returned buffer.
        *
        * @return  An iterator on a single object:  the record buffer backing this query.
        */
       public Iterator<RecordBuffer> recordBuffers()
       {
          Iterator<RecordBuffer> iter = new Iterator<RecordBuffer>()
          {
             private boolean avail = true;
             public boolean hasNext() { return avail; }
             public RecordBuffer next() { avail = false;  return buffer; }
             public void remove() { throw new UnsupportedOperationException(); }
          };

          return iter;
       }

       /**
        * Register all <code>RecordChangeListener</code>s associated with this
        * query (if any), with the context-local <code>ChangeBroker</code> at the
        * indicated scope.
        *
        * @param   scope
        *          Scope at which listeners should be registered with the change broker.
        */
       @Override
       public void registerRecordChangeListeners(Integer scope)
       {
          ChangeBroker broker = ChangeBroker.get();
          if (scope == null)
          {
             broker.addListener(this);
          }
          else
          {
             broker.addListener(this, scope);
          }

          if (placeholderCleaner != null)
          {
             if (scope == null)
             {
                broker.addListener(placeholderCleaner);
             }
             else
             {
                broker.addListener(this, scope);
             }
          }

          // if these were added as global listeners in ChangeBroker remember to remove them
          // when the query is destroyed. Otherwise, the ChangeBroker will drop them automatically
          // when the respective scope is finished.
          if (scope != null && scope == 0)
          {
             unregisterOnCleanup = true;
          }
       }

       /**
        * Explicitly close the prepared query.
        */
       @Override
       public void close()
       {
          close(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 && buffer == 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;
          }
          if (inResource && !unregisterOnCleanup)
          {
             // remove it explicitly
             ChangeBroker broker = ChangeBroker.get();
             broker.removeListener(this);
          }

          super.close();
       }

       /**
        * The cleanup method is overridden to allow the listener(s) to be removed from the
        * {@code ChangeBroker} if they were added in the global scope, otherwise, the
        * {@code ChangeBroker} will drop them automatically when the respective scope is finished.
        */
       @Override
       public void cleanup()
       {
          recordAlreadyFound = false;

          if (unregisterOnCleanup)
          {
             ChangeBroker broker = ChangeBroker.get();
             broker.removeFromGlobal(this);
             if (placeholderCleaner != null)
             {
                broker.removeFromGlobal(placeholderCleaner);
             }

             unregisterOnCleanup = false;
          }

          // let the super class do the rest of the cleanup
          super.cleanup();
       }

       /**
        * Respond to a record change event.
        *
        * @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
       {
          if (cursor == null)
          {
             return;
          }

          if (event.isInsert())
          {
             cursor.reset();

             return;
          }

          if (event.isDelete())
          {
             int idx = -1;
             RecordBuffer source = event.getSource();
             Iterator<RecordBuffer> iter = recordBuffers();
             for (int i = 0; iter.hasNext() && idx < 0; i++)
             {
                if (source == iter.next())
                {
                   idx = i;
                }
             }

             // Reset the cache if it contains a reference to the deleted DMO.
             if (idx >= 0 && cursor.contains(event.getDMO().primaryKey(), idx))
             {
                cursor.reset();
             }
          }
       }

       /**
        * Get the off-end status of this query, which indicates whether the query has run off either
        * end of its results.
        *
        * @return  One of the {@link OffEnd} constants {@code NONE}, {@code FRONT}, or {@code BACK}.
        */
       public OffEnd getOffEnd()
       {
          if (helper == null)
          {
             return OffEnd.NONE;
          }

          SortIndex si = helper.getSortIndex();

          return buffer.getOffEnd(si, inverseSorting);
       }

       /**
        * Conversion of INDEX-INFORMATION attribute (KW_IDX_INFO).
        *
        * Getter of INDEX-INFORMATION attribute
        *
        * @return A character string consisting of a comma-separated list of the index or
        *         indexes the query uses at the level of join specified.
        */
       @LegacyAttribute(name = "INDEX-INFORMATION")
       public character indexInformation(NumberType n)
       {
          return new character(indexInfo);
       }

       /**
        * Release the current record, if any, from the buffer which is used by
        * this query.
        *
        * @throws  QueryOffEndException
        *          if the buffer contained a record and it was released.
        */
       public void releaseBuffers()
       {
          // this method is called in the context of this query being a component in an iterating
          // query, so do not allow write trigger to fire
          buffer.release(false);
       }

       /**
        * Adds a new constraint term to the where predicate of this query, effectively filtering out
        * any rows whose referenced field does not match the requested value. Semantically, the query
        * will select rows that match
        * <p>
        * {@code new-where := (fr = val) AND (old-query)}.
        * <p>
        * Note: The constraints are exclusive meaning that if constraints will be added for same field of same
        *       buffer, the query will return an empty result because the field cannot be at the same time equals
        *       to two different values.
        * <p>
        * Use {@link #clearDynamicFilters} 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 filtering criterion. Only the rows that matches the
        *          specified value for this field will be selected. Must not be {@code null}.
        * @param   val
        *          The filtering value for this constraint. Can be {@code null} or {@code unknown}, in
        *          which case the SQL {@code null} value will be assumed. In this case the predicate
        *          will look like:
        *           <p>
        *          {@code new-where := (fr IS  NULL) AND (old-query)}
        * @param   format
        *          The format to be used when comparing values. The reason for this parameter is that
        *          for some datatype (ex: {@code decimal}) the on-screen value can be different from
        *          the database (because of rounding).
        *
        * @return  {@code true} if the filtering constraint was successfully added. If the field
        *          reference is not related to the buffer(s) of this query, {@code false} is returned.
        */
       @Override
       public boolean addDynamicFilter(FieldReference fr, BaseDataType val, String format)
       {
          if (fr.getParentBuffer() != getBuffer())
          {
             return false; // not my buffer
          }

          if (dynamicFilters == null)
          {
             dynamicFilters = new LinkedHashMap<>(5);
             dynamicFormats = new HashMap<>(5);
          }
          dynamicFilters.remove(fr);
          dynamicFilters.put(fr, val);
          dynamicFormats.put(fr, format);

          StringBuilder sb = new StringBuilder();
          List<Object> dArgs = new ArrayList<>(dynamicFilters.size());
          Iterator<Map.Entry<FieldReference, BaseDataType>> dynamicFilters =
                this.dynamicFilters.entrySet().iterator();
          while (dynamicFilters.hasNext())
          {
             Map.Entry<FieldReference, BaseDataType> filter = dynamicFilters.next();
             BaseDataType bdtVal = filter.getValue();
             String fieldName = filter.getKey().toString();
             boolean isNull = (bdtVal == null) || bdtVal.isUnknown();
             boolean isCharType = bdtVal instanceof Text;
             sb.append("(");

             if (isNull)
             {
                sb.append(fieldName).append(" is null");
             }
             else
             {
                if (isCharType)
                {
                   // the user will not know the case-sensitivity of the database fields, so it is
                   // best to make the filter for text fields always be case-insensitive. This has the
                   // greatest likelihood of using an index (most fields are case-insensitive), and it
                   // is the most user-friendly.
                   sb.append("upper(trim(").append(fieldName).append("))").append(" like ?");
                   String pattern = TextOps.trim(TextOps.toUpperCase((Text) bdtVal)).getValue();
                   bdtVal = new character("%" + pattern + "%");
                }
                else if (bdtVal instanceof decimal)
                {
                   // because of rounding, the displayed value may not be equals to the one from
                   // database. To make sure we filter the right rows, we will use the format matching
                   // the one used by shown data
                   String fmt = dynamicFormats.get(filter.getKey());
                   if (fmt != null)
                   {
                      sb.append("toString(").append(fieldName).append(",'").append(fmt).append("')= ?");
                      bdtVal = new character(bdtVal.toString(fmt));
                   }
                   else
                   {
                      // format not provided, we cannot use it, compare directly instead
                      sb.append(fieldName).append(" = ?");
                   }
                }
                else
                {
                   sb.append(fieldName).append(" = ?");
                }
                dArgs.add(bdtVal);
             }

             if (dynamicFilters.hasNext() || where != null)
             {
                sb.append(") and ");
             }
             else
             {
                sb.append(")");
             }
          }

          if (where != null)
          {
             sb.append("(").append(where).append(")");
          }
          dfWhere = sb.toString();

          int dArgCount = dArgs.size();
          if (args != null && dArgCount != 0)
          {
             dfArgs = new Object[args.length + dArgCount];
             System.arraycopy(dArgs.toArray(), 0, dfArgs, 0, dArgCount);
             System.arraycopy(args, 0, dfArgs, dArgCount, args.length);
          }
          else if (dArgCount != 0)
          {
             dfArgs = new Object[dArgCount];
             dArgs.toArray(dfArgs);
          }
          else
          {
             dfArgs = null;
          }

          helper = null;
          nonNullArgsHelper = null;

          return true;
       }

       /**
        * Clears any dynamically filters added at runtime. The query predicate will be restored to its
        * form from generated at conversion time.
        */
       @Override
       public void clearDynamicFilters()
       {
          if (dynamicFilters == null)
          {
             return;
          }

          // restore the where predicate and parameters:
          dfWhere = where;
          dfArgs = args;

          // cleanup and prepare for re-initialization
          dynamicFilters = null;
          dynamicFormats = null;
          helper = null;
          nonNullArgsHelper = null;
          dynamicFilterArgs = null;
       }

       /**
        * Add one or more buffers to the list of those which this query references externally. These typically
        * will be buffers referenced by converted CAN-FIND expressions embedded in the query's FQL expression,
        * which are not the primary buffers associated with the query.
        * <p>
        * If the bound buffer differs from the definition buffer (see {@link #buffersMatch}) for any of the
        * external buffers, then the {@link #translateWhere} will be enabled (if not already enabled).
        *
        * @param   dmos
        *          One or more external buffers.
        *
        * @return  This query instance.
        */
       @Override
       public P2JQuery addExternalBuffers(DataModelObject... dmos)
       {
          super.addExternalBuffers(dmos);

          if (translateWhere != null)
          {
             // the translation is already registered, external buffers will be automatically picked up
             return this;
          }

          RecordBuffer[] buffers = getExternalBuffers();
          if (buffers == null)
          {
             return this;
          }

          RecordBuffer[] defs = getExternalBuffersDefs();
          boolean translate = false;
          for (int i = 0; i < buffers.length && !translate; i++)
          {
             translate = !buffersMatch(buffers[i], defs[i]);
          }

          if (translate)
          {
             translateWhere = this::getTranslatedWhere;
          }

          return this;
       }

       /**
        * Adds an alternative execution query, for the multi-tenant case in which the CAN-FIND nested buffer and
        * the main buffer of query component do not belong to same physical database.
        * <p>
        * This method will be generated only in case of multi-tenant projects, in the condition above-mentioned.
        *
        * @param   where
        *          FQL where clause.  May be {@code null}.
        * @param   whereExpr
        *          Client-side where clause expression. May be {@code null} but this is probably caused by an
        *          incorrect conversion.
        * @param   sort
        *          The sort order.
        *
        * @return  Self object so that the call to this method can be optionally chained with the constructor.
        */
       @Override
       public RandomAccessQuery setMultiTenantAlternative(String where, Supplier<logical> whereExpr, String sort)
       {
          return setMultiTenantAlternative(where, whereExpr, sort, null);
       }

       /**
        * Adds an alternative execution query, for the multi-tenant case in which the CAN-FIND nested buffer and
        * the main buffer of query component do not belong to same physical database.
        * <p>
        * This method will be generated only in case of multi-tenant projects, in the condition above-mentioned.
        *
        * @param   where
        *          FQL where clause.  May be {@code null}.
        * @param   whereExpr
        *          Client-side where clause expression. May be {@code null} but this is probably caused by an
        *          incorrect conversion.
        * @param   sort
        *          The sort order.
        * @param   args
        *          Substitution parameters for FQL queries. These will be used if not overridden by a record
        *          retrieval method.
        *
        * @return  Self object so that the call to this method can be optionally chained with the constructor.
        */
       @Override
       public RandomAccessQuery setMultiTenantAlternative(String where,
                                                          Supplier<logical> whereExpr,
                                                          String sort,
                                                          Object[] args)
       {
          RecordBuffer[] buffers = getExternalBuffers();
          if (buffers == null || buffers.length == 0)
          {
             return this; // no-op. This should not happen
          }

          Persistence.Context privateContext = buffer.getPersistence().getContext(Persistence.PRIVATE_CTX);
          if (privateContext.getTenantId() == TenantManager.DEFAULT_TENANT_ID)
          {
             // running as default tenant, the query is intra-database (using the default database)
             return this;
          }

          boolean isMt = buffer.getDmoInfo().multiTenant;
          for (RecordBuffer eb : buffers)
          {
             if (eb.getDmoInfo().multiTenant != isMt)
             {
                // if the query is cross-physical database, re-initialize with the alternative execution path
                return initialize(buffer, where, whereExpr, sort, null, null, args, lockType);
             }
          }

          // if we reach this point, all table are either public or all private, so the optimised query can be
          // safely executed
          return this;
       }

       /**
        * Create and return the FQL of this query.
        *
        * This function is used for logging purposes.
        *
        * @return  String
        *          The FQL of the query.
        */
       public String getBundleFql()
       {
          if (activeBundleFql != null)
          {
             return activeBundleFql;
          }

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

          Iterator<String> fqls = activeBundle.statements();
          if (fqls == null || !fqls.hasNext())
          {
             return null;
          }

          StringBuilder fql = new StringBuilder();
          while (true)
          {
             fql.append(fqls.next());
             if (fqls.hasNext())
             {
                fql.append(", ");
             }
             else
             {
                break;
             }
          }

          activeBundleFql = fql.toString();
          return activeBundleFql;
       }

       /**
        * Set the included fields.
        *
        * @param   included
        *          The included properties for each DMO (FIELDS option).
        */
       public void overrideIncludeFields(Map<DataModelObject, Property[]> included)
       {
          this.included = included;
       }

       /**
        * Set the excluded fields.
        *
        * @param   excluded
        *          The excluded properties for each DMO (EXCEPT option).
        */
       public void overrideExcludeFields(Map<DataModelObject, Property[]> excluded)
       {
          this.excluded = excluded;
       }

       /**
        * Prepare to perform a fetch.  This involves checking for a pending error
        * in the error manager and releasing the current buffer record if an
        * error is pending.  Releasing the record may result in an exception
        * being thrown by the record buffer.
        *
        * @return  <code>false</code> if an error is pending, else
        *          <code>true</code>.
        */
       @Override
       protected boolean prepareFetch()
       {
          translateWhere();

          return super.prepareFetch();
       }

       /**
        * Prepare buffer associated with this query for the query's execution.
        * This resets certain per-query state tracked by the record buffer.
        *
        * @throws  ValidationException
        *          if the parent's implementation is invoked and a buffer flush
        *          triggers a validation error for the record currently stored in
        *          the buffer.
        */
       protected void prepareBuffer()
       throws ValidationException
       {
          buffer.flush();
          buffer.setLocked(false);
       }

       /**
        * Update the record buffer with the given DMO, applying the specified
        * lock type and error-if-null setting.  This implementation causes the
        * record buffer's internal placeholder snapshot to be reset if
        * <code>dmo</code> is null.
        *
        * @param   dmo
        *          Record to be stored in buffer.
        * @param   lockType
        *          Lock type associated with the record, which will be remembered
        *          by the buffer.
        * @param   errorIfNull
        *          If <code>true</code>, setting a <code>null</code> value as the
        *          current record in an underlying buffer will raise an error
        *          condition (if not in silent error mode); if <code>false</code>,
        *          this action will raise an end condition instead.
        * @param   offEnd
        *          Enum indicating whether query is off-end, and if so, in which
        *          direction.
        */
       protected void updateBuffer(Record dmo,
                                   LockType lockType,
                                   boolean errorIfNull,
                                   OffEnd offEnd)
       {
          SortIndex sortIndex = getHelper().getSortIndex();
          buffer.setRecord(dmo,
                           lockType,
                           errorIfNull,
                           lenientOffEnd,
                           sortIndex,
                           offEnd,
                           inverseSorting,
                           dirtyCopy,
                           true);

          Buffer bufferImpl = (Buffer) buffer.getDMOProxy();
          if (bufferImpl.autoSynchronize().booleanValue())
          {
             bufferImpl.querySynchronize();
          }
       }

       /**
        * Return an object which represents a property value which defines the
        * end of a <i>sort band</i>.  A <i>sort band</i> is a series of one or
        * more records which share the same value for a particular property,
        * where that property is used as the coarsest sorting criterion defined
        * by this query's <code>order by</code> clause.
        * <p>
        * If the break value for this query currently is non-<code>null</code>,
        * this indicates that the most recently retrieved record has crossed a
        * <i>sort band</i> boundary, and that a new band has begun.  This value
        * will only be non-<code>null</code> at the point at which such a record
        * is found, and will be <code>null</code> for all other records.
        * <p>
        * Note:  break values are only tracked for invocations of
        * <code>next</code> and <code>previous</code>.  Other retrievals will
        * results in break value being <code>null</code>.
        *
        * @return  Break value if dynamic query crossed a sort band boundary,
        *          else <code>null</code>.
        */
       protected Object getBreakValue()
       {
          return breakValue;
       }

       /**
        * Get the record buffer associated with this query.
        *
        * @return  Record buffer.
        */
       protected RecordBuffer getBuffer()
       {
          return buffer;
       }

       /**
        * Retrieve this query's arguments, as currently resolved.
        *
        * @return  Current arguments.
        */
       protected Object[] getCurrentArgs()
       {
          return currentArgs;
       }

       /**
        * Retrieve this query's lock type.
        *
        * @return  Lock type
        */
       protected LockType getLockType()
       {
          return lockType;
       }

       /**
        * Indicate whether a nested query error should be suppressed (i.e., reported only as a warning, but not
        * interrupt control flow), when detected while preprocessing this query's substitution parameters.
        *
        * @return  {@code true}, when this query is used outside of the context of a converted FIND statement.
        */
       protected boolean suppressNestedQueryError()
       {
          return true;
       }

       /**
        * Execute each query stored in the active FQL bundle in turn until we
        * find a result or have no more statements to execute.  Each statement is
        * decreasingly specific.
        * <p>
        * The latest record to be stored in the record buffer, if any, marks our
        * current position in the query's results.  Its data is used to substitute
        * into the placeholder parameters in the query statements' augmented where
        * clauses.  These are added to the <code>values</code> provided as part of
        * the base query.
        * <p>
        * <b>Client-Side Where Clause Handling</b><br>
        * If this query involves a client-side where clause expression, an
        * additional layer of criteria checking takes place.  In addition to the
        * search described above, each returned result is temporarily stored in
        * the associated record buffer, and the where expression is executed.
        * If the expression indicates a match, the record is returned.  Otherwise,
        * additional records are retrieved and tested until either a match is
        * found or no more records are available in the requested navigation
        * direction.
        * <p>
        * Unique queries combined with client-side where clauses require special
        * handling and may perform particularly poorly as a result.  In this
        * case, every record returned by the first-pass query must be tested
        * against the client-side where expression, even once a result has been
        * found.  This must be done to ensure <i>only one</i> record matches the
        * specified criteria.  The only exception is the case where a second
        * record is actually found, in which case the uniqueness requirement has
        * been violated, and the remainder of the scan is aborted due to the
        * error.
        *
        * @param   values
        *          Array of substitution parameter values for the base where
        *          clause (i.e., the un-augmented portion of the clause).
        * @param   lockType
        *          Type of lock to acquire for the found record.
        * @param   unique
        *          <code>true</code> if there should be no more than one match
        *          for the conditions specified;  <code>false</code> if multiple
        *          matches are possible.
        * @param   updateLock
        *          <code>true</code> if the status of the lock on the retrieved
        *          record should be modified;  else <code>false</code>.  This is
        *          set to <code>true</code> for actual retrievals, and to
        *          <code>false</code> when only detecting whether a record would
        *          be found, or when peeking a record.
        *
        * @return  The record resulting from the query, or <code>null</code> if
        *          no record was found.
        *
        * @throws  ErrorConditionException
        *          if a recoverable error occurred while executing.
        */
       protected Record execute(Object[] values, LockType lockType, boolean unique, boolean updateLock)
       {
          if (buffer.isTemporary() && buffer.referenceOnly() && buffer.getMultiplexID() == null)
          {
             BufferReference proxy = buffer.getDMOProxy();
             if (((BufferImpl) proxy).ref() == proxy)
             {
                // not bound?
                ((AbstractTempTable) buffer.getParentTable()).displayUninitialized();
                return null;
             }
          }

          // if the JOINed buffer has no record, then we must return NULL
          if (join != null)
          {
             Record record = join.getInverse().buffer().getCurrentRecord();

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

          if (whereExpr == null)
          {
             FQLPreprocessor fqlPreproc = helper.getFQLPreprocessor();
             RecordIdentifier<String> cacheResult = null;
             FastFindCache.Key key = null;
             boolean simpleQuery = isSimpleQuery();
             String entityName = buffer.getEntityName();
             DirtyShareContext dirtyContext = buffer.getDirtyContext();
             if (DirtyShareSupport.isEnabledCrossSession() &&
                 dirtyContext != null                      &&
                 fqlPreproc.getEarlyPublishEntities().contains(entityName))
             {
                try
                {
                   // we only need to leak entities for the current buffer
                   List<Record> dmos = dirtyContext.updateSnapshots(Collections.singleton(entityName),
                                                                    buffer.getPersistence());
                   for (Record dmo : dmos)
                   {
                      // as only current buffer entities are in scope we are save to refer it
                      ffCache.invalidate(buffer.getDmoInfo().getId(), buffer.getMultiplexID());
                      // Make sure DMOs do not bloat the ORM session.
                      buffer.evictDMOIfUnused(dmo);
                   }
                }
                catch (PersistenceException e)
                {
                   // fall-back to execute method
                }
             }
             // rule out find-by-rowid queries fast. If we already have the record in our session cache,
             // avoid doing extra work on retrieving it from the database or ffcache.
             if (fqlPreproc.isFindByRowid() && whereExpr == null && simpleQuery)
             {
                Session session = buffer.getSession(true);
                try
                {
                   Class<? extends Record> dmoClass = buffer.getDmoInfo().getImplementationClass();
                   Record dmo = session.getCached(dmoClass, fqlPreproc.getFindByRowid(values));
                   // If it is incomplete, let the ffCache or direct-access do its job faster
                   if (dmo != null && !dmo.checkState(DmoState.STALE) && !dmo.checkState(DmoState.INCOMPLETE))
                   {
                      RecordLockContext lockCtx = buffer.getRecordLockContext();
                      lockCtx.lock(new RecordIdentifier<>(buffer.getTable(), dmo.primaryKey()), lockType);
                      return dmo;
                   }
                }
                catch (PersistenceException e)
                {
                   // fall-back to execute method
                }
             }

             // attempt to get the result from fast-find cache, avoiding the expensive database trip.
             // This is not possible and the cache lookup is skipped when:
             //  - in NEXT/PREVIOUS find modes
             //  - the query has a join
             //  - the query has an embedded CAN-FIND
             //  - the query has a client-side where expression
             if (ffCache != null && simpleQuery)
             {
                BitSet properties = fqlPreproc.getQueryProperties();
                key = FastFindCache.createKey(buffer, index, where, properties, activeBundleKey, values);
                Session session = buffer.getSession(true);
                cacheResult = ffCache.get(key);
                if (cacheResult != null)
                {
                   if (cacheResult == FastFindCache.NO_RECORD)
                   {
                      // record will not be found in database
                      return null;
                   }
                   else
                   {
                      try
                      {
                         // if we get a hit, get the requested lock with RecordLockContext
                         RecordLockContext lockCtx = buffer.getRecordLockContext();
                         lockCtx.lock(new RecordIdentifier<>(buffer.getTable(), cacheResult.getRecordID()),
                                      lockType);

                         // the lock operation might have taken some time. Check whether the result still exists
                         // in the cache, now that we hold the lock
                         if (session != null && cacheResult.equals(ffCache.get(key)))
                         {
                            // the result is still in the cache, meaning no one changed the index(es) while we
                            // were acquiring the lock; it is safe to use:
                            try
                            {
                                Record dmo = session.get(cacheResult);
                                if (dmo != null)
                                {
                                   return dmo;
                                }
                            }
                            catch (PersistenceException e)
                            {
                               if (LOG.isLoggable(Level.WARNING))
                               {
                                  LOG.log(Level.WARNING,
                                          "Failed to get fast-find cached value for " + cacheResult,
                                          e);
                               }
                            }
                         }
                      }
                      catch (LockUnavailableException exc)
                      {
                         // in case of a quick-return NO-WAIT lock request: do not throw now error condition 445:
                         // "XXX record is locked", let the normal executeImpl() handle it
                      }
                   }

                   // set result to null so the cache will be updated/refreshed with value from full executeImpl()
                   cacheResult = null;
                }
             }

             Record record = null;

             // use direct-access after ffCache failed
             Optional<Record> directAccessResult = executeDirectAccess(values);
             if (directAccessResult != null)
             {
                record = directAccessResult.isPresent() ? directAccessResult.get() : null;
             }
             else
             {
                try
                {
                   record = P2JQueryExecutor.getInstance().execute(buffer.getDatabase(),
                                                                   (String) null,
                                                                   RandomAccessQuery::executeImpl,
                                                                   this,
                                                                   values,
                                                                   lockType,
                                                                   unique,
                                                                   updateLock);
                }
                catch (PersistenceException exc)
                {
                   ErrorManager.recordOrThrowError(exc);
                }
             }

             if (ffCache != null                                        &&
                 (record == null || record.checkState(DmoState.CACHED)) &&
                 cacheResult == null                                    && // NOTE: always null ?
                 key != null                                            &&
                 !fqlPreproc.isDependsOnSessionAttribute()              &&
                 !dirtyCopy)
             {
                // save the result if cacheable and result was not found in cache, even if null (not found)
                ffCache.put(key, record);
             }

             return record;
          }

          Record dmo = null;
          Record oldRefRec = referenceRecord;
          FQLBundle bundle0 = activeBundle;
          FQLBundle bundle1;
          FQLBundle bundle2;

          // prepare the FQL bundles which will be used in the iterative, first pass scan
          FQLHelper cachedHelper = getHelper();
          BitSet joinKey = getJoinKey();
          switch (activeBundleKey)
          {
             case NEXT:
                bundle1 = bundle2 = cachedHelper.next(joinKey);
                break;
             case PREVIOUS:
                bundle1 = bundle2 = cachedHelper.previous(joinKey);
                break;
             case FIRST:
             case UNIQUE:
                bundle1 = cachedHelper.first(joinKey);
                bundle2 = cachedHelper.next(joinKey);
                break;
             case LAST:
                bundle1 = cachedHelper.last(joinKey);
                bundle2 = cachedHelper.previous(joinKey);
                break;
             default:
                throw new IllegalStateException("No active FQL bundle");
          }

          Persistence persistence = buffer.getPersistence();
          try
          {
             Record uniqueDMO = null;
             buffer.pushTempContext();

             for (int i = 0; ; i++)
             {
                activeBundle = (i == 0 ? bundle1 : bundle2);

                // Find a candidate record, but do not lock it yet.
                dmo = P2JQueryExecutor.getInstance().execute(buffer.getDatabase(),
                                                             (String) null,
                                                             RandomAccessQuery::executeImpl,
                                                             this,
                                                             values,
                                                             LockType.NONE,
                                                             false,
                                                             false);

                if (dmo == null)
                {
                   break;
                }

                referenceRecord = dmo;
                // Execute where expression.
                buffer.setTempRecord(dmo);
                if (whereExpr.get().booleanValue())
                {
                   if (unique)
                   {
                      if (uniqueDMO == null)
                      {
                         uniqueDMO = dmo;
                      }
                      else
                      {
                         persistence.uniqueResultViolation(buffer, null);
                      }
                   }
                   else
                   {
                      break;
                   }
                }
             }

             if (unique)
             {
                dmo = uniqueDMO;
             }
          }
          catch (PersistenceException exc)
          {
             ErrorManager.recordOrThrowError(exc);

             return null;
          }
          finally
          {
             try
             {
                buffer.popTempContext();
             }
             catch (PersistenceException exc)
             {
                ErrorManager.recordOrThrowError(exc);
             }
             referenceRecord = oldRefRec;
             activeBundle = bundle0;
          }

          // If a record was found and successfully passed the client-side where
          // expression filter, lock it now, if necessary.
          if (dmo != null && !lockType.equals(LockType.NONE) && updateLock)
          {
             try
             {
                String table = buffer.getTable();
                RecordIdentifier<String> ident = new RecordIdentifier<>(table, dmo.primaryKey());
                RecordLockContext lockCtx = buffer.getRecordLockContext();
                lockCtx.lock(ident, lockType);
             }
             catch (LockUnavailableException exc)
             {
                ErrorManager.recordOrThrowError(445, buffer.getLegacyName() + " record is locked", exc);

                // Cannot return record if it could not be locked.
                dmo = null;
             }
          }

          return dmo;
       }

       /**
        * This method is invoked when a locking exception is caught during query
        * execution.  This implementation delegates handling of the error to the
        * <code>ErrorManager</code>.
        * <p>
        * Subclasses which require different handling must override this method.
        *
        * @param   exc
        *          Cause exception.
        */
       protected void handleExecuteException(LockUnavailableException exc)
       {
          buffer.setLocked(true);
          ErrorManager.recordOrThrowError(445, buffer.getLegacyName() + " record is locked", exc);
       }

       /**
        * This method is invoked when a validation exception is caught during
        * query execution.  This implementation delegates handling of the error
        * to the <code>ErrorManager</code>.
        * <p>
        * Subclasses which require different handling must override this method.
        *
        * @param   exc
        *          Cause exception.
        */
       protected void handleExecuteException(ValidationException exc)
       {
          ErrorManager.recordOrThrowError(exc);
       }

       /**
        * This method is invoked when a persistence exception is caught during
        * query execution.  This implementation delegates handling of the error
        * to the <code>ErrorManager</code>.
        * <p>
        * Subclasses which require different handling must override this method.
        *
        * @param   exc
        *          Cause exception.
        */
       protected void handleExecuteException(PersistenceException exc)
       {
          ErrorManager.recordOrThrowError(exc);
       }

       /**
        * Obtain the {@link FQLHelper} object which supports this query, creating it first if
        * necessary.
        * <p>
        * In particular, for LEFT OUTER-JOINs, when the helper is reset at each iteration, a new
        * instance will be created unless all the elements from the argument list are not
        * {@code null} / {@code unknown}. In this case the cached instance will be reused.
        *
        * @return  FQL helper.
        */
       protected FQLHelper getHelper()
       {
          if (helper == null)
          {
             boolean hasUnknowns = false;
             if (dfArgs != null)
             {
                for (Object arg : dfArgs)
                {
                   if (arg == null)
                   {
                      hasUnknowns = true;
                      break;
                   }

                   if (arg instanceof FieldReference)
                   {
                      arg = ((FieldReference) arg).resolve(); // NOTE: result is not saved
                   }

                   if (arg instanceof BaseDataType && ((BaseDataType) arg).isUnknown())
                   {
                      hasUnknowns = true;
                      break;
                   }
                }
             }

             if (!hasUnknowns && nonNullArgsHelper != null)
             {
                // reuse the already built helper
                helper = nonNullArgsHelper;
             }
             else
             {
                Property[] iprops = included == null ? null : included.get(buffer.getDMOProxy());
                Property[] xprops = excluded == null ? null : excluded.get(buffer.getDMOProxy());
                helper = FQLHelper.obtain(buffer,
                                          getReferencedBuffers(),
                                          dfArgs,
                                          isIdOnly(),
                                          dfWhere,
                                          sort,
                                          join,
                                          iprops,
                                          xprops);
                if (!hasUnknowns)
                {
                   // cache it for future use when we encounter non-null args
                   nonNullArgsHelper = helper;
                }
             }

             if (helper.getSortIndex().isInverseSortPhrase(sort))
             {
                inverseSorting = true;
             }
          }

          return helper;
       }

       /**
        * Set as active the FQL bundle corresponding with a search for the single
        * record, if any, which matches this query's criteria.
        */
       protected void activateUnique()
       {
          activeBundleKey = UNIQUE;
          activeBundle = getHelper().unique(getJoinKey());
       }

       /**
        * Set as active the FQL bundle corresponding with a search for the first
        * record matching this query's criteria.
        */
       protected void activateFirst()
       {
          activeBundleKey = FIRST;
          activeBundle = getHelper().first(getJoinKey());
       }

       /**
        * Test whether the {@code first} / {@code last} navigation will throw {@code QueryOffEndException} if the
        * initialization failed. It is used to test whether the query loop must be broken when the initialization
        * failed with fatal error.
        *
        * @return  always {@code true}.
        */
       protected boolean throwExceptionOnFatalError()
       {
          return true;
       }

       /**
        * Register the RecordChangeListener instance with the ChangeBroker.
        */
       protected void registerChangeListener()
       {
          // Listen for changes to our reference record.  If it is modified, we
          // can no longer rely upon it as an accurate placeholder.
          placeholderCleaner = new RecordChangeListener()
          {
             public Iterator<RecordBuffer> recordBuffers()
             {
                return Collections.singletonList(buffer).iterator();
             }
             public void stateChanged(RecordChangeEvent event)
             throws PersistenceException
             {
                if (event.getDMO() == referenceRecord &&
                    event.isUpdate()                  &&
                    !event.isExtentFieldChangeOnly())
                {
                   // Rely on buffer snapshot as placeholder instead.
                   referenceRecord = null;
                }
             }
          };
          ChangeBroker.get().addListener(placeholderCleaner);
       }

       /**
        * Indicate whether this query should be a projection query, retrieving only the primary key
        * of the target record, or whether it should retrieve the entire record as early as possible.
        *
        * @return  <code>true</code> for a projection query, else <code>false</code>.
        */
       protected boolean isIdOnly()
       {
          // Generally, we want to execute as few queries as possible to retrieve a record. The
          // minimum is one, whereby we get all the columns in one pass. However, we have to
          // consider that for NEXT and PREVIOUS requests, we may execute a query per sort component
          // (plus 1 for the primary key if the sort clause does not represent a unique index).
          // In that case, we want to execute a projection query (i.e., get only the primary key).
          // Also we can only retrieve all the columns in the first pass for a no-lock query,
          // because in order to ensure integrity of a locked record, we have to apply the lock
          // before retrieving the full data. Finally, honor the fullRecords request if possible.
          switch (activeBundleKey)
          {
             // TODO: if a next/previous sort clause contains only 1 component, we could be less
             // strict about forcing a projection query here; however, that would require making
             // a separate call to SortCriterion.parse(String, RecordBuffer), which is somewhat
             // expensive, and we are about to do it momentarily in FQLHelper
             case NEXT:
             case PREVIOUS:
                return true;
             default:
                return !(fullRecords || LockType.NONE.equals(lockType) || buffer.isTemporary());
    //            return !(fullRecords || buffer.isTemporary());
    //            return !buffer.isTemporary();
    //            return true;
    //            return !fullRecords;
          }
       }

       /**
        * Checks if the buffer associated with this query will be loaded as the result of this query execution.
        * This should always hold, except for CAN-FIND queries that do not modify the state of the buffer.
        *
        * @return   {@code true} if the buffer is going to be loaded by the query.
        */
       protected boolean isBufferLoadedByQuery()
       {
          return true;
       }

       /**
        * Marks this query with recordAlreadyFound flag. This is only used when an {@code AdaptiveQuery} created a
        * simple delegate query, and it has meaning only in case of an FIND-BY-ROWID query.
        *
        * @param   b
        *          The new value for the flag. Usually {@code true}.
        */
       void setRecordAlreadyFound(boolean b)
       {
          recordAlreadyFound = b;
       }

       /**
        * Set the record whose data defines current "place" in query results.
        *
        * @param referenceRecord
        *        The record whose data defines current "place" in query results.
        */
       void setReferenceRecord(Record referenceRecord)
       {
          this.referenceRecord = referenceRecord;
       }

       /**
        * Set as active the FQL bundle corresponding with a search for the last
        * record matching this query's criteria.
        */
       private void activateLast()
       {
          activeBundleKey = LAST;
          activeBundle = getHelper().last(getJoinKey());
       }

       /**
        * Set as active the FQL bundle corresponding with a search for the next
        * record matching this query's criteria, relative to the most recently
        * fetched record.
        */
       private void activateNext()
       {
          activeBundleKey = NEXT;
          activeBundle = getHelper().next(getJoinKey());
       }

       /**
        * Set as active the FQL bundle corresponding with a search for the
        * previous record matching this query's criteria, relative to the most
        * recently fetched record.
        */
       private void activatePrevious()
       {
          activeBundleKey = PREVIOUS;
          activeBundle = getHelper().previous(getJoinKey());
       }

       /**
        * Finalizes a find (FIRST / LAST / UNIQUE) operation, by updating buffer to new {@code dmo}, and
        * optionally throwing an error if record not found and notifying the accumulators.
        *
        * @param   lockType
        *          Lock type to acquire for the retrieved record.
        * @param   dmo
        *          The record found or {@code null} if operation failed.
        * @param   errorReport
        *          How will be the error reported?
        * @param   unique
        *          When called for a UNIQUE FIND. In this case the {@code referenceRecord} is not reset and the
        *          accumulators are not notified.
        */
       private void finalizeFind(LockType lockType, Record dmo, Runnable errorReport, boolean unique)
       {
          boolean errNull = isErrorIfNull();
          updateBuffer(dmo, lockType, errNull, dmo == null ? OffEnd.BACK : OffEnd.NONE);

          if (!unique)
          {
             // reset reference record
             referenceRecord = null;
          }

          // reset dirty flag
          dirtyCopy = false;

          if (dmo == null && errNull)
          {
             errorReport.run();
          }

          if (dmo != null && !unique)
          {
             // notify accumulators of an iteration
             accumulate();
          }
       }

       /**
        * Translate this {@link #where} clause to use the bound DMO's alias and property names,
        * instead the definition (conversion-time) alias and property names.
        *
        * @return The translated FQL.
        */
       private String getTranslatedWhere()
       {
          // just create two empty lists where the binding from the external buffers will be collected.
          List<RecordBuffer> binding = new ArrayList<>();
          List<RecordBuffer> definition = new ArrayList<>();
          return translateWhere(binding, definition, where);
       };

       /**
        * Resolve query substitution arguments in preparation for query execution.
        * This method is invoked only once, at construction time, for simple
        * queries (i.e., those corresponding with simple FIND statements in the
        * original Progress code).  For iterating queries (i.e., those which
        * correspond with FOR loops in the original Progress code), it is
        * invoked once per each entrance into the loop.  Thus, for an outer loop,
        * it is only invoked once, for an inner loop it is invoked once per each
        * iteration of the <i>outer</i> loop, and so on.
        * <p>
        * This causes all variable (i.e. <code>BaseDataType</code>) arguments to
        * be duplicated, and all <code>Resolvable</code>s, <i>except</i>
        * <code>FieldReference</code>s, to be resolved and the result stored.
        * <code>FieldReference</code>s are simply stored, and are resolved
        * separately, on demand, each time the query is executed.
        *
        * @param   forceOrigArgs
        *          Flag indicating that the original arguments used at the construction of the query must be
        *          re-calculated.
        */
       private void resolveArgs(boolean forceOrigArgs)
       {
          if (forceOrigArgs && origArgs != null)
          {
             System.arraycopy(origArgs, 0, args, 0, origArgs.length);
             AbstractQuery.preprocessSubstitutionArguments(buffer.getBufferManager(),
                                                           this::isFindByRowid,
                                                           suppressNestedQueryError(),
                                                           args);
             dfArgs = args;
          }

          // nothing to process or there is an error pending
          if (dfArgs == null || isFatalError())
          {
             return;
          }

          // resolve regular query substitution parameters
          int len = dfArgs.length;
          int dLen = (dynamicFilterArgs == null) ? 0 : dynamicFilterArgs.size();

          currentArgs = new Object[len + dLen];

          // dynamic filters always come in front
          if (dynamicFilterArgs != null)
          {
             for (int k = 0; k < dLen; k++)
             {
                currentArgs[k] = resolveArg(dynamicFilterArgs.get(k));
             }
          }

          for (int i = 0; i < len; i++)
          {
             currentArgs[dLen + i] = resolveArg(dfArgs[i]);
          }
       }

       /**
        * Resolve a single resolvable query substitution parameter, with the exception of
        * {@code FieldReference}s.
        *
        * @return  The resolved query substitution parameter or {@code arg} itself if it is an
        *          instance of {@code FieldReference}.
        */
       private Object resolveArg(Object arg)
       {
          if (arg instanceof FieldReference)
          {
             return arg;
          }
          else if (arg instanceof P2JQuery.Parameter)
          {
             return ((P2JQuery.Parameter) arg).resolve();
          }
          else if (arg instanceof Resolvable)
          {
             return ((Resolvable) arg).resolve();
          }
          else if (arg instanceof P2JQuery.ParamResolver)
          {
             return arg;
          }
          else
          {
             return ((BaseDataType) arg).duplicate();
          }
       }

       /**
        * Find the next record matching the search criteria, or the first such
        * record if no previous search has been executed against this query.
        *
        * @param   values
        *          Array of substitution parameter values for the base where
        *          clause (i.e., the un-augmented portion of the clause).
        * @param   lockType
        *          Type of lock to acquire for the found record.
        * @param   updateLock
        *          <code>true</code> if the status of the lock on the retrieved
        *          record should be modified;  else <code>false</code>.  This is
        *          set to <code>true</code> for actual retrievals, and to
        *          <code>false</code> when only detecting whether a record would
        *          be found, or when peeking a record.
        *
        * @return  Next (or first) record to match the query criteria.
        */
       private Record findNext(Object[] values, LockType lockType, boolean updateLock)
       {
          SortIndex sortIndex = getHelper().getSortIndex();
          buffer.disableReversibleOffEnd();
          OffEnd offEnd = buffer.getOffEnd(sortIndex, inverseSorting);
          if (offEnd == OffEnd.BACK)
          {
             return null;
          }
          if (offEnd == OffEnd.FRONT || (buffer.getSnapshot() == null && referenceRecord == null))
          {
             activateFirst();
          }
          else
          {
             activateNext();
          }

          return execute(values, lockType, false, updateLock);
       }

       /**
        * Find the previous record matching the search criteria, or the last such
        * record if no previous search has been executed against this query.
        *
        * @param   values
        *          Array of substitution parameter values for the base where
        *          clause (i.e., the un-augmented portion of the clause).
        * @param   lockType
        *          Type of lock to acquire for the found record.
        * @param   updateLock
        *          <code>true</code> if the status of the lock on the retrieved
        *          record should be modified;  else <code>false</code>.  This is
        *          set to <code>true</code> for actual retrievals, and to
        *          <code>false</code> when only detecting whether a record would
        *          be found, or when peeking a record.
        *
        * @return  Previous (or last) record to match the query criteria.
        */
       private Record findPrevious(Object[] values, LockType lockType, boolean updateLock)
       {
          SortIndex sortIndex = getHelper().getSortIndex();
          OffEnd offEnd = buffer.getOffEnd(sortIndex, inverseSorting || buffer.isOffEndReversible());
          buffer.disableReversibleOffEnd();
          if (offEnd == OffEnd.FRONT)
          {
             return null;
          }

          if (offEnd == OffEnd.BACK  || (buffer.getSnapshot() == null && referenceRecord == null))
          {
             activateLast();
          }
          else
          {
             activatePrevious();
          }

          return execute(values, lockType, false, updateLock);
       }

       /**
        * Retrieve a record, given an array of size one, containing a primary key
        * ID value of the record to be retrieved, or the record itself.  Acquire
        * the specified lock type.
        *
        * @param   data
        *          Array containing a single primary key ID or DMO.
        * @param   lockType
        *          Type of lock to be applied to the record loaded.
        *
        * @return  The record associated with the given ID, or <code>null</code>
        *          if there is no record matching the ID.
        */
       private Record loadByValue(Object[] data, LockType lockType)
       {
          return loadByValue(data, lockType, false);
       }

       /**
        * Retrieve a record, given an array of size one, containing a primary key
        * ID value of the record to be retrieved, or the record itself.  Acquire
        * the specified lock type.
        *
        * @param   data
        *          Array containing a single primary key ID or DMO.
        * @param   lockType
        *          Type of lock to be applied to the record loaded.
        * @param   silentIfNullId
        *          If <code>true</code>, do not raise error if some of the provided
        *          IDs are <code>null</code>. <code>null</code> IDs are valid for
        *          queries with OUTER join.
        *
        * @return  The record associated with the given ID, or <code>null</code>
        *          if there is no record matching the ID.
        */
       private Record loadByValue(Object[] data, LockType lockType, boolean silentIfNullId)
       {
          Record dmo = null;

          try
          {
             if (data != null)
             {
                Object datum = data[0];
                boolean isDMO = (datum instanceof Record);

                if (isDMO && lockType.equals(LockType.NONE))
                {
                   dmo = (Record) datum;
                }
                else
                {
                   Long id;
                   if (isDMO)
                   {
                      dmo = (Record) datum;
                      id = dmo.primaryKey();
                   }
                   else
                   {
                      id = (Long) datum;
                   }

                   if (id == null && silentIfNullId)
                   {
                      releaseBuffers();
                      return null;
                   }
                   else
                   {
                      Persistence persistence = buffer.getPersistence();
                      long timeout = persistence.getTimeOut();
                      dmo = persistence.load(buffer.getDMOImplementationClass(), id, lockType, timeout, true);
                   }
                }
             }
          }
          catch (MissingRecordException exc)
          {
             // record must have been deleted in another context
             dmo = null;
          }
          catch (PersistenceException exc)
          {
             ErrorManager.recordOrThrowError(exc);
          }

          return dmo;
       }

       /**
        * Create an array of size one, containing the primary key ID of the given
        * DMO.
        *
        * @param   dmo
        *          Data record whose ID is to be stored in the array.
        *
        * @return  Single element array or <code>null</code> if <code>dmo</code>
        *          is <code>null</code>.
        */
       private Object[] getIDs(Record dmo)
       {
          if (dmo == null)
          {
             return null;
          }

          return (new Object[] { dmo.primaryKey() });
       }

       /**
        * Record or throw a FIND FIRST/LAST failed error condition for the current DMO type. In the event a
        * fatal error already has been processed, use an anonymous error to leave the error flag set but with
        * no error number or text recorded (to mimic 4GL behavior).
        *
        * @throws  ErrorConditionException
        *          if silent error mode is suppressed.
        */
       private void errorFindFirstLast()
       {
          if (isFatalError())
          {
             ErrorManager.noRecordOrThrowError("");
          }
          else
          {
             String msg = "FIND FIRST/LAST failed for table " + buffer.getLegacyName();
             ErrorManager.recordOrThrowError(565, msg);
          }
       }

       /**
        * Execute each query stored in the active FQL bundle in turn until we
        * find a result or have no more statements to execute.  Each statement is
        * decreasingly specific.
        * <p>
        * The latest record to be stored in the record buffer, if any, marks our
        * current position in the query's results.  Its data is used to substitute
        * into the placeholder parameters in the query statements' augmented where
        * clauses.  These are added to the <code>values</code> provided as part of
        * the base query.
        *
        * @param   values
        *          Array of substitution parameter values for the base where
        *          clause (i.e., the un-augmented portion of the clause).
        * @param   lockType
        *          Type of lock to apply to the found record.
        * @param   unique
        *          <code>true</code> if there should be no more than one match
        *          for the conditions specified;  <code>false</code> if multiple
        *          matches are possible.
        * @param   updateLock
        *          <code>true</code> if the status of the lock on the retrieved
        *          record should be modified;  else <code>false</code>.  This is
        *          set to <code>true</code> for actual retrievals, and to
        *          <code>false</code> when only detecting whether a record would
        *          be found (i.e., the <code>hasXXXX()</code> methods).
        *
        * @return  The record resulting from the query, or <code>null</code> if
        *          no record was found.
        *
        * @throws  ErrorConditionException
        *          if a recoverable error occurred while executing.
        */
       private Record executeImpl(Object[] values, LockType lockType, boolean unique, boolean updateLock)
       {
          boolean debug = LOG.isLoggable(Level.FINE);

          DirtyShareContext dirtyContext = buffer.getDirtyContext();
          ParameterIndices pi = activeBundle.getParameterIndices();
          int origArgCount = (values == null ? 0 : values.length);
          int baseArgCount = (pi == null ? origArgCount : pi.getCount());
          // query should be continued from explicitly set referenceRecord or DirtyShare version of current record
          // as DirtyShare version affects record order
          Record placeholder = referenceRecord != null ? referenceRecord : buffer.getSnapshot();
          if ((DirtyShareSupport.isEnabledCrossSession() || DirtyShareSupport.isEnabledIntraSession())
                 && dirtyContext != null && referenceRecord == null && placeholder != null)
          {
             // we should start from the last record or Dirty DB version of that record if present
             try
             {
                Record dirtyDMO = dirtyContext.getDirtyDMO(buffer.getEntityName(), placeholder.primaryKey());
                if (dirtyDMO != null)
                {
                   placeholder = referenceRecord = dirtyDMO;
                }
             }
             catch (PersistenceException e)
             {
                LOG.warning("PersistenceException while retrieving DirtyDMO:" + buffer.getEntityName() + " "
                            + placeholder.primaryKey()  , e);
             }
          }
          Record dmo = null;
          Set<Record> primaryDMOs = null;
          Integer mpxID = buffer.getMultiplexID();

          try
          {
             // do a prepare buffer before honoring the record nursery - this way we can flush
             // the record in the buffer and avoid dirty work with the nursery.
             prepareBuffer();

             FQLPreprocessor fqlPreproc = getHelper().getFQLPreprocessor();
             honorRecordNursery(fqlPreproc);

             List<Serializable> parameters = null;
             List<FqlType> parameterTypes = null;

             if (join != null)
             {
                parameters = join.getParameters();
                parameterTypes = join.getParameterTypes();

                if (parameterTypes != null)
                {
                   int s = parameterTypes.size();
                   for (Serializable param : parameters)
                   {
                      if (isNull(param))
                      {
                         s--; // do not take null parameters into consideration
                              // the equality test was replaced by "is null"
                      }
                   }

                   baseArgCount += s;
                   origArgCount += s;
                }
             }

             Persistence persistence = buffer.getPersistence();
             boolean multiplex = buffer.isMultiplexed();

             if (multiplex)
             {
                baseArgCount++;
             }

             Object[] baseArgs = new Object[baseArgCount];
             int startIndex = 0;

             if (multiplex)
             {
                startIndex++;
                baseArgs[0] = mpxID;
             }

             // Add the join parameters (if available) as the next arguments.
             if (join != null && parameterTypes != null)
             {
                int size = parameterTypes.size();
                for (int i = 0; i < size; i++)
                {
                   Serializable param = parameters.get(i);
                   if (!isNull(param))
                   {
                      baseArgs[startIndex] = param;
                      // skip null/unknown parameters
                      startIndex++;
                   }
                }
             }

             boolean needsLock = updateLock && lockType != LockType.NONE;
             boolean findByRowid = fqlPreproc.isFindByRowid();

             while (true)
             {
                // Iterate all arguments provided for the base portion of the
                // where clause only.  If any of these are FieldReferences,
                // resolve these parameters.
                for (int j = 0, i = startIndex; i < baseArgCount; j++)
                {
                   int k = -1;
                   if (pi == null)
                   {
                      k = j;
                   }
                   else
                   {
                      Integer idx = pi.getIndex(j);
                      if (idx == null)
                      {
                         // original substitution parameter was inlined into FQL by preprocessor;  skip it
                         continue;
                      }
                      k = idx;
                   }

                   // Substitution placeholders may have been reordered if this
                   // statement was rewritten by the FQL preprocessor. Make sure
                   // we access the correct substitution parameter.
                   Object next = values[k];

                   // store parameters in the base args array, resolving them first if necessary
                   if (next instanceof Resolvable)
                   {
                      baseArgs[i] = ((Resolvable) next).resolve();
                   }
                   else
                   {
                      baseArgs[i] = next;
                   }

                   i++;
                }

                // determine call count
                int callCount = (placeholder != null ? activeBundle.gettersSize() : 0);

                // Collect all arguments required by the augmented portion of the
                // where clause only, by invoking getters on the latest DMO stored
                // in the buffer.
                // each query for NEXT/PREV mode requires two parameters
                Object[] augmentArgs = new Object[callCount * 2];
                Iterator<Method> getters = activeBundle.getters();
                // each query for NEXT/PREV mode requires two parameters
                for (int i = 0; getters.hasNext(); i += 2)
                {
                   Method method = getters.next();

                   // Arguments to this call can be null, because the augmented
                   // part of the where is based on non-extent properties only.
                   augmentArgs[i] = Utils.invoke(method, placeholder);
                   // each query for NEXT/PREV mode requires two parameters
                   // just duplicate second query segment argument
                   augmentArgs[i + 1] = augmentArgs[i];
                }

                // Collect the full arrays of parameters and types, including base
                // arguments and augmented.  In the loop below, we will copy the
                // necessary portions of these arrays to temporary ones.
                // each query for NEXT/PREV mode requires two parameters
                int fullCount = baseArgCount + (callCount * 2);
                Object[] allArgs = new Object[fullCount];

                // copy base parameters and types into full arrays
                System.arraycopy(baseArgs, 0, allArgs, 0, baseArgCount);

                // copy augmented parameters into full array
                // each query for NEXT/PREV mode requires two parameters
                System.arraycopy(augmentArgs, 0, allArgs, baseArgCount, (callCount * 2));

                // possibly overriding result from uncommitted transaction in another session
                DirtyInfo info = null;

                // execute FQL statements in turn, until we find a result (or run out of statements)
                Iterator<String> statements = activeBundle.statements();
                while (dmo == null && info == null && statements.hasNext())
                {
                   String fql = statements.next();
                   // each query for NEXT/PREV mode requires two parameters
                   int length = baseArgCount + (callCount * 2);
                   Object[] parms = new Object[length];

                   // copy arguments needed for this pass from full argument list
                   System.arraycopy(allArgs, 0, parms, 0, length);

                   if (callCount > 0)
                   {
                      callCount--;
                   }

                   FibonacciCounter fib;
                   long timeout;

                   needsLock = updateLock && lockType != LockType.NONE;
                   if (needsLock)
                   {
                      fib = new FibonacciCounter();
                      timeout = fib.next();
                   }
                   else
                   {
                      fib = null;
                      timeout = 0L;
                   }

                   do
                   {
                      // find and optionally lock the record
                      try
                      {
                         dmo = persistence.load(
                               buffer, fql, parms, lockType, timeout, unique, updateLock, false, findByRowid);

                         // either got the lock or didn't need one
                         needsLock = false;

                         break;
                      }
                      catch (LockTimeoutException exc)
                      {
                         // a DMO was found but could not be locked and we could not determine it
                         // has been deleted in an uncommitted transaction in another session
                         RecordIdentifier<String> ident = exc.getIdentifier();

                         if (LOG.isLoggable(Level.FINE))
                         {
                            LOG.fine("[" +
                                     Utils.describeContext() +
                                     "] FIND lock timeout: "
                                     + timeout
                                     + " ms: "
                                     + ident);
                         }
                         Long id = ident.getRecordID();
                         String entity = buffer.getEntityName();
                         RecordIdentifier<String> entityIdent = new RecordIdentifier<>(entity, id);

                         // try to load the DMO from our session
                         dmo = persistence.quickLoad(entityIdent, !buffer.getDmoInfo().multiTenant);

                         // If the record came back null, this indicates another session deleted it
                         // and has since (as in, since we timed out on the lock but before we were
                         // able to quick-load the DMO) committed its transaction. We have to
                         // execute the current query again with the same arguments to try to get
                         // (and lock) another qualifying record. Don't recalculate the timeout in
                         // this case.

                         // if DMO came back non-null, we need to hold a reference to it temporarily
                         // as a placeholder, in case we have to execute another query
                         if (dmo != null)
                         {
                            // evict from the session if not otherwise in use
                            buffer.evictDMOIfUnused(dmo);

                            if (dirtyContext != null &&
                                DirtyShareSupport.isEnabledCrossSession() &&
                                dirtyContext.isDirtyDelete(entity, id, true))
                            {
                               // a record was found but it is being deleted in another session's
                               // uncommitted transaction
                               info = new DirtyInfo();
                               info.setDeleted();

                               // NOTE: we are breaking out of the load/lock loop WITHOUT acquiring
                               // the requested lock! This only works because the logic below forces
                               // the DMO back to null before we can use it. We don't null it here
                               // because we may need it as a placeholder record for another pass.
                               break;
                            }
                         }

                         // prepare for another pass
                         if (dmo == null)
                         {
                            // if same query is to be executed again, reset the timeout value
                            fib.reset();
                            timeout = fib.next();
                         }
                         else
                         {
                            // otherwise, steadily increase the timeout value
                            timeout = fib.next();

                            // log deadlock warning if timeout is getting too long and wait indefinitely
                            if (timeout > 3600000L)
                            {
                               timeout = 0L;
                               if (LOG.isLoggable(Level.WARNING))
                               {
                                  LOG.log(Level.WARNING,
                                          "[" + Utils.describeContext() + "] Possible deadlocked record: " + ident,
                                          new Throwable());
                               }
                            }
                         }
                      }
                      catch (MissingRecordException exc)
                      {
                         if (needsLock)
                         {
                            // we got the lock, but the record was deleted in another session's
                            // transaction, which has since been committed; reset the timeout and
                            // try to find another qualifying record
                            fib.reset();
                            timeout = fib.next();
                         }
                         else
                         {
                            break;
                         }
                      }
                   } while (true);

                   if (debug)
                   {
                      logQueryDetails(fql, parms, dmo);
                   }
                }

                if (dmo != null           &&
                    callCount == 0        &&
                    augmentArgs != null   &&
                    augmentArgs.length > 0)
                {
                   // This won't pick up break value for FIRST and LAST, but we're
                   // only concerned with NEXT and PREV anyway.
                   breakValue = augmentArgs[0];
                }

                // check the dirty share manager for an overriding result
                // TODO: checking the number of referenced buffers is a temporary workaround;
                //       a query with server-side joins will not return the right information from the
                //       dirty database and may cause a fatal error, if the joined table does not yet
                //       exist in the dirty database; however, this is not a correct solution!
                if (index != 0           &&
                    info == null         &&
                    dirtyContext != null &&
                    getReferencedBuffers().length == 1)
                {
                   info = dirtyContext.getDirtyInfo(buffer,
                                                    index,
                                                    activeBundleKey,
                                                    activeBundle,
                                                    allArgs,
                                                    dmo,
                                                    fqlPreproc.isFindByRowid(),
                                                    isBufferLoadedByQuery());
                }

                // if check found nothing of interest, we're done
                if (info == null)
                {
                   break;
                }

                // keep track of the DMO(s) we found in the primary database; in the event we don't
                // use them otherwise, we'll have to evict them from the session
                if (dmo != null)
                {
                   if (primaryDMOs == null)
                   {
                      primaryDMOs = new HashSet<>();
                   }

                   primaryDMOs.add(dmo);
                }

                // can't trigger revalidation of AdaptiveQuery if dirty database result is in use
                breakValue = null;

                // Get the DMO found in the dirty database, if any.  This will be
                // a deep copy of the original, so it is safe to use.
                Record dirtyDMO = info.getDirtyDMO();

                // If found DMO was deleted, update the placeholder and try again.
                // Also handle the case where the DMO found in the primary
                // database has been modified in such a way that it may no longer
                // satisfy the search criteria.
                if ((DirtyShareSupport.isEnabledCrossSession() || DirtyShareSupport.isEnabledIntraSession()) &&
                    activeBundleKey != UNIQUE &&
                    (info.isDeleted() ||
                       (info.isModified() &&
                          ((!info.isInserted() && !info.isLeaked()) ||
                             (dirtyDMO != null && dirtyDMO.primaryKey().equals(dmo.primaryKey()))))))
                {
                   // Test if the record has moved along the index we're walking.
                   if (info.isModified() && dirtyDMO != null && dmoSorter != null)
                   {
                      Record compDirtyDMO = info.getComparableDMO();
                      int comp = dmoSorter.compare(dmo, compDirtyDMO);
                      if (comp == 0)
                      {
                         // Both the primary DMO and dirty DMO sort equivalently
                         // on the index being navigated.  If the dirty DMO does
                         // not represent the full set of uncommitted changes
                         // being tracked in the dirty database, that indicates we
                         // published uncommitted changes early and we must use
                         // the dirty DMO as our result...
                         if (!info.isFullyPublished())
                         {
                            dmo = dirtyDMO;
                            dirtyCopy = true;
                         }

                         // ...otherwise, we use the primary DMO.
                         break;
                      }
                   }

                   // We are about to update the placeholder and try another pass,
                   // so if the navigation request was for the first or last
                   // record, we have to modify the mode to be next or previous,
                   // respectively.  Otherwise, we'll just find the same record in
                   // an infinite loop.
                   switch (activeBundleKey)
                   {
                      case FIRST:
                         activateNext();
                         break;
                      case LAST:
                         activatePrevious();
                         break;
                   }

                   // The found record must be skipped because it was either
                   // deleted or modified on the index we're walking.  Update the
                   // placeholder to be the record we actually found...
                   placeholder = dmo;
                   dmo = null;

                   // ...then try another pass.
                   continue;
                }

                if (needsLock && dmo != null)
                {
                   // If we are here, we determined a provisional DMO we found was deleted in another
                   // session's uncommitted transaction and we broke out of the load/lock loop
                   // without acquiring the requested lock. The DMO was loaded only for placeholder
                   // purposes. Null it out now, since we haven't got the lock; we will either find
                   // another qualifying record or give up. A bit redundant with the delete logic
                   // in processDirtyResults, but safety first.
                   dmo = null;
                }

                // determine which of the two candidate DMOs is most appropriate as a result
                dmo = processDirtyResults(persistence, dmo, info, lockType, updateLock);

                break;
             }

             if (dirtyCopy && primaryDMOs != null)
             {
                // We are not going to use the primary DMO(s) we found, so make
                // sure they are evicted from the session if not in use elsewhere
                // in this context.
                for (Record pDMO : primaryDMOs)
                {
                   buffer.evictDMOIfUnused(pDMO);
                }
             }
          }
          catch (IllegalAccessException exc)
          {
             throw new RuntimeException(exc);
          }
          catch (InvocationTargetException exc)
          {
             DBUtils.handleException(buffer.getDatabase(), exc);

             throw new RuntimeException(exc);
          }
          catch (LockUnavailableException exc)
          {
             handleExecuteException(exc);

             // Cannot return record if it could not be locked.
             dmo = null;
          }
          catch (ValidationException exc)
          {
             // a validation error can only result from flushing an unrelated record currently in
             // the buffer in preparation for a query result; since this will not be within the
             // context of any silent error mode under which this query is running, we instruct
             // the error manager to not suppress the message
             ErrorManager.throwError(exc, false);
          }
          catch (PersistenceException exc)
          {
             handleExecuteException(exc);
          }
          finally
          {
             if (dmo == null)
             {
                breakValue = null;
             }
          }

          return dmo;
       }

       /**
        * Core method of attempting to retrieve the record for this query using direct access.
        * This does all the prerequisites checks and is safely implemented such that a failed direct
        * access allows easy recovery.
        * <p>
        * A simple query (no join, external buffers and single result) over the temporary database
        * can be resolved using a direct access driver (currently supported by H2). This avoids generating
        * an FQL/SQL and simply uses the internal H2 structures (tables, indexes, etc.) to find the
        * searched record.
        * <p>
        * Currently, this can be used only if the H2 row structure is very similar to the FWD record
        * structure (to allow hydration) and the searched row can be uniquely identified: using recid
        * or an unique index.
        *
        * @param    values
        *           The resolved substitution parameters to be used by the query.
        *
        * @return   An optional depending on the success of this attempt. If a direct access exception
        *           occurred, allow fallback by returning an empty optional. If a record could be retrieved,
        *           get it through the optional.
        */
       private Optional<Record> executeDirectAccess(Object[] values)
       {
          try
          {
             // reduce expenses; don't create a session just to check direct access
             Session currentSession = buffer.getSession(false);
             DmoMeta meta = buffer.getDmoInfo();
             FQLPreprocessor fqlPreproc = helper.getFQLPreprocessor();
             if (currentSession != null &&
                 isSimpleQuery() &&
                 buffer.isTemporary() && // prerequisites
                 fqlPreproc.isUniqueFind(true) && // this is the core condition right now
                 DirectAccessHelper.hasDirectAccess(currentSession) && // only if we have direct access to H2
                 !meta.hasComputedColumns(false) && // only if all H2 columns represent the DMO fields
                 !fqlPreproc.hasContains())
             {
                Record r;

                // first prepare the buffer to force a flush, so that the record nursery won't deal with
                // dirty data from the same buffer.

                prepareBuffer();
                honorRecordNursery(fqlPreproc);
                if (fqlPreproc.isFindByRowid())
                {
                   long recid = fqlPreproc.getFindByRowid(values);
                   r = DirectAccessHelper.findByRowid(currentSession, meta, recid);
                }
                else if (fqlPreproc.isUniqueFind(false))
                {
                   // make a copy to add the custom multiplex
                   UniqueIndexLookup lookup = new UniqueIndexLookup(fqlPreproc.getUniqueIndexLookup());
                   if (!lookup.fill(values))
                   {
                      throw new DirectAccessException("Invalid fill operation");
                   }
                   lookup.addMapping(TemporaryBuffer.MULTIPLEX_FIELD_NAME, buffer.getMultiplexID());
                   r = DirectAccessHelper.findByUniqueIndex(currentSession, meta, lookup);
                }
                else
                {
                   throw new DirectAccessException("Invalid state for direct access");
                   // not a valid state
                }

                // the record can be null; make sure we let the caller know that we succeeded
                return Optional.ofNullable(r);
             }
          }
          catch (PersistenceException e)
          {
             // this is not ideal as it assumes that we failed doing some persistence work already
             handleExecuteException(e);
             return null;
          }
          catch (ValidationException exc)
          {
             // a validation error can only result from flushing an unrelated record currently in
             // the buffer in preparation for a query result; since this will not be within the
             // context of any silent error mode under which this query is running, we instruct
             // the error manager to not suppress the message
             ErrorManager.throwError(exc, false);
          }
          catch (DirectAccessException e)
          {
             if (LOG.isLoggable(Level.WARNING) && !e.isExpected())
             {
                LOG.log(Level.WARNING, "Failed direct-access due to " + e.getMessage());
             }
             return null;
          }

          // don't use exceptions if we fail here; if we do, there will be a lot of exception being thrown
          // it is faster to just let the caller know that we failed or not using DirectAccessResult
          return null;
       }

       /**
        * Convenient common method which exposes the nursery records to the persistence layer. This should be
        * used to honor transient records when executing a query. This way, the look-up is able to find
        * the records which are not yet flushed to the database and are stored in the record nursery. The logic
        * is not trivial, as it validates and flushes record to the database, honoring triggers. Use this
        * before attempting to use the database.
        *
        * @param   preproc
        *          The FQL preprocessor instance of {@code null} if it should be retrieved here.
        *
        * @throws  PersistenceException
        *          if there is any error persisting the record to its primary or dirty database.
        */
       void honorRecordNursery(FQLPreprocessor preproc)
       throws PersistenceException
       {
          FQLPreprocessor fqlPreproc = preproc == null ? getHelper().getFQLPreprocessor() : preproc;
          // find by recid is a special case using index "0"; otherwise, use index associated with find
          int idxUid = fqlPreproc.isFindByRowid() ? 0 : index;
          RecordNursery nursery = buffer.getNursery();
          nursery.makeVisible(buffer.getDmoInfo().getId(), buffer.getMultiplexID(), idxUid);
       }

       /**
        * Process the results of a dirty index check and return the most
        * appropriate result for the query.  This method may acquire a record lock
        * for the selected result.
        *
        * @param   persistence
        *          Persistence services object.
        * @param   primaryDMO
        *          Candidate DMO found in the primary database.  Will not be
        *          <code>null</code>.
        * @param   info
        *          Information found during the dirty database check.
        * @param   lockType
        *          Type of lock to apply to the found record.
        * @param   updateLock
        *          <code>true</code> if the status of the lock on the retrieved
        *          record should be modified;  else <code>false</code>.  This is
        *          set to <code>true</code> for actual retrievals, and to
        *          <code>false</code> when only detecting whether a record would
        *          be found (i.e., the <code>hasXXXX()</code> methods).
        *
        * @return  Either <code>primaryDMO</code> or <code>dirtyDMO</code>,
        *          depending upon which record is most appropriate in terms of the
        *          index being walked, and possibly the most recently found
        *          record, in the case of a relative move (NEXT/PREVIOUS).
        *
        * @throws  PersistenceException
        *          if any error occurs retrieving a record from the primary
        *          database, after determining its ID in the dirty database.
        * @throws  LockUnavailableException
        *          if a no-wait lock requested on <code>dirtyDMO</code> is
        *          currently unavailable.
        */
       private Record processDirtyResults(Persistence persistence,
                                          Record primaryDMO,
                                          DirtyInfo info,
                                          LockType lockType,
                                          boolean updateLock)
       throws PersistenceException
       {
          Record dmo = primaryDMO;
          Record dirtyDMO = info.getDirtyDMO();
          Record compDirtyDMO = info.getComparableDMO();
          boolean useDirtyDMO = true;

          // check if there is anything to compare to
          if (compDirtyDMO == null)
          {
             return primaryDMO;
          }
          if (primaryDMO != null && activeBundleKey != UNIQUE && dmoSorter != null)
          {
             // At this point, we have both a candidate DMO from the primary
             // database (dmo), and a candidate DMO from the dirty database
             // (dirtyDMO).  We need to sort them to determine which one is
             // the appropriate "next" record in the index being walked.
             int comp = dmoSorter.compare(primaryDMO, compDirtyDMO);
             if (activeBundleKey == LAST || activeBundleKey == PREVIOUS)
             {
                comp *= -1;
             }

             useDirtyDMO = (comp >= 0);
          }

          if (useDirtyDMO)
          {
             // all below can be gathered in a single if statement, but they are separated on purpose to allow
             // analysis of specific cases on intra- and cross- session DirtyShare
             if (info.isInserted() && LockType.NONE.equals(lockType))
             {
                // Use the dirty copy from inserts if no lock has been requested,
                dmo = dirtyDMO;

                // mark the record as dirty and potentially from another context
                dirtyCopy = true;
             }
             else if (info.isLeaked() &&  LockType.NONE.equals(lockType))
             {
                // Use the dirty copy from leaks if no lock has been requested
                dmo = dirtyDMO;
                // mark the record as dirty and potentially from another context
                dirtyCopy = true;
             }
             else if (info.isLocal())
             {
                // the dirty record actually is the local original. This comes into play when a DMO is created
                // in one buffer and is queried by another buffer in the same session, before the first buffer has
                // validated and flushed the record (it is validated as a side effect of the
                // getDirtyInfo() call). The record is not found in primary database (since it has
                // not yet been flushed), but it is found in the dirty database, and the dirty
                // context substitutes the original DMO for the dirty one in getDirtyInfo().
                dmo = dirtyDMO;
                // mark the record as dirty and potentially from another, since this prevents us from trying
                // to reassociate the record with the current session more than once, in certain cases (see
                // Persistence$Context.getSession())
                dirtyCopy = true;
             }
             else if (dirtyDMO != null)
             {
                // Just use the primary key of the record found in the dirty
                // database, but reload the record from the primary database.
                // Note:  it might not be there, if the record existed only in
                // another context, and was never committed.
                // in that case return primaryDMO as there is no dirtyDMO to replace it
                Long id = dirtyDMO.primaryKey();
                try
                {
                   long timeout = persistence.getTimeOut();
                   dmo = persistence.load(buffer.getDMOImplementationClass(), id, lockType, timeout, updateLock);
                }
                catch (MissingRecordException exc)
                {
                   dmo = primaryDMO;
                }
                if (dmo == null)
                {
                   dmo = primaryDMO;
                }
             }
             else if (info.isDeleted() || info.isModified())
             {
                dmo = null;
             }
          }

          return dmo;
       }

       /**
        * Check if this is a "simple" query. The term simple is vague. It refers to queries which can be
        * easier resolved through fast-find cache or direct access. "simple" means no join, no external
        * buffer and no dependent bundle key (FIRST, LAST and UNIQUE do not depend on a previous
        * sorted query).
        *
        * @return   {@code true} if this query can be used by fast-find or direct access.
        */
       private boolean isSimpleQuery()
       {
          return (activeBundleKey == FIRST || activeBundleKey == LAST || activeBundleKey == UNIQUE) &&
                  join == null && getExternalBuffers() == null;
       }

       /**
        * Check if constraints are present in the query.
        *
        * @return  {@code true} if there are constraints present in the query
        */
       protected boolean hasConstraints()
       {
          return where != null || args != null || join != null ||
                 getExternalBuffers() != null || whereExpr != null;
       }

       /**
        * Log debug details about the inputs and results of a query.
        *
        * @param   fql
        *          FQL query statement.
        * @param   parms
        *          Query substitution parameters
        * @param   dmo
        *          DMO record found, if any.
        */
       private void logQueryDetails(String fql, Object[] parms, Record dmo)
       {
          LOG.log(Level.FINE, "FQL:  " + fql);

          StringBuilder buf = new StringBuilder("PARMS:  ");
          int len = parms.length;
          if (len == 0)
          {
             buf.append("N/A");
          }

          for (int k = 0; k < len; k++)
          {
             if (k > 0)
             {
                buf.append(", ");
             }
             Object next = parms[k];
             if (next instanceof BaseDataType)
             {
                buf.append(((BaseDataType) next).toStringMessage());
             }
             else
             {
                buf.append(next);
             }
          }

          LOG.log(Level.FINE, buf.toString());

          buf.setLength(0);
          buf.append("DMO:  ");
          if (dmo != null)
          {
             buf.append(dmo.primaryKey());
          }
          else
          {
             buf.append("<not found>");
          }

          LOG.log(Level.FINE, buf.toString());

          if (LOG.isLoggable(Level.FINEST))
          {
             buf.setLength(0);
             buf.append("DMO Detail:  ");
             buf.append(buffer.toString(dmo));

             LOG.log(Level.FINEST, buf.toString());
          }
       }

       /**
        * Analyses the set of join parameters and return a bit set key for the join pare of the where predicate.
        * The key contains 1 on position 1 iif the value is not null (not unknown). If the {@code join} is
        * {@code null} or all its elements are not null, {@code null} value is returned.
        *
        * @return  A key for join clauses of the where predicate, as described above.
        */
       private BitSet getJoinKey()
       {
          if (join == null)
          {
             return null;
          }

          List<Serializable> joinParams = join.getParameters();
          BitSet ret = null;

          if (joinParams != null)
          {
             boolean allKnown = true;
             ret = new BitSet(joinParams.size());
             for (int i = 0; i < joinParams.size(); i++)
             {
                Serializable param = joinParams.get(i);
                boolean isNotNull = !isNull(param);
                allKnown &= isNotNull;

                ret.set(i, isNotNull);
             }

             if (allKnown)
             {
                return null;
             }
          }

          return ret;
       }

       /**
        * Test whether an object is {@code null} or {@code unknown}.
        *
        * @param   param
        *          The {@code Object} to test.
        *
        * @return  {@code true} iif the object is {@code null} or {@code unknown}.
        */
       private static boolean isNull(Object param)
       {
          if (param == null)
          {
             return true;
          }

          if (param instanceof BaseDataType)
          {
             return ((BaseDataType) param).isUnknown();
          }

          return false;
       }
    }