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