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;
   }
}