FQLHelper.java

/*
** Module   : FQLHelper.java
** Abstract : Helper object for random access queries
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20051007   @23251 Created initial version. Generates all HQL
**                           submitted on behalf of random access queries.
**                           Augments the base where clause of a query to
**                           provide the single-record retrieval and
**                           navigation semantic of Progress FIND and FOR
**                           queries.
** 002 ECF 20051111   @23379 Cleaned up exception handling. Removed throws
**                           clauses where possible, due to RecordBuffer
**                           changes.
** 003 ECF 20060114   @23916 Removed references to NameConverter. This is
**                           a conversion-only class. Use StringHelper
**                           instead.
** 004 ECF 20060204   @24357 Added temp table support. Account for temp
**                           table multiplexing in HQL statements.
** 005 ECF 20060306   @25021 Added support for foreign relation joins.
**                           Requires additional preprocessing of the
**                           where clause to inject a join phrase.
** 006 ECF 20060316   @25099 Integrated HQLPreprocessor. Rewrites certain
**                           base where clauses before augmenting them.
**                           Targets for preprocessing are where clauses
**                           which reference properties of a composite
**                           DMO inner class stored in an associated list.
** 007 ECF 20060529   @26592 Fixed generation of augmented where clauses
**                           for equality matches of text. These were not
**                           being properly uppercased.
** 008 ECF 20060602   @26941 Removed select clause for DMO ID. Persistence
**                           class has been optimized to retrieve records
**                           in a single step, rather than querying the ID
**                           first, then loading in a second step.
** 009 ECF 20060712   @28048 Migrated utility code to DBUtils class.
** 010 ECF 20060728   @28266 Rolled back #008 (@26941). Required for more
**                           reliable locking. The use of the second level
**                           cache should negate any loss of performance.
** 011 ECF 20060802   @28351 Moved inner class SortComponent to its own
**                           top-level class, SortCriterion. Related
**                           functionality in the HQLHelper constructor
**                           also migrated to the new class.
** 012 ECF 20060831   @29086 Simplified retrieval of schema name in
**                           factory method obtain().
** 013 ECF 20070228   @32244 Enable differentiation between ID-only
**                           projection queries and full queries. The
**                           former returns only the primary key ID of a
**                           record. The latter fetches the full DMO.
** 014 ECF 20070328   @32644 Changed to support HQLPreprocessor caching.
**                           Use factory method to retrieve an instance of
**                           the preprocessor rather than invoking the
**                           constructor directly.
** 015 ECF 20070502   @33369 Changed to support HQLPreprocessor changes.
**                           The signature of that class' primary factory
**                           has changed.
** 016 ECF 20070827   @35000 Optimized certain temp table queries.
**                           Eliminate multiplex ID check from where
**                           clause where possible and hard code multiplex
**                           ID when this test is necessary, rather than
**                           using a query substitution parameter. Augment
**                           order by clauses with leading multiplex ID.
** 017 ECF 20071206   @36268 Added sortIndex field and getSortIndex()
**                           method. Tracks SortIndex used by associated
**                           queries. Integrated generics.
** 018 ECF 20080310   @37475 Added args field. Needed by HQLPreprocesor to
**                           derive data type information for query
**                           substitution parameters.
** 019 ECF 20080403   @38149 Minor cleanup. Removed dmoIface from instance
**                           variables and method signatures.
** 020 SVL 20080421   @38081 The type of the "join" field was changed from
**                           DynamicJoin to AbstractJoin.
** 021 ECF 20080606   @38605 Added support for parallel dirty database HQL
**                           in HQLBundles. This is required when there
**                           are different dialects in use between the
**                           primary and dirty database.
** 022 ECF 20081009   @40077 Integrated new support for inlining query
**                           substitution parameters. Queries supported by
**                           this class don't actually have their
**                           parameters inlined, but changes were needed
**                           to integrate HQLPreprocessor API changes and
**                           the ParameterIndices class.
** 023 ECF 20081107   @40404 Moved multiplex ID position in order by
**                           clause. It used to be the first element, but
**                           that sometimes resulted in a less efficient
**                           index being selected for a query plan. Now it
**                           is either the last element, or second to last
**                           (if the primary key is part of the clause).
** 024 SVL 20090118   @41168 HQL helpers which have inlined preprocessors
**                           are not stored in cache.
** 025 SVL 20090122   @41197 Do not look into cache if we have a query
**                           with unknown parameter(s).
** 026 ECF 20090302   @41596 Fixed order by clause for temp tables. In
**                           cases where both multiplex ID and primary key
**                           ID were needed, the order of these two did
**                           not match their order in temp table indices.
**                           Also reduced memory use of HQLBundle cache.
** 027 GES 20090424   @41940 Import change.
** 028 ECF 20090602   @42583 Expose primary HQLPreprocessor.
** 029 ECF 20090623   @42942 Partially backed out #016 (@35000). Replaced hard
**                           coded multiplex IDs with query substitution
**                           parameter placeholders. Some use cases generate
**                           many similar queries which are differentiated
**                           only by the hard coded multiplex IDs. This was
**                           bloating the query cache in the Persistence
**                           Context class.
** 030 ECF 20131001          Added metadata support.
** 031 SVL 20140210          Use HQLExpression instead of HQL string.
** 032 ECF 20150302          Reworked caching and replaced Apache commons logging with J2SE
**                           standard logging.
** 033 ECF 20150315          Fixed regression in #032; removed indirect reference to RecordBuffer
**                           via AbstractJoin. We no longer store any information specific to a
**                           particular buffer instance.
** 034 ECF 20150801          Retrofitted HQLPreprocessor.get API changes.
** 035 EVL 20160223          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 036 ECF 20160225          Changes required by PropertyHelper rewrite. Use DMO entity instead
**                           of implementation class when composing query strings.
** 037 OM  20160629          HQLPreprocessor needs the list of fields of the current index.
** 038 OM  20160715          Addded isFindTemplate() method.
** 039 ECF 20160720          Fixed buffer lookup during HQL preprocessing.
** 040 ECF 20160901          Added safety code to return a bogus helper object from obtain() when
**                           we have a fatal error preprocessing HQL; avoids NPE downstream.
** 041 OM  20171212          HQLPreprocessor.get() signature change.
** 042 CA  20191211          The SortCriterion must know the database, to be able to use the 
**                           correct UDF, in case of datetime-tz fields.
** 043 ECF 20200906          New ORM implementation.
** 044 OM  20201007          Optimized SortCriterion by using DmoMeta data instead of map lookups.
**     ECF 20210506          Use RecordBuffer.getDmoInfo() getter instead of direct field access to
**                           prevent NPE in proxy case.
**     CA  20221006          Added JMX instrumentation for FQL prepare (in 'obtain'). Refs #6814
**     OM  20221103          Class renamed to FQLHelper.
** 045 OM  20230404          The join navigation key may contain null/unknown values.
** 046 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 047 RAA 20230525          In createBundle, the orderBy clause will be null in FIRST/LAST case if a
**                           unique finding is encountered.
** 048 HC  20240222          Enabled JMX on FWD Client.
** 049 OM  20240909          Improved Database API.
** 050 TJD 20240705          Support for direct control of NULL supporting FQL queries
** 051 ES  20241017          Cache the FQLBundles only if all the elements of a join predicate are 
**                           fully known.
** 052 RNC 20241125          Used LinkedHashSet instead of HashSet in constructor to preserve
**                           the ordering of the indexed fields.
** 053 LS  20250415          Added dmoMeta and included/excluded properties.
**                           Updated the signature of obtain() and constructor accordingly.
**                           Changed assembleSelectClause() to honor the FIELDS/EXCEPT clause.
*/

/*
** 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.lang.reflect.*;
import java.util.*;
import java.util.logging.*;
import com.goldencode.util.*;
import com.goldencode.p2j.jmx.*;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.meta.*;
import com.goldencode.p2j.persist.orm.DmoMeta;
import com.goldencode.p2j.persist.orm.Property;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.CentralLogger;

/**
 * Helper class which provides services to random access style queries.
 * These include composing FQL statements for all possible query navigation
 * and creating an array of Hibernate <code>Type</code> objects to match a
 * substitution parameter array.
 * <p>
 * This class is the work horse behind {@link RandomAccessQuery}, generating
 * one or more FQL select statements for each type of navigation/retrieval
 * request issued by that query.  An instance of {@link FQLBundle} is created
 * lazily for each such command type received from the query.  These instances
 * are cached to speed subsequent lookups within the current context.
 * <p>
 * Given a {@link RecordBuffer}, an optional where clause, an optional sort
 * clause, an optional {@link DynamicJoin}, and an indicator whether this
 * query is a projection query which fetches only the primary key of a
 * record, this class will:
 * <ul>
 *   <li>{@link FQLPreprocessor preprocess} the where clause to account for
 *       any implicit ANSI joins necessary to resolve certain complex uses of
 *       subscripts;
 *   <li>augment the base where clause if necessary to account for temporary
 *       table semantics;
 *   <li>augment the base where clause if necessary to embed any natural,
 *       foreign key joins;
 *   <li>analyze the sort clause in order to:
 *   <ul>
 *     <li>create an <code>order by</code> clause for the query;
 *     <li>augment the base where clause to allow ordered, single record
 *         navigation and retrieval;
 *   </ul>
 *   <li>lazily create an {@link FQLBundle} instance for each type of
 *       navigation/retrieval request processed by the query;
 *   <li>populate each <code>FQLBundle</code> instance with the full FQL
 *       statement(s), accessor method(s), and query substitution parameter
 *       indices needed to service a particular navigation/retrieval request.
 * </ul>
 */
final class FQLHelper
implements QueryConstants
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(FQLHelper.class.getName());

   /** Instrumentation for {@link #obtain}. */
   private static final NanoTimer FQL_HELPER = NanoTimer.getInstance(FwdServerJMX.TimeStat.OrmHqlHelperParse);
   
   /** Cache of all active helper objects in the current context */
   private static final FQLHelperCache cache = new FQLHelperCache();
   
   /** Base where clause for associated query */
   private final String where;
   
   /** FQL where clause preprocessor */
   private final FQLPreprocessor fqlPreprocessor;
   
   /** FQL where clause preprocessor, using dirty database dialect */
   private final FQLPreprocessor altPreprocessor;
   
   /** DMO entity name used by query */
   private final String dmoEntity;
   
   /** Alias used to represent DMO instance in FQL statements */
   private final String dmoAlias;

   /** Meta information of the DMO associated with the buffer */
   private final DmoMeta dmoMeta;
   
   /** Whether query should return primary key ID only, or entire DMO */
   private final boolean idOnly;
   
   /** Are multiplex semantics required? */
   private final boolean multiplexed;
   
   /** Does sort clause need to be augmented with multiplex ID? */
   private final boolean multiplexSort;
   
   /** FQL expression for dynamic join via a foreign key relation */
   private final FQLExpression[][] joinFQL;
   
   /** Components of a parsed order by clause */
   private final List<SortCriterion> sortCriteria;
   
   /** Components of a parsed order by clause, using dirty database dialect */
   private final List<SortCriterion> altCriteria;
   
   /** Cache of FQL bundles, indexed by retrieval request type constant */
   private final FQLBundle[] bundleCache = new FQLBundle[5];
   
   /** Sort index used by query */
   private final SortIndex sortIndex;

   /** The included properties for each DMO (FIELDS option). */
   private Property[] included = null;

   /** The excluded properties for each DMO (EXCEPT option). */
   private Property[] excluded = null;
   
   /** Order by clause for query, adjusted for navigation direction */
   private String orderBy = null;
   
   /** Order by clause for dirty database use */
   private String altOrderBy = null;
   
   /**
    * Constructor which sets fundamental data upon which final FQL statements will be based.
    * Parses the base order by clause {@code sort} into its property and directional components,
    * which are later used to augment the base where clause for single-record retrievals.
    *
    * @param   buffer
    *          Record buffer against which the current query operates.
    * @param   referencedBuffers
    *          All buffers referenced by the where clause.
    * @param   schema
    *          Name of schema associated with query's buffer.
    * @param   dmoAlias
    *          Alias used to represent DMO instance in FQL statements.
    * @param   idOnly
    *          If {@code true}, the FQL statements generated by this object will begin with a
    *          select clause which specifies only the record's primary key identifier;  if
    *          {@code false}, no select clause will be prepended.
    * @param   joinFQL
    *          FQL expression snippet for a join via a foreign key relation.  May be {@code null}.
    * @param   where
    *          Base where clause for associated query.
    * @param   sort
    *          Base order by clause for associated query.
    * @param   included
    *          The included properties.
    * @param   excluded
    *          The excluded properties.
    * @param   args
    *          Query substitution parameters.
    * 
    * @throws  PersistenceException
    *          if there is any error parsing the given {@code sort} clause.
    */
   private FQLHelper(RecordBuffer buffer,
                     RecordBuffer[] referencedBuffers,
                     String schema,
                     String dmoAlias,
                     boolean idOnly,
                     FQLExpression[][] joinFQL,
                     String where,
                     String sort,
                     Property[] included,
                     Property[] excluded,
                     Object[] args)
   throws PersistenceException
   {
      this.dmoEntity = buffer.getEntityName();
      this.dmoAlias = dmoAlias;
      this.dmoMeta = buffer.getDmoInfo();
      this.idOnly = idOnly;
      this.joinFQL = joinFQL;
      this.where = where;
      this.sortCriteria = SortCriterion.parse(sort, buffer);
      this.included = included;
      this.excluded = excluded;
      Set<String> indexedFields = new LinkedHashSet<>(sortCriteria.size());
      for (SortCriterion criterion : this.sortCriteria)
      {
         indexedFields.add(criterion.getName());
      }
      
      // if dirty database dialect differs from that of primary dialect, we must generate an alternative
      // SortCriteria list and FQLPreprocessor (non-temp-tables only)
      Database primaryDB = buffer.getDatabase();
      Database dirtyDB = primaryDB.toType(Database.Type.DIRTY);
      Dialect primaryDialect = DatabaseManager.getDialect(primaryDB);
      Dialect dirtyDialect = DatabaseManager.getDialect(dirtyDB);
      
      this.fqlPreprocessor = FQLPreprocessor.get(where,
                                                 primaryDB,
                                                 primaryDialect,
                                                 referencedBuffers,
                                                 args,
                                                 null,
                                                 false,
                                                 null,
                                                 null,
                                                 false,
                                                 indexedFields);
      this.sortIndex = SortIndex.get(sort, buffer);
      boolean hasWhere = (where != null && where.length() > 0);
      
      // multiplex ID semantics are required in the where clause if we have either a multiplexed temp table,
      // or we have a where clause in a temp table query
      this.multiplexed = (buffer.isMultiplexed() || hasWhere) && buffer.getMultiplexID() != null;
      
      // the order by clause must be augmented with a multiplex ID for any temp table query
      this.multiplexSort = (buffer.getMultiplexID() != null);
      
      if (buffer.isTemporary() || dirtyDialect == null || dirtyDialect.equals(primaryDialect))
      {
         this.altCriteria = this.sortCriteria;
         this.altPreprocessor = this.fqlPreprocessor;
      }
      else
      {
         this.altCriteria = SortCriterion.parse(dirtyDialect, sort, dmoAlias, dmoMeta, true);
         this.altPreprocessor = fqlPreprocessor.get(where,
                                                    dirtyDB,
                                                    dirtyDialect,
                                                    referencedBuffers,
                                                    args,
                                                    null,
                                                    false,
                                                    null,
                                                    null,
                                                    false,
                                                    indexedFields);
      }
   }
   
   /**
    * Obtain an instance of an {@code FQLHelper} for the given buffer and query criteria. The
    * instance is obtained from the cache if present, otherwise, it is created and cached.
    *
    * @param   buffer
    *          Record buffer against which the current query operates.
    * @param   referencedBuffers
    *          All buffers referenced by the where clause.
    * @param   args
    *          Query substitution parameters.
    * @param   idOnly
    *          If {@code true}, the FQL statements generated by this object will begin with a
    *          select clause which specifies only the record's primary key identifier;  if
    *          {@code false}, no select clause will be prepended.
    * @param   where
    *          Base where clause supplied to the current query.
    * @param   sort
    *          Base order by clause supplied to the current query.
    * @param   join
    *          Helper object for a join via a foreign key relation.  May be {@code null}.
    * @param   included
    *          The included properties.
    * @param   excluded
    *          The excluded properties.
    *
    * @return  A new or cached instance of this class, specific to the given buffer and query
    *          criteria.
    */
   static FQLHelper obtain(RecordBuffer buffer,
                           RecordBuffer[] referencedBuffers,
                           Object[] args,
                           boolean idOnly,
                           String where,
                           String sort,
                           AbstractJoin join,
                           Property[] included,
                           Property[] excluded)
   {
      Class<? extends DataModelObject> joinIface = (join == null ? null : join.getInverseInterface());
      FqlType[] types = (args != null) ? DBUtils.makeTypeArray(args) : new FqlType[] {};
      
      // resolve all parameters and check whether we have any unknown value(s)
      boolean unknowns = false;
      if (args != null)
      {
         for (Object parm : args)
         {
            if (parm instanceof FieldReference)
            {
                parm = ((FieldReference) parm).resolve();
            }
            
            if (parm instanceof BaseDataType && ((BaseDataType) parm).isUnknown())
            {
               unknowns = true;
            }
         }
      }
      
      FQLHelper helper = null;
      Class<? extends DataModelObject> dmoIface = buffer.getDMOInterface();
      
      if (!unknowns)
      {
         helper = cache.get(dmoIface, types, idOnly, where, sort, joinIface, included, excluded);
      }
      
      if (helper == null)
      {
         String schema = buffer.getSchema();
         String dmoAlias = buffer.getDMOAlias();
         String fwhere = StringHelper.safeTrim(where);
         FQLHelper[] res = new FQLHelper[1];
         
         FQL_HELPER.timer(() -> 
         {
            try
            {
               res[0] = new FQLHelper(buffer,
                                      referencedBuffers,
                                      schema,
                                      dmoAlias,
                                      idOnly,
                                      (join != null ? join.getFQL() : null),
                                      fwhere,
                                      sort,
                                      included,
                                      excluded,
                                      args);

               if (!res[0].fqlPreprocessor.wasInlined())
               {
                  cache.put(dmoIface, types, idOnly, fwhere, sort, joinIface, res[0], included, excluded);
               }
            }
            catch (PersistenceException exc)
            {
               ErrorManager.recordOrThrowError(exc);
               
               // in the event of a fatal error preparing the FQL, we need a bogus helper which
               // will not load a record, but which we can use to avoid NPE downstream
               try
               {
                  res[0] = new FQLHelper(buffer,
                                         referencedBuffers,
                                         schema,
                                         dmoAlias,
                                         idOnly,
                                         (join != null ? join.getFQL() : null),
                                         "false",
                                         sort,
                                         included,
                                         excluded,
                                         args);
               }
               catch (PersistenceException exc2)
               {
                  // not much else we can do; NPE here we come...
                  if (LOG.isLoggable(Level.WARNING))
                  {
                     LOG.log(Level.WARNING, "Error preparing FQL", exc2);
                  }
               }
            }
         });

         helper = res[0];
      }
      
      return helper;
   }
   
   /**
    * Return the sort index used by the helper's query.
    * 
    * @return  Sort index.
    */
   SortIndex getSortIndex()
   {
      return sortIndex;
   }
   
   /**
    * Return the primary FQL preprocessor used for the underlying query's where clause.
    * 
    * @return  FQL preprocessor.
    */
   FQLPreprocessor getFQLPreprocessor()
   {
      return fqlPreprocessor;
   }
   
   /**
    * Create a bundle containing a single FQL statement and no getter methods,
    * designed to retrieve the first record which matches the base where
    * clause criteria.  Which record is first is determined by the sort order
    * specified in the base order by clause.
    * <p>
    * No augmented FQL statements are produced for the bundle.
    * <p>
    * The resulting bundle is cached within the current context;  subsequent
    * requests return the cached bundle.
    *
    * @param   key
    *          The bitset key for join clause (set bits are not null values).
    *
    * @return  FQL bundle containing the information necessary to retrieve the first record.
    */
   synchronized FQLBundle first(BitSet key)
   {
      return createBundle(FIRST, key);
   }
   
   /**
    * Create a bundle containing a single FQL statement and no getter methods,
    * designed to retrieve the last record which matches the base where
    * clause criteria.  Which record is last is determined by the sort order
    * specified in the base order by clause.
    * <p>
    * No augmented FQL statements are produced for the bundle.
    * <p>
    * The resulting bundle is cached within the current context;  subsequent
    * requests return the cached bundle.
    *
    * @param   key
    *          The bitset key for join clause (set bits are not null values).
    *
    * @return  FQL bundle containing the information necessary to retrieve the last record.
    */
   synchronized FQLBundle last(BitSet key)
   {
      return createBundle(LAST, key);
   }
   
   /**
    * Create a bundle containing the FQL statement(s) and getter method(s)
    * necessary to retrieve the next record which matches the base where
    * clause criteria.  Which record is next is determined by the sort order
    * specified in the base order by clause, along with the sort-relevant
    * data in the current record of the associated buffer.
    * <p>
    * One or more augmented FQL statements are produced for the bundle.
    * There will be one getter method per statement.  The statements are
    * ordered from most to least specific.  Refer to the class description of
    * {@link FQLBundle} for further implementation details.
    * <p>
    * The resulting bundle is cached within the current context;  subsequent
    * requests return the cached bundle.
    *
    * @param   key
    *          The bitset key for join clause (set bits are not null values).
    *
    * @return  FQL bundle containing the information necessary to retrieve the next record.
    */
   synchronized FQLBundle next(BitSet key)
   {
      FQLBundle bundle = bundleCache[NEXT];
      if (bundle == null)
      {
         bundle = new FQLBundle();
         
         // Use ascending sort.
         orderBy = buildSortClause(false, sortCriteria);
         
         // Augment base where clause;  one variant per sort component.
         // Store DMO getter methods associated with substitution parameters.
         fillBundleLists(bundle, false, sortCriteria, true, key);
         
         // Final processing to complete FQL statement.
         postProcessBundle(bundle, true);
         
         // If necessary, augment FQL bundle with parallel statements that are
         // compatible with the dirty database's dialect.
         if (altCriteria != sortCriteria)
         {
            altOrderBy = buildSortClause(false, altCriteria);
            fillBundleLists(bundle, false, altCriteria, false, key);
            postProcessBundle(bundle, false);
         }
         
         bundle.freeze();
         if (key == null)
         {            
            bundleCache[NEXT] = bundle;
         }
         
         if (LOG.isLoggable(Level.FINE))
         {
            LOG.log(Level.FINE, "NEXT:  " + bundle);
         }
      }
      
      return bundle;
   }
   
   /**
    * Create a bundle containing the FQL statement(s) and getter method(s)
    * necessary to retrieve the previous record which matches the base where
    * clause criteria.  Which record is previous is determined by the sort
    * order specified in the base order by clause, along with the
    * sort-relevant data in the current record of the associated buffer.
    * <p>
    * One or more augmented FQL statements are produced for the bundle.
    * There will be one getter method per statement.  The statements are
    * ordered from most to least specific.  Refer to the class description of
    * {@link FQLBundle} for further implementation details.
    * <p>
    * The resulting bundle is cached within the current context;  subsequent
    * requests return the cached bundle.
    *
    * @param   key
    *          The bitset key for join clause (set bits are not null values).
    *
    * @return  FQL bundle containing the information necessary to retrieve the previous record.
    */
   synchronized FQLBundle previous(BitSet key)
   {
      FQLBundle bundle = bundleCache[PREVIOUS];
      if (bundle == null)
      {
         bundle = new FQLBundle();
         
         // Use ascending sort.
         orderBy = buildSortClause(true, sortCriteria);
         
         // Augment base where clause;  one variant per sort component.
         // Store DMO getter methods associated with substitution parameters.
         fillBundleLists(bundle, true, sortCriteria, true, key);
         
         // Final processing to complete FQL statement.
         postProcessBundle(bundle, true);
         
         // If necessary, augment FQL bundle with parallel statements that are
         // compatible with the dirty database's dialect.
         if (altCriteria != sortCriteria)
         {
            altOrderBy = buildSortClause(true, altCriteria);
            fillBundleLists(bundle, true, altCriteria, false, key);
            postProcessBundle(bundle, false);
         }
         
         bundle.freeze();
         if (key == null)
         {            
            bundleCache[PREVIOUS] = bundle;
         }
         
         if (LOG.isLoggable(Level.FINE))
         {
            LOG.log(Level.FINE, "PREVIOUS:  " + bundle);
         }
      }
      
      return bundle;
   }
   
   /**
    * Create a bundle containing a single FQL statement and no getter methods, designed to retrieve a single,
    * unique record which matches the base where clause criteria. Since only a single record is sought,
    * sorting is irrelevant.
    * <p>
    * If the join key is {@code null}, the FQL statements returned are not augmented. Otherwise, the returned
    * bundle contains statements augmented for matching 4GL {@code unknown} semantics.
    * <p>
    * The resulting bundle is cached within the current context if the join component is not present or if all
    * fields from the inverse are not null; subsequent requests return the cached bundle in these conditions.
    * 
    * @param   key
    *          The bitset key for join clause (set bits are not null values).
    *
    * @return  FQL bundle containing the information necessary to retrieve a unique record.
    */
   synchronized FQLBundle unique(BitSet key)
   {
      return createBundle(UNIQUE, key);
   }
   
   /**
    * Create a bundle containing a single FQL statement and no getter methods, designed to retrieve the last,
    * the first or a single, unique record which matches the base where clause criteria.
    * See {@link #first(BitSet)}, {@link #last(BitSet)} or {@link #unique(BitSet)} for each specific case.
    *
    * @param   type
    *          Target bundle type. Can be {@link #FIRST}, {@link #LAST} or {@link #UNIQUE}.
    * @param   key
    *          The bitset key for join clause (set bits are not null values).
    *
    * @return  FQL bundle containing the information necessary to retrieve the target record.
    */
   private FQLBundle createBundle(int type, BitSet key)
   {
      FQLBundle bundle = (key == null) ? bundleCache[type] : null;
      if (bundle == null)
      {
         bundle = new FQLBundle();
         
         switch (type)
         {
            case FIRST:
               // Use ascending sort.
               orderBy = fqlPreprocessor.isUniqueFind(true) ? null : buildSortClause(false, sortCriteria);
               break;
            case LAST:
               // Use descending sort.
               orderBy = fqlPreprocessor.isUniqueFind(true) ? null : buildSortClause(true, sortCriteria);
               break;
            case UNIQUE:
               // Null out order by clause.
               orderBy = null;
               break;
            default:
               throw new IllegalArgumentException("Bundle type should be FIRST, LAST or UNIQUE!");
         }
         
         // use base where clause; depending on the join substitution values, augmentation is performed.
         // if [joinFQL] is not in use, just add multiplex support if temp table.
         FQLExpression whereClause = preprocessWhere(true, fqlPreprocessor, key);
         String pWhere = whereClause != null ? whereClause.toFinalExpression() : null;
         bundle.addStatement(pWhere, false);
         
         // Final processing to complete FQL statement.
         postProcessBundle(bundle, true);
         
         if (fqlPreprocessor != altPreprocessor)
         {
            switch (type)
            {
               case FIRST:
                  altOrderBy = buildSortClause(false, altCriteria);
                  break;
               case LAST:
                  altOrderBy = buildSortClause(true, altCriteria);
                  break;
               case UNIQUE:
                  altOrderBy = null;
                  break;
            }
            
            whereClause = preprocessWhere(true, altPreprocessor, key);
            pWhere = whereClause != null ? whereClause.toFinalExpression() : null;
            bundle.addStatement(pWhere, true);
            postProcessBundle(bundle, false);
         }
         
         bundle.freeze();
         if (key == null)
         {
            bundleCache[type] = bundle;            
         }
         
         if (LOG.isLoggable(Level.FINE))
         {
            StringBuilder sb = new StringBuilder();
            switch (type)
            {
               case FIRST:
                  sb.append("FIRST");
                  break;
               case LAST:
                  sb.append("LAST");
                  break;
               case UNIQUE:
                  sb.append("UNIQUE");
                  break;
            }
            sb.append(":  ");
            sb.append(bundle.toString());
            LOG.log(Level.FINE, sb.toString());
         }
      }
      
      return bundle;
   }
   
   /**
    * Test whether the predicate of this query looks up for the template record (a direct access
    * using rowid/recid of the table using a negative index).
    * <p>
    * The current implementation only identifies simple form where the predicate is composed from
    * a single equality test between the rowid/recid of the record and a negative value (as P2J
    * convention). This is enough for supporting the standard idiom for loading template records
    * in P4GL.
    * <p>
    * More complex queries can be defined using AND/OR clauses in the WHERE predicate but they are
    * not supported by P2J. They do not make sense since, if if the template record exists for a
    * table, it is well defined only by it rowid/recid and immutable. 
    *
    * @param   queryParams
    *          The current parameter list for the query.
    * @param   dmoClass
    *          The dmo class. We verify whether this is indeed the correct template id for
    *          respective table.
    *
    * @return  {@code true} if the query should load the template record for the buffer. 
    */
   boolean isFindTemplate(Object[] queryParams, Class<? extends Record> dmoClass)
   {
      if (fqlPreprocessor == null || !fqlPreprocessor.isFindByRowid())
      {
         return false;
      }
      
      long queryRowid = fqlPreprocessor.getFindByRowid(queryParams);
      if (queryRowid >= 0)
      {
         return false;
      }
      
      Long templateRowid = MetadataManager.getTemplateRowid(dmoClass);
      return templateRowid != null && templateRowid == queryRowid;
   }
   
   /**
    * Preprocess the where clause to account for temp table multiplexing and
    * foreign relation joins.  This is performed on the intermediate form of
    * the base where clause as created by the {@link FQLPreprocessor}, but
    * before it is augmented to account for the sort criteria in {@link #fillBundleLists}.
    * <p>
    * If multiplexing is called for, this will prepend a test for the proper
    * multiplex ID before the where clause, if any.
    * <p>
    * If a foreign relation join is called for, the proper FQL will be
    * generated to compare a local entity with a foreign one (the foreign
    * entity is represented by a query substitution character:
    * <code>?</code>).
    *
    * @param   needWhere
    *          {@code true} if "where" prefix should be added.
    * @param   preproc
    *          FQL preprocessor to use for the base where clause.
    * @param   key
    *          The bitset key for join clause (set bits are not null values).
    *
    * @return  The preprocessed where clause or {@code null} if no where clause was generated.
    */
   private FQLExpression preprocessWhere(boolean needWhere, FQLPreprocessor preproc, BitSet key)
   {
      boolean hasWhere = (where != null && where.length() > 0);
      boolean hasJoin = (joinFQL != null);
      
      if (!multiplexed && !hasWhere && !hasJoin)
      {
         // No preprocessing required.
         return null;
      }
      
      FQLExpression whereClause = new FQLExpression();
      
      if (needWhere)
      {
         whereClause.append("where ");
      }
      
      // Handle temp table multiplexing.
      if (multiplexed)
      {
         if (hasWhere || hasJoin)
         {
            whereClause.append("(");
         }
         
         whereClause.append(dmoAlias);
         whereClause.append(".");
         whereClause.append(TemporaryBuffer.MULTIPLEX_FIELD_NAME);
         whereClause.append(" = ", true);
         
         if (hasWhere || hasJoin)
         {
            whereClause.append(") and ");
         }
      }
      
      // Handle a join.
      if (hasJoin)
      {
         if (multiplexed || hasWhere)
         {
            whereClause.append("(");
         }
         
         for (int i = 0; i < joinFQL.length; i++)
         {
            boolean isNull = (key != null) && !key.get(i);
            whereClause.append(joinFQL[i][isNull ? AbstractJoin.NULL_FIELD : AbstractJoin.NOT_NULL_FIELD]);
         }
         
         if (multiplexed || hasWhere)
         {
            whereClause.append(")");
         }
         if (hasWhere)
         {
            whereClause.append(" and ");
         }
      }
      
      // Embed the base where clause, if any.
      if (hasWhere)
      {
         if (multiplexed || hasJoin)
         {
            whereClause.append("(");
         }
         whereClause.append(preproc.getFQL());
         if (multiplexed || hasJoin)
         {
            whereClause.append(")");
         }
      }
      
      return whereClause;
   }
   
   /**
    * Post-process the augmented where clauses stored during the first pass
    * of FQL bundle creation.  Post-processing involves prepending each where
    * clause with select and from clauses, and appending with an optional
    * order by clause.  The original where clauses in the bundle are replaced
    * with the resulting, full FQL statements.
    *
    * @param   bundle
    *          FQL bundle to be post-processed.
    * @param   primary
    *          <code>true</code> to process FQL statement for primary database
    *          dialect;  <code>false</code> to process for dirty database
    *          dialect.
    */
   private void postProcessBundle(FQLBundle bundle, boolean primary)
   {
      int size = bundle.statementsSize();
      for (int i = 0; i < size; i++)
      {
         StringBuilder buf = new StringBuilder();
         
         assembleSelectClause(buf);
         assembleFromClause(buf);
         
         String where = bundle.getStatement(i, !primary);
         if (where != null)
         {
            buf.append(where);
            buf.append(" ");
         }
         
         if (primary)
         {
            assembleOrderByClause(buf, orderBy);
            bundle.setStatement(i, buf.toString().trim(), false);
         }
         else
         {
            assembleOrderByClause(buf, altOrderBy);
            bundle.setStatement(i, buf.toString().trim(), true);
         }
      }
      
      // Store query substitution parameter indices.
      // TODO:  currently, these are shared, and not dialect specific, but only because we do not inline
      //        parameters from this class.  Were this to change, we would need to maintain separate
      //        [ParameterIndices] objects for the primary and dirty database statements.
      if (primary)
      {
         bundle.setParameterIndices(fqlPreprocessor.getParameterIndices());
      }
   }
   
   /**
    * Compose an FQL compliant order by clause from the sort components parsed from the base order by clause
    * submitted at construction, optionally inverting the sort direction of each component.  The list of
    * original components will have been augmented with the DMO's primary key, if this is necessary to
    * disambiguate among non-unique records.
    *
    * @param   invert
    *          {@code true} to invert sort direction (i.e., {@code asc-->desc}, {@code desc-->asc});
    *          else {@code false}.
    * @param   sortCrit
    *          List of {@code SortCriterion} objects appropriate to the database dialect.
    *
    * @return  An FQL compliant order by clause.
    */
   private String buildSortClause(boolean invert, List<SortCriterion> sortCrit)
   {
      StringBuilder buf = new StringBuilder("order by ");
      
      // inject multiplex ID for temp table before primary key, so as to match the way these components are
      // ordered in the table's index.
      if (multiplexSort)
      {
         injectMultiplexSort(invert, buf);
         buf.append(", ");
      }
      
      int i = 0;
      Iterator<SortCriterion> iter = sortCrit.iterator();
      for (; iter.hasNext(); i++)
      {
         if (i > 0)
         {
            buf.append(", ");
         }
         SortCriterion next = iter.next();
         
         buf.append(next.toSortExpression(invert));
      }
      
      return buf.toString();
   }
   
   /**
    * Inject a sort expression into the given buffer using the well-known
    * multiplex ID field name with the appropriate DMO alias, and a sort
    * direction.
    * 
    * @param   invert
    *          If <code>true</code> the sort direction is descending;  if
    *          <code>false</code>, it is ascending.
    * @param   buf
    *          String builder into which the expression is injected.
    */
   private void injectMultiplexSort(boolean invert, StringBuilder buf)
   {
      buf.append(dmoAlias);
      buf.append(".");
      buf.append(TemporaryBuffer.MULTIPLEX_FIELD_NAME);
      buf.append(" ");
      buf.append(invert ? "desc" : "asc");
   }
   
   /**
    * Fill an FQL bundle's internal FQL statement and getter method lists
    * with the appropriate statements and methods, respectively.  The number
    * of statements and methods correlates directly with the number of sort
    * components managed by the associated query.  The FQL statements are
    * generated and stored in order from most specific to least specific,
    * such that as they are executed, the criteria become decreasingly
    * restrictive.
    * <p>
    * This method represents the first pass in a two step process to create
    * a set of full, FQL statements ({@link #postProcessBundle} represents
    * the second pass).  In this pass, only augmented where clauses (and
    * their associated getter methods) are stored in the bundle.  The base
    * where clause is augmented with a binary match phrase for each sort
    * component, each of which is AND'd together.  The last test in the
    * augmented where clause is always a range match, whereas the previous
    * tests are always equality matches.  The range match operator is
    * determined by the sort direction of the sort component, and the value
    * of the <code>invert</code> parameter.
    * <p>
    * For example, given the following query criteria, with
    * <code>invert</code> set to <code>false</code>...
    * <pre>
    *    DMO:        Customer
    *    alias:      customer
    *    where:      customer.number = ? and customer.creditLimit = ?
    *    order by:   customer.creditLimit asc, customer.name asc
    * </pre>
    * <p>
    * ...this method would produce the following list of augmented where
    * clauses...
    * <pre>
    * 1) where (customer.number = ? and customer.creditLimit = ?)
    *      and (customer.creditLimit = ? and customer.name = ? and customer.recid &gt; ?)
    *
    * 2) where (customer.number = ? and customer.creditLimit = ?)
    *      and (customer.creditLimit = ? and customer.name &gt; ?)
    *
    * 3) where (customer.number = ? and customer.creditLimit = ?)
    *      and (customer.creditLimit &gt; ?)
    * </pre>
    * <p>
    * ...and the following list of DMO "getter" methods...
    * <pre>
    * 1) Customer.getCreditLimit()
    * 2) Customer.getName()
    * 3) Customer.primaryKey()
    * </pre>
    * <p>
    * Note that the above example assumes neither <code>creditLimit</code>
    * nor <code>name</code> (nor the combination of the two) represent a
    * unique constraint.  If this assumption was not valid, the method would
    * produce the following augmented where clauses instead...
    * <pre>
    * 1) where (customer.number = ? and customer.creditLimit = ?)
    *      and (customer.creditLimit = ? and customer.name &gt; ?)
    *
    * 2) where (customer.number = ? and customer.creditLimit = ?)
    *      and (customer.creditLimit &gt; ?)
    * </pre>
    * <p>
    * ...and the following list of DMO "getter" methods...
    * <pre>
    * 1) Customer.getCreditLimit()
    * 2) Customer.getName()
    * </pre>
    * <p>
    * If a temp table is involved in the where clause, and/or if a foreign relation join between tables is
    * required, additional preprocessing of the where clause must occur.
    *
    * @param   bundle
    *          FQL bundle to be populated.
    * @param   invert
    *          {@code true} to invert sort direction (i.e., {@code asc-->desc}, {@code desc-->asc} of each
    *          sort component when generating the range match portion of the augmented where clause;
    *          else {@code false}. Inverting the sort direction in this context means using the opposite range
    *          operator in the match test ({@code >} becomes {@code <} and vice versa).
    * @param   sortCrit
    *          List of {@code SortCriterion} objects appropriate to the database dialect.
    * @param   primary
    *          {@code true} to add generated FQL statement to the FQL bundle for use with the primary
    *          database; {@code false} to add it for use with the dirty database.
    * @param   key
    *          The bitset key for join clause (set bits are not null values).
    *
    * @see     #preprocessWhere
    */
   private void fillBundleLists(FQLBundle bundle,
                                boolean invert,
                                List<SortCriterion> sortCrit,
                                boolean primary,
                                BitSet key)
   {
      // Store methods.  These are shared between primary and dirty database
      // statements and are not dialect specific.  They must only be added once.
      if (primary)
      {
         Iterator<SortCriterion> iter = sortCrit.iterator();
         while (iter.hasNext())
         {
            SortCriterion next = iter.next();
            Method getter = next.getMethod();
            if (getter != null)
            {
               bundle.addGetter(getter);
            }
         }
      }
      
      // Compose where clause variants.
      int size = sortCrit.size();
      FQLExpression prepClause = preprocessWhere(false, (primary ? fqlPreprocessor : altPreprocessor), key);
      
      // Create one augmented where clause variant for each sort component.
      for (int i = size - 1; i >= 0; i--)
      {
         FQLExpression whereClause = new FQLExpression("where ");
         
         if (prepClause != null)
         {
            whereClause.append("(");
            whereClause.append(prepClause);
            whereClause.append(") and (");
         }
         
         for (int j = 0; j <= i; j++)
         {
            SortCriterion next = sortCrit.get(j);
            if (next.getMethod() == null)
            {
               j--;
               continue;
            }
            
            if (j > 0)
            {
               whereClause.append(" and ");
            }
            
            whereClause.append(next.toWhereExpression((j == i), invert, !next.isPrimaryKey()));
         }
         
         if (prepClause != null)
         {
            whereClause.append(")");
         }
         
         bundle.addStatement(whereClause.toFinalExpression(), !primary);
      }
   }

   /**
    * Assemble the <code>select</code> clause for an FQL query.
    * <ul>
    *    <li>
    *       For cases when {@link #idOnly} is <code>true</code> uses the form
    *       <pre>select {dmo}.recid</pre>
    *    </li>
    *    <li>
    *       Otherwise, honors the FIELDS and EXCEPT clauses.
    *    </li>
    * </ul>

    *
    * @param   buf
    *          String buffer into which clause is assembled.
    */
   private void assembleSelectClause(StringBuilder buf)
   {
      if (idOnly)
      {
         buf.append("select ");
         buf.append(dmoAlias);
         buf.append('.');
         buf.append(DatabaseManager.PRIMARY_KEY);
         buf.append(" ");
      }
      else if (included != null || excluded != null)
      {
         buf.append("select ");

         int size = dmoMeta.getFieldSize();
         Property[] props = new Property[size];
         List<String> indexedFields = new ArrayList<>();

         for (int j = 0; j < sortCriteria.size(); j++)
         {
            String name = sortCriteria.get(j).getUnqualifiedName();
            if (!name.equals(DatabaseManager.PRIMARY_KEY))
            {
               indexedFields.add(name);
            }
         }
         // only permanent buffers can use include/exclude
         if (included == null)
         {
            // include all fields
            Iterator<Property> piter = dmoMeta.getFields(false);
            while (piter.hasNext())
            {
               Property p = piter.next();

               if (p.propId > 0)
               {
                  props[p.propId - 1] = p;
               }
            }
         }
         else
         {
            System.arraycopy(included, 0, props, 0, size);
         }

         if (excluded != null)
         {
            // exclude properties
            for (int j = 0; j < size; j++)
            {
               if (excluded[j] != null) props[j] = null;
            }
         }

         buf.append(dmoAlias).append('.').append(DatabaseManager.PRIMARY_KEY);
         for (int j = 0; j < size; j++)
         {
            Property p = props[j];
            // extents are handled like scalars. The FQL converter will filter them out if necessary,
            // and the SQLQuery will fetch them from secondary tables at hydration time
            if (p == null)
            {
               continue;
            }
            indexedFields.remove(p.name);
            buf.append(", ").append(dmoAlias).append(".").append(p.name);
         }

         // add the fields included in the used index
         for (int j = 0; j < indexedFields.size(); j++)
         {
            buf.append(", ").append(dmoAlias).append(".").append(indexedFields.get(j));
         }

         buf.append(" ");
      }
   }
   
   /**
    * Assemble the <code>from</code> clause for an FQL query.  Uses the form:
    * <pre>
    *   from {DmoImpl} as {dmo}
    *   [ join {dmo}.{property} as {dmo}_{property} [...] ]
    * </pre>
    *
    * @param   buf
    *          String buffer into which clause is assembled.
    */
   private void assembleFromClause(StringBuilder buf)
   {
      String entity = dmoEntity.substring(dmoEntity.lastIndexOf(".") + 1);
      
      buf.append("from ");
      buf.append(entity);
      buf.append(" as ");
      buf.append(dmoAlias);
      buf.append(" ");
      
      // Add any required ANSI joins created by the FQL preprocessor.  These
      // are shared for primary and dirty database use, and are not dialect
      // specific.
      Iterator<String> iter = fqlPreprocessor.ansiJoins();
      while (iter.hasNext())
      {
         buf.append(iter.next());
         buf.append(" ");
      }
   }
   
   /**
    * Assemble the <code>order by</code> clause for an FQL query.  Uses the
    * form:
    * <pre>
    *   order by {dmo1} {asc/desc} [, {dmo2} {asc/desc} [...]]
    * </pre>
    *
    * @param   buf
    *          String buffer into which clause is assembled.
    * @param   orderPhrase
    *          Order by phrase to appended to the buffer if not
    *          <code>null</code>.
    */
   private void assembleOrderByClause(StringBuilder buf, String orderPhrase)
   {
      if (orderPhrase != null)
      {
         buf.append(orderPhrase);
      }
   }
}