# PresortQuery.java

    /*
    ** Module   : PresortQuery.java
    ** Abstract : A query whose results are preselected and sorted by a client
    **            expression or field reference
    **
    ** Copyright (c) 2004-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 ECF 20060103   @23795 Created initial version. A specialized
    **                           preselect query which uses client-side record
    **                           sorting to presort query results before loop
    **                           iteration occurs.
    ** 002 ECF 20060208   @24722 Changed to support client-side where clause
    **                           processing. Also restructured inner classes
    **                           to take better advantage of shared Results
    **                           implementations.
    ** 003 ECF 20060425   @25720 Added Initialization logics for single
    **                           table instances. These mirror the
    **                           constructors of the superclass, only without
    **                           the order by (sort) string parameter.
    **                           Tentatively removed existing, single table
    **                           constructors, which are most likely obsolete.
    ** 004 ECF 20060503   @25959 Partially reworked break group processing.
    **                           Added isLastOfBreakGroup implementation.
    ** 005 ECF 20060602   @26953 Use DMO instances instead of primary key IDs.
    **                           Corresponds with changes in parent class.
    ** 006 ECF 20060608   @27003 Removed assembleSelectClause() method. It is
    **                           no longer different than the superclass'
    **                           implementation.
    ** 007 ECF 20060720   @28117 Backed out #005-006 (@26953, @27003). The
    **                           parent has been refitted to collect primary
    **                           keys instead of full records in the first
    **                           pass query.
    ** 008 ECF 20060725   @28189 Modified createResults() signature to match
    **                           changes in the superclass.
    ** 009 ECF 20060730   @28271 Code cleanup. Removed unused constructors.
    ** 010 ECF 20060803   @28400 Simplified createResults() method. Use
    **                           getRecordBuffers() method instead of building
    **                           up list manually.
    ** 011 ECF 20061027   @30753 Replaced Object with DataModelObject for DMO
    **                           arguments to methods/constructors. Required
    **                           for compile-time type safety.
    ** 012 ECF 20070225   @32228 Plug results resource leak. Clean up results
    **                           resources once results have been sorted. This
    **                           was causing both prepared statements and
    **                           result sets to remain unclosed in the backing
    **                           JDBC connection.
    ** 013 ECF 20070709   @34420 Fixed assembleSelectClause(). Changed
    **                           parameter type to StringBuilder to properly
    **                           override the same method of the parent class.
    ** 014 ECF 20070815   @34855 Support converted nested CAN-FIND statements.
    **                           Use RecordBuffer's new API to push/pop temp
    **                           record contexts.
    ** 015 ECF 20080125   @36996 Refactored sorting logic to Presorter class.
    **                           PresortQuery now delegates all sorting work
    **                           to Presorter.
    ** 016 SVL 20080523   @38373 Generated HQL query is ordered by <dmo>.id.
    ** 017 ECF 20090313   @41601 Forced full-records mode. Added overridden
    **                           addComponent() method which calls
    **                           setFullRecords(). Changed createResults().
    ** 018 SVL 20090316   @41612 Initialization logics that take the sort
    **                           string parameter were added.
    ** 019 SVL 20090505   @42093 More Initialization logics added.
    ** 020 ECF 20090618   @42769 Added executeScroll(). Overrides parent's
    **                           implementation to use FORWARD_ONLY scroll mode,
    **                           which may permit certain JDBC drivers to use
    **                           server-side cursors.
    ** 021 SVL 20090723   @43345 Make this type of queries not natively
    **                           scrollable, but enable scrolling by default.
    **                           Use IncrementalResults instead of SortedResults
    **                           for some cases.
    ** 022 SVL 20090730   @43460 Re-activated #021.
    ** 023 SVL 20111111          Make the class implement PresortDelegate.
    ** 024 ECF 20131028          Import change.
    ** 025 SVL 20140604          Added support for INDEX-INFORMATION.
    ** 026 ECF 20150801          Retrofitted API changes and refactoring for compound query
    **                           optimization.
    ** 027 ECF 20151214          Prevent releasing the current buffer record(s) when break groups are
    **                           enabled and no record is found by the query.
    ** 028 ECF 20160614          Pass entity names associated with this query to Persistence.scroll.
    ** 029 GES 20160804          Switched from WhereExpression to lambda usage.
    **         20160919          All initialization is now done with a default constructor followed
    **                           by a call to initialize(). This enables query instance members in
    **                           converted code to be local variables that are effectively final.
    **                           This is needed to support lambdas without moving queries to be
    **                           instance members. Queries as instance members breaks recursion
    **                           use cases.
    ** 030 OM  20180515          Added dynamic filters and dynamic sort.
    ** 031 ECF 20181106          Added runtime support for {FIRST|LAST}-OF methods.
    ** 032 ECF 20200906          New ORM implementation.
    ** 033 OM  20201001          Dropped redundant ORDER BY elements in multi-table queries.
    **     OM  20201007          Optimized SortCriterion by using DmoMeta data instead of map lookups.
    **     CA  20210310          A dynamic predicate must set the default lock to NONE, instead of SHARE.
    **     OM  20220701          Dropped overriding methods which were duplicating code from super class.
    **     SVL 20230109          Reflected PreselectQuery.components() now providing direct access to the
    **                           components array in order to improve performance.
    ** 034 IAS 20230116          Added DATE-RELATION to the 'scroll' parameters
    ** 035 SR  20230615          Fixed duplicate recid bug on assembleOrderByClause.
    **     SR  20230630          Extracted .size() outside for and changed .getUnqualifiedName.equals() into
    **                           .isPrimaryKey().
    ** 036 AL2 20231027          Use ForwardResults to honor TYPE_FORWARD_ONLY properly.
    **     AL2 20231027          Made preserveBuffersOnEmptyResults return true only when results are truly empty
    ** 037 AD  20231124          Added override for shouldRegisterRecordChangeListener().
    ** 038 CA  20240331          Use ArrayList and Java for loops in query assembler.
    ** 039 ES  20240404          Overridden isPesort method.
    ** 040 LS  20241101          Changed assembleSelectClause to return both recIds and full records.
    **                           Created getComponentsRequiringFullRecords to identify which tables require full
    **                           records.
    **                           Added a constant to establish a threshold for determining which tables should
    **                           avoid full record retrieval.
    */

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

    import com.goldencode.p2j.persist.lock.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.util.*;

    /**
     * A specialized preselect query which uses one or more instances of
     * <code>Resolvable</code> provided by business application code to sort the
     * result set returned by the database.  This class supports the Progress
     * semantics of "client-side" results sorting and break groups.  Multiple
     * levels of nested break groups are supported.
     * <p>
     * Sorting criteria is specified using the <code>addSortCriterion</code>
     * method.  The order in which criteria are added is significant;  the first
     * criterion added dictates the coarsest level of sorting.  Each criterion
     * added subsequently only sorts rows within that criterion added immediately
     * previous to it.
     */
    public final class PresortQuery
    extends PreselectQuery
    implements Presortable,
               PresortDelegate
    {
       /** The maximum number of fields allowed in a table for full record retrieval. */
       private static final int MAX_TABLE_FIELDS = 20;

       /** Helper which sorts preselected, unsorted results */
       private Presorter sorter;

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

       /**
        * Initialization logic which is typically used for a multi-table query. Details
        * specific to each table component of the query are added via one of the
        * <code>addComponent</code> method variants.
        * <p>
        * This 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 PresortQuery initialize()
       {
          super.initialize(null);

          sorter = new Presorter(this);
          setScrolling();

          return this;
       }

       /**
        * Initialization logic which is typically used for a multi-table query. Details
        * specific to each table component of the query are added via one of the
        * <code>addComponent</code> method variants.
        * <p>
        * This variant is used by code which is converted from an OPEN QUERY
        * statement, which permits advancing off the end of a result set without
        * raising a <code>QueryOffEndException</code>.
        * <p>
        * This code is intentionally separated from the constructor. The purpose of this separation
        * is to allow construction to occur in the prior scope while initialization can still occur
        * within a method or lambda that is inside the next block's scope.
        *
        * @param    lenientOffEnd
        *           <code>true</code> if advancing past either end of the result
        *           set should NOT raise a <code>QueryOffEndException</code>;
        *           <code>false</code> if doing so should raise the exception.
        *
        * @return   This query instance.
        */
       public PresortQuery initialize(boolean lenientOffEnd)
       {
          super.initialize(lenientOffEnd, null);

          sorter = new Presorter(this);
          setScrolling();

          return this;
       }

       /**
        * Initialization logic designed for a single-table preselect query.
        * The where clause must expect no substitution parameters.  A share lock
        * will be applied to retrieved records.
        * <p>
        * This code is intentionally separated from the constructor. The purpose of this separation
        * is to allow construction to occur in the prior scope while initialization can still occur
        * within a method or lambda that is inside the next block's scope.
        *
        * @param    dmo
        *           DMO proxy which defines buffer and record type.
        * @param    where
        *           HQL where clause.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        *
        * @return   This query instance.
        */
       public PresortQuery initialize(DataModelObject dmo, String where, Supplier<logical> whereExpr)
       {
          return initialize(dmo, where, whereExpr, null, null, null, null, null);
       }

       /**
        * Initialization logic designed for a single-table preselect query.
        * The where clause must expect no substitution parameters.  A share lock
        * will be applied to retrieved records.
        * <p>
        * This code is intentionally separated from the constructor. The purpose of this separation
        * is to allow construction to occur in the prior scope while initialization can still occur
        * within a method or lambda that is inside the next block's scope.
        *
        * @param    dmo
        *           DMO proxy which defines buffer and record type.
        * @param    where
        *           HQL where clause.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    inverse
        *           DMO to which this query should join via a foreign relation.
        *           May be <code>null</code>.
        *
        * @return   This query instance.
        */
       public PresortQuery initialize(DataModelObject   dmo,
                                      String            where,
                                      Supplier<logical> whereExpr,
                                      DataModelObject   inverse)
       {
          return initialize(dmo, where, whereExpr, null, null, inverse, null, null);
       }

       /**
        * Initialization logic designed for a single-table preselect query.
        * A share lock will be applied to retrieved records.
        * <p>
        * This code is intentionally separated from the constructor. The purpose of this separation
        * is to allow construction to occur in the prior scope while initialization can still occur
        * within a method or lambda that is inside the next block's scope.
        *
        * @param    dmo
        *           DMO proxy which defines buffer and record type.
        * @param    where
        *           HQL where clause.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    args
        *           Substitution parameters to be inserted into placeholders within
        *           the where clause.
        *
        * @return   This query instance.
        */
       public PresortQuery initialize(DataModelObject   dmo,
                                      String            where,
                                      Supplier<logical> whereExpr,
                                      Object[]          args)
       {
          return initialize(dmo, where, whereExpr, null, null, null, args, null);
       }

       /**
        * Initialization logic designed for a single-table preselect query.
        * A share lock will be applied to retrieved records.
        * <p>
        * This code is intentionally separated from the constructor. The purpose of this separation
        * is to allow construction to occur in the prior scope while initialization can still occur
        * within a method or lambda that is inside the next block's scope.
        *
        * @param    dmo
        *           DMO proxy which defines buffer and record type.
        * @param    where
        *           HQL where clause.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    inverse
        *           DMO to which this query should join via a foreign relation.
        *           May be <code>null</code>.
        * @param    args
        *           Substitution parameters to be inserted into placeholders within
        *           the where clause.
        *
        * @return   This query instance.
        */
       public PresortQuery initialize(DataModelObject   dmo,
                                      String            where,
                                      Supplier<logical> whereExpr,
                                      DataModelObject   inverse,
                                      Object[]          args)
       {
          return initialize(dmo, where, whereExpr, null, null, inverse, args, null);
       }

       /**
        * Initialization logic designed for a single-table preselect query.
        * The where clause must expect no substitution parameters.
        * <p>
        * This code is intentionally separated from the constructor. The purpose of this separation
        * is to allow construction to occur in the prior scope while initialization can still occur
        * within a method or lambda that is inside the next block's scope.
        *
        * @param    dmo
        *           DMO proxy which defines buffer and record type.
        * @param    where
        *           HQL where clause.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    lockType
        *           Lock type to apply to records retrieved by this query.
        *
        * @return   This query instance.
        */
       public PresortQuery initialize(DataModelObject   dmo,
                                      String            where,
                                      Supplier<logical> whereExpr,
                                      LockType          lockType)
       {
          return initialize(dmo, where, whereExpr, null, null, null, null, lockType);
       }

       /**
        * Initialization logic designed for a single-table preselect query.
        * The where clause must expect no substitution parameters.
        * <p>
        * This code is intentionally separated from the constructor. The purpose of this separation
        * is to allow construction to occur in the prior scope while initialization can still occur
        * within a method or lambda that is inside the next block's scope.
        *
        * @param    dmo
        *           DMO proxy which defines buffer and record type.
        * @param    where
        *           HQL where clause.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    inverse
        *           DMO to which this query should join via a foreign relation.
        *           May be <code>null</code>.
        * @param    lockType
        *           Lock type to apply to records retrieved by this query.
        *
        * @return   This query instance.
        */
       public PresortQuery initialize(DataModelObject   dmo,
                                      String            where,
                                      Supplier<logical> whereExpr,
                                      DataModelObject   inverse,
                                      LockType          lockType)
       {
          return initialize(dmo, where, whereExpr, null, null, inverse, null, lockType);
       }

       /**
        * Initialization logic designed for a single-table preselect query.
        * <p>
        * This code is intentionally separated from the constructor. The purpose of this separation
        * is to allow construction to occur in the prior scope while initialization can still occur
        * within a method or lambda that is inside the next block's scope.
        *
        * @param    dmo
        *           DMO proxy which defines buffer and record type.
        * @param    where
        *           HQL where clause.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    args
        *           Substitution parameters to be inserted into placeholders within
        *           the where clause.
        * @param    lockType
        *           Lock type to apply to records retrieved by this query.
        *
        * @return   This query instance.
        */
       public PresortQuery initialize(DataModelObject   dmo,
                                      String            where,
                                      Supplier<logical> whereExpr,
                                      Object[]          args,
                                      LockType          lockType)
       {
          return initialize(dmo, where, whereExpr, null, null, null, args, lockType);
       }

       /**
        * Initialization logic designed for a single-table preselect query.
        * <p>
        * This code is intentionally separated from the constructor. The purpose of this separation
        * is to allow construction to occur in the prior scope while initialization can still occur
        * within a method or lambda that is inside the next block's scope.
        *
        * @param    dmo
        *           DMO proxy which defines buffer and record type.
        * @param    where
        *           HQL where clause.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    inverse
        *           DMO to which this query should join via a foreign relation.
        *           May be <code>null</code>.
        * @param    args
        *           Substitution parameters to be inserted into placeholders within
        *           the where clause.
        * @param    lockType
        *           Lock type to apply to records retrieved by this query.
        *
        * @return   This query instance.
        */
       public PresortQuery initialize(DataModelObject   dmo,
                                      String            where,
                                      Supplier<logical> whereExpr,
                                      DataModelObject   inverse,
                                      Object[]          args,
                                      LockType          lockType)
       {
          return initialize(dmo, where, whereExpr, null, null, inverse, args, lockType);
       }

       /**
        * Initialization logic designed for a single-table preselect query.
        * <p>
        * This code is intentionally separated from the constructor. The purpose of this separation
        * is to allow construction to occur in the prior scope while initialization can still occur
        * within a method or lambda that is inside the next block's scope.
        *
        * @param    dmo
        *           DMO proxy which defines buffer and record type.
        * @param    where
        *           HQL where clause.  May be <code>null</code>.
        * @param    whereExpr
        *           Client-side where clause expression.  May be <code>null</code>.
        * @param    sort
        *           Sort clause that corresponds the index which defines the sort
        *           order for this query. May be <code>null</code>.
        * @param    indexInfo
        *           Index information string as it is returned by INDEX-INFORMATION.
        *           May be <code>null</code> if it is not an OPEN QUERY case or if
        *           you don't need debug information about selected indexes.
        * @param    inverse
        *           DMO to which this query should join via a foreign relation.
        *           May be <code>null</code>.
        * @param    args
        *           Substitution parameters to be inserted into placeholders within
        *           the where clause.
        * @param    lockType
        *           Lock type to apply to records retrieved by this query. If {@code null}, the default lock level
        *           is assumed.
        *
        * @return   This query instance.
        */
       public PresortQuery initialize(DataModelObject   dmo,
                                      String            where,
                                      Supplier<logical> whereExpr,
                                      String            sort,
                                      String            indexInfo,
                                      DataModelObject   inverse,
                                      Object[]          args,
                                      LockType          lockType)
       {
          super.initialize(dmo, where, whereExpr, sort, indexInfo, inverse, args, lockType);

          sorter = new Presorter(this);
          setScrolling();

          return this;
       }

       /**
        * Add a single sort criterion, in the form of a <code>Resolvable</code>
        * object which is resolved at sort time.  It is the value of the resolved
        * result which is used for the sort.
        * <p>
        * The sort is ascending, from lowest resolved value to highest.
        *
        * @param   sort
        *          A resolvable object whose resolved result is used for sorting.
        */
       public void addSortCriterion(Resolvable sort)
       {
          sorter.addSortCriterion(sort);
       }

       /**
        * Add a single sort criterion, in the form of a <code>Resolvable</code>
        * object which is resolved at runtime.  It is the value of the resolved
        * result which is used for the sort.
        *
        * @param   descending
        *          if <code>true</code>, the sort is descending, from highest
        *          resolved value to lowest;  otherwise, the sort is ascending.
        *
        * @param   sort
        *          A resolvable object whose resolved result is used for sorting.
        */
       public void addSortCriterion(Resolvable sort, boolean descending)
       {
          sorter.addSortCriterion(sort, descending);
       }

       /**
        * Add a single dynamic sort criterion, in the form of a {@code Resolvable} object which is
        * resolved at runtime.  It is the value of the resolved result which is used for the sort.
        * <p>
        * Unlike {@link #addSortCriterion} methods, the criterion is added with higher priority.
        *
        * @param   sort
        *          A resolvable object whose resolved result is used for sorting.
        * @param   ascending
        *          if {@code true}, the sort is ascending, from lowest resolved value to highest;
        *          otherwise, the sort is descending.
        */
       @Override
       public void addDynamicSortCriterion(Resolvable sort, boolean ascending)
       {
          sorter.addDynamicSortCriterion(sort, ascending);
       }

       /**
        * Clears all sort criteria added dynamically.
        */
       @Override
       public void clearDynamicSortCriteria()
       {
          sorter.clearDynamicSortCriteria();
       }

       /**
        * Enable all sort criteria set for this query to act as break group
        * categories.  When the query's results are presorted, the contents of
        * each break group are determined.
        */
       public void enableBreakGroups()
       {
          sorter.enableBreakGroups();
       }

       /**
        * Check if this type of query should register record change listeners. Use this
        * to avoid computing any scope for registration and end up with a no registration
        * behavior.
        *
        * @return   {@code true} if we should invest time into computing the scope in order
        *           to call {@link #registerRecordChangeListeners} or {@code false} if we
        *           can skip doing any work to honor {@link #registerRecordChangeListeners}.
        */
       @Override
       public boolean shouldRegisterRecordChangeListeners()
       {
          return false;
       }

       /**
        * Determine whether the query result row currently being visited is the
        * first row in the presorted results list.
        *
        * @return  <code>true</code> if the current result row is the first row,
        *          else <code>false</code>.
        */
       public logical isFirst()
       {
          return sorter.isFirst();
       }

       /**
        * Indicate whether the query is of presort type.
        *
        * @param   iterating
        *          <code>true</code> indicating that a query of presort type.
        */
       @Override
       public boolean isPresort()
       {
          return true;
       }

       /**
        * Determine whether the query result row currently being visited is the
        * first row within a break group whose category is identified by the
        * specified resolvable object.
        *
        * @param   key
        *          Break group category key.  Must be the same object reference
        *          as was specified previously when invoking one of the
        *          <code>addSortCriterion</code> method variants.
        *
        * @return  <code>true</code> if break groups are enabled and current row
        *          is first in the specified break group;
        *          <code>false</code> if break groups have not been enabled or
        *          current row is not first in the specified break group.
        */
       public logical isFirstOfGroup(Resolvable key)
       {
          return sorter.isFirstOfGroup(key);
       }

       /**
        * Determine whether the query result row currently being visited is the first row of the
        * query's result set or the first row within a break group whose category is identified by
        * the specified BREAK-BY level.
        *
        * @param   level
        *          Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
        *          criterion, etc. Must not be negative or greater than the number of sort criteria.
        *
        * @return  {@code true} if the current row is first in the result set or within the specified
        *          break group; else {@code false}.
        */
       public logical isFirstOfGroup(integer level)
       {
          return sorter.isFirstOf(level);
       }

       /**
        * Determine whether the query result row currently being visited is the first row of the
        * query's result set or the first row within a break group whose category is identified by
        * the specified BREAK-BY level.
        *
        * @param   level
        *          Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
        *          criterion, etc. Must not be negative or greater than the number of sort criteria.
        *
        * @return  {@code true} if the current row is first in the result set or within the specified
        *          break group; else {@code false}.
        */
       public logical isFirstOfGroup(int level)
       {
          return isFirstOfGroup(new integer(level));
       }

       /**
        * Determine whether the query result row currently being visited is the
        * last row in the presorted results list.
        *
        * @return  <code>true</code> if the current result row is the last row,
        *          else <code>false</code>.
        */
       public logical isLast()
       {
          return sorter.isLast();
       }

       /**
        * Determine whether the query result row currently being visited is the
        * last row within a break group whose category is identified by the
        * specified resolvable object.
        *
        * @param   key
        *          Break group category key.  Must be the same object reference
        *          as was specified previously when invoking one of the
        *          <code>addSortCriterion</code> method variants.
        *
        * @return  <code>true</code> if break groups are enabled and current row
        *          is last in the specified break group;
        *          <code>false</code> if break groups have not been enabled or
        *          current row is not last in the specified break group.
        */
       public logical isLastOfGroup(Resolvable key)
       {
          return sorter.isLastOfGroup(key);
       }

       /**
        * Determine whether the query result row currently being visited is the last row of the
        * query's result set or the last row within a break group whose category is identified by
        * the specified BREAK-BY level.
        *
        * @param   level
        *          Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
        *          criterion, etc. Must not be negative or greater than the number of sort criteria.
        *          Must not be unknown value.
        *
        * @return  {@code true} if the current row is last in the result set or within the specified
        *          break group; else {@code false}.
        */
       public logical isLastOfGroup(integer level)
       {
          return sorter.isLastOf(level);
       }

       /**
        * Determine whether the query result row currently being visited is the last row of the
        * query's result set or the last row within a break group whose category is identified by
        * the specified BREAK-BY level.
        *
        * @param   level
        *          Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
        *          criterion, etc. Must not be negative or greater than the number of sort criteria.
        *
        * @return  {@code true} if the current row is last in the result set or within the specified
        *          break group; else {@code false}.
        */
       public logical isLastOfGroup(int level)
       {
          return isLastOfGroup(new integer(level));
       }

       /**
        * Access the current, sorted results for this query.
        *
        * @return  <code>Results</code> object used to retrieve sorted results.
        */
       public Results getSortedResults()
       {
          return getResults();
       }

       /**
        * Add a component to the query.
        *
        * @param   comp
        *          Query component to be added.
        *
        * @throws  UnsupportedOperationException
        *          if attempting an outer join (temporary);
        *          if iteration type is not <code>NEXT</code> (temporary);
        *          if the component's DMO's associated database is different than
        *          that of a previously added component's database (cross-database
        *          preselect joins are not possible).
        */
       protected void addComponent(QueryComponent comp)
       {
          setFullRecords();

          super.addComponent(comp);
       }

       /**
        * Does this query support non-NEXT iteration components?
        *
        * @return  <code>true</code>
        */
       protected boolean supportsArbitraryIteration()
       {
          return true;
       }

       /**
        * Indicate whether the record buffer(s) current record(s) should be preserved, in the event
        * the query does not find any results.
        * <p>
        * This implementation returns true only if break groups are enabled. Also, this checks if the
        * results are truly empty. For a presort query, the results are always "full".
        *
        * @param   results
        *          The results instance to decide if they are truly empty. The results are not visible
        *          in some sub-classes, so let them be passed by parameter.
        *
        * @return  <code>true</code> if break groups are enabled, else <code>false</code>.
        */
       @Override
       protected boolean preserveBuffersOnEmptyResults(Results results)
       {
          if (!(results instanceof FullResults))
          {
             return false;
          }

          return sorter.isBreakGroupsEnabled() && ((FullResults) results).isEmpty();
       }

       /**
        * Determine whether the query result row currently being visited is the
        * last row within a break group whose category is identified by the
        * specified resolvable object.
        *
        * @param   key
        *          Break group category key.  Must be the same object reference
        *          as was specified previously when invoking one of the
        *          <code>addSortCriterion</code> method variants.
        *
        * @return  <code>true</code> if break groups are enabled and current row
        *          is last in the specified break group;
        *          <code>false</code> if break groups have not been enabled or
        *          current row is not last in the specified break group.
        */
       protected boolean isLastOfBreakGroup(Resolvable key)
       {
          return sorter.isLastOfBreakGroup(key);
       }

       /**
        * Determine whether the query result row currently being visited is the
        * first row within a break group whose category is identified by the
        * specified resolvable object.
        * <p>
        * This is the backing worker method for {@link #isFirstOfGroup} and is
        * used internally by the query to manage accumulators.
        *
        * @param   key
        *          Break group category key.  Must be the same object reference
        *          as was specified previously when invoking one of the
        *          <code>addSortCriterion</code> method variants.
        *
        * @return  <code>true</code> if current row is first in a break group;
        *          <code>false</code> if it is not;  unknown value if break groups
        *          have not been enabled.
        */
       protected boolean isNewBreakGroup(Resolvable key)
       {
          return sorter.isNewBreakGroup(key);
       }

       /**
        * Execute the query to create a first level result set.
        * <p>
        * This default implementation returns an object which is a thin wrapper around
        * {@code ScrollableResults}.  This overrides the parent method as it needs a
        * specific {@code Results} implementation: {@code ForwardResults}.
        * <p>
        * In case when the {@code fql} represents a valid template record query (a test on recid /
        * rowid of the record against the unique {@code _file._template} for current record of the
        * single {@code QueryComponent}), a special {@link Results} object is returned with a single
        * element that is the template record for the buffer of the component.
        *
        * @param   persistence
        *          Object which is used to execute the query.
        * @param   fql
        *          FQL query string.
        * @param   args
        *          Substitution parameters to be inserted into placeholders within the FQL query string.
        *
        * @return  An instance of {@code ForwardResults} which wrappers the
        *          {@code ScrollableResults} object returned by the execution of the query.
        *
        * @throws  PersistenceException
        *          if there is an error executing the query.
        */
       protected Results executeQuery(Persistence persistence, String fql, Object[] args)
       throws PersistenceException
       {
          Long templateRowid = getTemplateQueryRowid(args);
          if (templateRowid != null)
          {
             return super.executeQuery(persistence, fql, args);
          }

          // this query will always use the forward only mode
          return new ForwardResults(persistence, executeScroll(persistence, fql, args));
       }

       /**
        * Execute the query to create a {@code ScrollableResults} result set.
        * <p>
        * Override the parent's implementation to use a {@code ScrollMode} of {@code FORWARD_ONLY},
        * which may permit the use of a server-side cursor by the JDBC driver.
        *
        * @param   persistence
        *          Object which is used to execute the query.
        * @param   hql
        *          HQL query string.
        * @param   args
        *          Substitution parameters to be inserted into placeholders within the HQL query
        *          string.
        *
        * @return  The {@code ScrollableResults} object returned by the execution of the query.
        *
        * @throws  PersistenceException
        *          if there is an error executing the query.
        */
       @Override
       protected <T> ScrollableResults<T> executeScroll(Persistence persistence,
                                                        String hql,
                                                        Object[] args)
       throws PersistenceException
       {
          return persistence.scroll(getEntities(),
                                    hql,
                                    args,
                                    0,
                                    0,
                                    getDataRelation(),
                                    ResultSet.TYPE_FORWARD_ONLY);
       }

       /**
        * Given the scrollable results set for the query, create an object which
        * implements the <code>Results</code> interface.  It is this object which
        * will actually be used to retrieve the primary key values of records to
        * be loaded from the database.
        * <p>
        * This implementation returns a specialized {@link Results} object,
        * which sorts the results obtained from the caller, according to one or
        * more sort criterion provided by client code.
        *
        * @param   results
        *          An object used to access the records which meet this query's criteria.
        *
        * @return  <code>Results</code> object which presorts the results.
        *
        * @throws  PersistenceException
        *          if there is any error creating results
        */
       protected Results createResults(Results results)
       throws PersistenceException
       {
          if (isClientWhere())
          {
             // Need to override FilteredResults to store entire records, since
             // this is what SortedResults expects.
             results = new FilteredResults(results,
                                           Arrays.asList(getRecordBuffers()),
                                           getWhereExpressions(),
                                           false);
          }

          // Use incremental results for single-table queries (multi-table queries
          // do not use sort phrase) which back {{DO | REPEAT} PRESLECT |
          // FOR EACH} BREAK BY statements and which sort criteria added to the
          // Presorter matches the leading components of the query's sort phrase.
          if (getTableCount() == 1 && sorter.isBreakGroupsEnabled())
          {
             List<SortCriterion> criteria =
                SortCriterion.parse(sort, components().get(0).getBuffer());

             if (sorter.sortCriteriaMatchSortPhrase(criteria))
             {
                return sorter.createIncrementalResults(results);
             }
          }

          return sorter.createSortedResults(results, false);
       }

       /**
        * Indicate whether this query type is scrolling by its nature.
        * <p>
        * Subclasses which are not natively scrolling should override this method.
        *
        * @return  <code>false</code>.
        */
       protected boolean isNativelyScrolling()
       {
          return false;
       }

       /**
        * Assemble a mixed select clause for an HQL query that returns recIds or full records.
        * <ul>
        *    <li>
        *       For the tables that have any fields used in the BREAK BY clause
        *       or the number of columns is lower than the threshold,
        *       return all fields.
        *    </li>
        *    <li>
        *       Otherwise, retrieve only the recId.
        *    </li>
        * </ul>
        * @param   buf
        *          String buffer into which clause is assembled.
        */
       protected void assembleSelectClause(StringBuilder buf)
       {
          buf.append("select ");

          ArrayList<QueryComponent> components = components();
          if (components.size() == 1)
          {
             assembleComponent(buf, components.get(0), false);
          }
          else
          {
             Set<Integer> ids = getComponentsRequiringFullRecords(components);
             for (int i = 0; i < components.size(); i++)
             {
                QueryComponent comp = components.get(i);
                if (i > 0)
                {
                   buf.append(", ");
                }

                boolean onlyPk = !ids.contains(i);
                assembleComponent(buf, comp, onlyPk);
             }
          }

          buf.append(" ");
       }

       /**
        * Assemble the {@code order by} clause for an HQL query.
        *
        * @return  List of <code>SortCriterion</code> objects which describe the sort behavior for this query.
        *
        * @throws  PersistenceException
        *          never.
        */
       protected ArrayList<SortCriterion> assembleOrderByClause()
       throws PersistenceException
       {
          ArrayList<SortCriterion> sortCriteria = (sort == null) ? new ArrayList<>() : super.buildSortCriteria(sort);
          Set<String> recIdsSet = null;
          int sortCriteriaSize = sortCriteria.size();
          for (int i = 0; i < sortCriteriaSize; i++)
          {
             if (sortCriteria.get(i).isPrimaryKey())
             {
                if (recIdsSet == null)
                {
                   recIdsSet = new HashSet<>();
                }
                recIdsSet.add(sortCriteria.get(i).getAlias());
             }
          }
          ArrayList<QueryComponent> components = components();
          int componentsSize = components.size();
          if (recIdsSet == null || recIdsSet.size() != componentsSize)
          {
             for (int i = 0; i < componentsSize; i++)
             {
                QueryComponent comp = components.get(i);
                RecordBuffer buffer = comp.getBuffer();
                String bufferAlias = buffer.getDMOAlias();
                if (recIdsSet == null || !recIdsSet.contains(bufferAlias))
                {
                   String critName = bufferAlias + "." + DatabaseManager.PRIMARY_KEY + " asc";
                   SortCriterion crit = new SortCriterion(buffer, critName);
                   sortCriteria.add(crit);
                }
             }
          }

          return assembleOrderByClause(sortCriteria);
       }

       /**
        * Loop through a list of components and save the indices of elements from which all fields are needed.
        *
        * @param   components
        *          List of query components.
        *
        * @return a {@code Set<Integer>} containing the indices of elements that meet the condition.
        */
       private Set<Integer> getComponentsRequiringFullRecords(List<QueryComponent> components)
       {
          Set<Integer> ids = new HashSet<>();
          Set<String> usedTables = new HashSet<>();
          Iterator<Resolvable> iter = sorter.getSortKeys().iterator();

          boolean hasOnlyFieldReferenceSortKeys = true;

          for (int i = 0; iter.hasNext() && hasOnlyFieldReferenceSortKeys; i++)
          {
             Resolvable resolver = iter.next();
             if (resolver instanceof FieldReference)
             {
                usedTables.add(((FieldReference) resolver).getDMOInterface().getName());
             }
             else
             {
                hasOnlyFieldReferenceSortKeys = false;
             }
          }

          for (int i = 0; i < components.size(); i++)
          {
             // could not determine all tables used in the break by clause, therefore
             // full records will be retrieved for all tables
             if (!hasOnlyFieldReferenceSortKeys)
             {
                ids.add(i);
             }
             else
             {
                QueryComponent comp = components.get(i);
                RecordBuffer buffer = comp.getBuffer();

                // fetch full records if the number of columns < threshold or
                // if fields from this table are used in the sorting
                if (buffer.getNumFields() < MAX_TABLE_FIELDS ||
                    usedTables.contains(buffer.getDMOInterface().getName()))
                {
                   ids.add(i);
                }
             }
          }

          return ids;
       }
    }