QueryComponent.java

/*
** Module   : QueryComponent.java
** Abstract : A joinable, single-table query component used in a preselect query.
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20150801 Refactored from PreselectQuery, which previously contained this as an inner class.
** 002 ECF 20150907 Reimplemented silent error mode processing of query substitution parameters.
** 003 CA  20160518 Change ParameterIndices usage to collect all substitution parameters, as their
**                  number might change (up or down) after ternary is normalized.
** 004 ECF 20160610 Changed access modifier of isIdOnly to protected.
** 005 OM  20160629 HQLPreprocessor needs the list of fields of the current index.
** 006 ECF 20160720 Fixed buffer lookup during HQL preprocessing.
** 007 ECF 20160825 Added missing javadoc.
** 008 ECF 20171212 Added handling for errors processing substitution parameters.
** 009 OM  20171212 HQLPreprocessor.get() has a new parameter used by subselects.
**                  SortCriterion.toSortExpression() is subselect aware.
** 010 OM  20180515 Added dynamic filters and dynamic sort.
** 011 OM  20180901 Improved dynamic filtering.
** 012 OM  20180918 Fixed addDynamicFilter() return value. Filtering is case insensitive and
**                  uses LIKE patterns.
**     OM  20180924 Filtering on decimal fields uses STRING(f, format) function.
** 013 CA  20190722 Fixed datetimetz query arguments - they are treated like a ISO date, in string
**                  representation. UDF are used to compare them.
**     CA  20190829 Allow Java native values as arguments.
** 014 CA  20191009 Added where and sort clause translation in case of bound buffers.
** 015 ECF 20200906 New ORM implementation.
** 016 ECF 20210820 Retrofit for query substitution parameter preprocessing fixes.
**     OM  20221103 New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
**     CA  20221130 Refactored the WHERE clause translation (when the bound and definition buffers are not the
**                  same), to be aware of the external buffers (i.e. added for CAN-FIND sub-select clauses).
**                  The translate will be performed before the query is being executed.
** 017 CA  20230227 Fixed where clause translation when the external buffer's binding and definition does not
**                  match.
** 018 OM  20230404 The join navigation key may contain null/unknown values.
** 019 IAS 20230706 Runtime changes for CONTAINS with non-constant search expression.
** 020 OM  20231115 Fixed generation of nested multiplexed sub-queries.
** 021 CA  20240422 Allow the query to be reset to its initialization state, so the same instance can be used 
**                  to execute child buffer FILL operations.
** 022 AL2 20240625 Query parameters that throw an exception on evaluation will now return ?.
** 023 TJD 20230724 Migrate earlyPublishEntities from List to Set to improve performance
** 024 ICP 20240114 The subselect inside a join will no longer be generated if it matches optimization
*                   criteria.
** 025 ICP 20250129 Used integer.of and int64.of to leverage caches instances.
*/

/*
** 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.persist.lock.*;
import com.goldencode.p2j.util.*;

/**
 * A helper container for a query component. A query component defines the buffer to be updated,
 * the type of join to perform with the previous component (if any), the lock type for records
 * fetched into the buffer, the where clause to be applied, and any query substitution parameters.
 */
class QueryComponent
implements QueryConstants
{
   /** Joinable query which contains this query component */
   private final Joinable query;
   
   /** Original where clause, <i>before</i> preprocessing */
   private String where;
   
   /** A function to translate the where clause, just before it gets executed. */
   private Supplier<String> translateWhere = null;
   
   /** The where clause, including the dynamic filters, <i>before</i> preprocessing. */
   private String dfWhere = null;
   
   /** Record buffer updated by this component of the query */
   private RecordBuffer buffer;
   
   /** For bound buffers, this is the definition buffer*/
   private final RecordBuffer definition;
   
   /** Helper object for a join via a foreign key relation */
   private final AbstractJoin join;
   
   /** Record lock type */
   private final LockType lockType;
   
   /** Original arguments before query substitution arguments preprocessing. */
   private final Object[] origArgs;
   
   /** Base substitution arguments for this component */
   private final Object[] args;
   
   /** The substitution arguments with dynamic filtered ones inserted in front. */
   private Object[] dfArgs = null;
   
   /** {@code true} if component forms outer join with previous component; else {@code false} */
   private final boolean outer;
   
   /** Whether query should return primary key ID only, or entire DMO */
   private final boolean idOnly;
   
   /** Is this component the top/outermost among the query's components? */
   private boolean top = true;
   
   /**
    * Array of field reference names and nulls used to replace substitution parameters when
    * converting a nested looping (client-side) join to a server-side join
    */
   private String[] referenceSubs;
   
   /** Set of related buffers, if component participates in a server-side join */
   private Set<RecordBuffer> relatedBuffers = null;
   
   /** Iteration/retrieval mode (first, last, next, etc.) */
   private int iteration;
   
   /** Order by clause for FIRST/LAST iteration components */
   private String sort;
   
   /** FQL preprocessor */
   private FQLPreprocessor fqlPreprocessor;
   
   /** Position of {@code null} elements in argument list with which FQL was preprocessed */
   private boolean[] nullArguments = null;
   
   /** Sort index associated with this query component */
   private SortIndex sortIndex;
   
   /** Intermediately resolved arguments for this component */
   private Object[] currentArgs = null;
   
   /** Index information string as it is returned by INDEX-INFORMATION. */
   private String indexInfo = null;
   
   /**
    * The list of dynamic filters. These are filters added dynamically, at runtime and can also be
    * removed dynamically.
    */
   private Map<FieldReference, BaseDataType> dynamicFilters = null;
   
   /**
    * The list of format used by dynamic filters. These are filters added dynamically, at runtime
    * and can also be removed dynamically.
    */
   private Map<FieldReference, String> dynamicFormats = null;
   
   /**
    * Constructor.
    * 
    * @param   query
    *          Enclosing query.
    * @param   proxy
    *          DMO proxy which references the record buffer updated by this component of the query.
    * @param   join
    *          Helper object for a join via a foreign key relation.  May be <code>null</code>.
    * @param   where
    *          Restriction criteria in FQL where clause format.
    * @param   translateWhere
    *          A function to translate the where clause, just before it gets executed.
    * @param   indexInfo
    *          Index information string as it is returned by INDEX-INFORMATION.
    *          May be <code>null</code> if it is not an OPEN QUERY case or if
    *          you don't need debug information about selected indexes.
    * @param   lockType
    *          Requested record lock type.
    * @param   args
    *          Default (unresolved) query substitution arguments.
    * @param   iteration
    *          Type of iteration:  FIRST, LAST, NEXT, PREVIOUS, or UNIQUE.
    * @param   outer
    *          <code>true</code> if this component is joined with the
    *          previous component via an outer join;  <code>false</code>
    *          if there is no previous component, or if this component is
    *          joined with the previous component via an inner join.
    */
   protected QueryComponent(Joinable query,
                            BufferReference proxy,
                            AbstractJoin join,
                            String where,
                            Supplier<String> translateWhere,
                            String indexInfo,
                            LockType lockType,
                            Object[] args,
                            int iteration,
                            boolean outer)
   {
      if (lockType == null)
      {
         throw new NullPointerException("Lock type must be specified");
      }
      
      if (args != null)
      {
         this.origArgs = new Object[args.length];
         System.arraycopy(args, 0, origArgs, 0, args.length);
      }
      else
      {
         this.origArgs = null;
      }
      
      RecordBuffer buffer = proxy.buffer();
      
      AbstractQuery.preprocessSubstitutionArguments(buffer.getBufferManager(),
                                                    query::isFindByRowid,
                                                    true,
                                                    args);
      this.join = join;
      this.where = where;
      this.translateWhere = translateWhere;
      this.args = args;
      
      this.dfArgs = this.args;
      this.dfWhere = this.where;
      this.query = query;
      this.buffer = buffer;
      this.definition = proxy.definition();
      this.iteration = iteration;
      this.outer = outer;
      this.indexInfo = indexInfo;
      
      // override lock type if this is a temporary buffer to allow some downstream optimizations
      this.lockType = (buffer.isTemporary() ? LockType.NONE : lockType);
      
      // pull back entire record if possible, to avoid additional round trips to the database
      this.idOnly = !LockType.NONE.equals(this.lockType);
   }
   
   /**
    * Prepare the information needed to convert a nested looping (client-side) join of two
    * tables to a server-side join of those tables.
    * 
    * @param   joinList
    *          List of enclosing query components, from outermost to innermost (if any), from the
    *          original nested query. Does not include the component currently being visited.
    * @param   buffer
    *          Record buffer associated with the query component currently being visited.
    * @param   join
    *          Original join helper object which joins the current query component with an outer
    *          component or external buffer.
    * @param   args
    *          Query substitution arguments for the where clause of the current query component.
    * 
    * @return  An object containing the information needed to convert the join from client-side
    *          to server-side.
    */
   static ServerJoinData prepareServerJoinData(List<? extends QueryComponent> joinList,
                                               RecordBuffer buffer,
                                               AbstractJoin join,
                                               Object[] args)
   {
      Object[] processedArgs = args;
      String[] referenceSubs = null;
      AbstractJoin newJoin = join;
      Set<RecordBuffer> relatedBuffers = null;
      
      if (!joinList.isEmpty())
      {
         relatedBuffers = new HashSet<>();
         for (QueryComponent related : joinList)
         {
            relatedBuffers.add(related.getBuffer());
         }
         
         if (args != null)
         {
            int len = args.length;
            List<Object> survivingArgs = new ArrayList<>();
            referenceSubs = new String[len];
            
            for (int i = 0; i < len; i++)
            {
               Object next = args[i];
               
               if (next instanceof FieldReference)
               {
                  FieldReference ref = (FieldReference) next;
                  if (relatedBuffers.contains(ref.getParentBuffer()))
                  {
                     referenceSubs[i] = ref.toString();
                     
                     continue;
                  }
               }
               
               survivingArgs.add(next);
            }
            
            int survived = survivingArgs.size();
            if (survived < len)
            {
               processedArgs = survivingArgs.toArray();
            }
         }
         
         if (join != null)
         {
            RecordBuffer inverseBuffer = join.getInverse().buffer();
            if (relatedBuffers.contains(inverseBuffer))
            {
               newJoin = new ServerLegacyKeyJoin(buffer, inverseBuffer);
            }
         }
      }
      
      return new ServerJoinData(newJoin, processedArgs, relatedBuffers, referenceSubs);
   }
   
   /**
    * Translate the {@link #where} clause, using the {@link #translateWhere} function.
    */
   protected void translateWhere()
   {
      if (translateWhere == null)
      {
         return;
      }
      
      this.where = translateWhere.get();
      this.dfWhere = this.where;
      this.translateWhere = null;
   }
   
   /**
    * Get the FQL preprocessor associated with this component.
    * 
    * @return  FQL preprocessor.
    * 
    * @throws  PersistenceException
    *          if the preprocessor must first be created, and there is an error during its creation.
    */
   protected FQLPreprocessor getFQLPreprocessor()
   throws PersistenceException
   {
      if (fqlPreprocessor == null)
      {
         // NOTE: dynamic filtering must be done before resolveArgs() !
         processDynamicFilters();
         
         Object[] resolvedArgs = resolveArgs();
         
         fqlPreprocessor = prepareFQLPreprocessor(resolvedArgs);
         
         if (resolvedArgs == null || resolvedArgs.length == 0)
         {
            nullArguments = null;
         }
         else
         {
            nullArguments = new boolean[resolvedArgs.length];
            for (int k = 0; k < resolvedArgs.length; k++)
            {
               Object param = resolvedArgs[k];
               if (param instanceof FieldReference)
               {
                  param = ((FieldReference) param).resolve();
               }
               
               nullArguments[k] = param instanceof BaseDataType && 
                                  ((BaseDataType) param).isUnknown();
            }
         }
      }
      
      return fqlPreprocessor;
   }
   
   /**
    * Create the <code>FQLPreprocessor</code> which backs this component. If this component
    * represents a nested FIRST or LAST iteration, the where clause portion associated with this
    * component will be embedded within a subselect. To avoid conflicts with the enclosing
    * where clause, this portion will be emitted without DMO alias qualifiers for its references
    * to the associated buffer.
    * 
    * @param   queryArgs
    *          Query substitution parameters used in the preprocessor's creation.
    * 
    * @return  FQL preprocessor.
    * 
    * @throws  PersistenceException
    *          if there is an error creating the preprocessor.
    */
   protected FQLPreprocessor prepareFQLPreprocessor(Object[] queryArgs)
   throws PersistenceException
   {
      boolean subselect = !top && (iteration == FIRST || iteration == LAST);
      RecordBuffer buffer = getBuffer();
      String dropAlias = subselect ? buffer.getDMOAlias() : null;
      String replacementAlias = dropAlias != null ? DBUtils.getSubselectAlias(dropAlias) : null;
      return FQLPreprocessor.get(dfWhere,
                                 buffer.getDatabase(),
                                 buffer.getDialect(),
                                 getReferencedBuffers(),
                                 dfArgs,
                                 referenceSubs,
                                 true,
                                 dropAlias,
                                 replacementAlias,
                                 false,
                                 null,
                                 true);
   }
   
   /**
    * Get an array of all record buffers referenced by this query, including those directly
    * involved in the query, as well as those (if any) indirectly referenced by inlined,
    * converted, CAN-FIND expressions, and those related via a server-side join (if any).
    * 
    * @return  All record buffers referenced by this query.
    */
   protected RecordBuffer[] getReferencedBuffers()
   {
      RecordBuffer[] qbufs = query.getReferencedBuffers();
      
      if (relatedBuffers == null || relatedBuffers.isEmpty())
      {
         return qbufs;
      }
      
      int len1 = qbufs.length;
      int len = len1 + relatedBuffers.size();
      RecordBuffer[] all = new RecordBuffer[len];
      
      int i = 0;
      while (i < len1)
      {
         all[i] = qbufs[i];
         i++;
      }
      for (RecordBuffer buf : relatedBuffers)
      {
         all[i] = buf;
         i++;
      }
      
      return all;
   }
   
   /**
    * Get substitution arguments for this component, resolving them first if necessary.
    *
    * @return  Substitution arguments.
    * 
    * @throws  PersistenceException
    *          if there is any error creating the FQL preprocessor, which
    *          is necessary to properly filter/order the arguments.
    */
   protected Object[] getArgs()
   throws PersistenceException
   {
      if (dfArgs != null && currentArgs == null)
      {
         currentArgs = filterArgs(getFQLPreprocessor());
      }
      
      return currentArgs;
   }
   
   /**
    * Reset current arguments, so that they will need to be resolved the next time they are
    * requested. The fqlPreprocessor is dropped only if it was inlined (and the inlined nodes
    * need to be re-evaluated with the new arguments) or if the argument list contains some
    * other configuration of null values (they were also replaced with "is [not] null" fql constructs).
    * 
    * @param   forceOrigArgs
    *          Flag indicating that the original arguments used at the construction of the query must be
    *          re-calculated.
    */
   protected void resetArgs(boolean forceOrigArgs)
   {
      if (forceOrigArgs && origArgs != null)
      {
         System.arraycopy(origArgs, 0, args, 0, origArgs.length);
         AbstractQuery.preprocessSubstitutionArguments(buffer.getBufferManager(),
                                                       query::isFindByRowid,
                                                       true,
                                                       args);
         dfArgs = args;
      }
      
      currentArgs = null;
      
      // Since the arguments were reset, the FQL preprocessor may need to be reset, too.
      if (fqlPreprocessor == null)
      {
         return; // nothing to do
      }
      
      // if some FQL nodes were inlined (?)
      if (fqlPreprocessor.wasInlined())
      {
         resetFQLPreprocessor();
         return;
      }
      
      // check the size of the parameter list (should always be the same, anyway)
      processDynamicFilters();
      Object[] params = resolveArgs();
      if ((params == null && nullArguments != null) || (params != null && nullArguments == null))
      {
         resetFQLPreprocessor();
         return;
      }
      
      if (params == null)
      {
         // nullArguments is null too, so we keep the fql
         return;
      }
      
      if (params.length != nullArguments.length)
      {
         resetFQLPreprocessor();
         return;
      }
      
      // if params and nullArguments have the same length check for null pattern
      for (int k = 0; k < params.length; k++)
      {
         Object param = params[k];
         if (!forceOrigArgs && param instanceof FieldReference)
         {
            param = ((FieldReference) param).resolve();
         }
         
         if (nullArguments[k] != (param instanceof BaseDataType &&
                                  ((BaseDataType) param).isUnknown()))
         {
            resetFQLPreprocessor();
            return;
         }
      }
   }
   
   /**
    * Reset the FQL preprocessor, so that it needs to be recreated the next time it is requested.
    */
   protected void resetFQLPreprocessor()
   {
      fqlPreprocessor = null;
      nullArguments = null; // keep in sync
   }
   
   /**
    * Get the (mostly) resolved query substitution parameters required by
    * this query component and return them in an array.  The array's
    * elements will match the positional placeholders in the query's where
    * clause <em>after</em> it has been preprocessed.  The preprocessing
    * step may have reordered or dropped certain parameters.  The only
    * parameters which will not have been fully resolved will be those
    * <code>FieldReference</code> parameters which are actually resolved by
    * the query itself, when the query's full FQL is being assembled and
    * when the final array of substitution parameters is prepared.
    * 
    * @param   preproc
    *          FQL preprocessor which maps parameter indices to the original parameters, in case any were
    *          changed during FQL where clause preprocessing.
    * 
    * @return  Array of filtered, resolved parameters.
    */
   protected Object[] filterArgs(FQLPreprocessor preproc)
   {
      // in this case the processDynamicFilters() is not required to be called before calling
      // resolveArgs() because it was called before constructing the preprocessor
      Object[] intermediate = resolveArgs();
      if (intermediate == null)
      {
         return null;
      }
      
      ParameterIndices pi = preproc.getParameterIndices();
      int len = pi.getCount();
      Object[] filteredArgs = new Object[len];
      for (int i = 0, count = 0; count < len; i++)
      {
         Integer nextIndex = pi.getIndex(i);
         if (nextIndex == null)
         {
            // original substitution parameter was inlined into FQL by preprocessor;  skip it
            continue;
         }
         
         // Substitution placeholders may have been reordered if this
         // statement was rewritten by the FQL preprocessor. Make sure
         // we access the correct substitution parameter.
         Object next = intermediate[nextIndex];
         
         filteredArgs[count++] = next;
      }
      
      return filteredArgs;
   }
   
   /**
    * Set the array of field reference names and nulls used when converting a nested loop where
    * clause to a server-join where clause.
    * 
    * @param   referenceSubs
    *          Array of field reference names and nulls.
    */
   protected void setReferenceSubs(String[] referenceSubs)
   {
      this.referenceSubs = referenceSubs;
   }
   
   /**
    * Store a set of outer component buffers to which this component is related through a server
    * side join.
    * 
    * @param   relatedBuffers
    *          Set of related buffers; may be {@code null}.
    */
   protected void setRelatedBuffers(Set<RecordBuffer> relatedBuffers)
   {
      this.relatedBuffers = relatedBuffers;
   }
   
   /**
    * Processes all dynamic filters altering the original {@code where} predicate and inserting
    * optional substitution arguments in the list of static ones.
    * <p>
    * This method must be always called before accessing the where predicate or substitutions
    * arguments, otherwise the dynamic filtering will not be correctly applied.
    */
   void processDynamicFilters()
   {
      if (dynamicFilters == null || dynamicFilters.isEmpty())
      {
         dfArgs = args;
         dfWhere = where;
         return;
      }
      
      StringBuilder sb = new StringBuilder();
      List<Object> dArgs = new ArrayList<>(dynamicFilters.size());
      Iterator<Map.Entry<FieldReference, BaseDataType>> dynamicFilters =
            this.dynamicFilters.entrySet().iterator();
      while (dynamicFilters.hasNext())
      {
         Map.Entry<FieldReference, BaseDataType> filter = dynamicFilters.next();
         BaseDataType bdtVal = filter.getValue();
         String fieldName = filter.getKey().toString();
         boolean isNull = (bdtVal == null) || bdtVal.isUnknown();
         boolean isCharType = bdtVal instanceof Text;
         sb.append("(");
         
         if (isNull)
         {
            sb.append(fieldName).append(" is null");
         }
         else
         {
            if (isCharType)
            {
               // the user will not know the case-sensitivity of the database fields, so it is
               // best to make the filter for text fields always be case-insensitive. This has the
               // greatest likelihood of using an index (most fields are case-insensitive), and it
               // is the most user-friendly.
               sb.append("upper(trim(").append(fieldName).append("))").append(" like ?");
               String pattern = TextOps.trim(TextOps.toUpperCase((Text) bdtVal)).getValue();
               bdtVal = new character("%" + pattern + "%");
            }
            else if (bdtVal instanceof decimal)
            {
               // because of rounding, the displayed value may not be equals to the one from
               // database. To make sure we filter the right rows, we will use the format matching
               // the one used by shown data
               String fmt = dynamicFormats.get(filter.getKey());
               if (fmt != null)
               {
                  sb.append("toString(").append(fieldName).append(",'").append(fmt).append("')= ?");
                  bdtVal = new character(bdtVal.toString(fmt));
               }
               else
               {
                  // format not provided, we cannot use it, compare directly instead
                  sb.append(fieldName).append(" = ?");
               }
            }
            else
            {
               sb.append(fieldName).append(" = ?");
            }
            dArgs.add(bdtVal);
         }
         
         if (dynamicFilters.hasNext() || where != null)
         {
            sb.append(") and ");
         }
         else
         {
            sb.append(")");
         }
      }
      
      if (where != null)
      {
         sb.append("(").append(where).append(")");
      }
      dfWhere = sb.toString();
      
      int dArgCount = dArgs.size();
      if (args != null && dArgCount != 0)
      {
         dfArgs = new Object[args.length + dArgCount];
         System.arraycopy(dArgs.toArray(), 0, dfArgs, 0, dArgCount);
         System.arraycopy(args, 0, dfArgs, dArgCount, args.length);
      }
      else if (dArgCount != 0)
      {
         dfArgs = new Object[dArgCount];
         dArgs.toArray(dfArgs);
      }
      else 
      {
         dfArgs = null;
      }
   }
   
   /**
    * Resolve all resolvable query substitution parameters, with the exception of
    * {@code FieldReference}s.  The latter are only query substitution parameters when it
    * is necessary to join the enclosing query's results with those of a related query, as
    * would be the case when each query is a component of a {@link CompoundQuery}.
    * 
    * @return  Array of query substitution parameters after all non-{@code FieldReference}
    *          resolvable parameters have been resolved.
    */
   private Object[] resolveArgs()
   {
      if (dfArgs == null)
      {
         return null;
      }
      
      int len = dfArgs.length;
      Object[] resolvedArgs = new Object[len];
      
      for (int i = 0; i < len; i++)
      {
         resolvedArgs[i] = resolveArg(dfArgs[i]);
      }
      
      return resolvedArgs;
   }
   
   /**
    * Resolve a single resolvable query substitution parameter, with the exception of
    * {@code FieldReference}s.
    *
    * @return  The resolved query substitution parameter or {@code arg} itself if it is an
    *          instance of {@code FieldReference}.
    */
   private Object resolveArg(Object arg)
   {
      if (arg instanceof FieldReference)
      {
         return arg;
      }
      else if (arg instanceof Resolvable)
      {
         return ((Resolvable) arg).resolve();
      }
      else if (arg instanceof BaseDataType)
      {
         return ((BaseDataType) arg).duplicate();
      }
      else if (arg instanceof Integer)
      {
         return integer.of((Integer) arg);
      }
      else if (arg instanceof Long)
      {
         return int64.of((Long) arg);
      }
      else if (arg instanceof Double)
      {
         return new decimal((Double) arg);
      }
      else if (arg instanceof Float)
      {
         return new decimal((Float) arg);
      }
      else if (arg instanceof String)
      {
         return new character((String) arg);
      }
      else if (arg instanceof P2JQuery.ParamResolver)
      {
         return arg;
      }
      else
      {
         throw new IllegalArgumentException("Argument type " + arg.getClass() + " is not known.");
      }
   }
   
   /**
    * Indicate whether the query component should return a primary key ID
    * only, or the entire DMO instance.
    *
    * @return  <code>true</code> for a projection query which returns only
    *          the ID;  <code>false</code> to fetch the entire DMO.
    */
   protected boolean isIdOnly()
   {
      return idOnly;
   }
   
   /**
    * Get the query which encloses this component.
    * 
    * @return  Enclosing query.
    */
   protected Joinable getEnclosingQuery()
   {
      return query;
   }

   /**
    * Enable the {@link #translateWhere} for this component (if not already enabled).
    * 
    * @param    translate
    *           The translation function, which will receive as argument the {@link #where} predicate.
    */
   void enableTranslateWhere(Function<String, String> translate)
   {
      if (translateWhere != null)
      {
         return;
      }
      
      translateWhere = () -> translate.apply(where);
   }

   /**
    * Get record buffer for this component of the query.
    *
    * @return  Record buffer.
    */
   RecordBuffer getBuffer()
   {
      return buffer;
   }
   
   /**
    * Set the record buffer for this component's query.
    * 
    * @param    buffer
    *           Record buffer.
    */
   void setBuffer(RecordBuffer buffer)
   {
      this.buffer = buffer;
   }
   
   /**
    * Get definition record buffer for this component of the query.
    *
    * @return  Record buffer.
    */
   RecordBuffer getDefinitionBuffer()
   {
      return definition;
   }
   
   /**
    * Get restriction criteria in FQL where clause format for this component (before the FQL
    * preprocessor has preprocessed it).  If some dynamic filters were added, the returned string
    * contains them, too.
    *
    * @return  Un-preprocessed where clause.
    */
   String getOriginalWhere()
   {
      return dfWhere;
   }
   
   /**
    * Get helper object for a join via a foreign key relation, if any.
    *
    * @return  Join helper, or <code>null</code> if there is none.
    */
   AbstractJoin getJoin()
   {
      return join;
   }

   /**
    * Report if component forms a sub select inside a join.
    *
    * @return  <code>true</code> if there is a sub select in the join; else <code>false</code>.
    */
   public boolean isJoinWithSubselect()
   throws PersistenceException
   {
      return !top && (iteration == FIRST || iteration == LAST) && getFQLPreprocessor().isSubselectNeeded();
   }
   
   /**
    * Get restriction criteria in FQL where clause format for this component (pre-processed).
    *
    * @return  Where clause.
    * 
    * @throws  PersistenceException
    *          if there is an error creating the {@code FQLPreprocessor} from which the
    *          preprocessed where clause is retrieved.
    */
   FQLExpression getWhere()
   throws PersistenceException
   {
      FQLExpression baseWhere = getFQLPreprocessor().getFQL();

      if (!isJoinWithSubselect())
      {
         return baseWhere;
      }
      
      // generate a where expression which tests for equality between the DMO's primary key and
      // the primary key of the record found using the core where clause in a sub-select which
      // finds a single, FIRST or LAST record, as determined by the sort clause of this component:
      //    dmo.id = (select from DMO where <baseWhere> order by <sort> single)
      // 'single' is a custom FQL extension which instructs Hibernate to emit dialect-specific
      // SQL to limit the result to 1 row
      String dmoAlias = buffer.getDMOAlias();
      String uniqueAlias = DBUtils.getSubselectAlias(dmoAlias);
      FQLExpression exp = new FQLExpression();
      boolean multiplex = buffer.isMultiplexed();
      exp.append(dmoAlias).append(".").append(DatabaseManager.PRIMARY_KEY)
         .append(" = (select ").append(uniqueAlias).append(".").append(DatabaseManager.PRIMARY_KEY)
         .append(" from ").append(buffer.getDMOImplementationName()).append(" as ").append(uniqueAlias);
      if (baseWhere != null || join != null || multiplex)
      {
         exp.append(" where ");
         
         if (multiplex)
         {
            boolean joinOrWhere = (join != null || baseWhere != null);
            if (joinOrWhere)
            {
               exp.append("(");
            }
            exp.append(uniqueAlias);
            exp.append(".");
            exp.append(TemporaryBuffer.MULTIPLEX_FIELD_NAME);
            exp.append(" = ", true);
            if (joinOrWhere)
            {
               exp.append(") and ");
            }
            exp.setMultiplexed(true); // mark the exp this component as already multiplexed
         }
         
         if (baseWhere != null)
         {
            exp.append("(").append(baseWhere).append(")");
         }
         
         if (join != null)
         {
            if (baseWhere != null)
            {
               exp.append(" and ");
            }
            
            // need to strip out DMO aliases
            Object[] joinArgs = join.getUnresolvedParameters().toArray();
            FQLExpression[][] fqls = join.getFQL();
            
            FQLExpression joinWhere = new FQLExpression();
            for (int i = 0; i < fqls.length; i++)
            {
               boolean nullArg = (joinArgs.length != 0) && 
                                 ((joinArgs[i] == null) || 
                                  (joinArgs[i] instanceof BaseDataType && 
                                   ((BaseDataType) joinArgs[i]).isUnknown()));
               joinWhere.append(fqls[i][nullArg ? AbstractJoin.NULL_FIELD : AbstractJoin.NOT_NULL_FIELD]);
            }
            
            FQLPreprocessor preproc = FQLPreprocessor.get(joinWhere.toFinalExpression(),
                                                          buffer.getDatabase(),
                                                          buffer.getDialect(),
                                                          query.getReferencedBuffers(),
                                                          joinArgs,
                                                          null,
                                                          false,
                                                          dmoAlias,
                                                          uniqueAlias,
                                                          false,
                                                          null);
            FQLExpression fql = preproc.getFQL();
            if (fql != null)
            {
               exp.append("(").append(fql).append(")");
            }
         }
      }
      exp.append(" order by ").append(sort).append(" single)");
      
      return exp;
   }
   
   /**
    * Adds a new constraint term to the where predicate of this query, effectively filtering out
    * any rows whose referenced field does not match the requested value. Semantically, the query
    * will select rows that match
    * <p>
    * {@code new-where := (fr = val) AND (old-query)}.
    * <p>
    * Note: The constraints are exclusive meaning that if constraints will be added for same
    * field of same buffer, the query will return an empty result because the field cannot be at
    * the same time equals to two different values.
    * <p>
    * Use {@link #clearDynamicFilters} to clean all dynamically added criteria and restore the
    * sorting order of the query to its original form.
    *
    * @param   fr
    *          A reference to field used as filtering criterion. Only the rows that matches the
    *          specified value for this field will be selected. Must not be {@code null}. 
    * @param   val
    *          The filtering value for this constraint. Can be {@code null} or {@code unknown}, in
    *          which case the SQL {@code null} value will be assumed. In this case the predicate
    *          will look like:
    *           <p>
    *          {@code new-where := (fr IS  NULL) AND (old-query)}
    * @param   format
    *          The format to be used when comparing values. The reason for this parameter is that
    *          for some datatype (ex: {@code decimal}) the on-screen value can be different from
    *          the database (because of rounding).
    *
    * @return  {@code true} if the filtering constraint was successfully added. If the field
    *          reference is not related to the buffer(s) of this query, {@code false} is returned. 
    */
   boolean addDynamicFilter(FieldReference fr, BaseDataType val, String format)
   {
      if (dynamicFilters == null)
      {
         dynamicFilters = new LinkedHashMap<>();
         dynamicFormats = new HashMap<>();
      }
      dynamicFilters.remove(fr); // the list is reordered, not reassigned to same position
      dynamicFilters.put(fr, val);
      dynamicFormats.put(fr, format);
      
      currentArgs = null;
      resetFQLPreprocessor(); // force re-parsing the enriched FQL
      return true;
   }
   
   /**
    * Clears any dynamically filters added at runtime. The query predicate will be restored to its
    * form from generated at conversion time.
    */
   void clearDynamicFilters()
   {
      currentArgs = null;
      dynamicFilters = null;
      dynamicFormats = null;
      resetFQLPreprocessor(); // force re-parsing the enriched FQL
   }
   
   /**
    * Get lock type for records fetched into this component's buffer.
    *
    * @return  Lock type.
    */
   LockType getLockType()
   {
      return lockType;
   }
   
   /**
    * Get unresolved substitution arguments for this component, if any.
    *
    * @return  Substitution arguments, or <code>null</code> if none.
    */
   Object[] getRawArgs()
   {
      return dfArgs;
   }
   
   /**
    * Get iteration/retrieval mode (first, last, next, etc.).
    *
    * @return  Retrieval mode from {@link QueryConstants}.
    */
   int getIteration()
   {
      return iteration;
   }
   
   /**
    * Set iteration/retrieval mode (first, last, next, etc.).
    *
    * @param   iteration
    *          Retrieval mode from {@link QueryConstants}.
    * @param   sort
    *          Order by clause which should be associated with this query component. Must be
    *          non-<code>null</code> for iteration types <code>FIRST</code> and
    *          <code>LAST</code>.
    * 
    * @throws  IllegalArgumentException
    *          if <code>iteration</code> type is <code>FIRST</code> or <code>LAST</code>, but
    *          <code>sort</code> is <code>null</code>.
    * @throws  PersistenceException
    *          if <code>sort</code> clause is invalid.
    */
   void setIteration(int iteration, String sort)
   throws PersistenceException
   {
      if (iteration == FIRST || iteration == LAST)
      {
         if (sort == null)
         {
            throw new IllegalArgumentException(
               "Sort cannot be null with FIRST/LAST iteration type");
         }
         
         // generate an order by clause using no DMO aliases, suitable for a sub-select
         List<SortCriterion> criteria = SortCriterion.parse(sort, buffer);
         StringBuilder buf = new StringBuilder();
         for (SortCriterion sc : criteria)
         {
            if (buf.length() > 0)
            {
               buf.append(", ");
            }
            
            buf.append(sc.toSortExpression(iteration == LAST, false, !top, true));
         }
         
         this.sort = buf.toString();
      }
      else
      {
         this.sort = null;
      }
      
      this.iteration = iteration;
   }
   
   /**
    * Mark this query component as being nested within an outer component (i.e., not the top
    * component in a nested looping query).
    */
   void setNotTop()
   {
      this.top = false;
   }
   
   /**
    * Indicate whether this component is joined with the previous component
    * via a left outer join.
    *
    * @return  <code>true</code> if an outer join, <code>false</code> if
    *          an inner join or no join (i.e., no previous component).
    */
   boolean isOuter()
   {
      return outer;
   }
   
   /**
    * Get an iterator on the ANSI-style join subexpressions which must be
    * merged by the enclosing query into the overall FQL query statement
    * in order to properly join associated composite element lists (if
    * any).
    *
    * @return  An iterator on join subexpressions.  The iterator may be
    *          empty, but will not be <code>null</code>.
    * 
    * @throws  PersistenceException
    *          if there is an error creating the
    *          <code>FQLPreprocessor</code> from which the composite join
    *          subexpressions are gathered.
    */
   Iterator<String> compositeJoins()
   throws PersistenceException
   {
      return getFQLPreprocessor().ansiJoins();
   }
   
   /**
    * Get an unmodifiable map of DMO entity names to sets of the names of
    * the DMO properties used as restriction criteria in the preprocessed
    * where clause.
    *
    * @return  Map of DMO entity names to sets of property names, or
    *          <code>null</code> if the where clause for this component
    *          contained no restriction properties.
    * 
    * @throws  PersistenceException
    *          if there is an error creating the <code>FQLPreprocessor</code> from which the restriction
    *          properties are gathered.
    */
   Map<String, Set<String>> getRestrictionProperties()
   throws PersistenceException
   {
      return getFQLPreprocessor().getRestrictionProperties();
   }
   
   /**
    * Get sort criteria in FQL order by clause format for this component.
    *
    * @return  Sort index associated with this component.
    */
   SortIndex getSortIndex()
   {
      return sortIndex;
   }
   
   /**
    * Set sort criteria in FQL order by clause format for this component.
    * 
    * @param   sortIndex
    *          Sort index.
    */
   void setSortIndex(SortIndex sortIndex)
   {
      this.sortIndex = sortIndex;
   }
   
   /**
    * Get the index information string as it is returned by INDEX-INFORMATION.
    * 
    * @return  Index information string.
    */
   String getIndexInfo()
   {
      return indexInfo;
   }
   
   /**
    * Information required to create a server-side join between two tables.
    */
   static class ServerJoinData
   {
      /** Helper object which generates a where clause snippet to join two tables */
      final AbstractJoin join;
      
      /** Query substitution arguments used in the where clause snippet, which survive the join */
      final Object[] args;
      
      /** Set of related buffers referenced by the join */
      final Set<RecordBuffer> relatedBuffers;
      
      /** Array of field reference names and nulls */
      final String[] referenceSubs;
      
      /**
       * Constructor.
       * 
       * @param   join
       *          Helper object which generates a where clause snippet to join two tables.
       * @param   args
       *          Query substitution arguments used in the where clause snippet, which survive
       *          the join.
       * @param   relatedBuffers
       *          Set of related buffers referenced by the join.
       * @param   referenceSubs
       *          Array of field reference names and nulls, where the names are to be substituted
       *          for the positionally equivalent substitution parameters when converting a
       *          where clause used by a nested loop to a where clause using a server-side join.
       */
      ServerJoinData(AbstractJoin join,
                     Object[] args,
                     Set<RecordBuffer> relatedBuffers,
                     String[] referenceSubs)
      {
         this.join = join;
         this.args = args;
         this.relatedBuffers = relatedBuffers;
         this.referenceSubs = referenceSubs;
      }
   }
}