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