Presorter.java

/*
** Module   : Presorter.java
** Abstract : Helper class which performs client-side sorting
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description-----------------------------
** 001 ECF 20080112   @36995 Created initial version. Helper class which
**                           performs client-side sorting.
** 002 CA  20080529   @38435 Unknown values must be sorted using a high 
**                           order. Ref: #38432
** 003 ECF 20080530   @38482 Refactored SortHelper inner class. Now uses
**                           DataSorter to perform comparisons.
** 004 ECF 20090416   @41807 Added test for thread interruption during
**                           retrieve loop in SortedResults c'tor. For
**                           large result sets, this loop could be long
**                           running, so it needs to honor stop requests.
** 005 ECF 20090619   @42770 Minor memory and performance optimizations.
** 006 SVL 20090723   @43347 IncrementalResults class was added.
** 007 SVL 20090730   @43461 Added session closing handling into
**                           IncrementalResults.
** 008 ECF 20090810   @43589 Changed to accommodate SessionListener API
**                           change. sessionClosing() method was replaced with
**                           sessionEvent().
** 009 ECF 20150327          Throw NPE from addSortCriterion to detect null resolvables earlier
**                           (i.e., before we attempt to resolve them during results sorting).
** 010 ECF 20150801          Minor simplification of code.
** 011 ECF 20160227          Use FieldReference.property() instead of propertyName(), which was
**                           removed.
** 012 ECF 20171128          Allow for outer joins, which can cause null DMOs in results.
** 013 OM  20180515          Added dynamic sorting of the result set.
** 014 OM  20180901          Improved dynamic sorting.
** 015 ECF 20181106          Added runtime support for {FIRST|LAST}-OF methods.
** 016 ECF 20190427          Fixed results caching.
** 017 ECF 20200906          New ORM implementation.
** 018 CA  20210923          Fixed SortedResults, when the component retrieves only the DMO id.
** 019 DDF 20240207          Only cache the results once when TRANSACTION_COMMITTING event is notified.
**                           Removed unnecessary import.
**     DDF 20240209          Do not cache previously cached results.
** 020 AL2 20240906          Implemented get(forceOnlyId).
** 021 LS  20241101          Added a flag to createSortedResults() and SortedResults constructor
**                           that indicates if the records should be loaded into their buffers.
** 022 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
**                           based on [sharedDb] parameter.
** 023 ICP 20250129          Used logical constants to leverage cached values.
*/

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

package com.goldencode.p2j.persist;

import java.util.*;
import java.util.function.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.persist.event.SessionListener;

/**
 * This helper class performs client-side (i.e., relative to the database
 * server) sorting of a result set on behalf of a query.  One or more
 * instances of <code>Resolvable</code> provided by business application code
 * are used 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>
 * methods.  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.
 * <p>
 * The sort operation is performed once, and it is up to the owning query to
 * cache these results for later record retrieval requests.
 */
final class Presorter
{
   /** Reference to the owning query object */
   private final Presortable query;
   
   /** Map of break group keys to <code>SortHelper</code> values */
   private final Map<Resolvable, SortHelper> sortHelpers = new LinkedHashMap<>(4);
   
   /** The original set of sort helpers. */
   private Map<Resolvable, SortHelper> originalSortHelpers = null;
   
   /** Are sort criteria used as break groups? */
   private boolean breakGroups = false;
   
   /**
    * Constructor which stores a reference to the owning query object.  This
    * reference is used to extract necessary information during and after the
    * sort operation.
    * 
    * @param   query
    *          Owning query object.
    */
   Presorter(Presortable query)
   {
      this.query = query;
   }

   /**
    * Get the break group keys.
    *
    * @return    a <code>Set</code> containing the break group keys.
    */
   public Set<Resolvable> getSortKeys()
   {
      return sortHelpers.keySet();
   }
   
   /**
    * Determines whether the sort criteria added to this <code>Presorter</code> match the leading
    * components of the given criteria.
    *
    * @param   criteria
    *          Criteria to be compared with the sort criteria added to this
    *          <code>Presorter</code>.
    *
    * @return  See above.
    */
   public boolean sortCriteriaMatchSortPhrase(List<SortCriterion> criteria)
   {
      if (criteria.size() < sortHelpers.size())
      {
         return false;
      }
      
      int i = 0;
      for (Map.Entry<Resolvable, SortHelper> entry : sortHelpers.entrySet())
      {
         Resolvable resolvable = entry.getKey();
         if (!(resolvable instanceof FieldReference))
         {
            return false;
         }
         
         FieldReference fieldReference = (FieldReference) resolvable;
         SortHelper helper = entry.getValue();
         SortCriterion sortCriterion = criteria.get(i);
         
         String propertyName1 = fieldReference.getProperty();
         String propertyName2 = sortCriterion.getUnqualifiedName();
         boolean ascending1 = (helper.getSorter().getDirection() > 0);
         boolean ascending2 = sortCriterion.isAscending();
         
         if (!propertyName1.equals(propertyName2) ||
             (ascending1 != ascending2))
         {
            return false;
         }
         
         i++;
      }
      
      return true;
   }
   
   /**
    * 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.
    */
   void addSortCriterion(Resolvable sort)
   {
      addSortCriterion(sort, false);
   }
   
   /**
    * 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   sort
    *          A resolvable object whose resolved result is used for sorting. May not be
    *          <code>null</code>.
    * @param   descending
    *          if <code>true</code>, the sort is descending, from highest
    *          resolved value to lowest;  otherwise, the sort is ascending.
    * 
    * @throws  NullPointerException
    *          if <code>sort</code> is <code>null</code>.
    */
   void addSortCriterion(Resolvable sort, boolean descending)
   {
      if (sort == null)
      {
         throw new NullPointerException("Resolvable sort object may not be null");
      }
      
      sortHelpers.put(sort, new SortHelper(!descending));
   }
   
   /**
    * Add a single 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. May not be
    *          {@code null}.
    * @param   ascending
    *          if {@code true}, the sort is ascending, from lowest resolved value to highest;
    *          otherwise, the sort is descending.
    * 
    * @throws  NullPointerException
    *          if {@code sort} is {@code null}.
    */
   void addDynamicSortCriterion(Resolvable sort, boolean ascending)
   {
      if (sort == null)
      {
         throw new NullPointerException("Resolvable sort object may not be null");
      }
      
      if (originalSortHelpers == null)
      {
         // save a copy of the original [sortHelpers] before adding the first sort criterion  
         originalSortHelpers = new HashMap<>();
         originalSortHelpers.putAll(sortHelpers);
      }
      
      // put in front, with higher priority
      Map<Resolvable, SortHelper> newSorters = new LinkedHashMap<>();
      newSorters.put(sort, new SortHelper(ascending));
      
      // remove old sort direction of this criterion, avoiding overwriting with old direction
      sortHelpers.remove(sort);
      
      newSorters.putAll(sortHelpers);
      sortHelpers.clear();
      sortHelpers.putAll(newSorters);
   }
   
   /**
    * Drop all current sort criteria added dynamically.
    */
   void clearDynamicSortCriteria()
   {
      if (originalSortHelpers == null)
      {
         return;  // no-op
      }
      
      // restore the original sort configuration
      sortHelpers.clear();
      sortHelpers.putAll(originalSortHelpers);
   }
   
   /**
    * 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.
    */
   void enableBreakGroups()
   {
      breakGroups = true;
   }
   
   /**
    * Determines whether sort criteria set for this query acts as break group
    * categories. 
    *
    * @return See above.
    */
   boolean isBreakGroupsEnabled()
   {
      return breakGroups;
   }
   
   /**
    * 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>.
    */
   logical isFirst()
   {
      return (new logical(query.getSortedResults().isFirst()));
   }
   
   /**
    * 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.
    */
   logical isFirstOfGroup(Resolvable key)
   {
      if (!breakGroups)
      {
         return logical.FALSE;
      }
      
      return logical.of(isNewBreakGroup(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.
    *v
    * @return  {@code true} if the current row is first in the result set or within the specified
    *          break group; else {@code false}.
    */
   logical isFirstOf(integer level)
   {
      return alignsWithBreakGroup(level, this::isFirst, this::isFirstOfGroup);
   }
   
   /**
    * 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}.
    */
   logical isLastOf(integer level)
   {
      return alignsWithBreakGroup(level, this::isLast, this::isLastOfGroup);
   }
   
   /**
    * 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>.
    */
   logical isLast()
   {
      return (new logical(query.getSortedResults().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.
    */
   logical isLastOfGroup(Resolvable key)
   {
      return (new logical(isLastOfBreakGroup(key)));
   }
   
   /**
    * 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.
    */
   boolean isLastOfBreakGroup(Resolvable key)
   {
      if (!breakGroups || key == null)
      {
         return false;
      }
      
      Results results = query.getSortedResults();
      int row = results.getRowNumber();
      
      return (results.isLast() || getSortHelper(key).isLastOfGroup(row));
   }
   
   /**
    * 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.
    */
   boolean isNewBreakGroup(Resolvable key)
   {
      if (!breakGroups)
      {
         return false;
      }
      
      int row = query.getSortedResults().getRowNumber();
      SortHelper helper = getSortHelper(key);
      
      return helper.isFirstOfGroup(row);
   }
   
   /**
    * Create a result set that is sorted according to the sort criteria
    * provided to this object.  Unsorted results are extracted from the
    * <code>unsorted</code> parameter.  They are sorted and stored into
    * another <code>Results</code> object, which is returned.
    * 
    * @param   unsorted
    *          A set of unsorted, preselect results.
    * @param   loadDMOs
    *          A flag that indicates if the records
    *          should be loaded into their associated record buffers.
    *          
    * @return  The sorted results retrieved from <code>unsorted</code>.
    * 
    * @throws  PersistenceException
    */
   Results createSortedResults(Results unsorted, boolean loadDMOs)
   throws PersistenceException
   {
      return (new SortedResults(unsorted, loadDMOs));
   }
   
   /**
    * Create a result set that will interrogate <code>sorted</code> results
    * iteratively on request (not up front, as results set returned by
    * {@link #createSortedResults} does). Note that passed results should
    * already be in a proper order.
    *
    * @param   sorted
    *          A set of sorted, preselect results.
    *
    * @return  Iterative results set for the <code>sorted</code> results.
    *
    * @throws  PersistenceException
    */
   Results createIncrementalResults(Results sorted)
   throws PersistenceException
   {
      return (new IncrementalResults(sorted));
   }
   
   /**
    * Get the sort helper, if any, associated with the given break group key.
    *
    * @param   key
    *          Break group category key.
    *
    * @return  Sort helper object associated with the target break group
    *          category.
    *
    * @throws  IllegalArgumentException
    *          if the break group key is invalid.
    */
   private SortHelper getSortHelper(Resolvable key)
   {
      SortHelper helper = sortHelpers.get(key);
      if (helper == null)
      {
         throw new IllegalArgumentException("Invalid break group key");
      }
      
      return helper;
   }
   
   /**
    * Determine whether the query result row currently being visited aligns with the first or
    * last row of the query's result set or within a break group whose category is identified by
    * the specified BREAK-BY level. The nature of the alignment (first or last) is determined by
    * the lambda expressions passed into this method.
    * 
    * @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.
    * @param   wholeFn
    *          Lambda expression to be used to determine alignment with the whole query's result
    *          set.
    * @param   groupFn
    *          Lambda expression to be used to determine alignment with a particular break group.
    * 
    * @return  {@code true} if the current row aligns with the results as specified above;
    *          else {@code false}.
    */
   private logical alignsWithBreakGroup(integer level,
                                        Supplier<logical> wholeFn,
                                        Function<Resolvable, logical> groupFn)
   {
      int lvl = level.intValue();
      if (level.isUnknown() || lvl < 0 || lvl > sortHelpers.size())
      {
         String msg = "First argument to FIRST-OF or LAST-OF must be the index of which " +
                      "BREAK-BY phrase is relevant.";
         ErrorManager.recordOrThrowError(14294, msg);
         
         return (new logical(false));
      }
      
      // level 0 indicates whole query, not a specific break-by group
      if (lvl == 0)
      {
         return wholeFn.get();
      }
      
      if (breakGroups)
      {
         // sortHelpers iterates in the order break-by groups were added
         int i = 1;
         for (Resolvable key : sortHelpers.keySet())
         {
            if (i++ == lvl)
            {
               return groupFn.apply(key);
            }
         }
      }
      
      return (new logical(false));
   }
   
   /**
    * Update break rows information for the given sort criterion.
    *
    * @param lastValues
    *        Array which contains the last sort criteria values for all criteria.
    * @param lastIndex
    *        Index of the required criterion in the <code>last</code> array.
    * @param currentValue
    *        Current value of the given sort criterion.
    * @param breakRows
    *        Set of break rows for the given criterion.
    * @param currentRow
    *        Position of the current row into the whole result set.
    */
   private void calculateBreakGroups(BaseDataType[] lastValues,
                                     int lastIndex,
                                     BaseDataType currentValue,
                                     Set<Integer> breakRows,
                                     int currentRow)
   {
      BaseDataType lastBDT = lastValues[lastIndex];
      if (lastBDT == null || !currentValue.equals(lastBDT))
      {
         breakRows.add(currentRow);
         
         // A new "outer" break group necessarily defines a
         // new start for all of its "inner" break groups.
         if (lastIndex + 1 < lastValues.length)
         {
            lastValues[lastIndex + 1] = null;
         }
         
         lastValues[lastIndex] = currentValue;
      }
   }
   
   /**
    * Push a temporary record context onto a set of buffers. See
    * {@link RecordBuffer#pushTempContext()}.
    *
    * @param buffers
    *        Buffers on which a temporary record context should be set.
    */
   private void pushTempContexts(RecordBuffer[] buffers)
   {
      for (RecordBuffer buffer : buffers)
      {
         buffer.pushTempContext();
      }
   }
   
   /**
    * Pop the current, temporary record context from the given buffers.
    * <p>
    * As a convenience, the currently active temporary records for this
    * buffers is set to <code>null</code> first.
    *
    * @param   buffers
    *          Buffers from which the current temporary record contexts 
    *          should be popped.
    *
    * @throws  PersistenceException
    *          if setting any current, temporary record to <code>null</code>
    *          triggers a failure to evict the temporary record from the
    *          active Hibernate session.
    *
    * @see     #pushTempContexts(RecordBuffer[])
    */
   private void popTempContexts(RecordBuffer[] buffers)
   throws PersistenceException
   {
      for (RecordBuffer buffer : buffers)
      {
         buffer.popTempContext();
      }
   }
   
   /**
    * A helper object which provides services related to sorting and break
    * groups.  An instance of this class is created for each sort criterion
    * added to the query.  Each instance contains the <code>DataSorter</code>
    * used to implement a particular sort criterion.  If break groups are
    * enabled, a set of the row numbers for each record which is the first in
    * its respective break group is generated during sorting.  This set is
    * maintained by this object.
    */
   private static class SortHelper
   {
      /** Directional <code>BaseDataType</code> sorter */
      private final DataSorter sorter;
      
      /** Optional set of first row numbers per break group */
      private Set<Integer> breakRows = null;
      
      /** Evaluator used to obtain break groups information. */
      private LazyEvaluator lazyEvaluator = null;
      
      /**
       * Constructor which stores the sort value resolver and generates the
       * appropriate comparator for an ascending or descending sort.
       *
       * @param   ascending
       *          <code>true</code> if sort should be ascending;
       *          <code>false</code> if it should be descending.
       */
      SortHelper(boolean ascending)
      {
         this.sorter = DataSorter.getInstance(ascending);
      }
      
      /**
       * Store a set of row numbers, each of which represents the first row
       * in a break group.
       *
       * @param   breakRows
       *          Set of break group first row numbers.
       */
      void setBreakRows(Set<Integer> breakRows)
      {
         this.breakRows = breakRows;
      }
      
      /**
       * Get the set of row numbers, each of which represents the first row
       * in a break group.
       *
       * @return Set of break group first row numbers.
       */
      Set<Integer> getBreakRows()
      {
         return breakRows;
      }
      
      /**
       * Indicate whether the given row number represents the first row in a
       * break group.
       *
       * @param   row
       *          Zero-based row number to test.
       *
       * @return  <code>true</code> if <code>row</code> represents the first
       *          row in a break group;  else <code>false</code>.
       */
      boolean isFirstOfGroup(int row)
      {
         if (breakRows == null)
         {
            return false;
         }
         
         if (lazyEvaluator != null)
         {
            // quick check
            if (breakRows.contains(row))
            {
               return true;
            }
            
            lazyEvaluator.evaluate(row, true);
         }
         
         return breakRows.contains(row);
      }
      
      /**
       * Indicate whether the given row number represents the last row in a
       * break group.
       *
       * @param   row
       *          Zero-based row number to test.
       *
       * @return  <code>true</code> if <code>row</code> represents the last
       *          row in a break group;  else <code>false</code>.
       */
      boolean isLastOfGroup(int row)
      {
         if (breakRows == null)
         {
            return false;
         }
         
         if (row < 0)
         {
            return false;
         }
         
         if (lazyEvaluator != null)
         {
            // quick check
            if (breakRows.contains(row + 1))
            {
               return true;
            }
            
            lazyEvaluator.evaluate(row, false);
         }
         
         return (breakRows.contains(row + 1));
      }
      
      /**
       * Get the data sorter.
       * 
       * @return  Data sorter.
       */
      DataSorter getSorter()
      {
         return sorter;
      }
      
      /**
       * Indicates that this <code>SortHelper</code> from now should evaluate
       * break groups information in the lazy mode using the given evaluator.
       *
       * @param lazyEvaluator
       *        Evaluator used to obtain break groups information. It should
       *        have access to the break rows. 
       */
      void evaluateLazy(IncrementalResults lazyEvaluator)
      {
         this.lazyEvaluator = lazyEvaluator;
      }
   }
   
   /**
    * Interface for the classes which are used for lazy evaluation of the break rows.
    */
   private interface LazyEvaluator
   {
      /**
       * Evaluate break row(s) which help to determine whether the given row
       * if the first/last in a break group.  
       *
       * @param row
       *        Row to be checked as the beginning/end of a break group.
       * @param firstOf
       *        <code>true</code> indicates that the row should be checked as
       *        the beginning of a break group. <code>false</code> indicates
       *        that the row should be checked as the end of a break group
       */
      public void evaluate(int row, boolean firstOf);
   }
   
   /**
    * This class should be used instead of {@link SortedResults} when the sort
    * criteria added to the Presorter match the leading components of the
    * presort query sort phrase (i.e. results are already sorted in the proper
    * order). This class retrieves results iteratively on demand which lead
    * to faster initial response / less memory consumption compared to
    * {@link SortedResults} which caches and sorts all results up front.
    * <p>
    * If session expires, all remaining results (record IDs) are cached. Break
    * group information in this case will be retrieved iteratively on request. 
    */
   private class IncrementalResults
   extends SimpleResults
   implements SessionListener,
              LazyEvaluator
   {
      /** Error message if a non-scrolling query is scrolled backward. */
      private final static String BACKWARD_SCROLL_ERROR =
         "Backward scroll is not permitted for non-scrolling queries.";
      
      /** Original sorted results. */
      private Results backingResults = null;
      
      /**
       * Offset that is used to determine the row number into the whole result
       * set (global row number = offset + row number into cached result set).
       */
      private int backingResultsOffset = 0;
      
      /** Array which contains the sort criteria values for the last loaded row. */
      private BaseDataType[] currentBDT = null;
      
      /** Array which contains the sort criteria values for the row before the last loaded row. */
      private BaseDataType[] prevBDT = null;
      
      /**
       * Array which contains the sort criteria values for the row after the
       * last loaded row (i.e. pre-loaded row).
       */
      private BaseDataType[] nextBDT = null;
      
      /** Determines whether this <code>Results</code> are scrolling. */
      private final boolean scrolling;
      
      /** Number of the row which corresponds the <code>currentBDT</code>. */
      private int lastEvaluatedRow = -1;
      
      /** Cache for non-scrolling queries. IDs which correspond with last loaded row. */
      private Long[] currentRow = null;
      
      /**
       * Cache for non-scrolling queries. IDs which correspond with row before last loaded row.
       */
      private Long[] prevRow = null;
      
      /**
       * Cache for non-scrolling queries. IDs which correspond with row after last loaded row
       * (i.e. pre-loaded row).
       */
      private Long[] nextRow = null;
      
      /** Number of loaded rows (for non-scrolling queries). */
      private int rowsSize = 0;
      
      /** Indicates whether backing results have been cached because of session closing. */
      private boolean resultsCached = false;
      
      /**
       * Create a result set that will interrogate <code>sorted</code> results
       * iteratively on request.
       *
       * @param sorted
       *        Original sorted results.
       */
      private IncrementalResults(Results sorted)
      {
         super(new ArrayList<>());
         backingResults = sorted;
         
         if (!(query instanceof PreselectQuery))
         {
            throw new IllegalStateException("Incremental results was " +
               "designed to be used by preselect queries or its descendants!");
         }
         
         PreselectQuery preselectQuery = (PreselectQuery) query;
         scrolling = preselectQuery.isScrolling() || preselectQuery.isBrowsed();
         currentBDT = new BaseDataType[sortHelpers.size()];
         
         for (SortHelper helper : sortHelpers.values())
         {
            helper.setBreakRows(new HashSet<>());
         }
         
         SessionListener.Scope scope =
               scrolling || !preselectQuery.isStandalone()
               ? SessionListener.Scope.ENCLOSING_EXTERNAL
               : SessionListener.Scope.CURRENT;
         preselectQuery.getPersistence().registerSessionListener(this, scope, preselectQuery.isSharedContext());
      }
      
      /**
       * Returns the row at the current position. It can be either only an array of PK or
       * an array of DMOs. 
       * 
       * @param   forceOnlyId
       *          Indicates whether it's only the primary keys that must be returned, 
       *          or original data found is returned (either PK or DMO).
       *
       * @return  Object array or {@code null}.
       */
      public Object[] get(boolean forceOnlyId)
      {
         if (scrolling)
         {
            return super.get(forceOnlyId);
         }
         else
         {
            Object[] datum = (Object[]) currentRow;
            // currentRow has only ids, so simply ignore forceOnlyId
            return datum;
         }
      }
      
      /**
       * Move cursor to the first results row.
       *
       * @return  <code>true</code> if there are any results.
       */
      public boolean first()
      {
         if (!scrolling && 0 < getRowNumber())
         {
            throw new UnsupportedOperationException(BACKWARD_SCROLL_ERROR);
         }
         
         loadRow(0);
         
         return super.first();
      }
      
      /**
       * Move cursor to the last results row.
       *
       * @return  <code>true</code> if there are any results.
       */
      public boolean last()
      {
         loadRow(-1);
         
         return super.last();
      }
      
      /**
       * Move cursor to the next results row.
       *
       * @return  <code>true</code> if there is a result under the cursor
       *          under the move.
       */
      public boolean next()
      {
         loadRow(getRowNumber() + 1);
         
         return super.next();
      }
      
      /**
       * Set the row number currently under the cursor.
       *
       * @param   row
       *          Zero-based index of the row to be set as the current row.
       *
       * @return  <code>true</code> if there is a row at the specified row
       *          number;  else <code>false</code>.
       */
      public boolean setRowNumber(int row)
      {
         if (!scrolling && row < getRowNumber())
         {
            throw new UnsupportedOperationException(BACKWARD_SCROLL_ERROR);
         }
         
         if (row >= 0)
         {
            loadRow(row);
         }
         
         return super.setRowNumber(row);
      }
      
      /**
       * Scroll the cursor ahead by the specified number of rows.  If the
       * number is negative, the cursor is moved backward.  This may put the
       * cursor off the end (either end) of the query's results.
       *
       * @param   rows
       *          Number of rows to jump ahead or back.
       *
       * @return  <code>true</code> if there is a row at the new location;
       *          else <code>false</code>.
       */
      public boolean scroll(int rows)
      {
         if (!scrolling && rows < 0)
         {
            throw new UnsupportedOperationException(BACKWARD_SCROLL_ERROR);
         }
         
         if (rows > 0)
         {
            loadRow(getRowNumber() + rows);
         }
         
         return super.scroll(rows);
      }
      
      /**
       * Returns the number of rows which have been loaded by this
       * <code>Results</code>.
       *
       * @return See above.
       */
      @Override
      public int getNumberOfLoadedRows()
      {
         if (!scrolling)
         {
            return rowsSize;
         }
         else
         {
            return super.getNumberOfLoadedRows();
         }
      }
      
      /**
       * Clears the backing results.
       */
      public void sessionClosing()
      {
         cleanupBackingResults();
      }
      
      /**
       * Clear the internal list of result rows and the backing results.
       */
      public void cleanup()
      {
         super.cleanup();
         
         cleanupBackingResults();
      }
      
      /**
       * Indicate whether the full result set is cached in this object.
       * 
       * @return  {@code true} if all results have been collected and cached, else {@code false}.
       */
      @Override
      public boolean isResultSetCached()
      {
         return resultsCached;
      }
      
      /**
       * Invoked when a Hibernate session event occurs.  This implementation
       * is interested in receiving events related to the current transaction.
       * In the case of a transaction about to commit, this method caches all
       * remaining results (record IDs) into a <code>SimpleResults</code>
       * instance.  On rollback, it just cleans up.
       *
       * @param   event
       *          Session event type.
       *
       * @return  <code>true</code> in this implemetation (i.e. this listener
       *          should be removed from the current set of registered
       *          listeners).
       *
       * @throws  PersistenceException
       *          if any error occurs during the implementor's processing.
       */
      public boolean sessionEvent(SessionListener.Event event)
      throws PersistenceException
      {
         if (backingResults == null)
         {
            return true;
         }
         
         switch (event)
         {
            case TRANSACTION_COMMITTING:
               if (resultsCached && backingResults.isResultSetCached())
               {
                  break;
               }
               
               // read all uniterated results from old backingResults into a
               // SimpleResults, and set this SimpleResults as new backingResults
               ArrayList<Object[]> rows = new ArrayList<>(64);
               backingResultsOffset = getNumberOfLoadedRows();
               int cols = query.getRecordBuffers().length;
               while (backingResults.next())
               {
                  Long[] ids = new Long[cols];
                  for (int i = 0; i < cols; i++)
                  {
                     Record dmo = (Record) backingResults.get(i);
                     ids[i] = dmo.primaryKey();
                  }
                  
                  rows.add(ids);
               }
               
               cleanupBackingResults();
               backingResults = new SimpleResults(rows);
               resultsCached = true;
               
               // notify sort helper that break group information should be
               // obtained lazily on request
               for (Presorter.SortHelper helper : sortHelpers.values())
               {
                  helper.evaluateLazy(this);
               }
               break;
               
            case SESSION_CLOSING_NORMALLY:
               if (!resultsCached)
               {
                  cleanupBackingResults();
               }
               break;
               
            case SESSION_CLOSING_WITH_ERROR:
               cleanupBackingResults();
               break;
               
            default:
               return false;
         }
         
         return true;
      }
      
      /**
       * No-op in this implementation.
       */
      public void deregisteredSessionListener()
      {
      }
      
      /**
       * Evalualte break row(s) which help to determine whether the given row
       * if the first/last in a break group.
       *
       * @param row
       *        Row to be checked as the beginning/end of a break group.
       * @param firstOf
       *        <code>true</code> indicates that the row should be checked as
       *        the beginning of a break group. <code>false</code> indicates
       *        that the row should be checked as the end of a break group
       */
      public void evaluate(int row, boolean firstOf)
      {
         if (row >= 0 &&                        // 1. we have a valid row
             !(isLast(row) && !firstOf) &&      // 2. it's not an edge case
             !(isFirst(row) && firstOf) &&
             !(row == lastEvaluatedRow &&       // 3. we haven't already calculated break group
               (firstOf && prevBDT != null ||
               !firstOf && nextBDT != null)))   // information for this row
         {
            // we need to store only two history rows to maintain break
            // groups information for non-scrolling queries
            if (!scrolling)
            {
               for (SortHelper helper : sortHelpers.values())
               {
                  Iterator<Integer> iter = helper.breakRows.iterator();
                  while (iter.hasNext())
                  {
                     Integer val = iter.next();
                     if (val < row - 2)
                     {
                        iter.remove();
                     }
                  }
               }
            }
            
            try
            {
               // Our aim is to load the given row and the previous row if we
               // want to obtain firstOf(row) information, or load the given
               // row and the next row if we want to obtain lastOf(row)
               // information. Then we pass data (sort criteria values) into
               // calculateBreakGroups(), and it adds a proper information
               // to breakRows to make firstOf()/lastOf() return a proper
               // result
               
               Long[] ids;
               int helpersSize = sortHelpers.size();
               if (firstOf)
               {
                  if (row == lastEvaluatedRow - 1 && prevBDT != null)
                  {
                     // we have already loaded the given row as the "previous
                     // row", so make the previous row the current one
                     currentBDT = prevBDT;
                     nextBDT = null;
                  }
                  else if (row != lastEvaluatedRow)
                  {
                     ids = getRow(row);
                     loadBDTIntoArray(ids, currentBDT);
                     nextBDT = null;
                  }
                  
                  ids = getRow(row - 1);
                  prevBDT = new BaseDataType[helpersSize];
                  loadBDTIntoArray(ids, prevBDT);
               }
               else
               {
                  if (row == lastEvaluatedRow + 1 && nextBDT != null)
                  {
                     // we have already loaded the given row as the "next
                     // row", so make the next row the current one
                     currentBDT = nextBDT;
                     prevBDT = null;
                  }
                  else if (row != lastEvaluatedRow)
                  {
                     ids = getRow(row);
                     loadBDTIntoArray(ids, currentBDT);
                     prevBDT = null;
                  }
                  
                  ids = getRow(row + 1);
                  nextBDT = new BaseDataType[helpersSize];
                  loadBDTIntoArray(ids, nextBDT);
               }
               
               int sortCount = sortHelpers.size();
               
               BaseDataType[] prev = new BaseDataType[currentBDT.length];
               System.arraycopy(firstOf ? prevBDT : currentBDT,
                                0,
                                prev,
                                0,
                                currentBDT.length);
               
               BaseDataType[] next = firstOf ? currentBDT : nextBDT;
               
               Iterator<SortHelper> iter = sortHelpers.values().iterator();
               for (int i = 0; i < sortCount; i++)
               {
                  SortHelper helper = iter.next();
                  
                  calculateBreakGroups(prev,
                                       i,
                                       next[i],
                                       helper.getBreakRows(),
                                       firstOf ? row : row + 1);
               }
               
               lastEvaluatedRow = row;
            }
            catch (PersistenceException e)
            {
               ErrorManager.recordOrThrowError(e);
            }
         }
      }
      
      /**
       * Clear the backing results.
       */
      private void cleanupBackingResults()
      {
         if (backingResults != null)
         {
            backingResults.cleanup();
            backingResults = null;
         }
      }
      
      /**
       * Load the specified row into the cache.
       *
       * @param rowNumber
       *        Position of the requested row in the results set.
       */
      private void loadRow(int rowNumber)
      {
         if (backingResults == null)
         {
            return;
         }
         
         boolean iterateToLast = rowNumber == -1;
         
         // pre-load an extra row to get the break group information
         rowNumber++;
         
         int actualRow = backingResultsOffset + backingResults.getRowNumber();
         if (iterateToLast ||  actualRow < rowNumber)
         {
            int rowsToIterate = rowNumber - actualRow;
            RecordBuffer[] buffers = query.getRecordBuffers();
            int cols = buffers.length;
            
            // push temp context for all buffers
            if (!resultsCached)
            {
               pushTempContexts(buffers);
            }
            
            try
            {
               try
               {
                  for (int i = 0; iterateToLast || i < rowsToIterate; i++)
                  {
                     boolean res = backingResults.next();
                     if (!res)
                     {
                        // add dummy row to push the last row to the zero position
                        if (!scrolling)
                        {
                           addRow(null);
                        }
                        
                        cleanupBackingResults();
                        return;
                     }
                     else
                     {
                        Long[] ids = new Long[cols];
                        for (int j = 0; j < cols; j++)
                        {
                           if (!resultsCached)
                           {
                              Record dmo = (Record) backingResults.get(j);
                              ids[j] = dmo.primaryKey();
                              buffers[j].setTempRecord(dmo);
                           }
                           else
                           {
                              ids[j] = (Long) backingResults.get(j);
                           }
                        }
                        
                        addRow(ids);
                        
                        if (!resultsCached)
                        {
                           int sortCount = sortHelpers.size();
                           
                           // Collect sort criteria values.
                           Iterator<Map.Entry<Resolvable, SortHelper>> iter =
                              sortHelpers.entrySet().iterator();
                           for (int j = 0; j < sortCount; j++)
                           {
                              Map.Entry<Resolvable, SortHelper> entry = iter.next();
                              Resolvable resolver = entry.getKey();
                              SortHelper helper   = entry.getValue();
                              
                              Set<Integer> breakRows = helper.getBreakRows();
                              int currentRow = backingResults.getRowNumber();
                              
                              // we need to store only two rows to maintain
                              // break groups information for non-scrolling
                              // queries
                              if (!scrolling)
                              {
                                 breakRows.remove(currentRow - 2);
                              }
                              
                              calculateBreakGroups(currentBDT,
                                                   j,
                                                   resolver.resolve(),
                                                   breakRows,
                                                   currentRow);
                           }
                        }
                     }
                  }
               }
               finally
               {
                  // Clear buffers' temporary records.
                  if (!resultsCached)
                  {
                     popTempContexts(buffers);
                  }
               }
            }
            catch(PersistenceException e)
            {
               ErrorManager.recordOrThrowError(e);
            }
         }
      }
      
      /**
       * Store the given record identifiers to the rows cache.
       *
       * @param ids
       *        Identifiers to be stored.
       */
      private void addRow(Long[] ids)
      {
         if (scrolling)
         {
            List<Object[]> rowData = getRows();
            rowData.add(ids);
         }
         else
         {
            if (currentRow == null)
            {
               currentRow = ids;
            }
            else if (nextRow == null)
            {
               nextRow = ids;
            }
            else
            {
               prevRow = currentRow;
               currentRow = nextRow;
               nextRow = ids;
            }
            
            if (ids != null)
            {
               rowsSize++;
            }
         }
      }
      
      /**
       * Get the specified row (an array of record identifiers).
       *
       * @param  row
       *         Number of row to return.
       * @return See above.
       */
      private Long[] getRow(int row)
      {
         Object datum;
         
         if (scrolling)
         {
            datum = getRows().get(row);
         }
         else
         {
            if (row == getRowNumber())
            {
               datum = currentRow;
            }
            else if (row == getRowNumber() + 1)
            {
               datum = nextRow;
            }
            else if (row == getRowNumber() - 1)
            {
               datum = prevRow;
            }
            else
            {
               throw new IllegalStateException("For non-scrolling query it " +
                  "is possible to reach only current, next or previous row.");
            }
         }
         
         if (datum instanceof Long[])
         {
            return (Long[]) datum;
         }
         
         return (new Long[] { (Long) datum });
      }
      
      /**
       * Tests whether the given row is the first row in the results set.
       *
       * @param  row
       *         Row to test.
       *
       * @return <code>true</code> if the given row is the first row.
       */
      private boolean isFirst(int row)
      {
         return row == 0;
      }
      
      /**
       * Tests whether the given row is the last row in the results set.
       *
       * @param  row
       *         Row to test.
       *
       * @return <code>true</code> if the given row is the last row.
       */
      private boolean isLast(int row)
      {
         return (row >= 0 && row == getNumberOfLoadedRows() - 1);
      }
      
      /**
       * Load sort criteria values using identifiers of corresponding records.
       *
       * @param  ids
       *         Array of record identifiers.
       * @param  targetArray
       *         Array for storing sort criteria values.
       *
       * @throws PersistenceException
       *         if there was an error creating a Hibernate session or getting
       *         a record or record eviction problems.
       */
      private void loadBDTIntoArray(Long[] ids, BaseDataType[] targetArray)
      throws PersistenceException
      {
         RecordBuffer[] buffers = query.getRecordBuffers();
         int cols = buffers.length;
         
         pushTempContexts(buffers);
         
         try
         {
            Persistence persistence = ((PreselectQuery) query).getPersistence();
            
            for (int i = 0; i < cols; i++)
            {
               String entity = buffers[i].getEntityName();
               RecordIdentifier<String> ident = new RecordIdentifier<>(entity, ids[i]);
               Record dmo = persistence.quickLoad(ident, !buffers[i].getDmoInfo().multiTenant);
               buffers[i].setTempRecord(dmo);
            }
            
            int sortCount = sortHelpers.size();
            
            // Collect sort criteria values.
            Iterator<Resolvable> iter = sortHelpers.keySet().iterator();
            for (int j = 0; j < sortCount; j++)
            {
               Resolvable resolver = iter.next();
               targetArray[j] = resolver.resolve();
            }
         }
         finally
         {
            popTempContexts(buffers);
         }
      }
   }
   
   /**
    * A specialized implementation of <code>Results</code> which
    * <i>presorts</i> all query results according to the sort criteria
    * specified for this query.  The sorting takes place during construction
    * of this object.
    */
   private class SortedResults
   extends SimpleResults
   {
      /** Comparator used to presort rows */
      private final RowComparator comparator = new RowComparator();
      
      /**
       * Constructor which presorts all results according to this query's
       * sort criteria and retains a list of the primary key ID arrays
       * representing the records which comprise each result row.  If break
       * groups are enabled for this query, the set of leading rows for each
       * break group is generated as well.
       * <p>
       * The sort is performed by accessing each result row returned by the
       * initial query execution.  These rows are in no particular order, but
       * are visited from first to last.  Each row consists of one or more
       * records.  Each such record is loaded temporarily into its associated
       * record buffer.  The resolvable data source for each sort criterion
       * is then asked to resolve its sort value, and these values are stored
       * into a sort key array in a temporary,
       * <code>PresortQuery.SortedResults.Row</code> object within a list.
       * Once sort keys have been generated for all result rows, the list is
       * then sorted, primary key arrays for each row are preserved, and the
       * sort keys and original result set are discarded.
       *
       * @param   results
       *          Scrollable result set generated by the query.  Unlike the
       *          scrollable result set used by <code>PreselectQuery</code>,
       *          this result set could contain full data records or primary key IDs.
       * @param   loadDMOs
       *          When the row contains only the PK, indicate if the record
       *          should be loaded into its associated record buffer.
       *
       * @throws  PersistenceException
       *          if there is an error evicting a temporary record from the
       *          persistence session after it is used.
       */
      private SortedResults(Results results, boolean loadDMOs)
      throws PersistenceException
      {
         super(new ArrayList<>());
         
         List<Object[]> rowData = getRows();
         
         List<Row> rows = new ArrayList<>();
         
         RecordBuffer[] buffers = query.getRecordBuffers();
         int cols = buffers.length;
         int sortCount = sortHelpers.size();
         
         // Collect buffers.
         pushTempContexts(buffers);
         
         try
         {
            for (int x = 0; results.next(); x++)
            {
               // collect primary keys and prepare buffers with temporary records
               Long[] ids = new Long[cols];
               for (int i = 0; i < cols; i++)
               {
                  Object res = results.get(i);
                  Record dmo = null;
                  Long pk = null;

                  if (res instanceof Record)
                  {
                     dmo = (Record) res;
                     pk = dmo.primaryKey();
                  }
                  else if (res instanceof Long)
                  {
                     pk = (Long) res;

                     if (loadDMOs)
                     {
                        String entity = buffers[i].getEntityName();
                        RecordIdentifier<String> ident = new RecordIdentifier<>(entity, pk);
                        dmo = buffers[i].getPersistence().quickLoad(ident, !buffers[i].getDmoInfo().multiTenant);
                     }
                  }
                  buffers[i].setTempRecord(dmo);
                  
                  // a null DMO is legal (in the case of an outer join)
                  ids[i] = pk;
               }
               
               // Collect sort key results.
               BaseDataType[] sortKey = new BaseDataType[sortCount];
               Iterator<Resolvable> iter2 = sortHelpers.keySet().iterator();
               for (int i = 0; iter2.hasNext(); i++)
               {
                  Resolvable resolver = iter2.next();
                  sortKey[i] = resolver.resolve();
               }
               Row row = new Row(sortKey, ids);
               
               // Add the row to the (still unsorted) list of rows.
               rows.add(row);
               
               // For large result sets, we could be in this loop for a long
               // while;  test here to honor thread interruptions.  This
               // requires multiple JNI transitions, so we don't do it on
               // every pass.
               if (x % 100 == 0)
               {
                  TransactionManager.honorStopCondition();
               }
            }
         }
         finally
         {
            results.cleanup();
            
            // Clear buffers' temporary records.
            popTempContexts(buffers);
         }
         
         if (!rows.isEmpty())
         {
            // sort the rows using the resolved sort keys and registered comparators
            rows.sort(comparator);
            
            // Drop any unneeded records in the case of FIRST/LAST iteration.
//            filterResults(rows);
            
            // Replace interim Row objects with final primary key IDs.
//            rowData.ensureCapacity(rows.size());
            for (Row row : rows)
            {
               rowData.add(row.ids);
            }
            
            // Drop sort criteria and set up break group list.
            BaseDataType[] last = new BaseDataType[sortCount];
            Set<Integer>[] breakRows = (Set<Integer>[]) new Set[sortCount];
            if (breakGroups)
            {
               for (int i = 0; i < sortCount; i++)
               {
                  breakRows[i] = new HashSet<>();
               }
            }
            
            int size = rows.size();
            for (int i = 0; i < size; i++)
            {
               Row row = rows.get(i);
               
               if (breakGroups)
               {
                  // Record the first row number of each break group.
                  for (int j = 0; j < sortCount; j++)
                  {
                     calculateBreakGroups(last, j, row.sortKey[j], breakRows[j], i);
                  }
               }
            }
            
            if (breakGroups)
            {
               Iterator<SortHelper> iter2 = sortHelpers.values().iterator();
               for (int i = 0; iter2.hasNext(); i++)
               {
                  SortHelper helper = iter2.next();
                  helper.setBreakRows(breakRows[i]);
               }
            }
         }
         
//         rowData.trimToSize();
      }
      
      /**
       * In cases where iteration type is not EACH (i.e., FIRST, LAST), drop
       * the unneeded records.
       * 
       * @param   rows
       *          Unfiltered (but sorted) results.
       */
      /*
      private void filterResults(List<Row> rows)
      {
         boolean filter = false;
         int tableCount = getTableCount();
         int[] modes = new int[tableCount];
         Iterator<Component> iter = components();
         for (int i = 0; iter.hasNext(); i++)
         {
            Component next = iter.next();
            modes[i] = next.getIteration();
            if (modes[i] != NEXT)
            {
               filter = true;
            }
         }
         
         ArrayList<Serializable[]> rowData =
            (ArrayList<Serializable[]>) getRows();
         
         if (!filter)
         {
            for (Row row : rows)
            {
               rowData.add(row.ids);
            }
            
            return;
         }
         
         Serializable[] prevIDs = new Serializable[tableCount];
         Serializable[] nextIDs = new Serializable[tableCount];
         Iterator<Row> iter2 = rows.iterator();
         boolean keepNext = true;
         boolean dropPrev = false;
         
         for (int i = 0; iter2.hasNext(); i++)
         {
            Row row = iter2.next();
            nextIDs = row.ids;
            keepNext = true;
            
            tableLoop:
            for (int j = 0; j < tableCount; j++)
            {
               switch (modes[j])
               {
                  case NEXT:
                     break;
                     
                  case FIRST:
                     
                     if (j == 0)
                     {
                        // If we are the outermost table, drop all rows after
                        // the first.
                        if (i > 0)
                        {
                           keepNext = false;
                           
                           break tableLoop;
                        }
                     }
                     else
                     {
                        // If we are on the right side of a join, drop any row
                        // where the ID of the left side is the same as that
                        // of the previous row.
                        int x = j - 1;
                        if (i > 0 && nextIDs[x] == prevIDs[x])
                        {
                           keepNext = false;
                           
                           break tableLoop;
                        }
                     }
                     
                     break;
                     
                  case LAST:
                     
                     if (prevIDs != null)
                     {
                        if (j == 0)
                        {
                           dropPrev = true;
                        }
                        else
                        {
                           int x = j - 1;
                           if (prevIDs[x] == nextIDs[x])
                           {
                              dropPrev = true;
                           }
                        }
                     }
                     
                     break;
                     
                  default:
                     break;
               }
            }
            
            if (dropPrev)
            {
               int x = rowData.size() - 1;
               if (x >= 0)
               {
                  rowData.remove(x);
                  prevIDs = (x > 0 ? rowData.get(x - 1) : null);
               }
            }
            
            if (keepNext)
            {
               rowData.add(nextIDs);
               prevIDs = nextIDs;
            }
         }
      }
      */
      
      /**
       * A transient object which is used only during the presorting phase
       * and which is discarded once the results have been sorted (at which
       * time it is replaced with only the array of primary key IDs).
       */
      private class Row
      {
         /** Array of <code>Resolvable</code> results used for sorting */
         private final BaseDataType[] sortKey;
         
         /** Array of primary keys of result row records */
         private final Long[] ids;
         
         /**
          * Convenience constructor.
          *
          * @param   sortKey
          *          Array of <code>Resolvable</code> results used for
          *          sorting.
          * @param   ids
          *          Array of primary keys of result row records.
          */
         Row(BaseDataType[] sortKey, Long[] ids)
         {
            this.sortKey = sortKey;
            this.ids = ids;
         }
      }
      
      /**
       * Specialized comparator which uses the sort criteria previously added
       * to the query, along with the corresponding values resolved using
       * those criteria against each result row.
       */
      private class RowComparator
      implements Comparator<Row>
      {
         /**
          * Compare two objects, where each is expected to be an instance of
          * <code>PresortQuery.SortedResults.Row</code>.
          * <p>
          * For each comparison, the sort key array is extracted from each
          * row.  Each pair of elements in the arrays is compared using the
          * corresponding sort criterion comparator, until an unequivalent
          * result is obtained, or until no further elements remain (and the
          * rows consequently are determined to sort at the same precedence).
          *
          * @param   r1
          *          First row to be compared.
          * @param   r2
          *          Second row to be compared.
          *
          * @return  Negative number if <code>r1</code> is less than <code>r2</code>;
          *          positive number if <code>r1</code> is greater than <code>r2</code>;
          *          <code>0</code> if they compare as equivalent.
          */
         public int compare(Row r1, Row r2)
         {
            BaseDataType[] key1 = r1.sortKey;
            BaseDataType[] key2 = r2.sortKey;
            
            Iterator<SortHelper> iter = sortHelpers.values().iterator();
            for (int i = 0; iter.hasNext(); i++)
            {
               SortHelper helper = iter.next();
               int result = helper.getSorter().compare(key1[i], key2[i]);
               if (result != 0)
               {
                  return result;
               }
            }
            
            return 0;
         }
      }
   }
}