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