CompoundQuery.java

/*
** Module   : CompoundQuery.java
** Abstract : Implementation of an iterative, multi-table query
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20051112   @23394 Created initial version. Implementation of an
**                           iterative, multi-table query. Component
**                           queries are added and the join of the results
**                           is performed at the client (i.e., P2J server,
**                           but database client).
** 002 ECF 20051216   @23746 Added scrolling support. Now extends the new
**                           DynamicQuery base class, which provides
**                           cursoring support services. Modified
**                           retrieval logic to allow for repositioning.
** 003 ECF 20060104   @23826 Changes to support new hierarchy. Added
**                           unsupported variants of unique() method
**                           required by P2JQuery interface. Both throw
**                           UnsupportedOperationException.
** 004 ECF 20060215   @24715 Added support for client-side where clause
**                           processing. Also refitted to implement more
**                           correct handling of substitution parameter
**                           resolution for iterative looping queries.
** 005 ECF 20060531   @26849 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.
** 006 ECF 20060705   @27881 Fixed preselect mode of retrieve method.
**                           Invocations of this method after the first
**                           would always take the dynamic route rather
**                           than loading records by IDs.
** 007 ECF 20060801   @28349 Added getBreakValue() method to meet
**                           superclass requirement. Currently throws
**                           UnsupportedOperationException.
** 008 ECF 20060925   @29962 Check for a pending error before retrieving a
**                           record. Retrieval is not attempted if an
**                           error is pending in the ErrorManager.
** 009 ECF 20061003   @30152 Backed out #009 (@29962). This logic is only
**                           necessary for FindQuery and AdaptiveFind.
** 010 ECF 20061006   @30196 Implemented scrolling mode. When set to
**                           scrolling mode, non-linear scrolling is
**                           possible in preselect mode. Previously, only
**                           linear scrolling from first to last was
**                           possible. This is necessitated by the fact
**                           that CompoundQuery may back a browse widget.
**                           Scrolling support is actually implemented in
**                           component queries. At this level, a flag is
**                           simply set to enable the deferred setting of
**                           scrolling mode on component queries.
** 011 ECF 20061019   @30525 Fixes to better support scrolling mode.
**                           Modified the behavior of some state variables
**                           to fix the use of AdaptiveQuery components in
**                           scrolling mode.
** 012 ECF 20061204   @31574 Implemented is/setBrowsed() methods. Browse
**                           association status is propagated to component
**                           queries.
** 013 ECF 20070323   @32548 Fixed ClassCastException in setScrolling().
** 014 ECF 20070504   @33407 Fixed loadByIDs(). If a component query can
**                           not load the given ID set, return false.
** 015 ECF 20070509   @33453 Fixed problem of resetting component queries
**                           too early. Added notifyRepositionListeners()
**                           implementation to avoid sending notifications
**                           before query is fully initialized.
** 016 ECF 20070511   @33471 Fixed loadByIDs(). Reset component queries
**                           before loading records.
** 017 ECF 20070518   @33678 Implement recordBuffers(). Required by the
**                           RecordChangeListener interface, which is now
**                           implemented by the superclass.
** 018 ECF 20070716   @34507 Add a cleanup() method implementation. All
**                           component queries are marked as not being
**                           standalone, so that they do not clean up
**                           their resources too aggressively, but rather
**                           defer to this object's cleanup mechanism.
** 019 ECF 20070726   @34720 Fixed problem of outer buffers being cleared
**                           prematurely on loop iteration. When we begin
**                           a new iteration series, we temporarily
**                           disable RecordBuffer's default release on
**                           iterate behavior, but only for the current
**                           block scope.
** 020 ECF 20070821   @34906 Added support for temporary silent error
**                           mode. This allows query substitution parms to
**                           be processed safely during initialization of
**                           a query.
** 021 ECF 20071011   @35443 Optimization to loadByIDs(). The outer
**                           table's query does not need to be reset when
**                           loading buffers by ID.
** 022 ECF 20071203   @36119 Integrated generics for RecordChangeListener
**                           implementation.
** 023 ECF 20080112   @36997 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. Removed final modifier from
**                           class.
** 024 ECF 20080509   @38241 Added open() method. Necessary for converted
**                           OPEN QUERY statements.
** 025 ECF 20080509   @38251 Rolled back #024 (@38241). Caused regression.
** 026 ECF 20080510   @38479 Changed setScrolling(). Do not process method
**                           body more than once.
** 027 SIY 20080625   @38922 Removed duplicate browsed flag which under
**                           some circumstances may be set in opposite
**                           state to identical flag in AbstractQuery
**                           and disable correct browse notifications.
** 028 ECF 20081031   @40306 Minor memory improvement.
** 029 ECF 20081105   @40216 Overrode parent's open() method. Registers
**                           all query components with the ChangeBroker at
**                           the appropriate scope.
** 030 ECF 20081106   @40320 Fixed open(). In case of a scrolling query,
**                           the CompoundQuery itself must be registered
**                           with the ChangeBroker at the proper scope.
** 031 CA  20081210   @40837 Implemented CLOSE QUERY statement.
** 032 ECF 20090223   @41351 Fixed retrieveImpl(). Reset activeIndex on
**                           FIRST/LAST navigation types to ensure we are
**                           fetching all components' results.
** 033 ECF 20090312   @42980 Improved reposition support. Track number of
**                           result rows visited, and which direction we go
**                           off end. Implement loadRowAtCursor() for
**                           scrolling queries.
** 034 ECF 20090703   @43056 Fixed loadByValues(). If an expected record can
**                           not be loaded, MissingRecordException is thrown
**                           by Joinable.load() and the backing buffer is
**                           assumed by contract to be in unknown (safe) mode,
**                           such that a request for the buffer record's
**                           property values will return unknown value.
** 035 ECF 20090716   @43215 Ensure backing record buffers are initialized
**                           before use.
** 036 CA  20090727   @43393 When the query goes off-end, all buffers for this
**                           query are registered for off-end notifications, 
**                           in case of rollback (when current block is FOR
**                           or FOR loop).
** 037 CA  20090731   @43465 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.
** 038 SVL 20091030   @44288 getOffEnd() function was made public in order to
**                           support API change in P2JQuery. 
** 039 SIY 20100303   @44702 Allow enabling/disabling of accumulator processing.
** 040 ECF 20131028          Import change.
** 041 SVL 20140604          Implemented indexInformation.
** 042 SVL 20140909          Fixed some aspects of query invalidation. Implemented
**                           peekNext/peekPrevious and deleteResultListEntry.
** 043 SVL 20150331          Added deleteResultListEntry(boolean).
** 044 ECF 20150520          Replaced Apache commons logging with J2SE logging; cleaned up format
**                           issues.
** 045 OM  20150421          resolveOnce parameter ignored for OPEN QUERY FOR constructs.
** 046 SVL 20150722          Updated notifyRepositionListeners signature.
** 047 ECF 20150806          Implemented server-join optimization algorithm. Determines whether
**                           it is possible (and advisable) to join groups of compound query
**                           components at the database server and does so.
** 048 ECF 20150906          Reimplemented silent error mode processing of query substitution
**                           parameters. Fixed NPE related to outer joins.
** 049 ECF 20160105          Fixed out-of-bounds error in indexInformation method.
** 050 EVL 20160217          Clean up comments from symbols invalid for Solaris to compile.
** 051 ECF 20160206          Minor logging change.
** 052 SVL 20160314          Support for reference row.
** 053 ECF 20160320          Always return null from getBreakValue to disallow shift out of
**                           dynamic mode.
** 054 SVL 20160526          Updated due to changes in PresortCompoundQuery. Fixed the case when
**                           a record from a cached row in a scrolling query was deleted.
** 055 SVL 20160608          The query can handle null IDs (for scrolling compound queries
**                           with OUTER join).
** 056 OM  20160629          HQLPreprocessor needs the list of fields of the current index.
** 057 SVL 20160721          Proper reset on query reopen.
** 058 ECF 20160720          Fixed buffer lookup during HQL preprocessing. Added unsupported
**                           operation implementations of several Joinable methods.
** 059 SVL 20160923          Proper setScrolling() for components.
** 060 CA  20160928          H059 changed addComponent() so that an outer-join component is marked
**                           as inner only if it uses one or more buffers as the current component.
**                           H060 changes retrieveImpl to fix OUTER-JOIN cases exposed by H059.
** 061 GES 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.
** 062 CA  20161006          Update class and addComponent javadoc for H060 changes.
** 063 CA  20161012          Fixed scope processing when the query is from a persistent 
**                           procedure, and is part of a QUERY resource.
** 064 OM  20171129          releaseBuffers() is implemented from P2JQuery.
**     OM  20171212          HQLPreprocessor.get() signature change.
** 065 ECF 20180201          Defer optimization until after first result is retrieved to prevent
**                           overly aggressive buffer release in the event the query runs off end
**                           immediately. Cleaned up inheritance hierarchy.
**     ECF 20180220          The current deferred optimization implementation causes regressions.
**                           It is being left intact, but disabled for the time being.
** 066 ECF 20180418          Optimizer fix.
** 067 ECF 20180618          Removed releaseBuffers from loadByValues when data is null.
** 068 OM  20180515          Added dynamic filters and dynamic sort. [preselected] is reset when
**                           query is open fresh.
** 069 ECF 20180727          Verify an outer-joined query buffer has a record available before
**                           invoking current() on it.
** 070 ECF 20180820          Added logging to Optimizer.
**     OM  20180901          Improved dynamic filtering.
**     OM  20180924          Filtering on decimal fields uses STRING(f, format) function.
** 071 OM  20181025          When peeking on a presorted set, do not add result to cursor because
**                           it already has the full set of results.
** 072 ECF 20190302          Renamed setExternalBuffers to addExternalBuffers.
** 073 OM  20190330          Ignore close request when query not fully initialized.
** 074 ECF 20190414          Ensure presorted query's components fetch full records.
** 075 ECF 20190813          Change to RecordBuffer.release API.
** 076 ECF 20190802          Fixed cleanup processing to prevent a listener leak in ChangeBroker.
**     CA  20190812          Cleanup components on close.
**     EVL 20190815          Fix for NPE on query cleanup when components have been optimized.
** 077 SVL 20191002          Implemented createResultListEntry.
** 078 CA  20191005          A query must know if it is dynamic, when closing.
** 079 AIL 20200610          Added support for afterReposition: validate buffers and release last buffer.
**     SVL 20200619          Fixed issues caused by the previous entry.
** 080 ECF 20200906          New ORM implementation.
** 081 OM  20201012          Use locally cached meta information instead of map lookup.
**     ECF 20210502          Fixed tableCount when closing query.
**     ECF 20210506          Use RecordBuffer.getDmoInfo() getter instead of direct field access to prevent
**                           NPE in proxy case.
**     ECF 20210712          When optimizing, use the iteration type of the original, outermost query in the
**                           optimized join.
**     ECF 20210723          Changed signature of query close worker method; do not removed query components
**                           when closing the query, remove them in initialize method instead.
**     OM  20220112          Added implementation of SKIP-DELETED-RECORD attribute. Fixed management of
**                           'browsed' property.
**     CA  20220214          Register a finalizable cleaner for the compound query (required when is used in
**                           its own block and not via a QUERY resource). 
**     IAS 20220510          Resetting activeIndex before iterating over the components for current(LockType)
**     IAS 20220707          Resetting nullOuter on new iteration step.
**     CA  20220901          Refactored scope notification support: ScopeableFactory was removed, and the 
**                           registration is now specific to each type of scopeable.  For each case, the block
**                           will be registered for scope support (for that particular scopeable) only when
**                           the scopeable is 'active' (i.e. unnamed streams or accumulators are used).  This
**                           allows a lazy registration of scopeables, to avoid the unnecessary overhead of
**                           processing all the scopeables for each and every block.
**     OM  20221103          New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
**     CA  20221130          Refactored the WHERE clause translation (when the bound and definition buffers
**                           are not the same), to be aware of the external buffers (i.e. added for CAN-FIND
**                           sub-select clauses).  The translate will be performed before the query is being
**                           executed.
**     SVL 20230108          Improved performance by replacing some "for-each" loops with indexed "for" loops.
**     SVL 20230113          More performance improvements by replacing some "for-each" loops with indexed
**                           "for" loops.
** 082 CA  20230221          Javadoc fixes.
** 083 IAS 20230405          Fixed FILL support for recursive DATA-RELATION
** 084 OM  20230404          The join navigation key may contain null/unknown values.
** 085 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 086 RAA 20230522          Added instance of P2JQueryStatistics for storing some statistics.
**     RAA 20230526          Function processComponent now goes through P2JQueryLogger
**                           in case logging is intended.
** 087 OM  20230410          Removed outer-join to inner-join optimisation in addComponent().
** 088 CA  20230414          In a compound query, the component groups must always have the EACH components at
**                           the beginning, in a contiguous prefix.  They can not be preceded by FIRST or 
**                           other single-record components.
** 089 RAA 20230607          P2J Queries are now executed in P2JQueryExecutor instead of P2JQueryLogger.
** 090 AB  20230803          Deleted part of isServerJoinPossible() that sets "outer" components automatically 
**                           as not join-able.
**     AB  20230828          Modified isServerJoinPossible() to not allow combining components when the total
**                           number of columns is bigger than the limit of the dialect.
**     AB  20230828          Modified isServerJoinPossible() to log the case when we have to many columns
**                           in select.
**     AL2 20230828          Made optimized compound components outer if their first component is outer.
** 091 IAS 20230921          Added initializa2f() method.
** 092 AB  20230925          Modified isServerJoinPossible() to not allow optimizing when the keyword
**                           "contains" is present in the FQL.
**     AB  20230926          Added checks related to "contains" with non-constant expression
**                           in isServerJoinPossible().
**     AB  20230929          Changed iterating method from for-each to for in isServerJoinPossible(), fixed
**                           spacing in javadoc.    
** 093 RAA 20231006          Added getCombinedFqls and getCombinedExecutionTime.
**     RAA 20231011          Removed databaseName from getCombinedExecutionTime.
**     RAA 20231122          Removed database caching from retrieveImpl. Refactored getCombinedExecutionTime
**                           to use indexed looping.
** 093 ES  20231206          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 thrown an QQE exception. 
** 094 ASL 20240105          Fixed switch statement CURRENT case mistake to call 
**                           current() instead of unique().
** 095 ASL 20240111          Added server join constraint for single component groups containing outer joins.
** 096 OM  20240106          Do not optimize queries with "contains()" function in their predicate.
** 097 CA  20240314          Mark the query to retrieve the entire result set (useful for FILL operations).
** 098 CA  20240320          Avoid BDTs within internal FWD runtime.  Replaced iterators with Java 'for',  
**                           where it applies.
** 099 ES  20240327          Added cases for invalidation of of join in PreselectQuery.
** 100 ES  20240404          Persist the record in buffer in case of iteration in FOR EACH with break by statement.
**                           Setting iteration and presort flag for query components.
** 101 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.
** 102 CA  20240425          Do not join components which use the same SQL table.
** 103 AL2 20240424          Detect if the optimized query is going to be invalidated early. 
**                           If so, deny the optimization.
** 104 CA  20240809          Avoid iterator usage, for performance improvement.
** 105 DDF 20230712          Added setForwardOnly() method.
**     DDF 20230713          Complete setForwardOnly() method since utility methods for the FORWARD-ONLY
**                           attribute were moved to AbstractQuery.
**     DDF 20230717          Added missing @Override annotation.
**     DDF 20240130          Set the forwardOnly flag when adding a new component.
** 106 SB  20241105          Release the last bound buffer of a query if REPOSITION-BY-ID has an unknown
**                           argument. Refs #9199.
** 107 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
**                           based on [sharedDb] parameter.
**                           Implementation of [setMultiTenantAlternative] method.
** 108 AOG 20250127          Cache the entire result set when last(boolean, LockType) is called and
**                           the query is attached to a browse.
** 109 AL2 20250416          Enforce database join if force-database-join attribute is set.
** 110 RNC 20250428          Always register the dynamic query as a global scope listener.
** 111 SP  20250509          Reload component query buffers up to activeIndex for scrolling queries.
** 112 AL2 20250520          Better support for CURRENT to honor record set by previous loadByValues.
** 113 AL2 20250521          Loading a prefix of buffers should honor the provided lock type.
*/

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

package com.goldencode.p2j.persist;

import java.util.*;
import java.util.function.*;
import java.util.logging.*;
import com.goldencode.p2j.persist.event.*;
import com.goldencode.p2j.persist.hql.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.orm.P2JQueryExecutor;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.*;

/**
 * A client-side (relative to the database server) implementation of a
 * multi-table, join query.  Queries of individual tables are added via one
 * of the <code>addComponent</code> method variants.  This query type can
 * combine both {@link PreselectQuery}s and {@link RandomAccessQuery}s as its
 * components.  However, preselected tables must always be to the left of
 * (i.e., outward from) non-preselected tables in the query.  That is, a
 * <code>PreselectQuery</code> object can never be added, once a
 * <code>RandomAccessQuery</code> object has been added to a compound query.
 * <p>
 * Inner and left outer joins are supported.  However, inner joins must
 * always occur to the left (i.e., outward from) outer joins.  If, at the
 * time a component query specified as an inner join is added, there are
 * previously added outer-join components which in their <code>where</code>
 * clause use one or more buffers as the inner-join component's
 * <code>where</code> clause, these outer-join components will automatically
 * be converted to an inner join, to satisfy the condition that all inner 
 * joins are on the left of the outer-join components.
 * <p>
 * This query retrieves result records in two modes:  iterating and random
 * access.  Iterating mode is active when using the {@link #iterate} method
 * to advance the query to the next, composite row of records in the result
 * set.  Random access mode is active when using any of the other retrieval
 * methods (<code>first</code>, <code>last</code>, <code>next</code>,
 * <code>previous</code>, <code>current</code>).  The difference between the
 * modes is that iterating mode always terminates with a
 * <code>QueryOffEndException</code> when no further record retrieval is
 * possible;  random access mode does not.  Random access mode thus allows
 * client code to advance off the end of the result set and continue
 * processing.
 * <p>
 * <code>CompoundQuery</code> abstracts the use of multiple query
 * implementations to achieve a dynamic, multi-table query.  It delegates the
 * record retrieval work to the underlying queries, but manages the overall
 * navigation of the results.  Delegate queries must implement the {@link
 * Joinable} interface.  Each "row" in the "result set" is a logical
 * construct, the virtual composite of multiple record buffers.  Advancing to
 * a new row (be it forwards, backwards, first, last) means advancing the
 * most specific query possible to the next matching record in its
 * predetermined progression.  Once the most specific query can no longer
 * advance, the next most specific query is advanced, the most specific query
 * is reset to its starting state, and we try to find a joining match for the
 * most specific query again, given that its outer query has a new value with
 * which to join.  This continues until the most specific possible query has
 * advanced, or we have moved to our least specific query component, and no
 * further match is possible.  At this point, the query is "off-end".  For
 * iterating mode, this is the end of query execution and an end condition is
 * raised.  In random access mode, further navigation is possible.
 * <p>
 * <b>Substitution Parameters</b><br>
 * Query substitution parameters require different modes of resolution,
 * depending upon the type of parameter and the nature of the query.  A query
 * may be set at construction to resolve all of its parameters once, up front
 * or iteratively, each time an iterative loop construct is entered.  The
 * former corresponds with Progress' OPEN QUERY style queries;  the latter
 * corresponds with all other iterative record retrieval constructs in
 * Progress (e.g., FOR EACH, DO PRESELECT, REPEAT PRESELECT, REPEAT FOR).
 * <p>
 * The number of times a parameter is resolved and the timing of such
 * resolution becomes important if an iterative record retrieval construct
 * modifies the values of such parameters during record retrieval.  This may
 * occur, for example, if the body of an outer FOR loop modifies the value of
 * a variable used in the where clause of an inner FOR loop.  While less of
 * an issue for preselect type queries, these can still be affected if the
 * where clause of a nested query component invokes a client-side function,
 * which may in turn modify state on which the query or related logic depends.
 * <p>
 * The following conversion rules apply:
 * <pre>
 * ------------------------------------------------------------------------
 * Progress Construct                             CompoundQuery Modes
 *                                        resolveOnce             preselect
 * ------------------------------------------------------------------------
 * OPEN QUERY FOR                         true (forced to false)  false
 * OPEN QUERY PRESELECT                   true (ignored?)         true
 * FOR {EACH | FIRST | LAST}              false                   false
 * DO PRESELECT                           false                   true
 * REPEAT PRESELECT                       false                   true
 * ------------------------------------------------------------------------
 * </pre>
 */
public class CompoundQuery
extends DynamicQuery
implements QueryConstants,
           RecordChangeListener
{
   /** Logger */
   private static final CentralLogger log = CentralLogger.get(CompoundQuery.class.getName());
   
   /** Defer optimization until after first composite row has been retrieved (experimental) */
   // TODO: current implementation of this feature introduces regressions; needs to be fixed
   private static final boolean deferOptimization = false;
   
   /**
    * Determines if the current row was deleted by DELETE-RESULT-LIST-ENTRY. Note that if the
    * query is off-end, it also can be "deleted".
    */
   protected boolean currentRowDeleted = false;
   
   /** Determines if we have fetched a result row. Only for non-scrolling queries. */
   protected boolean foundFirst = false;
   
   /** Determines if this will be unregistered from ChangeBroker on cleanup. */
   protected boolean unregisterOnCleanup = false;
   
   /** Force preselection of all results? */
   private boolean preselect;
   
   /** Query components originally added to the compound query */
   private final ArrayList<CompoundComponent> originalComponents = new ArrayList<>(2);
   
   /** Resolve arguments once, up front, or on every loop cycle? */
   private boolean resolveOnce;
   
   /** Query components currently in use by the compound query */
   private ArrayList<CompoundComponent> components = null;
   
   /** Have results been preselected? */
   private boolean preselected = false;
   
   /** Current index position in each component query loop */
   private int[] counters = null;
   
   /**
    * Specifies if snapshot values (values at the time when the record was loaded) should be used
    * as FQL sub-query parameters for specific component instead of the actual values. The array
    * has size <code>components.size() - 1</code> and excludes the first (outermost) component
    * because it cannot have FQL which includes fields of an outer component.
    */
   private boolean[] useSnapshot = null;
   
   /** Index of component query currently being iterated */
   private int activeIndex = -1;
   
   /** Total number of tables involved in this compound query */
   private int tableCount = 0;
   
   /** Postpone accumulation on retrieve. */
   private boolean postponeAccumulation = false;
   
   /** Map of all buffers participating in this query to their 0-based index positions */
   private Map<RecordBuffer, Integer> buffersMap = null;
   
   /** Has query been registered for resource cleanup? */
   private boolean regCleaner = false;
   
   /** Flag indicating the entire result set will be retrieved (useful for FILL operations). */
   private boolean forceFullResults = false;
   
   /** Cleaner executed when this query's block is exit. */
   private final Finalizable cleaner = new Finalizable()
   {
      public void finished() { cleanup(); }
      public void iterate() {}
      public void retry() {}
      public void deleted() {}
   };

   /** Flag indicated if the WHERE clause has been translated. */
   private boolean whereTranslated = false;

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

   /**
    * 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();
      
      ArrayList<CompoundComponent> components = getComponents();
      for (int i = 0; i < components.size(); i++)
      {
         CompoundComponent cc = components.get(i);
         cc.getQuery().clearResults();
      }
   }

   /**
    * Initialization logic.
    * <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    preselect
    *           {@code true} to force preselection of all results; {@code false} otherwise.
    *
    * @return   The query instance.
    */
   public CompoundQuery initialize(boolean resolveOnce, boolean preselect)
   {
      this.resolveOnce = false; // TODO: remove resolveOnce parameter completely
      this.preselect = preselect;
      
      // initialize will be invoked on an existing query after close(), when the query is re-opened in
      // dynamic mode (i.e., dynamically prepared), so reset any structural state here
      originalComponents.clear();
      tableCount = 0;
      
      TransactionManager.registerOffEndQuery(this);
      
      return this;
   }
   
   /**
    * Initialization logic.
    * <p>
    * This code is intentionally separated from the constructor. The purpose of this separation
    * is to allow construction to occur in the prior scope while initialization can still occur
    * within a method or lambda that is inside the next block's scope.
    *
    * @return   This query instance.
    */
   public CompoundQuery initialize2f()
   {
      return initialize(false, false);
   }

   /**
    * Create a cursor for this query and ensure that any component queries are scrollable, whether they
    * already have been added, or will be added in the future.
    */
   public void setScrolling()
   {
      if (!isScrolling())
      {
         super.setScrolling();
         
         if (components != null)
         {
            for (int i = 0; i < components.size(); i++)
            {
               CompoundComponent component = components.get(i);
               component.getQuery().setScrolling();
            }
         }
         
         for (int i = 0; i < originalComponents.size(); i++)
         {
            CompoundComponent originalComponent = originalComponents.get(i);
            originalComponent.getQuery().setScrolling();
         }
      }
   }
   
   /**
    * Set the forward-only attribute for any component queries to the given value.
    *
    * @param   value
    *          <code>true</code> to enable forward-only, <code>false</code> to disable it.
    */
   @Override
   public void setForwardOnly(boolean value)
   {
      super.setForwardOnly(value);
      
      if (components != null)
      {
         for (int i = 0; i < components.size(); i++)
         {
            CompoundComponent component = components.get(i);
            component.getQuery().setForwardOnly(value);
         }
      }
      
      for (int i = 0; i < originalComponents.size(); i++)
      {
         CompoundComponent originalComponent = originalComponents.get(i);
         originalComponent.getQuery().setForwardOnly(value);
      }
   }
   
   /**
    * Get the number of tables joined by this query.
    * 
    * @return  Number of tables joined by this query.
    */
   public int getTableCount()
   {
      return tableCount;
   }
   
   /**
    * Get all the off-end listeners associated with this query.
    * 
    * @return  the off-end listeners associated with this query.
    */
   public ArrayList<QueryOffEndListener> getOffEndListeners()
   {
      LinkedHashSet<QueryOffEndListener> listeners = new LinkedHashSet<>();
      
      List<CompoundComponent> compoundComponents = getComponents();
      for (int i = 0; i < compoundComponents.size(); i++)
      {
         CompoundComponent comp = compoundComponents.get(i);
         listeners.addAll(comp.getQuery().getOffEndListeners());
      }
      
      return new ArrayList<>(listeners);
   }
   
   /**
    * Mark the query to retrieve the entire result set (useful for FILL operations).
    */
   @Override
   public void hintFullResults()
   {
      this.forceFullResults = true;
   }
   
   /**
    * Add a joinable query to the compound query.  If this is not the first
    * component to be added to the query, its results will be joined to the
    * immediately previous component by an inner join.  This query will be
    * iterated using the <code>NEXT</code> iteration type.
    *
    * @param   query
    *          Query which will perform the fundamental work for this
    *          component when the compound query is iterated.
    *
    * @throws  IllegalStateException
    *          if this method is invoked during an iteration cycle.
    *
    * @see     #iterate
    */
   public void addComponent(Joinable query)
   {
      addComponent(query, NEXT, false);
   }
   
   /**
    * Add a joinable query to the compound query, and specify the nature of
    * the iteration which will be performed (first, last, next, etc.) on that
    * component, when the compound query is iterated.  If this is not the
    * first component to be added to the query, its results will be joined to
    * the immediately previous component by an inner join.
    *
    * @param   query
    *          Query which will perform the fundamental work for this
    *          component when the compound query is iterated.
    * @param   iteration
    *          A {@link QueryConstants constant} indicating the type of work
    *          to be performed upon an iteration of the compound query.  This
    *          parameter determines whether the underlying query will be asked
    *          to perform a first, last, next, previous, or unique record
    *          retrieval.
    *
    * @throws  IllegalStateException
    *          if this method is invoked during an iteration cycle.
    *
    * @see     #iterate
    */
   public void addComponent(Joinable query, int iteration)
   {
      addComponent(query, iteration, false);
   }
   
   /**
    * Add a joinable query to the compound query, and specify the nature of
    * the iteration which will be performed (first, last, next, etc.) on that
    * component, when the compound query is iterated.  If this is not the
    * first component to be added to the query, the type of join performed
    * with the immediately previous component is determined by the value of
    * <code>outer</code>.
    *
    * @param   query
    *          Query which will perform the fundamental work for this
    *          component when the compound query is iterated.
    * @param   iteration
    *          A {@link QueryConstants constant} indicating the type of work
    *          to be performed upon an iteration of the compound query.  This
    *          parameter determines whether the underlying query will be asked
    *          to perform a first, last, next, previous, or unique record
    *          retrieval.
    * @param   outer
    *          <code>true</code> if this component should be joined to the
    *          previously added component via a left outer join;
    *          <code>false</code> if an inner join is required. 
    *          <p>
    *          If, at the time a component query specified as an inner join is added, there are  
    *          previously added outer-join components which in their <code>where</code> clause
    *          use one or more buffers as the inner-join component's <code>where</code> clause,
    *          these outer-join components will automatically be converted to an inner join, 
    *          to satisfy the condition that all inner joins are on the left of the outer-join 
    *          components.
    *
    * @throws  IllegalStateException
    *          if this method is invoked during an iteration cycle.
    *
    * @see     #iterate
    */
   public void addComponent(Joinable query, int iteration, boolean outer)
   {
      if (activeIndex >= 0)
      {
         throw new IllegalStateException("Cannot add query component while iterating");
      }
      
      initializeComponentQuery(query);
      
      query.setSkipDeletedRecord(isSkipDeletedRecord());
      addCompoundComponent(new CompoundComponent(query, iteration, outer));
   }
   
   /**
    * 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>
    * This method overrides the default implementation to set external buffers on the most
    * recently added component of this compound query. It therefore must be called immediately
    * after the target component is added and before the query is opened, optimized, or executed.
    * 
    * @param   dmos
    *          One or more external buffers to add to the most recently added query component.
    * 
    * @throws  IllegalStateException
    *          if called before a component has been added or after query has been optimized.
    */
   @Override
   public P2JQuery addExternalBuffers(DataModelObject... dmos)
   {
      if (components != null)
      {
         throw new IllegalStateException(
            "Cannot set external buffers on a query component once query has been optimized");
      }
      
      int size = originalComponents.size();
      if (size == 0)
      {
         throw new IllegalStateException("Add a query component before setting external buffers");
      }
      
      CompoundComponent comp = originalComponents.get(size - 1);
      comp.getQuery().addExternalBuffers(dmos);
      
      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 P2JQuery 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 P2JQuery setMultiTenantAlternative(String where,
                                             Supplier<logical> whereExpr,
                                             String sort,
                                             Object[] args)
   {
      if (components != null)
      {
         throw new IllegalStateException(
               "Cannot setMultiTenantAlternative on a query component once query has been optimized");
      }
      
      int size = originalComponents.size();
      if (size == 0)
      {
         throw new IllegalStateException("Add a query component before setting external buffers");
      }
      
      CompoundComponent comp = originalComponents.get(size - 1);
      comp.getQuery().setMultiTenantAlternative(where, whereExpr, sort, args);
      
      return this;
   }
   
   /**
    * Setter for SKIP-DELETED-RECORD query attribute.
    *
    * @param   on
    *          New value for the attribute.
    */
   @Override
   public void setSkipDeletedRecord(logical on)
   {
      for (int i = 0; i < originalComponents.size(); i++)
      {
         CompoundComponent next = originalComponents.get(i);
         next.getQuery().setSkipDeletedRecord(on);
      }
      
      if (components != null && components != originalComponents)
      {
         for (int i = 0; i < components.size(); i++)
         {
            CompoundComponent next = components.get(i);
            next.getQuery().setSkipDeletedRecord(on);
         }
      }
   }
   
   /**
    * Indicate to this query that whether it is associated with a browse widget or not.
    *
    * @param   browsed
    *          {@code true} to indicate the query has an associated browse widget, else {@code false}.
    */
   @Override
   public void setBrowsed(boolean browsed)
   {
      super.setBrowsed(browsed);
      this.setComponentsBrowsed();
   }
   
   /**
    * Open the query once all query components have been added.
    */
   @Override
   public void open()
   {
      if (cursor != null)
      {
         cursor.reset();
      }
      currentRowDeleted = false;
      foundFirst = false;
      counters = null;
      useSnapshot = null;
      activeIndex = -1;
      preselected = false;
      
      boolean globalScope = registerRecordChangeListeners();
      
      super.open();
      
      if (preselect)
      {
         preselectResults();
      }
      
      initReferenceRowSupport(globalScope);
   }
   
   /**
    * Advance the compound query, triggering one or more of the underlying
    * queries to retrieve a new record.  This produces a virtual, composite
    * row of results which is the combination of the records retrieved by
    * each component query.
    *
    * @throws  QueryOffEndException
    *          if no more records can be retrieved by the underlying queries.
    * @throws  ErrorConditionException
    *          if an underlying query triggers an error.
    */
   public boolean iterate()
   {
      return retrieve(NEXT, null, true) != null;
   }
   
   /**
    * Retrieve the first virtual, composite row of results for the compound
    * query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @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 underlying query triggers an error.
    */
   public boolean first()
   {
      return first(false, null);
   }
   
   /**
    * Retrieve the first virtual, composite row of results for the compound
    * query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    *
    * @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  QueryOffEndException
    *          if no more results were available in iterating mode.
    * @throws  ErrorConditionException
    *          if an underlying query triggers an error.
    */
   public boolean first(boolean iterating)
   {
      return first(iterating, null);
   }
   
   /**
    * Retrieve the first virtual, composite row of results for the compound
    * query, overriding the lock type to apply to each underlying query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   lockType
    *          Lock type which should override that of any underlying query.
    *
    * @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 underlying query triggers an error.
    */
   public boolean first(LockType lockType)
   {
      return first(false, lockType);
   }
   
   /**
    * Retrieve the first virtual, composite row of results for the compound
    * query, overriding the lock type to apply to each underlying query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    * @param   lockType
    *          Lock type which should override that of any underlying query.
    * 
    * @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  QueryOffEndException
    *          if no more results were available in iterating mode.
    * @throws  ErrorConditionException
    *          if an underlying query triggers an error.
    */
   public boolean first(boolean iterating, LockType lockType)
   {
      boolean done = false;
      
      if (isScrolling())
      {
         if (preselect)
         {
            preselectResults();
         }
         
         Object[] data = cursor.getFirst();
         if (data == null && cursor.isFirstRowDeleted() && cursor.isForwardScroll())
         {
            return next(iterating, lockType);
         }
         
         done = loadByValues(data, lockType, iterating);
         
         if (data != null && !done)
         {
            return next(iterating, lockType);
         }
      }
      
      if (!done)
      {
         Object[] ids = retrieve(FIRST, lockType, iterating);
         if (isScrolling() && !isPresorted())
         {
            cursor.addResultFirst(ids);
         }
         
         return ids != null;
      }
      
      return true;
   }
   
   /**
    * Retrieve the last virtual, composite row of results for the compound
    * query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @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 underlying query triggers an error.
    */
   public boolean last()
   {
      return last(false, null);
   }
   
   /**
    * Retrieve the last virtual, composite row of results for the compound
    * query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    *
    * @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  QueryOffEndException
    *          if no more results were available in iterating mode.
    * @throws  ErrorConditionException
    *          if an underlying query triggers an error.
    */
   public boolean last(boolean iterating)
   {
      return last(iterating, null);
   }
   
   /**
    * Retrieve the last virtual, composite row of results for the compound
    * query, overriding the lock type to apply to each underlying query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   lockType
    *          Lock type which should override that of any underlying query.
    * 
    * @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 underlying query triggers an error.
    */
   public boolean last(LockType lockType)
   {
      return last(false, lockType);
   }
   
   /**
    * Retrieve the last virtual, composite row of results for the compound
    * query, overriding the lock type to apply to each underlying query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    * @param   lockType
    *          Lock type which should override that of any underlying query.
    * 
    * @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  QueryOffEndException
    *          if no more results were available in iterating mode.
    * @throws  ErrorConditionException
    *          if an underlying query triggers an error.
    */
   public boolean last(boolean iterating, LockType lockType)
   {
      boolean done = false;
      
      if (isScrolling())
      {
         if (isBrowsed() && !cursor.isFullSet())
         {
            Object[] data = cursor.getNext();
            Object[] lastData = null;

            while (data != null)
            {
               lastData = data;
               data = cursor.getNext();
            }

            if (lastData != null)
            {
               loadByValues(lastData, lockType, iterating);
            }
            
            Object[] ids = retrieve(NEXT, lockType, iterating);

            while (ids != null)
            {
               cursor.addResultNext(ids);
               ids = retrieve(NEXT, lockType, iterating);
            }
            // Mark that the set is full.
            cursor.addResultNext(null);
            // current row is null after addResultNext(null) and we need to go back to the last row
            previous(iterating, lockType);

            return true;
         }

         if (preselect)
         {
            preselectResults();
         }
         
         Object[] data = cursor.getLast();
         if (data == null && cursor.isFirstRowDeleted() && !cursor.isForwardScroll())
         {
            return previous(iterating, lockType);
         }
         
         done = loadByValues(data, lockType, iterating);
         
         if (data != null && !done)
         {
            return previous(iterating, lockType);
         }
      }
      
      if (!done)
      {
         Object[] ids = retrieve(LAST, lockType, iterating);
         if (isScrolling() && !isPresorted())
         {
            cursor.addResultLast(ids);
         }
         
         return ids != null;
      }
      
      return true;
   }
   
   /**
    * Retrieve the next virtual, composite row of results for the compound
    * query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    * 
    * @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 underlying query triggers an error.
    */
   public boolean next()
   {
      return next(isIterating(), null);
   }
   
   /**
    * Retrieve the next virtual, composite row of results for the compound
    * query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    * 
    * @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  QueryOffEndException
    *          if no more results were available in iterating mode.
    * @throws  ErrorConditionException
    *          if an underlying query triggers an error.
    */
   public boolean next(boolean iterating)
   {
      return next(iterating, null);
   }
   
   /**
    * Retrieve the next virtual, composite row of results for the compound
    * query, overriding the lock type to apply to each underlying query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   lockType
    *          Lock type which should override that of any underlying query.
    * 
    * @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 underlying query triggers an error.
    */
   public boolean next(LockType lockType)
   {
      return next(false, lockType);
   }
   
   /**
    * Retrieve the next virtual, composite row of results for the compound
    * query, overriding the lock type to apply to each underlying query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    * @param   lockType
    *          Lock type which should override that of any underlying query.
    * 
    * @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  QueryOffEndException
    *          if no more results were available in iterating mode.
    * @throws  ErrorConditionException
    *          if an underlying query triggers an error.
    */
   public boolean next(boolean iterating, LockType lockType)
   {
      if (isScrolling())
      {
         if (preselect)
         {
            preselectResults();
         }
         
         Object[] data;
         
         do
         {
            data = cursor.getNext();
            
            if (data == null && (cursor.isFullSet() ||
                                 !cursor.isForwardScroll()) && cursor.isFoundFirst())
            {
               // back off-end
               releaseBuffers();            
               
               return false;
            }
            
            if (data != null)
            {
               if (loadByValues(data, lockType, iterating))
               {
                  return true;
               }
            }
            
         } while (data != null);
      }
      
      Object[] ids = retrieve(NEXT, lockType, iterating);

      if (isScrolling() && !isPresorted())
      {
         cursor.addResultNext(ids);
      }
      
      return ids != null;
   }
   
   /**
    * Retrieve the previous virtual, composite row of results for the compound
    * query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @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 underlying query triggers an error.
    */
   public boolean previous()
   {
      return previous(false, null);
   }
   
   /**
    * Retrieve the previous virtual, composite row of results for the compound
    * query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    * 
    * @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  QueryOffEndException
    *          if no more results were available in iterating mode.
    * @throws  ErrorConditionException
    *          if an underlying query triggers an error.
    */
   public boolean previous(boolean iterating)
   {
      return previous(iterating, null);
   }
   
   /**
    * Retrieve the previous virtual, composite row of results for the compound
    * query, overriding the lock type to apply to each underlying query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   lockType
    *          Lock type which should override that of any underlying query.
    * 
    * @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 underlying query triggers an error.
    */
   public boolean previous(LockType lockType)
   {
      return previous(false, lockType);
   }
   
   /**
    * Retrieve the previous virtual, composite row of results for the compound
    * query, overriding the lock type to apply to each underlying query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    * @param   lockType
    *          Lock type which should override that of any underlying query.
    * 
    * @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  QueryOffEndException
    *          if no more results were available in iterating mode.
    * @throws  ErrorConditionException
    *          if an underlying query triggers an error.
    */
   public boolean previous(boolean iterating, LockType lockType)
   {
      if (isScrolling())
      {
         if (preselect)
         {
            preselectResults();
         }
         
         Object[] data;
         
         do
         {
            data = cursor.getPrevious();
            
            if (data == null && (cursor.isFullSet() ||
                                 cursor.isForwardScroll()) && cursor.isFoundFirst())
            {
               // front off-end
               releaseBuffers();
               
               if (iterating)
               {
                  throw new QueryOffEndException("",
                        getOffEndListeners().toArray(new QueryOffEndListener[0]));
               }
               return false;
            }
            
            if (data != null)
            {
               if (loadByValues(data, lockType, iterating))
               {
                  return true;
               }
            }
            
         } while (data != null);
      }
      
      Object[] ids = retrieve(PREVIOUS, lockType, iterating);
      if (isScrolling() && !isPresorted())
      {
         cursor.addResultPrevious(ids);
      }

      return ids != null;
   }
   
   /**
    * Retrieve the current virtual, composite row of results for the compound
    * query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    * 
    * @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 underlying query triggers an error.
    */
   public boolean current()
   {
      return current(null);
   }
   
   /**
    * Retrieve the current virtual, composite row of results for the compound
    * query, overriding the lock type to apply to each underlying query.
    * <p>
    * This is a combination of records retrieved by each component query.  If
    * the compound query involves outer joins, some of the underlying buffers
    * may be empty when this method returns.
    *
    * @param   lockType
    *          Lock type which should override that of any underlying query.
    * 
    * @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 underlying query triggers an error.
    */
   public boolean current(LockType lockType)
   {
      if (_isOffEnd())
      {
         ErrorManager.displayError(4114, "No query record is available", false);
         
         return false;
      }
      
      if (currentRowDeleted)
      {
         if (isScrolling())
         {
            ErrorManager.displayError(4114, "No query record is available", false);
         }
         
         return false;
      }
      activeIndex = 0;
      return retrieve(CURRENT, lockType, false) == null;
   }
   
   /**
    * Fetch the array of primary key IDs associated with the first result in
    * this query's result list.
    * <p>
    * This implementation actually updates the underlying record buffer(s).
    *
    * @return  Array of record IDs reflecting the first query result, or
    *          <code>null</code> if there is no such result.
    */
   public Object[] peekFirst()
   {
      return retrieve(FIRST, LockType.NONE, false, true);
   }
   
   /**
    * Fetch the array of primary key IDs associated with the last result in
    * this query's result list.
    * <p>
    * This implementation actually updates the underlying record buffer(s).
    *
    * @return  Array of record IDs reflecting the last query result, or
    *          <code>null</code> if there is no such result.
    */
   public Object[] peekLast()
   {
      return retrieve(LAST, LockType.NONE, false, true);
   }
   
   /**
    * Fetch the array of primary key IDs associated with the next result in this query's result
    * list.
    * <p>
    * This implementation actually updates the underlying record buffer(s). The difference between
    * this function and {@link #next()} function is that this function calls
    * {@link Scrollable#peekNext()} on sub-queries in order to get the next record from underlying
    * results, while {@link #next()} returns the next record after the current row.
    *
    * @return  Array of record IDs reflecting the next query result, or
    *          <code>null</code> if there is no such result.
    */
   public Object[] peekNext()
   {
      Object[] res = retrieve(NEXT, LockType.NONE, false, true);
      if (isScrolling() && !isPresorted())
      {
         cursor.addResultNext(res);
      }
      
      return res;
   }
   
   /**
    * Fetch the array of primary key IDs associated with the previous result
    * in this query's result list.
    * <p>
    * This implementation actually updates the underlying record buffer(s). The difference between
    * this function and {@link #previous()} function is that this function calls
    * {@link Scrollable#peekPrevious()} on sub-queries in order to get the next record from
    * underlying results, while {@link #previous()} returns the next record after the current row.
    *
    * @return  Array of record IDs reflecting the previous query result, or
    *          <code>null</code> if there is no such result.
    */
   public Object[] peekPrevious()
   {
      Object[] res = retrieve(PREVIOUS, LockType.NONE, false, true);
      if (isScrolling() && !isPresorted())
      {
         cursor.addResultPrevious(res);
      }
      
      return res;
   }
   
   /**
    * Notify all registered listeners that query is repositioned or closed.  
    * If this is invoked before any query components have been added to the 
    * compound query, it is ignored.
    * 
    * @param   closed
    *          A <code>true</code> value will inform all listeners that the
    *          query was explicitly closed.
    * @param   error
    *          <code>true</code> if an error happened during reposition.
    * @param   targetRepositionRow
    *          Row used as the target for reposition. If it was deleted, reposition with fetching
    *          may end up on a different row.
    */
   @Override
   public void notifyRepositionListeners(boolean closed, boolean error, int targetRepositionRow)
   {
      List<CompoundComponent> comps = getComponents();
      if (comps != null && !comps.isEmpty())
      {
         super.notifyRepositionListeners(closed, error, targetRepositionRow);
      }
   }
   
   /**
    * Report the record buffers 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 those returned by the {@link
    * RecordBuffer#getDMOInterface} methods of the returned buffers.
    *
    * @return  An iterator on the record buffers stored in each of this
    *          query's components.
    */
   public Iterator<RecordBuffer> recordBuffers()
   {
      if (buffersMap != null)
      {
         return buffersMap.keySet().iterator();
      }
      
      final List<CompoundComponent> comps = getComponents();
      
      if (comps == null)
      {
         return Collections.emptyIterator();
      }
      
      Iterator<RecordBuffer> iter = new Iterator<RecordBuffer>()
      {
         private Iterator<CompoundComponent> compIter = comps.iterator();
         
         private RecordBuffer nextBuffer = null;
         
         private RecordBuffer[] bufs = null;
         
         private int index = -1;
         
         public boolean hasNext()
         {
            if (nextBuffer == null)
            {
               advance();
            }
            
            return (nextBuffer != null);
         }
         
         public RecordBuffer next()
         {
            if (!hasNext())
            {
               throw new NoSuchElementException();
            }
            
            try
            {
               return nextBuffer;
            }
            finally
            {
               nextBuffer = null;
            }
         }
         
         public void remove()
         {
            throw new UnsupportedOperationException();
         }
         
         private void advance()
         {
            if (bufs != null)
            {
               if (++index < bufs.length)
               {
                  nextBuffer = bufs[index];
                  
                  return;
               }
               
               bufs = null;
            }
            
            // at this moment bufs is always null
            if (compIter.hasNext())
            {
               CompoundComponent nextComp = compIter.next();
               Joinable query = nextComp.getQuery();
               bufs = query.getRecordBuffers();
               index = -1;
            }
            else
            {
               nextBuffer = null;
               return;
            }
            
            // Assumes that array will not be empty or null.
            nextBuffer = bufs[++index];
         }
      };
      
      return iter;
   }
   
   /**
    * 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.
    */
   @Override
   public void stateChanged(RecordChangeEvent event)
   throws PersistenceException
   {
      if (useSnapshot == null)
      {
         // query opened but not iterated yet
         return;
      }
      
      if (cursor != null)
      {
         cursor.storeReferenceRow(event);
      }
      
      // update useSnapshot array
      RecordBuffer targetBuffer = event.getSource();
      Long eventRecordId = event.getDMO() != null ? event.getDMO().primaryKey() : null;
      
      if (eventRecordId == null)
      {
         return;
      }
      
      Iterator<RecordBuffer> iter = recordBuffers();
      while (iter.hasNext())
      {
         RecordBuffer buffer = iter.next();
         if (buffer.getDMOInterface().equals(targetBuffer.getDMOInterface()))
         {
            int index = buffersMap.get(buffer);
            if (index >= buffersMap.size() - 1)
            {
               // ignore last component
               break;
            }
            
            Long id;
            if (cursor != null && cursor.size().intValue() > 0)
            {
               Object o = cursor.peekLastLoadedResult()[index];
               
               id = o instanceof Record ? ((Record) o).primaryKey(): (Long) o;
            }
            else
            {
               id = buffer._rowID();
            }
            
            if (id != null && eventRecordId.equals(id))
            {
               Arrays.fill(useSnapshot, index, useSnapshot.length, true);
            }
            else
            {
               break;
            }
         }
      }
   }
   
   /**
    * Change the standalone nature of this query.  A query is standalone by
    * default, but can be changed to non-standalone by the wrapper which 
    * contains it.
    * 
    * @param   standalone
    *          <code>true</code> to set query as standalone;
    *          <code>false</code> to set query as contained.
    */
   public void setStandalone(boolean standalone)
   {
      // TODO: do we need the following?
      /*
      if (!standalone && preselect)
      {
         resolveOnce = true;
      }
      */
      
      super.setStandalone(standalone);
   }
   
   /**
    * Explicitly close the prepared query.
    */
   @Override
   public void close()
   {
      close(false);
   }
   
   /**
    * Explicitly close the prepared query. This resets execution state, but it should not change the query's
    * structural state (i.e., the original components), in case the query is re-opened afterward.
    * 
    * @param   inResource
    *          Flag indicating this query is used in a QUERY legacy resource.
    */
   @Override
   public void close(boolean inResource)
   {
      if (inResource && !unregisterOnCleanup)
      {
         // remove it explicitly
         ChangeBroker broker = ChangeBroker.get();
         broker.removeListener(this);
      }
      
      super.close();
      
      // reset the table count if the query was optimized
      if (components != originalComponents)
      {
         tableCount = 0;
         
         for (int i = 0; i < originalComponents.size(); i++)
         {
            CompoundComponent comp = originalComponents.get(i);
            tableCount += comp.getQuery().getTableCount();
         }
      }
      
      activeIndex = -1;
      components = null;
      
      if (regCleaner)
      {
         TransactionManager.removeFinalizable(cleaner);
         regCleaner = false;
      }
   }
   
   /**
    * Clean up resources associated with this query when its scope ends.
    */
   public void cleanup()
   {
      if (closed)
      {
         return;
      }
      
      // make sure we clean up every query associated with this compound query, including the
      // original component queries and any optimized component join queries
      ArrayList<CompoundComponent> comps = originalComponents;
      if (components != null && components.size() < originalComponents.size())
      {
         comps = new ArrayList<>(originalComponents);
         comps.addAll(components);
      }
      
      for (int i = 0; i < comps.size(); i++)
      {
         CompoundComponent next = comps.get(i);
         Joinable query = next.getQuery();
         query.cleanup();
      }
      
      if (unregisterOnCleanup)
      {
         ChangeBroker.get().removeFromGlobal(this);
      }
   }
   
   /**
    * Report the off-end status of this query, which is essentially the
    * off-end status of its outermost component query.  The query is not
    * considered off-end in its starting state;  that is, if a result has not
    * yet been retrieved.
    * 
    * @return  One of the {@link OffEnd} constants <code>NONE</code>,
    *          <code>FRONT</code>, or <code>BACK</code>.
    */
   public OffEnd getOffEnd()
   {
      AbstractQuery query = (AbstractQuery) getComponents().get(0).getQuery();
      
      return query.getOffEnd();
   }
   
   /**
    * Conversion of INDEX-INFORMATION attribute (KW_IDX_INFO).
    *
    * Getter of INDEX-INFORMATION attribute
    *
    * @param    n
    *           An integer expression that evaluates to the level of join for which you want index
    *           information.
    *
    * @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 originalComponents.get(n.intValue() - 1).getQuery().indexInformation();
   }
   
   /**
    * Deletes the current row of a query's result list. Implements the 4GL
    * DELETE-RESULT-LIST-ENTRY() method.
    *
    * @param  allowBetweenRows
    *         If <code>true</code> then the cursor can be positioned between rows (the next
    *         row will be deleted). <code>false</code> for conventional DELETE-RESULT-LIST-ENTRY()
    *         mode where the cursor should be positioned on a row (otherwise <code>false</code> is
    *         returned).
    *
    * @return <code>true</code> on success.
    */
   public logical deleteResultListEntry(boolean allowBetweenRows)
   {
      if (isScrolling())
      {
         boolean res = cursor.deleteResultListEntry(allowBetweenRows);
         if (res)
         {
            currentRowDeleted = true;
         }
         return new logical(res);
      }
      else if (!currentRowDeleted && foundFirst)
      {
         currentRowDeleted = true;
         
         return new logical(true);
      }
      
      return new logical(false);
   }

   /**
    * Creates an entry in the result list for the current row. Implements the 4GL
    * CREATE-RESULT-LIST-ENTRY() method. Created entry contains id(s) of the record(s) currently
    * located in the backing buffer(s).
    *
    * @return <code>true</code> on success.
    */
   @Override
   public logical createResultListEntry()
   {
      if (!deferOptimization)
      {
         // safe to call multiple times; returns immediately if called previously
         maybeOptimize();
      }

      Object[] data = new Object[tableCount];
      int index = 0;
      List<CompoundComponent> compoundComponents = getComponents();
      for (int i = 0; i < compoundComponents.size(); i++)
      {
         CompoundComponent component = compoundComponents.get(i);
         Joinable query = component.getQuery();
         RecordBuffer[] queryBuffers = query.getRecordBuffers();

         for (RecordBuffer buffer : queryBuffers)
         {
            if (!buffer.isAvailable())
            {
               data[index++] = null;
            }
            else
            {
               data[index++] = buffer._rowID();
            }
         }
      }

      cursor.addResultAtCurrentPosition(data);
      return new logical(true);
   }
   
   /** 
    * Set the recursive DATA-RELATION the query is to FILL for.  
    * @param relation
    *       the recursive DATA-RELATION the query is to FILL for
    */
   @Override
   public void setDataRelation(DataRelation relation) 
   {
      super.setDataRelation(relation);
      if (relation == null)
      {
         List<CompoundComponent> comps = this.getComponents();
         if (!comps.isEmpty()) {
             final CompoundComponent comp = comps.get(0);
             comp.setDataRelation(relation);
         }
      }
   }
   
   /**
    * Function that combines the FQL of the components from this query.
    * The FQLs are separated by a comma.
    * 
    * This function is used for logging purposes.
    * 
    * @return  String
    *          The combined FQLs.
    */
   public String getCombinedFqls()
   {
      // Will return null if not all components have a valid fql
      StringBuilder fql = new StringBuilder();
      List<CompoundComponent> comps = getComponents();
      int size = comps.size();
      for (int i = 0; i < size; i++)
      {
         Joinable query = comps.get(i).getQuery();
         String result = null;
         if (query instanceof PreselectQuery)
         {
            result = ((PreselectQuery) query).getHQL();
         }
         else if (query instanceof RandomAccessQuery)
         {
            result = ((RandomAccessQuery) query).getBundleFql();
         }
         
         if (result == null)
         {
            return null;
         }
         
         fql.append(result);
         if (i < size - 1)
         {
            fql.append(", ");
         }     
      }
      
      return fql.toString();
   }
   
   /**
    * Function that sums and returns the execution time of the components, denoting the total time of 
    * execution of this query.
    * 
    * This function is used for logging purposes.
    * 
    * @return  double
    *          The total time of execution of this {@code CompoundQuery}.
    */
   public double getCombinedExecutionTime()
   {
      // It presumes that all components have been executed
      double totalTime = 0;
      List<CompoundComponent> comps = getComponents();
      int size = comps.size();
      for (int i = 0; i < size; i++)
      {
         Joinable query = comps.get(i).getQuery();
         String result = null;
         if (query instanceof PreselectQuery)
         {
            result = ((PreselectQuery) query).getHQL();
         }
         else if (query instanceof RandomAccessQuery)
         {
            result = ((RandomAccessQuery) query).getBundleFql();
         }
         
         // Statistics about the query executed by this component
         P2JQueryStatistics statistics = P2JQueryStatistics.getStatistics(query, result);
         totalTime += statistics.getTime();
      }
      
      return totalTime;
   }
   
   /**
    * Reposition the cursor such that a request to retrieve the next result
    * will retrieve the result which matches the specified array of primary
    * key IDs.  A request to retrieve the previous result will retrieve the
    * result immediately previous to this, if any, in the results list.
    *
    * @param   id1
    *          A primary key ID, representing the target record (in the case
    *          of a join, this ID is associated with the left-most record in
    *          the join).
    * @param   joinIDs
    *          All remaining primary key IDs, if any, arranged from left to
    *          right to coincide with the records being joined by the
    *          underlying query.
    */
   @Override
   public void repositionByID(rowid id1, rowid...joinIDs)
   {
      if (id1 == null || id1.isUnknown())
      {
         RecordBuffer[] buffers = getRecordBuffersInternal();
         RecordBuffer lastSetBuffer = buffers[buffers.length - 1];
         lastSetBuffer.release(true);
         ErrorManager.recordOrThrowError(7316, "Invalid rowid argument(s) given in query object method", false, false);
         return;
      }
      
      super.repositionByID(id1, joinIDs);
   }

   /**
    * Register a <code>Finalizable</code> with the transaction manager for
    * resource cleanup when this query's scope ends.
    */
   protected void registerCleaner()
   {
      // If we are not a standalone query, we assume our container will
      // register for cleanup.
      if (regCleaner || !isStandalone())
      {
         return;
      }
      
      TransactionManager.registerFinalizable(cleaner, false);
      
      regCleaner = true;
   }

   /**
    * This method is an extension to the AbstractQuery normal behavior. On a 
    * reposition, the buffers should be validated and eventually fire their 
    * write triggers. Furthermore, the bottom-most buffer should be released.
    * The other buffers should be indeterminably released - this feature is safe to 
    * ignore.
    *
    * @param   permitFetch
    *          <code>true</code> to permit immediate record fetch;
    *          <code>false</code> to override <code>fetchOnReposition</code>
    *          flag and explicitly disallow immediate record fetch.
    * @param   error
    *          <code>true</code> if error happened during reposition.
    */
   @Override
   protected void afterReposition(boolean permitFetch, boolean error)
   {
      if (error)
      {
         super.afterReposition(permitFetch, true);
         return;
      }
      
      // after a reposition, the write triggers should be fired as part of a validation scenario
      RecordBuffer[] buffers = getRecordBuffersInternal();
      for (RecordBuffer buffer : buffers)
      {
         buffer.validate(false, false);
      }

      if (buffers.length > 0)
      {
         // the bottom-most buffer should be always released after repositioning
         RecordBuffer buf = buffers[buffers.length - 1];
         buf.release(true);
      }

      super.afterReposition(permitFetch, false);
   }

   /**
    * Init structures required to use reference row: create the map of all buffers, register
    * parameter filters for sub-queries and register this query as a record change listener.
    *
    * @param   globalScope
    *          Flag indicating if the block at which this query is registered as record change listener is
    *          the global scope.
    */
   protected void initReferenceRowSupport(boolean globalScope)
   {
      if (!preselect && buffersMap == null)
      {
         // cache set of all buffers and set parameter filters
         buffersMap = new LinkedHashMap<>();
         int bufferIndex = 0;
         List<CompoundComponent> compoundComponents = getComponents();
         for (int i = 0; i < compoundComponents.size(); i++)
         {
            CompoundComponent component = compoundComponents.get(i);
            Joinable query = component.getQuery();
            RecordBuffer[] queryBuffers = query.getRecordBuffers();
            
            for (RecordBuffer buffer : queryBuffers)
            {
               buffersMap.put(buffer, bufferIndex);
               
               if (bufferIndex > 0)
               {
                  query.setParameterFilter(new SnapshotParameterFilter(bufferIndex));
               }
               bufferIndex++;
            }
         }
         
         ChangeBroker.get().addListener(this);
         if (globalScope)
         {
            unregisterOnCleanup = true;
         }
      }
   }
   
   /**
    * 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
    * result in break value being <code>null</code>.
    *
    * @return  {@code null} always; when a compound query is the backing query for
    *          a dynamic results object, it should never return a non-null value,
    *          because it cannot shift into a preselect mode.
    */
   protected Object getBreakValue()
   {
      return null;
   }
   
   /**
    * Retrieve a virtual, composite row of results for the compound query.
    * This method implements a client-side join of multiple, single-row
    * record retrievals performed by the underlying, component queries.
    * It works first from outermost component inward, retrieving a record at
    * each level.  When each component's backing buffer contains a record,
    * the composite retrieval is complete and the method returns.  On
    * subsequent iterations, the innermost component is advanced first.  If
    * no more matches are possible at this level, the algorithm moves outward
    * one level and attempts to advance that level.  On success, it moves
    * inward again, on failure, it moves outward again.  This continues until
    * either a new composite row of results is assembled, or no further
    * records can be found.  This progression ensures that each inner query
    * cycles through all of its matches before the next outer query advances.
    * <p>
    * <b>Preselect Mode</b><br>
    * <code>CompoundQuery</code> generally operates as a dynamic query,
    * though it can be used to generate a fixed list of preselected results.
    * When in this mode, the first time this method is executed, a results
    * list is generated.  This is done by retrieving each virtual, composite
    * row which meets the overall query's criteria, and caching the primary
    * keys of these records in the underlying cursor.  Each underlying query
    * is assigned the correct subset of these IDs, such that subsequent
    * retrieval requests can safely use the cursor to retrieve the correct
    * records.  As a result, in preselect mode, this method is only ever
    * invoked once per query.
    *
    * @param   navigation
    *          Type of advancement applied to the compound query;  i.e., how
    *          the virtual composite result set is navigated as a whole:
    *          forward (next), backward (previous), jump to first or last, or
    *          reload current records in all buffers.
    * @param   lockType
    *          Overriding lock type to apply to each underlying buffer.
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    *
    * @return  An array of the primary key IDs for every table involved in
    *          the query, (which together identify a unique, virtual record),
    *          if a result row was successfully retrieved;
    *          <code>null</code> if no more results were available (and we
    *          were not iterating).
    *
    * @throws  QueryOffEndException
    *          if no more results were available in iterating mode.
    * @throws  ErrorConditionException
    *          if an error occurred retrieving the record for an underlying
    *          query.
    */
   protected Object[] retrieve(int navigation, LockType lockType, boolean iterating)
   {
      return retrieve(navigation, lockType, iterating, false);
   }
   
   /**
    * Retrieve a virtual, composite row of results for the compound query.
    * This method implements a client-side join of multiple, single-row
    * record retrievals performed by the underlying, component queries.
    * It works first from outermost component inward, retrieving a record at
    * each level.  When each component's backing buffer contains a record,
    * the composite retrieval is complete and the method returns.  On
    * subsequent iterations, the innermost component is advanced first.  If
    * no more matches are possible at this level, the algorithm moves outward
    * one level and attempts to advance that level.  On success, it moves
    * inward again, on failure, it moves outward again.  This continues until
    * either a new composite row of results is assembled, or no further
    * records can be found.  This progression ensures that each inner query
    * cycles through all of its matches before the next outer query advances.
    * <p>
    * <b>Preselect Mode</b><br>
    * <code>CompoundQuery</code> generally operates as a dynamic query,
    * though it can be used to generate a fixed list of preselected results.
    * When in this mode, the first time this method is executed, a results
    * list is generated.  This is done by retrieving each virtual, composite
    * row which meets the overall query's criteria, and caching the primary
    * keys of these records in the underlying cursor.  Each underlying query
    * is assigned the correct subset of these IDs, such that subsequent
    * retrieval requests can safely use the cursor to retrieve the correct
    * records.  As a result, in preselect mode, this method is only ever
    * invoked once per query. 
    *
    * @param   navigation
    *          Type of advancement applied to the compound query;  i.e., how
    *          the virtual composite result set is navigated as a whole:
    *          forward (next), backward (previous), jump to first or last, or
    *          reload current records in all buffers.
    * @param   lockType
    *          Overriding lock type to apply to each underlying buffer.
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    * @param   peek
    *          <code>true</code> if sub-queries should be interrogated using
    *          {@link Scrollable#peekNext()}/{@link Scrollable#peekPrevious()}
    *          rather than {@link P2JQuery#next()}/{@link P2JQuery#previous()}.
    *          The difference is that in the first case sub-queries return the
    *          next/previous record from underlying results, while in the second
    *          case they return the next/previous record after the current row.
    *
    * @return  An array of the primary key IDs for every table involved in
    *          the query, (which together identify a unique, virtual record),
    *          if a result row was successfully retrieved;
    *          <code>null</code> if no more results were available (and we
    *          were not iterating).
    *
    * @throws  QueryOffEndException
    *          if no more results were available in iterating mode.
    * @throws  ErrorConditionException
    *          if an error occurred retrieving the record for an underlying
    *          query.
    */
   protected Object[] retrieve(int navigation, LockType lockType, boolean iterating, boolean peek)
   {
      translateWhere();
      
      initReferenceRowSupport(false);
      
      Object[] rowData;
      
      if (!peek)
      {
         OffEnd offEnd = getOffEnd();
         if (!(offEnd == OffEnd.FRONT && navigation == PREVIOUS) &&
             !(offEnd == OffEnd.BACK && navigation == NEXT))
         {
            currentRowDeleted = false;
         }
      }
      
      registerCleaner();
      
      if (preselect)
      {
         preselectResults();
         
         // Fulfill the retrieval request using the cached results.
         switch (navigation)
         {
            case NEXT:
               rowData = cursor.getNext();
               break;
            case FIRST:
               rowData = cursor.getFirst();
               break;
            case PREVIOUS:
               rowData = cursor.getPrevious();
               break;
            case LAST:
               rowData = cursor.getLast();
               break;
            default:
               rowData = null;
         }
         
         boolean loaded = loadByValues(rowData, lockType, iterating);
         
         if (!isScrolling() && !foundFirst && rowData != null && loaded)
         {
            foundFirst = true;
         }
      }
      else
      {
         // Non-preselect mode;  retrieve composite record dynamically.
         rowData = retrieveImpl(navigation, lockType, iterating, peek);
      }
      
      return rowData;
   }
   
   /**
    * Determines if the query is presorted, i.e. if the query is sorted up front in some special
    * way.
    *
    * @return <code>true</code> if the query is presorted.
    */
   protected boolean isPresorted()
   {
      return false;
   }
   
   /**
    * If the query is a presort query, then presort the results stored in the cursor.
    * No-op for this implementation.
    */
   protected void presort()
   {
   }
   
   /**
    * Preselect all results by creating a common cursor and {@link SimpleResults} for each
    * component.
    */
   protected void preselectResults()
   {
      if (preselected)
      {
         return;
      }
      
      registerCleaner();

      Object[] rowData;
      
      // Create a cursor.
      setScrolling();
      
      int size = 0;
      
      // Logging setup.
      boolean trace = log.isLoggable(Level.FINEST);
      StringBuilder buf = null;
      String nl = null;
      if (trace)
      {
         nl = System.getProperty("line.separator");
         buf = new StringBuilder("Preselected IDs:");
      }
      
      // Prepare per-table, preliminary result sets.
      Set<List<Object>>[] idSets = null;
      int[] widths = null;
      
      // Preselect all records which match the query's criteria.
      do
      {
         rowData = retrieveImpl(NEXT, LockType.NONE, false, false);
         cursor.addResultNext(rowData);
         
         // initialize idSets and widths below retrieveImpl, so we are working with the
         // optimized form of the compound query
         if (size == 0)
         {
            size = components.size();
            
            idSets = new Set[size];
            for (int i = 0; i < size; i++)
            {
               idSets[i] = new LinkedHashSet<>();
            }
            
            widths = new int[size];
            Iterator<CompoundComponent> iter = components.iterator();
            for (int i = 0; iter.hasNext(); i++)
            {
               widths[i] = iter.next().getQuery().getTableCount();
            }
         }
         
         if (rowData != null)
         {
            int index = 0;
            for (int i = 0; i < size; i++)
            {
               // How many tables does this joinable represent?
               int w = widths[i];
               
               // Initialize the sub-array for this joinable component.
               // We use an ArrayList instead of an array directly so we
               // can add this to a Set.
               List<Object> subIDs = new ArrayList<>();
               for (int j = 0; j < w && index < tableCount; j++, index++)
               {
                  subIDs.add(rowData[index]);
               }
               idSets[i].add(subIDs);
            }
            
            if (trace)
            {
               buf.append(nl);
               buf.append("  ");
               for (int i = 0; i < size; i++)
               {
                  buf.append(" ");
                  buf.append(rowData[i]);
               }
            }
         }
      }
      while (rowData != null);
      
      if (isPresorted())
      {
         presort();
      }
      
      // Set up and install final result sets into underlying queries.
      // This is necessary because inner tables in a multi-table query
      // will have an incorrect result set left over from the first pass
      // scan for the last iteration of their enclosing loop.  This
      // would wreak havoc when trying to load from IDs later.
      for (int i = 0; i < size; i++)
      {
         List<Object[]> rows = new ArrayList<>();
         Iterator<List<Object>> iter2 = idSets[i].iterator();
         while (iter2.hasNext())
         {
            List<Object> subIDs = iter2.next();
            rows.add(subIDs.toArray());
         }
         
         SimpleResults results = new SimpleResults(rows);
         Joinable query = components.get(i).getQuery();
         query.setResults(results);
      }
      
      // Reset state and cursor.
      activeIndex = -1;
      preselected = true;
      cursor.addResultNext(null);
      cursor.beforeFirst();
      
      if (trace)
      {
         log.log(Level.FINEST, buf.toString());
      }
   }
   
   /**
    * Synchronize the query with the cursor by causing the query to load the
    * current row's results (if any) into its backing buffer(s).
    */
   protected void loadRowAtCursor()
   {
      Object[] row = cursor.getRow();
      if (row != null)
      {
         loadByValues(row, null, false);
      }
   }
   
   /**
    * Load a virtual, composite record by loading each record buffer managed
    * by this query with a DMO, given its primary key ID in the specified
    * <code>ids</code> array.  The IDs are arranged to coincide with the
    * tables underlying the query, from left-most to right-most (in the sense
    * of how they are joined by the query).
    *
    * @param   data
    *          Array of primary key IDs or DMOs, one per table involved in the
    *          query
    * @param   lockType
    *          Lock type which should override that of any underlying query.
    *          Set to <code>null</code> to allow underlying query to use its
    *          existing lock type.
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    *
    * @return  <code>true</code> if <code>ids</code> was not
    *          <code>null</code>;  else <code>false</code>.
    *
    * @see     #collectRowData
    */
   protected boolean loadByValues(Object[] data, LockType lockType, boolean iterating)
   {
      return loadByValues(data, lockType, iterating, true);
   }
   
   /**
    * Load a virtual, composite record by loading each record buffer managed
    * by this query with a DMO, given its primary key ID in the specified
    * <code>ids</code> array.  The IDs are arranged to coincide with the
    * tables underlying the query, from left-most to right-most (in the sense
    * of how they are joined by the query).
    *
    * @param   data
    *          Array of primary key IDs or DMOs, one per table involved in the
    *          query
    * @param   lockType
    *          Lock type which should override that of any underlying query.
    *          Set to <code>null</code> to allow underlying query to use its
    *          existing lock type.
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    * @param   loadAll
    *          <code>true</code> to load all components
    *          <code>false</code> to only load components up to the current active index;
    *
    * @return  <code>true</code> if <code>ids</code> was not
    *          <code>null</code>;  else <code>false</code>.
    *
    * @see     #collectRowData
    */
   protected boolean loadByValues(Object[] data, LockType lockType, boolean iterating, boolean loadAll)
   {
      if (data == null)
      {
         if (iterating)
         {
            throw new QueryOffEndException("",
                  getOffEndListeners().toArray(new QueryOffEndListener[0]));
         }
         
         return false;
      }
      
      int compsLoadCount = (loadAll) ? components.size() : activeIndex;
      
      // Reset active index and counters.
      if (loadAll)
      {
         activeIndex = 0;
         counters = new int[components.size()];
      }
      
      // Iterate over the query components, updating the buffers of each
      // with the appropriate record.
      int index = 0;
      for (int i = 0; i < compsLoadCount; i++)
      {
         // Get the next query component.
         CompoundComponent comp = components.get(i);
         
         // Get the underlying query.
         Joinable query = comp.getQuery();
         
         // How many tables does this joinable query represent?
         int width = query.getTableCount();
         
         // Initialize the sub-array for this joinable component.
         Object[] subData = new Object[width];
         for (int j = 0; j < width && index < tableCount; j++, index++)
         {
            subData[j] = data[index];
         }
         
         try
         {
            query.load(subData, lockType, true);
         }
         catch (MissingRecordException exc)
         {
            releaseAllBuffers();
            return false;
         }
         catch (PersistenceException exc)
         {
            if (log.isLoggable(Level.WARNING))
            {
               log.log(Level.WARNING, "Error loading data", exc);
            }
            
            return false;
         }
         catch (QueryOffEndException exc)
         {
            return false;
         }
         catch (ConditionException exc)
         {
            if (loadAll)
            {
               activeIndex = -1;
            }
            
            throw exc;
         }
         
         if (loadAll)
         {
            // Reflect the fact that this component has been updated.
            counters[i]++;
            activeIndex++;
         }
      }
      
      if (loadAll)
      {
         activeIndex--;
      }
      else
      {
         activeIndex = compsLoadCount;
      }
      
      return true;
   }
   
   /**
    * Indicate whether this object can be optimized.
    * 
    * @return  <code>true</code> by default. Subclasses should override this behavior as needed.
    */
   protected boolean canOptimize()
   {
      return true;
   }
   
   /**
    * Register the query within each component as a record change listener.
    * 
    * @return  <code>true</code> if the global scope is used for registration.
    */
   private boolean registerRecordChangeListeners()
   {
      // Find for each compound query component the most outer scope among
      // nearest external scope and the outermost scopes of buffers used by
      // this component and register the component into this scope.
      int externalScope = BufferManager.get().findNearestExternal();
      int lowestScope = externalScope;
      
      List<CompoundComponent> compoundComponents = getComponents();
      for (int i = 0; i < compoundComponents.size(); i++)
      {
         CompoundComponent comp = compoundComponents.get(i);
         Joinable query = comp.getQuery();
         if (!query.shouldRegisterRecordChangeListeners())
         {
            continue;
         }

         int scope;
         if (query.isDynamicPredicate())
         {
            scope = 0;
         }
         else
         {
            scope = externalScope;
            RecordBuffer[] buffers = query.getRecordBuffers();

            for (RecordBuffer buffer : buffers)
            {
               scope = ((scope == -1)
                  ? buffer.getScopeOpenDepth()
                  : Math.min(scope, buffer.getScopeOpenDepth()));
            }
         }

         lowestScope = ((lowestScope == -1) ? scope : Math.min(lowestScope, scope));
         
         if (scope != -1)
         {
            query.registerRecordChangeListeners(Integer.valueOf(scope));
         }
      }
      
      return lowestScope == 0;
   }
   
   /**
    * Method which attempts to optimize this object's query components into one or more
    * server-side joins. Returns immediately if this is not the first attempt to do so.
    * <p>
    * This method must only be invoked after the first query result is fetched via the
    * un-optimized query, to ensure we correctly preserve the pre-query state of the involved
    * record buffers, in the event the compound query runs off end immediately.
    */
   private void maybeOptimize()
   {
      if (components != null)
      {
         // already optimized (or at least tried)
         return;
      }
      
      try
      {
         if (!canOptimize())
         {
            components = originalComponents;
            
            return;
         }
         
         new Optimizer();
         
         if (deferOptimization && components.size() < originalComponents.size())
         {
            // register the new, optimized queries as change listeners; if a query was left
            // unoptimized, the change broker is tolerant of a listener being added multiple times
            registerRecordChangeListeners();
         }
      }
      finally
      {
         if (forceFullResults)
         {
            ArrayList<CompoundComponent> comps = getComponents();
            for (int i = 0; i < comps.size(); i++)
            {
               CompoundComponent cc = comps.get(i);
               cc.getQuery().hintFullResults();
            }
         }
         
         if (isPresort() && isIterating())
         {        
            ArrayList<CompoundComponent> comps = getComponents();
            for (int i = 0; i < comps.size(); i++)
            {
               CompoundComponent cc = comps.get(i);
               cc.getQuery().setIterating(true);
               cc.getQuery().setPresort(true);
            }
         }
      }
   }
   
   /**
    * Get an array of all record buffers managed by this query.
    *
    * @return  All record buffers managed by this query.
    */
   private RecordBuffer[] getRecordBuffersInternal()
   {
      RecordBuffer[] bufs = new RecordBuffer[getTableCount()];
      
      int i = 0;
      ArrayList<CompoundComponent> compoundComponents = getOriginalComponents();
      for (int j = 0; j < compoundComponents.size(); j++)
      {
         CompoundComponent comp = compoundComponents.get(j);
         for (RecordBuffer buf : comp.getQuery().getRecordBuffers())
         {
            bufs[i++] = buf;
         }
      }
      
      return bufs;
   }
   
   /**
    * Iterate the component queries of this object and set their browsed flags to match that of
    * this object.
    */
   private void setComponentsBrowsed()
   {
      boolean browsed = isBrowsed();
      
      for (int i = 0; i < originalComponents.size(); i++)
      {
         CompoundComponent next = originalComponents.get(i);
         next.getQuery().setBrowsed(browsed);
      }
      
      if (components != null && components != originalComponents)
      {
         for (int i = 0; i < components.size(); i++)
         {
            CompoundComponent next = components.get(i);
            next.getQuery().setBrowsed(browsed);
         }
      }
   }
   
   /**
    * Assemble an array of primary key IDs or DMOs for the current records in
    * the buffers underlying this query, from left-most to right-most (in the
    * sense of how the query joins the associated tables).  This method
    * essentially takes a lightweight snapshot of the currently loaded
    * records, so that this may be restored later.
    * <p>
    * This snapshot is only generated for scrolling queries.
    *
    * @param   navigation
    *          Type of advancement applied to the compound query;  i.e., how
    *          the virtual composite result set is navigated as a whole:
    *          forward (next), backward (previous), jump to first or last, or
    *          reload current records in all buffers.
    * 
    * @return  Array of primary key IDs or DMOs, one per table involved in the
    *          query.
    *
    * @see     #loadByValues
    */
   private Object[] collectRowData(int navigation)
   {
      if (isScrolling())
      {         
         switch (navigation)
         {
            case NEXT:
            case LAST:
               if (cursor.isOffEnd(OffEnd.BACK))
               {
                  return null;
               }
               break;
            case PREVIOUS:
            case FIRST:
               if (cursor.isOffEnd(OffEnd.FRONT))
               {
                  return null;
               }
            default:
               break;
         }
      }
      else
      {
         if (isOffEnd().booleanValue())
         {
            return null;
         }
      }
      
      Object[] rowData = new Object[tableCount];
      int dstIdx = 0;
      ArrayList<CompoundComponent> comps = getComponents();
      for (int i = 0; i < comps.size(); i++)
      {
         Joinable query = comps.get(i).getQuery();
         Object[] subData = query instanceof AdaptiveQuery
                            ? ((AdaptiveQuery) query).getCachedRow()
                            : query.getRow();
         if (subData != null)
         {
            System.arraycopy(subData, 0, rowData, dstIdx, subData.length);
         }
         dstIdx += query.getTableCount();
      }
      
      return rowData;
   }
   
   /**
    * Retrieve a virtual, composite row of results for the compound query.
    * This method implements a client-side join of multiple, single-row
    * record retrievals performed by the underlying, component queries.
    * It works first from outermost component inward, retrieving a record at
    * each level.  When each component's backing buffer contains a record,
    * the composite retrieval is complete and the method returns.  On
    * subsequent iterations, the innermost component is advanced first.  If
    * no more matches are possible at this level, the algorithm moves outward
    * one level and attempts to advance that level.  On success, it moves
    * inward again, on failure, it moves outward again.  This continues until
    * either a new composite row of results is assembled, or no further
    * records can be found.  This progression ensures that each inner query
    * cycles through all of its matches before the next outer query advances.
    *
    * @param   navigation
    *          Type of advancement applied to the compound query;  i.e., how
    *          the virtual composite result set is navigated as a whole:
    *          forward (next), backward (previous), jump to first or last, or
    *          reload current records in all buffers.
    * @param   lockType
    *          Overriding lock type to apply to each underlying buffer.
    * @param   iterating
    *          <code>true</code> if this method is being invoked within the
    *          context of an iterating loop, which can only be exited by
    *          raising an end condition;  <code>false</code> if end condition
    *          should not be raised, even if no further advancement of the
    *          composite result is possible.
    * @param   peek
    *          <code>true</code> if sub-queries should be interrogated using
    *          {@link Scrollable#peekNext()}/{@link Scrollable#peekPrevious()}
    *          rather than {@link P2JQuery#next()}/{@link P2JQuery#previous()}.
    *          The difference is that in the first case sub-queries return the
    *          next/previous record from underlying results, while in the second
    *          case they return the next/previous record after the current row.
    *
    * @return  An array of the primary key IDs for every table involved in
    *          the query, (which together identify a unique, virtual record),
    *          if a result row was successfully retrieved;
    *          <code>null</code> if no more results were available (and we
    *          were not iterating).
    *
    * @throws  QueryOffEndException
    *          if no more results were available in iterating mode.
    * @throws  ErrorConditionException
    *          if an error occurred retrieving the record for an underlying
    *          query.
    */
   private Object[] retrieveImpl(int navigation, LockType lockType, boolean iterating, boolean peek)
   {
      translateWhere();
      
      if (!deferOptimization)
      {
         // safe to call multiple times; returns immediately if called previously
         maybeOptimize();
      }
      
      ArrayList<CompoundComponent> comps = getComponents();
      
      int size = comps.size();
      if (size > 0)
      {
         final CompoundComponent comp = comps.get(0);
         comp.setDataRelation(this.getDataRelation());
      }
      
      // determine which outer components returned no record for the current search
      boolean[] nullOuter = new boolean[size];
      
      // If we are getting the first or last compound row, reset activeIndex
      // so that we are forced to get all components.  This prevents interim
      // state from interfering with a first/last request.
      switch (navigation)
      {
         case FIRST:
         case LAST:
            activeIndex = -1;
            break;
         default:
            break;
      }
      
      // Is this the first iteration in a new series?
      if (activeIndex < 0)
      {
         activeIndex = 0;
         
         // Create array to hold current loop counts, each initialized to 0.
         counters = new int[size];
         useSnapshot = new boolean[getTableCount() - 1];
         
         // Disable outer component queries' buffers' release-on-iterate
         // default behavior for the enclosing scope.  This is necessary to
         // prevent the premature release of an outer buffer's current
         // record while we are manually joining it with an inner buffer's
         // records.
         int end = size - 1;
         for (int i = 0; i < end; i++)
         {
            CompoundComponent comp = comps.get(i);
            RecordBuffer[] buffers = comp.getQuery().getRecordBuffers();
            int len = buffers.length;
            for (int j = 0; j < len; j++)
            {
               buffers[j].disableReleaseOnIterate();
            }
         }
      }
      
      Object[] rowData = null;
      
      // Array of indexes in rowData at which sub-data for each component begins
      int[] subIndexes = null;
      
      if (peek)
      {
         rowData = new Object[tableCount];
         subIndexes = new int[size];
         int dstIndex = 0;
         int j = 0;
         for (int i = 0; i < comps.size(); i++)
         {
            CompoundComponent component = comps.get(i);
            Joinable query = component.getQuery();
            subIndexes[j++] = dstIndex;
            dstIndex += query.getTableCount();
         }
      }
      
      if (activeIndex > 0 && isScrolling())
      {
         Object[] row = cursor.getRow();
         if (row != null)
         {
            loadByValues(row, lockType, false, false);
         }
      }
      
      boolean offEnd = false;
      boolean advancing = false;
      Database database = null;
      for (int i = activeIndex; i < size && i >= 0; activeIndex = i)
      {
         nullOuter[i] = false;
         CompoundComponent comp = comps.get(i);
         RecordBuffer[] recordBuffers = comp.getQuery().getRecordBuffers();
         comp.getQuery().setLenientOffEnd(true);
         
         if (recordBuffers != null && recordBuffers.length > 0)
         {
            database = recordBuffers[0].getDatabase();
         }
         
         try
         {
            Object[] subData = P2JQueryExecutor.getInstance().execute(database, 
                                                                      (String) null,
                                                                      CompoundQuery::processComponent, 
                                                                      this,
                                                                      navigation,
                                                                      comp,
                                                                      counters[i],
                                                                      lockType,
                                                                      peek);
            
            if (subData == null)
            {
               if (comp.isOuter() && counters[i] == 0)
               {
                  // if this is the first usage of this outer join component, and no record was found
                  // then either advance or go back, depending on if this component was queried
                  // before
                  boolean prevNullOuter = nullOuter[i];
                  
                  if (!prevNullOuter && advancing)
                  {
                     nullOuter[i] = true;
                     i++;
                  }
                  else
                  {
                     advancing = false;
                     i--;
                  }
                  
                  comp.getQuery().setUnknownRecord();
                  
                  Arrays.fill(useSnapshot, false);
               }
               else
               {
                  // this branch treats the case of an inner join or an outer join component (which 
                  // did found records)
                  
                  // Reset counter for this component.
                  counters[i] = 0;
                  if (i > 0)
                  {
                     Arrays.fill(useSnapshot, i - 1, useSnapshot.length, false);
                  }
                  
                  // If we've propagated back up to the left-most component and
                  // it has no more results, we're done.
                  offEnd = (--i < 0);
                  advancing = false;
                  if (offEnd)
                  {
                     // If not iterating, assume client code is relying on
                     // an availability test to end loop.
                     break;
                  }
               }
               
               continue;
            }
            
            if (peek)
            {
               try
               {
                  comp.getQuery().load(subData, lockType, false);
               }
               catch (MissingRecordException exc)
               {
                  continue;
               }
               
               System.arraycopy(subData, 0, rowData, subIndexes[i], subData.length);
            }
           
            counters[i]++;
            
            if (cursor != null)
            {
               cursor.resetReferenceRow(i);
            }
            
            advancing = true;
            i++;
         }
         catch (ErrorConditionException exc)
         {
            activeIndex = -1;
            
            throw exc;
         }
         catch (PersistenceException exc)
         {
            ErrorManager.recordOrThrowError(exc);
         }
      }
      
      // the next record search must start with the left-most component which was 
      // not an outer join component with no record found.
      while (activeIndex > 0 && nullOuter[activeIndex - 1])
      {
         activeIndex = activeIndex - 1;
      }
      activeIndex = activeIndex - 1;
      
      if (log.isLoggable(Level.FINEST))
      {
         log.log(Level.FINEST, "activeIndex = " + activeIndex);
         log.log(Level.FINEST, "nullOuter = " + Arrays.toString(nullOuter));
         StringBuilder buf = new StringBuilder("counters:  ");
         for (int i = 0; i < size; i++)
         {
            buf.append(counters[i]);
            buf.append(" ");
         }
         log.log(Level.FINEST, buf.toString());
      }
      
      if (!offEnd && !peek)
      {
         if (!postponeAccumulation)
         {
            // Notify accumulators of a successful iteration.
            accumulate();
         }
         
         incrementMoves();
         
         if (!isScrolling() && !foundFirst)
         {
            foundFirst = true;
         }
      }
      
      if (peek && !offEnd)
      {
         // In rowData we have only sub-data for components which have been iterated.
         // Fill data for remaining components.
         for (int i = 0; i < subIndexes.length; i++)
         {
            int index = subIndexes[i];
            if (rowData[index] == null)
            {
               Joinable query = comps.get(i).getQuery();
               Object[] subData = query instanceof AdaptiveQuery
                                  ? ((AdaptiveQuery) query).getCachedRow()
                                  : query.getRow();
               if (subData != null)
               {
                  System.arraycopy(subData, 0, rowData, index, subData.length);
               }
            }
         }
      }
      
      if (!offEnd)
      {
         if (!peek)
         {
            rowData = collectRowData(navigation);
         }
         
         if (deferOptimization)
         {
            // An attempt to optimize the compound query only will be made once, after the first
            // result is obtained successfully. On subsequent attempts, the optimize method is
            // effectively a no-op which returns immediately. We wait to optimize until after the
            // first result is fetched, because if we optimize before this and the optimized
            // query runs off end, the individual buffers may not be in the correct state. If an
            // individual query component fails to retrieve a record, any buffers of inner
            // components must maintain their original, pre-query state. If a query is optimized
            // such that a server-side join fails to return a composite record, we don't know
            // which buffers should be cleared and which should retain their pre-query state.
            // Although it might be considered an application-level bug to rely on this behavior,
            // we must maintain compatibility with this quirk to avoid discrepancies in behavior.
            maybeOptimize();
         }
      }
      
      return (offEnd ? null : rowData);
   }
   
   /**
    * Switch on postponed accumulation on next retrieve.
    */
   protected void postponeAccumulation()
   {
      postponeAccumulation = true;
   }
   
   /**
    * Switch on postponed accumulation on next retrieve.
    */
   protected void restoreAccumulation()
   {
      postponeAccumulation = false;
   }
   
   /**
    * Release all backing buffers of for all components.
    */
   protected void releaseAllBuffers()
   {
      ArrayList<CompoundComponent> compoundComponents = getComponents();
      for (int i = 0; i < compoundComponents.size(); i++)
      {
         CompoundComponent comp = compoundComponents.get(i);
         RecordBuffer[] bufs = comp.getQuery().getRecordBuffers();
         for (RecordBuffer buf : bufs)
         {
            buf.release(false);
         }
      }
   }
   
   /**
    * Add a component query to this compound query and adjust the table count accordingly.
    * 
    * @param   component
    *          Query component to be added.
    */
   void addCompoundComponent(CompoundComponent component)
   {
      if (isForwardOnly())
      {
         component.getQuery().setForwardOnly(true);
      }
      
      originalComponents.add(component);
      
      // Update total table count.
      tableCount += component.getQuery().getTableCount();
   }
   
   /**
    * Retrieve the list of original query components (unoptimized).
    *
    * @return  See above.
    */
   ArrayList<CompoundComponent> getOriginalComponents()
   {
      return originalComponents;
   }
   
   /**
    * Retrieve the list of query components. Depending on when this is called (before or after
    * optimization), it may return the list of original, unoptimized components, or the list of
    * optimized components.
    *
    * @return  See above.
    */
   private ArrayList<CompoundComponent> getComponents()
   {
      return components != null ? components : originalComponents;
   }
   
   /**
    * Initialize state of a component query.
    * 
    * @param   query
    *          Component query to be initialized.
    */
   private void initializeComponentQuery(Joinable query)
   {
      // Mark component query as not standalone.
      query.setStandalone(false);
      
      // In a compound query, joined first and last record retrievals must
      // not throw an ErrorConditionException if no record is found.
      query.setErrorIfNull(false);
      
      // Does the component query need to scroll?
      if (isScrolling())
      {
         query.setScrolling();
      }
      
      // a query which sorts on the client (application server) needs full records (not just
      // primary keys) to do so
      if (isPresorted())
      {
         query.setFullRecords();
      }
      
      // If necessary, indicate to the component query that it should behave
      // as if it were associated with a browse.
      if (isBrowsed())
      {
         query.setBrowsed(isBrowsed());
      }
   }
   
   /**
    * Advance a single query component, according to the overall query
    * navigation direction, and that component's natural iteration type.  It
    * is the intersection of these two criteria which determines the actual
    * type of retrieval which is performed.
    *
    * @param   navigation
    *          Type of advancement applied to the compound query;  i.e., how
    *          the virtual composite result set is navigated as a whole:
    *          forward (next), backward (previous), jump to first or last, or
    *          reload current records in all buffers.
    * @param   component
    *          Query component being asked to retrieve a record.
    * @param   count
    *          Index of this query component's result within the current join.
    * @param   lockType
    *          Lock type to apply to the retrieved record for this query
    *          component
    * @param   peek
    *          <code>true</code> if sub-queries should be interrogated using
    *          {@link Scrollable#peekNext()}/{@link Scrollable#peekPrevious()}
    *          rather than {@link P2JQuery#next()}/{@link P2JQuery#previous()}.
    *          The difference is that in the first case sub-queries return the
    *          next/previous record from underlying results, while in the second
    *          case they return the next/previous record after the current row.
    *          Peeking works only for NEXT and PREV iteration types.
    *
    * @throws  QueryOffEndException
    *          if query component is being asked to retrieve a FIRST, LAST,
    *          or UNIQUE record, and it has already done so in the current
    *          iteration pass (prevents looping infinitely on these
    *          constructs).
    *
    * @return  If <code>peek</code> is <code>true</code>: an array of
    *          the primary key IDs for the query component, if a result was
    *          successfully retrieved; <code>null</code> if no more results
    *          were available.<p>
    *          If <code>peek</code> is <code>false</code>: always returns
    *          <code>null</code>.
    *
    * @see     QueryConstants#RETRIEVE_MODES
    */
   private Object[] processComponent(int navigation,
                                     CompoundComponent component,
                                     int count,
                                     LockType lockType,
                                     boolean peek)
   {
      int iteration = component.getIteration();
      int retrieveMode = RETRIEVE_MODES[navigation][iteration];
      
      // Trigger end condition for one-shot iteration types if iterating and
      // we've already retrieved a record for this component in this iteration
      // pass.
      switch (retrieveMode)
      {
         case FIRST:
         case LAST:
         case UNIQUE:
            if (count > 0)
            {
               return null;
            }
            break;
            
         default:
            break;
      }
      
      Joinable query = component.getQuery();
      
      // Force the query to clear its state at the beginning of a loop pass.
      if (count == 0)
      {
         query.reset(!resolveOnce, false);
      }
      
      // Perform component query record retrieval.
      if (peek)
      {
         switch (retrieveMode)
         {
            case NEXT:
               return query.peekNext();
               
            case PREVIOUS:
               return query.peekPrevious();
               
            case FIRST:
               return query.peekFirst();
               
            case LAST:
               return query.peekLast();
               
            default:
               throw new UnsupportedOperationException(
                  "Unsupported record retrieval mode for peeking:  " + retrieveMode);
         }
      }
      else
      {
         boolean available = false;
         switch (retrieveMode)
         {
            case FIRST:
               available = query.first(lockType);
               break;
            case LAST:
               available = query.last(lockType);
               break;
            case NEXT:
               available = query.next(lockType);
               break;
            case PREVIOUS:
               available = query.previous(lockType);
               break;
            case UNIQUE:
               available = query.unique(lockType);
               break;
            case CURRENT:
               RecordBuffer[] buffers = query.getRecordBuffers();
               int len = buffers.length;
               
               if (!component.isOuter())
               {
                  available = query.current(lockType);
               }
               else
               {
                  boolean avail = true;
                  for (int i = 0; i < len && avail; i++)
                  {
                     avail = buffers[i].isAvailable();
                  }
                  if (avail)
                  {
                     available = query.current(lockType);
                  }
               }
               
               // getting the row after CURRENT is not easy because it may
               // be loaded by a loadValues and not part of results or cursor
               if (!available)
               {
                  return null;
               }
               
               if (this.isScrolling())
               {
                  Object[] row = new Object[len];
                  for (int i = 0; i < len; i++)
                  {
                     row[i] = buffers[i].getCurrentRecord();
                  }

                  return row;
               }
               else
               {
                  return query.getRow();
               }
               
            default:
               throw new UnsupportedOperationException(
                  "Unknown record retrieval mode:  " + retrieveMode);
         }
         
         if (available)
         {
            return query.getRow();
         }
      }
      
      return null;
   }
   
   /**
    * Translate the WHERE clause for all {@link #getComponents() components}.
    * <p>
    * This is done only once.
    */
   private void translateWhere()
   {
      if (whereTranslated)
      {
         return;
      }
      
      whereTranslated = true;
      ArrayList<CompoundComponent> compoundComponents = getComponents();
      for (int i = 0; i < compoundComponents.size(); i++)
      {
         CompoundComponent cc = compoundComponents.get(i);
         cc.getQuery().translateWhere();
      }
   }
   
   /**
    * Releases buffers from all components. The row position is not affected.
    */
   @Override
   public void releaseBuffers()
   {
      for (RecordBuffer buf : getRecordBuffersInternal())
      {
         buf.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)
   {
      for (int i = 0; i < originalComponents.size(); i++)
      {
         CompoundComponent comp = originalComponents.get(i);
         Joinable query = comp.getQuery();
         if (query.addDynamicFilter(fr, val, format))
         {
            components = null; // force re-optimize server-side joined queries
            return true;
         }
      }
      return false;
   }
   
   /**
    * 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()
   {
      for (int i = 0; i < originalComponents.size(); i++)
      {
         CompoundComponent comp = originalComponents.get(i);
         comp.getQuery().clearDynamicFilters();
      }
      
      components = null; // force re-optimize server-side joined queries
   }
   
   /**
    * Inner class which attempts to optimize the enclosing, compound query's individual query
    * components into server-side joins. This object will inspect and analyze the components to
    * determine whether they are likely to be suitable, server-side join candidates. If so, it
    * will create one or more instances of {@link AdaptiveQuery} or {@link PreselectQuery}
    * (depending on whether the original compound query is set to preselect its results) from
    * the suitable candidates it finds. Only query components from adjacent loops are joined,
    * as many as possible while still maintaining the likelihood that the join will be performant
    * (which is the whole point of the optimization).
    * <p>
    * The analysis considers factors such as the type of iteration (NEXT, FIRST, LAST) of the
    * original components, their substitution arguments, whether they utilize client-side
    * filtering, the complexity of their where clauses, the columns on which they would join and
    * the participation of those columns in database indexes, and of course whether the tables
    * being queried reside in the same physical database.
    * <p>
    * Zero or more optimized joins may result from this analysis. If the algorithm determines no
    * join is advisable, the original query components will be used unchanged. The algorithm will
    * attempt to create as few, multi-table, server-side join queries as possible from the
    * original components, ideally 1; however, the resulting, optimized queries may not represent
    * the entire original query, or even adjacent sets of nested queries. For example, a compound
    * query with 5 original components may be optimized to a single query representing all 5
    * tables; or a query for 2 of the tables and another for the other 3; 5 separate separate
    * queries (i.e., the original state); 2 and 2; 4 and 1; etc. The original, enclosing compound
    * query still will drive the record retrieval mechanics, it will just have fewer components
    * to which it delegates the actual work.
    */
   private class Optimizer
   {
      /**
       * Constructor which immediately performs the analysis and if possible, the optimizations.
       * Loops through each of the original query components and submits them to an analysis of
       * their suitability to be joined together at the database.
       */
      private Optimizer()
      {
         int max = originalComponents.size();
         components = new ArrayList<>(max);
         
         ArrayList<ArrayList<CompoundComponent>> grouped = new ArrayList<>(max);
         ArrayList<CompoundComponent> currentGroup = new ArrayList<>(max);
         grouped.add(currentGroup);
         
         // chunk original query components into sub-groups of server-joinable components; each
         // sub-group will have one component if no join is possible to that component, multiple
         // components if a server join among those components is possible, or may be empty
         
         boolean top = true;
         int i = 0;
         
         boolean finer = log.isLoggable(Level.FINER);
         
         Iterator<CompoundComponent> iter = originalComponents.iterator();
         while (iter.hasNext())
         {
            i++;
            
            CompoundComponent currentComp = iter.next();
            
            boolean possible = isServerJoinPossible(currentGroup, currentComp, top, i);
            
            if (activeIndex == i - 1)
            {
               // set activeIndex to index the correct component in the potentially reduced list
               // of joined components
               activeIndex = grouped.size() - (possible ? 1 : 0);
            }
            
            if (possible)
            {
               // the current component is join-able; add it to the current group
               currentGroup.add(currentComp);
               
               // nothing more to do for this component
               continue;
            }
            
            if (currentGroup.isEmpty())
            {
               // current group is empty and we determined that a downward join was not suitable
               // for this component, which is the only type of join we could achieve at this
               // point; all we can do is add the component to the group and create a new group
               // for the next pass
               currentGroup.add(currentComp);
               currentGroup = new ArrayList<>(max);
               grouped.add(currentGroup);
               top = false;
               
               continue;
            }
            
            if (finer)
            {
               log.log(Level.FINER, "no upward server join", new Throwable());
            }
            
            // current group is not empty and we determined that an upward join was not suitable
            // for this component, so we have to close off the last group and create a new one
            currentGroup = new ArrayList<>(max);
            grouped.add(currentGroup);
            top = false;
            
            // if there are no components after this one, we are done...
            if (!iter.hasNext())
            {
               // ...but we have to store the last failing component before we leave
               currentGroup.add(currentComp);
               
               break;
            }
            
            // now try again to see if the current component is suitable for a downward join as
            // the top of the new group
            possible = isServerJoinPossible(currentGroup, currentComp, top, i);
            
            currentGroup.add(currentComp);
            
            if (!possible)
            {
               if (finer)
               {
                  log.log(Level.FINER, "no downward server join", new Throwable());
               }
               
               // the current component is not suitable for a downward join either, so create a
               // new group for the next pass
               currentGroup = new ArrayList<>(max);
               grouped.add(currentGroup);
            }
         }
         
         for (int j = 0; j < grouped.size(); j++)
         {
            ArrayList<CompoundComponent> subgroup = grouped.get(j);
            int subSize = subgroup.size();
            
            if (subSize == 0)
            {
               continue;
            }
            
            CompoundComponent comp;
            
            if (subSize == 1)
            {
               // nothing to join; use original component
               comp = subgroup.get(0);
            }
            else
            {
               Joinable optimizedQuery;
               if (preselect)
               {
                  optimizedQuery = serverJoinPreselect(subgroup);
               }
               else
               {
                  optimizedQuery = serverJoinAdaptive(subgroup);
               }
               
               if (optimizedQuery == null)
               {
                  components.addAll(subgroup);
                  continue;
               }
               
               initializeComponentQuery(optimizedQuery);
               
               if (log.isLoggable(Level.FINE))
               {
                  log.fine("Optimized join of " + subSize + " compound query components");
               }
               
               // assign the iteration type of the outermost query in the original subgroup to the new,
               // joined component
               int iteration = subgroup.get(0).getIteration();
               comp = new CompoundComponent(optimizedQuery, iteration, subgroup.get(0).isOuter());
            }
            
            components.add(comp);
         }
         
         setComponentsBrowsed();
      }
      
      /**
       * Check if the two components use the same SQL table, in any referenced buffer.
       * 
       * @param    c1
       *           The first component.
       * @param    c2
       *           The second component.
       *           
       * @return   {@code true} if at least a buffer in one component has the same SQL name as in the other
       *           component.
       */
      private boolean hasSameSqlTable(CompoundComponent c1, CompoundComponent c2)
      {
         RecordBuffer[] buffs1 = c1.getQuery().getRecordBuffers();
         Set<String> sqlNames = new HashSet<>();
         for (int i = 0; i < buffs1.length; i++)
         {
            sqlNames.add(buffs1[i].getTable());
         }
         RecordBuffer[] buffs2 = c2.getQuery().getRecordBuffers();
         for (int i = 0; i < buffs2.length; i++)
         {
            if (sqlNames.contains(buffs2[i].getTable()))
            {
               return true;
            }
         }
         
         return false;
      }
      
      /**
       * Determine whether a server-side join is possible (and advisable) for the given component
       * and the zero or more candidates above it in the nesting. The analysis visits the original
       * query components from outermost to innermost nesting, so the component being tested will
       * always represent the innermost query being visited.
       * 
       * @param   candidates
       *          The list of candidate components, to the last of which the current candidate
       *          (<code>nextComp</code>) will potentially have an upward join. This list may
       *          be empty, indicating that <code>nextComp</code> is potentially the outermost
       *          query being considered for the current group of components being joined. Each
       *          component in the list represents a single-table component query. This argument
       *          may not be <code>null</code>.
       * @param   nextComp
       *          The single-table query currently being analyzed to join (upward) with the last
       *          of the queries in the <code>candidates</code> list (if any), and potentially
       *          downward with additional component queries (if any).
       * @param   top
       *          <code>true</code> if this component and the list of <code>candidates</code>
       *          are at the outermost nesting level.
       * @param   originalPosition
       *          The one-based position of <code>nextComp</code> in the original list of query
       *          components. Currently only used for debug logging.
       * 
       * @return  <code>true</code> if the algorithm has determined that <code>nextComp</code>
       *          should be part of a server-side join.
       */
      private boolean isServerJoinPossible(ArrayList<CompoundComponent> candidates,
                                           CompoundComponent nextComp,
                                           boolean top,
                                           int originalPosition)
      {
         int iteration = nextComp.getIteration();
         Joinable query = nextComp.getQuery();
         RecordBuffer buffer = query.getRecordBuffers()[0];
         boolean fine = log.isLoggable(Level.FINE);
         
         // calculate the number of fields in the possible optimized component in an worst case scenario
         // if the number is bigger than the max admitted by the dialect, we should not optimize
         int totalFields = buffer.getDmoInfo().getFieldCount(false);
         for (CompoundComponent candidate : candidates) 
         {
            totalFields += candidate.getQuery().getRecordBuffers()[0].getDmoInfo().getFieldCount(false);
            
            if (hasSameSqlTable(candidate, nextComp))
            {
               return false;
            }
         }
         
         if (totalFields > buffer.getDialect().getMaxEntriesInSelect()) 
         {
            if (fine)
            {
               log.fine(" --- too many columns: ");
            }
            return false;
         }
         
         if (candidates.size() == 1 && candidates.get(0).isOuter())
         {
            if (fine)
            {
               log.fine("   --- can't optimize if the previous component is the first one"
                     + " and is an outer component.");
            }
            
            return false;
         }
         
         String where = query.getOriginalWhere();

         if (fine)
         {
            StringBuilder buf = new StringBuilder(top ? "*" : " ");
            buf.append(originalPosition);
            
            switch (iteration)
            {
               case NEXT:
                  buf.append("N");
                  break;
               case FIRST:
                  buf.append("F");
                  break;
               case LAST:
                  buf.append("L");
                  break;
               default:
                  buf.append(iteration);
                  break;
            }
            
            AbstractJoin legacyJoin = query.getJoin();
            
            buf.append(legacyJoin != null ? "J" : " ");
            buf.append(" ");
            buf.append(buffer.getTable());
            buf.append(" [");
            buf.append(where);
            buf.append("]");
            
            if (legacyJoin != null)
            {
               buf.append(" / [");
               
               FQLExpression[][] fqls = legacyJoin.getFQL();
               FQLExpression joinWhere = new FQLExpression();
               for (FQLExpression[] fql : fqls)
               {
                  // log a generic query, where all fields are not unknown 
                  joinWhere.append(fql[AbstractJoin.NOT_NULL_FIELD]);
               }
               
               buf.append(joinWhere.toFinalExpression(false));
               buf.append("]");
            }
            
            log.fine(buf.toString());
         }
         
         boolean isFirst = candidates.isEmpty();
         
         switch (iteration)
         {
            case FIRST:
            case LAST:
            case NEXT:
               break;
            default:
               return false;
         }
         
         // presence of client-side where expression disallows server-side join
         if (query.hasWhereExpression())
         {
            if (fine)
            {
               log.fine("   --- client-side where expression");
            }
            
            return false;
         }
         
         // if the query references composites, it will not join well
         // TODO: fix this; it is due to the fact that in the generated SQL, Hibernate creates
         // an inner join on the list's parent id, but the test for the composite's index is
         // relegated to the where clause; this works OK for single entity queries, but compounds
         // the query plan's complexity when joining multiple entities
         Object[] args = query.getArgs();
         FQLPreprocessor fqlp = null;
         if (where != null)
         {
            try
            {
               fqlp = FQLPreprocessor.get(where,
                                          buffer.getDatabase(),
                                          buffer.getDialect(),
                                          query.getReferencedBuffers(),
                                          args,
                                          null,
                                          false,
                                          null,
                                          null,
                                          true,
                                          null);
               if (fqlp.hasAnsiJoins() || fqlp.hasContains())
               {
                  if (fine)
                  {
                     log.fine("   --- composite reference");
                  }
                  
                  return false;
               }
            }
            catch (PersistenceException exc)
            {
               if (log.isLoggable(Level.WARNING))
               {
                  log.log(Level.WARNING, "Error optimizing compound query", exc);
               }
               
               return false;
            }
         }
         
         // There are 2 cases when we should not allow the optimization regarding "contains" keyword:
         // 1) when a previous component has a contains keyword with non-constants expression and 
         //    the current component is outer
         // 2) when a previous component is outer and
         //    the current one has a contains keyword with non-constant expression
         boolean nonConstantContains = fqlp != null && fqlp.hasNonConstantContains();
         int len = candidates.size();
         for (int i = 0; i < len; i++)
         {                 
            CompoundComponent candidate = candidates.get(i);
            Joinable candidateQuery = candidate.getQuery();
            RecordBuffer candidateBuffer = candidateQuery.getRecordBuffers()[0];
            Object[] candidateArgs = candidateQuery.getArgs();
            String candidateWhere  = candidateQuery.getOriginalWhere();
            FQLPreprocessor candidateFqlp = null;
             
            if (candidateWhere != null)
            {
               try 
               {
                  candidateFqlp = FQLPreprocessor.get(candidateWhere,
                                                      candidateBuffer.getDatabase(),
                                                      candidateBuffer.getDialect(),
                                                      candidateQuery.getReferencedBuffers(), 
                                                      candidateArgs,
                                                      null,
                                                      false,
                                                      null,
                                                      null,
                                                      true,
                                                      null);
               }
               catch (PersistenceException exc)
               {
                   if (log.isLoggable(Level.WARNING))
                   {
                      log.log(Level.WARNING, "Error optimizing compound query", exc);
                   }          
                  
                  return false;
               }   
               
               if (nextComp.isOuter() && candidateFqlp.hasNonConstantContains())
               {
                   
                  if (fine)
                  {
                     log.fine("   --- can't optimize if a previous component has a contains keyword " +
                              "with non-constants expression and the current component is outer");
                  }
                  
                  return false;
               } 
                                 
               if (candidate.isOuter() && nonConstantContains)
               {
                  
                  if (fine)
                  {
                     log.fine("   --- can't optimize if a previous component is outer and the " +
                              "current one has a contains keyword with non-constants expression");
                  }
                  
                  return false;
               }
            }
        }
             
         // no further checks are necessary if this is the first component in a series
         if (isFirst)
         {
            if (fine)
            {
               log.fine("   +++ first in new series (" + (top ? "" : "not ") + "outermost)");
            }
            
            return true;
         }
         
         // server-side join only possibly if query components reference the same database
         Database database = buffer.getDatabase();
         Joinable first = candidates.get(0).getQuery();
         Database firstDatabase = first.getRecordBuffers()[0].getDatabase();
         if (!database.equals(firstDatabase))
         {
            if (fine)
            {
               log.fine("   --- databases do not match");
            }
            
            return false;
         }
         
         int countMulti = 0;
         int countSingle = 0;
         CompoundComponent parent = null;
         int parentIteration = NONE;
         
         for (int i = 0; i < candidates.size(); i++)
         {
            CompoundComponent comp = candidates.get(i);
            parent = comp;
            parentIteration = parent.getIteration();
            switch (comp.getIteration())
            {
               case NEXT:
                  countMulti++;
                  break;
               case FIRST:
               case LAST:
                  countSingle++;
                  break;
               default:
                  break;
            }
         }
         
         boolean mixed = countSingle > 0 && countMulti > 0;
         
         switch (iteration) 
         {
            case NEXT:
               if (mixed && parentIteration != NEXT)
               {
                   // mixed iteration types should be contiguous to simplify the sorting
                   if (fine)
                   {
                      log.fine("   --- non-contiguous iteration types");
                   }
                
                   return false;
               }
               else if (countSingle > 0 && countMulti == 0 && iteration == NEXT)
               {
                  if (fine)
                  {
                     log.fine("   --- NEXT can't follow FIRST/LAST/UNIQUE");
                  }
                
                  return false;
               }
               break;
            case FIRST:
            case LAST:
               if (mixed && parentIteration == NEXT)
               {
                  // mixed iteration types should be contiguous to simplify the sorting
                  if (fine)
                  {
                     log.fine("   --- non-contiguous iteration types");
                  }
                   
                  return false;
               }
               break;
            default:
               break;
         }
         
         // if not in preselect mode, we have to be careful about enabling server-side joins,
         // since a poor join query can be much slower than the original nested loops, and we may
         // well not need the full results, as in preselect mode, so the cost won't necessarily
         // be amortized
         if (!preselect)
         {
            // need to keep the sorting as simple as possible to make sure a server join that is
            // supposed to improve performance doesn't produce a very inefficient query plan; a
            // NEXT iteration always will add one or more elements to ORDER BY; FIRST/LAST won't
            
            
            // make some best-guess decisions on an appropriate join strategy, based on the
            // current component and its enclosing components
            
            switch (iteration)
            {
               case NEXT:
                  /*
                  if (countMulti > 1)
                  {
                     // limit to 2 levels of NEXT iteration components
                     return false;
                  }
                  */
                  /*
                  if (!top && parentIteration == NEXT)
                  {
                     // do not allow NEXT/NEXT joins below the outermost nesting loop; if the
                     // resulting server-side join is at all inefficient, the impact would be
                     // magnified
                     return false;
                  }
                  */
                  if (parentIteration == NEXT)
                  {
                     Joinable pQuery = parent.getQuery();
                     if (!validateJoin(pQuery, query, args, database, fqlp, true, fine))
                     {
                        return false;
                     }
                  }
                  
                  break;
                  
               case FIRST:
               case LAST:
                  
//                  if (parentIteration != NEXT)
//                  {
                     // there's very little advantage (and usually a heavy penalty) to joining
                     // a FIRST/LAST to another FIRST/LAST
//                     return false;
//                  }
                  if (!validateJoin(parent.getQuery(), query, args, database, fqlp, false, fine))
                  {
                     return false;
                  }
                  
                  break;
                  
               default:
                  break;
            }
         }
         
         if (fine)
         {
            log.fine("   +++ passed all validation");
         }
         
         return true;
      }
      
      /**
       * Validate whether a server-side join between <code>parentQuery</code> and
       * <code>query</code> is advisable to produce a performant result. At this point, calling
       * code has determined the join is physically possible; it is this method's job to
       * determine whether this would be a good idea. The join may be rejected at this point due
       * to perceived where clause or join parameter complexity, poor matches of join columns,
       * etc.
       * 
       * @param   parentQuery
       *          The nesting query.
       * @param   query
       *          The nested query.
       * @param   args
       *          Query substitution parameters, if any, for the original query.
       * @param   database
       *          Database containing the tables referenced by <code>parentQuery</code> and
       *          <code>query</code>.
       * @param   fqlp
       *          FQL preprocessor used in the analysis of the query's where clause, if any. May
       *          by <code>null</code>.
       * @param   noOpenCrossJoin
       *          <code>true</code> if an unrestricted cross join from <code>query</code> to
       *          <code>parentQuery</code> must be rejected; <code>false</code> if it is
       *          permissible.
       * @param   fine
       *          <code>true</code> if FINE level logging is enabled, else <code>false</code>.
       * 
       * @return  <code>true</code> if the join is recommended, <code>false</code> if rejected.
       */
      private boolean validateJoin(Joinable parentQuery,
                                   Joinable query,
                                   Object[] args,
                                   Database database,
                                   FQLPreprocessor fqlp,
                                   boolean noOpenCrossJoin,
                                   boolean fine)
      {
         RecordBuffer parentBuf = parentQuery.getRecordBuffers()[0];
         AbstractJoin legacyJoin = query.getJoin();
         
         if (noOpenCrossJoin && args == null && legacyJoin == null)
         {
            // open cross join == bad
            if (fine)
            {
               log.fine("   --- unrestricted cross join - no where clause or legacy join");
            }
            
            return false;
         }
         
         if (legacyJoin != null && parentBuf != legacyJoin.getInverse().buffer())
         {
            // a legacy join must reference the direct parent
            if (fine)
            {
               log.fine(
                  "   --- join inverse does not reference buffer of immediate enclosing loop");
            }
            return false;
         }
         
         if (forceDatabaseJoin) 
         {
            return true;
         }
         
         ArrayList<FieldReference> refList = null;
         if (args != null || legacyJoin != null)
         {
            refList = new ArrayList<>();
            
            if (args != null)
            {
               for (Object arg : args)
               {
                  if (arg instanceof FieldReference)
                  {
                     refList.add((FieldReference) arg);
                  }
               }
            }
            
            if (legacyJoin != null)
            {
               refList.addAll(legacyJoin.getUnresolvedParameters());
            }
         }
         
         if (refList != null)
         {
            FieldReference joinRef = null;
            Class<? extends DataModelObject> dmoIface = parentBuf.getDMOInterface();
            for (int i = 0; i < refList.size(); i++)
            {
               FieldReference ref = refList.get(i);
               boolean test;
               if (ref.getDMO() != null)
               {
                  // more exact: we know the actual buffer instances are the same
                  test = ref.getParentBuffer() == parentBuf;
               }
               else
               {
                  // less exact: we know buffer types are the same; hopefully the actual buffer
                  // instances are, too; have to settle for this because ref.getParentBuffer()
                  // will throw an NPE when the ref's DMO is null
                  test = ref.getDMOInterface().equals(dmoIface);
               }
               
               if (test)
               {
                  if (joinRef != null)
                  {
                     // don't want to join by more than one column
                     if (fine)
                     {
                        log.fine("   --- more than one join property");
                     }
                     
                     return false;
                  }
                  
                  joinRef = ref;
               }
            }
            
            if (noOpenCrossJoin && joinRef == null)
            {
               // open cross join == bad
               if (fine)
               {
                  log.fine("   --- unrestricted cross join - no join property identified");
               }
               
               return false;
            }
            
            if (joinRef != null)
            {
               try
               {
                  // we need to know the index of the joining reference in the array of
                  // substitution parameters
                  Integer refSubstIndex = null;
                  
                  RecordBuffer buffer = query.getRecordBuffers()[0];
                  
                  if (fqlp == null && legacyJoin != null)
                  {
                     Object[] parms = legacyJoin.getUnresolvedParameters().toArray();
                     FQLExpression[][] fqls = legacyJoin.getFQL();
                     
                     FQLExpression joinWhere = new FQLExpression();
                     for (int j = 0; j < fqls.length; j++)
                     {
                        boolean nullArg = (parms.length != 0) &&
                                          ((parms[j] == null) ||
                                           (parms[j] instanceof BaseDataType && 
                                            ((BaseDataType) parms[j]).isUnknown()));
                        joinWhere.append(fqls[j][nullArg ? AbstractJoin.NULL_FIELD : AbstractJoin.NOT_NULL_FIELD]);
                     }
                     
                     fqlp = FQLPreprocessor.get(joinWhere.toFinalExpression(false),
                                                buffer.getDatabase(),
                                                buffer.getDialect(),
                                                query.getReferencedBuffers(),
                                                parms,
                                                null,
                                                false,
                                                null,
                                                null,
                                                true,
                                                null);
                     for (int i = 0; i < parms.length; i++)
                     {
                        if (parms[i] == joinRef)
                        {
                           refSubstIndex = i;
                        }
                     }
                  }
                  
                  if (refSubstIndex == null && args != null)
                  {
                     for (int i = 0; i < args.length; i++)
                     {
                        if (args[i] == joinRef)
                        {
                           refSubstIndex = i;
                        }
                     }
                  }
                  
                  if (fqlp != null && refSubstIndex != null)
                  {
                     if (!evaluatePropertyMatches(fqlp, refSubstIndex, buffer, fine))
                     {
                        return false;
                     }
                  }
                  else
                  {
                     if (fine)
                     {
                        if (fqlp == null)
                        {
                           log.fine("   --- unexpected - missing fql preprocessor");
                        }
                        else
                        {
                           log.fine("   --- unexpected - error finding join ref in args");
                        }
                     }
                     
                     return false;
                  }
               }
               catch (PersistenceException exc)
               {
                  if (log.isLoggable(Level.WARNING))
                  {
                     log.log(Level.WARNING, "Error retrieving index data", exc);
                  }
                  
                  return false;
               }
            }
         }
         
         return true;
      }
      
      /**
       * Analyze the property match expressions found in the current component's where clause or
       * join clause FQL, to determine suitability for a server-side join. This phase of the
       * analysis considers the type of match forming the join and whether the property used for
       * the join is the leading component of any index on the backing table.
       * 
       * @param   fqlp
       *          FQL preprocessor which has parsed the where or join clause.
       * @param   joinSubstIndex
       *          Index of the substitution parameter which represents the field reference used
       *          to join the table referenced by the current query to the table in the nesting
       *          query above it.
       * @param   buffer
       *          Record buffer referenced by the current query.
       * @param   fine
       *          <code>true</code> if FINE level logging is enabled, else <code>false</code>.
       * 
       * @return  <code>true</code> if an analysis of the property match expressions in the FQL
       *          informs a decision to create a server-side join, else <code>false</code>.
       * 
       * @throws  PersistenceException
       *          if there is an error looking up index information.
       */
      private boolean evaluatePropertyMatches(FQLPreprocessor fqlp,
                                              int joinSubstIndex,
                                              RecordBuffer buffer,
                                              boolean fine)
      throws PersistenceException
      {
         List<FQLPreprocessor.PropertyMatch> matches = fqlp.getPropertyMatches();
         if (matches == null)
         {
            if (fine)
            {
               log.fine("   --- no match expression found in where clause");
            }
            
            return false;
         }
         
         String joinProp = null;
         int i = 0;
         
         for (FQLPreprocessor.PropertyMatch pm : matches)
         {
            if (pm.rvalType == HQLParserTokenTypes.SUBST && i++ == joinSubstIndex)
            {
               if (pm.operator != HQLParserTokenTypes.EQUALS)
               {
                  if (fine)
                  {
                     log.fine("   --- server join requires equality match");
                  }
                  
                  return false;
               }
               
               joinProp = pm.property;
            }
            
            if (!pm.alias.equals(buffer.getDMOAlias()))
            {
               if (fine)
               {
                  log.fine("   --- DMO alias in where clause does not reference current buffer");
               }
               
               return false;
            }
         }
         
         if (joinProp == null)
         {
            if (fine)
            {
               log.fine("   --- unexpected - no reference to join property in where clause [" +
                        joinProp +
                        "]");
            }
            
            return false;
         }
         
         if (!buffer.getDmoInfo().isLeadingIndexComponent(joinProp))
         {
            if (fine)
            {
               log.fine("   --- join property not a leading index component [" + joinProp + "]");
            }
            
            return false;
         }
         
         return true;
      }
      
//      /**
//       * Determine whether the given DMO property represents the leading component of any index
//       * on the table represented by the given buffer.
//       * 
//       * @param   buffer
//       *          Record buffer which represents the table associated with the property.
//       * @param   property
//       *          DMO property to be tested.
//       * 
//       * @return  <code>true</code> if <code>property</code> is a leading index component; else
//       *          <code>false</code>.
//       *          
//       * @throws  PersistenceException
//       *          if there is an error looking up the backing table's indexes.
//       */
//      private boolean isLeadingIndexComponent(RecordBuffer buffer, String property)
//      throws PersistenceException
//      {
//         String schema = buffer.getSchema();
//         Database database = buffer.getDatabase();
//         Class<? extends DataModelObject> dmoIface = buffer.getDMOInterface();
//         
//         Iterator<LinkedHashSet<String>> indexIter =
//            DMOIndex.indexProperties(schema, database, dmoIface);
//         while (indexIter.hasNext())
//         {
//            LinkedHashSet<String> props = indexIter.next();
//            Iterator<String> propsIter = props.iterator();
//            if (propsIter.hasNext())
//            {
//               String leadingProp = propsIter.next();
//               if (property.equals(leadingProp))
//               {
//                  return true;
//               }
//            }
//         }
//         
//         return false;
//      }
      
      /**
       * Create a multi-table, adaptive query from the given compound query components, which
       * will be joined at the database server.
       * 
       * @param   originals
       *          Compound query components which need to be joined.
       * 
       * @return  Multi-table adaptive query. If the {@link AdaptiveQuery} detects that it
       *          is not able to be properly optimized, this will return {@code null} instead.
       */
      private AdaptiveQuery serverJoinAdaptive(ArrayList<CompoundComponent> originals)
      {
         AdaptiveQuery optimizedQuery = new AdaptiveQuery().initialize();
         
         List<AdaptiveComponent> acList = new ArrayList<>();
         
         for (int i = 0; i < originals.size(); i++)
         {
            CompoundComponent next = originals.get(i);
            int iteration = next.getIteration();
            boolean outer = next.isOuter();
            Joinable original = next.getQuery();
            AdaptiveComponent serverCopy =
               original.makeAdaptiveServerJoinComponent(acList,
                                                        optimizedQuery,
                                                        iteration,
                                                        outer,
                                                        next);
            optimizedQuery.addComponent(serverCopy);
            acList.add(serverCopy);
         }
         
         if (optimizedQuery.earlyInvalidate()) 
         {
            return null;
         }
         
         return optimizedQuery;
      }
      
      /**
       * Create a multi-table, preselect query from the given compound query components, which
       * will be joined at the database server.
       * 
       * @param   originals
       *          Compound query components which need to be joined.
       * 
       * @return  Multi-table preselect query.
       */
      private PreselectQuery serverJoinPreselect(ArrayList<CompoundComponent> originals)
      {
         // generate sort clause from the original queries
         StringBuilder buf = new StringBuilder();
         for (int i = 0; i < originals.size(); i++)
         {
            CompoundComponent next = originals.get(i);
            if (next.getIteration() == NEXT)
            {
               Joinable j = next.getQuery();
               String sort = j.getSortPhrase();
               if (sort != null && sort.length() > 0)
               {
                  if (buf.length() > 0)
                  {
                     buf.append(", ");
                  }
                  buf.append(sort);
               }
            }
         }
         String sort = buf.toString();
         if (sort.trim().length() == 0)
         {
            sort = null;
         }
         
         PreselectQuery optimizedQuery = new PreselectQuery().initialize(sort);
         
         List<QueryComponent> qcList = new ArrayList<>();
         
         for (int i = 0; i < originals.size(); i++)
         {
            CompoundComponent next = originals.get(i);
            int iteration = next.getIteration();
            boolean outer = next.isOuter();
            Joinable original = next.getQuery();
            QueryComponent serverCopy = original.makePreselectServerJoinComponent(qcList,
                                                                                  iteration,
                                                                                  outer);
            optimizedQuery.addComponent(serverCopy);
            qcList.add(serverCopy);
         }
         
         return optimizedQuery;
      }
   }
   
   /**
    * Filter that handles parameters passed to the component's subquery which handles the target
    * buffer.
    */
   private class SnapshotParameterFilter
   implements ParameterFilter
   {
      /** Index of target buffer in the set of buffers belonging to this compound query. */
      final int bufferIndex;
      
      /**
       * Create filter that handles parameters passed to the component's subquery which handles
       * the target buffer.
       *
       * @param   bufferIndex
       *          Index of target buffer in the set of buffers belonging to this compound query.
       */
      public SnapshotParameterFilter(int bufferIndex)
      {
         this.bufferIndex = bufferIndex;
      }
      
      /**
       * Handles parameters passed to the component's subquery which handles the target buffer:
       * according to the state kept in {@link #useSnapshot} it returns original parameters or
       * may substitute some of them with snapshot values for specific outer components.
       *
       * @param   parameters
       *          Component's subquery parameters.
       *
       * @return  see above.
       */
      @Override
      public Object[] filterParameters(Object[] parameters)
      {
         if (parameters == null || parameters.length == 0 || !useSnapshot[bufferIndex - 1])
         {
            return parameters;
         }
         
         Object[] res = new Object[parameters.length];
         for (int i = 0; i < parameters.length; i++)
         {
            Object arg = parameters[i];
            if (arg instanceof FieldReference)
            {
               FieldReference ref = (FieldReference) arg;
               RecordBuffer buffer = ref.getParentBuffer();
               if (buffersMap.containsKey(buffer))
               {
                  Record snapshot = null;
                  if (cursor != null)
                  {
                     Record[] referenceRow = cursor.getReferenceRow();
                     if (referenceRow != null)
                     {
                        snapshot = referenceRow[buffersMap.get(buffer)];
                     }
                  }
                  
                  if (snapshot == null)
                  {
                     snapshot = buffer.getSnapshot();
                  }
                  
                  res[i] = ref.getTemp(snapshot);
                  continue;
               }
            }
            
            res[i] = arg;
         }
         
         return res;
      }
   }
}