# IndexSelectionWorker.java

    /*
    ** Module   : IndexSelectionWorker.java
    ** Abstract : provides access to schema index data and helpers to calc the
    **            actual implicit index in use
    **
    ** Copyright (c) 2005-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 GES 20051013   @23061 Provides access to schema index data and
    **                           helpers to calc the actual implicit index in
    **                           use.
    ** 002 GES 20051020   @23078 Major rewrite to support more complex rules
    **                           and more complex tie breaks.
    ** 003 GES 20051020   @23084 Added sort and index overlap calculation.
    ** 004 GES 20051020   @23086 Added simple order by generation.
    ** 005 GES 20051026   @23100 Added import for persist. Updated order-by
    **                           generation to use fully qualified field
    **                           names.
    ** 006 ECF 20060202   @24257 Added support for temp/work-table schema
    **                           data lookups. Implemented visitAst method to
    **                           load current, temporary schema data into
    **                           P2OLookup's state each time a new source file
    **                           is visited during conversion.
    ** 007 ECF 20060406   @25392 Fixed order by clauses for buffers. The
    **                           backing table's name was always being used as
    **                           the field qualifier, even in cases where an
    **                           explicitly named buffer was in use.
    ** 008 ECF 20060509   @26041 Added findBeginsIndexMatch() method. Testing
    **                           indicates that Progress actually gives begins
    **                           matches precedence over range matches.
    **                           Previously, these were given equal weight.
    ** 009 ECF 20060824   @28871 Added support for by clauses which use extent
    **                           field elements. This changes the content of
    **                           the resulting, HQL order by clauses.
    ** 010 ECF 20080721   @39140 Minor optimization. Use StringBuilder instead
    **                           of StringBuffer.
    ** 011 ECF 20090306   @41433 Fixed getIndex(). Use case-insensitive test
    **                           when finding a P2JIndex for a given name,
    **                           since that name may have been provided by the
    **                           original code via USE-INDEX, and its case may
    **                           not match the name stored in a P2JIndex.
    ** 012 SVL 20090505   @42094 mergeOrderByCriteria() added. Sort components
    **                           can be matched in the reverse way.
    ** 013 GES 20090518   @42380 Import change.
    ** 014 SVL 20091003   @44104 Added getIndexNameByPrefix() function.
    ** 015 SVL 20110813          Fixed leadingSortComponentsMatch: reverse match
    **                           only of the first component should trigger an
    **                           attempt to check the possibility of reverse
    **                           match of the whole sort string.
    ** 016 SVL 20140604          Added support for WHOLE-INDEX flag produced by INDEX-INFORMATION.
    ** 017 ECF 20150923          Enabled nested record phrases to support index selection of embedded
    **                           CAN-FINDs.
    ** 018 ECF 20160215          Added support for denormalized extent fields.
    ** 019 CA  20180528          Changed findSortIndexMatch to use first found index if nothing else
    **                           is involved (word index, equality, etc).
    ** 020 OM  20190312          Fixed conversion of sorting by extent fields with non-constant index.
    ** 021 CA  20190508          Buffers must be looked in the class hierarchy, not only in the
    **                           current class (thus we need to recursively load .p2o files).
    ** 022 CA  20200412          Added incremental conversion support.
    ** 023 OM  20200924          P2JIndexComponent carries multiple information to avoid map lookups for them.
    ** 024 IAS 20210209          Do not use word index field for sorting
    **     CA  20211214          The AST manager plugin must be context-local, for runtime conversion, as the
    **                           InMemoryRegistryPlugin is not thread-safe.
    **                           All worker's state must be context-local, for runtime conversion to work in
    **                           multi-context mode, as the pattern workers are singletons.
    **     SVL 20230110          P2JIndex.components() provides direct access to the components array in order to
    **                           improve performance.
    ** 025 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 026 RAA 20240910          Made calcOrderByIndex and mergeOrderByCriteria return the legacy name criteria.
    ** 027 ES  20241030          Select the fully matching indices in case of a tie in findLeadingIndex
    *                            Added Flag indicating whether record phrase is in a find statement.
    *                            Added isSimpleWhereClause flag.
    ** 028 AP  20240807          Improved index selection for the case when wholeIndex flag is true, but
    **                           further selection can be performed in order to avoid returning WHOLE-INDEX.
    ** 029 CA  20250319          This worker can be used before the 'qualified_oo_name' is set in annotation
    **                           (like in schema conversion phase), so ensure we can load the ClassDefinition.
    */

    /*
    ** 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.convert;

    import java.util.*;
    import java.util.logging.*;

    import com.goldencode.ast.*;
    import com.goldencode.p2j.schema.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.uast.*;
    import com.goldencode.p2j.pattern.*;

    /**
     * A pattern worker whose purpose is to provide helpers for the calculation
     * of all implicit index selections (based on record phrase and sorting
     * criteria analysis).  This worker also provides rule-set access to the
     * schema index data.
     * <p>
     * The rules by which this index selection is handled are very complex.
     * Please see the <a href="../schema/package.html">Schema Package Summary</a>
     * for complete details.
     */
    public class IndexSelectionWorker
    extends AbstractPatternWorker
    implements ProgressParserTokenTypes
    {
       /** Logger */
       private static final ConversionStatus LOG = ConversionStatus.get(IndexSelectionWorker.class);

       /** Context local work area. */
       private static final ContextLocal<WorkArea> context = new ContextLocal<WorkArea>()
       {
          protected WorkArea initialValue()
          {
             return new WorkArea();
          }
       };

       /**
        * Default constructor which initializes libraries.
        */
       public IndexSelectionWorker()
       {
          super();
          setLibrary(new IndexHelper());
       }

       /**
        * This method is called each time a new AST is visited, just before the
        * pattern engine processes that AST.  We exploit this hook by loading the
        * current temp/work-table schema (P2O) AST into the <code>P2OLookup</code>
        * class' state.  This enables us to resolve references to temp and work
        * tables, which are local to the currently loaded source file AST.
        *
        * @param   ast
        *          The root node of the source AST about to be processed by the
        *          pattern engine.
        */
       public void visitAst(Aast ast)
       {
          ClassDefinition clsDef = SymbolResolver.resolveClassDefinition(ast);
          String sourceFile = ast.getFilename();
          P2OLookup.loadTemporarySchema(sourceFile, clsDef);
       }

       /**
        * Obtains an iterator to the current record's list of indexes.
        *
        * @param    schemaname
        *           The fully qualified schema name of the record being processed.
        *
        * @return   The list of indexes.
        */
       private Iterator<P2JIndex> indexes(String schemaname)
       {
          Iterator<P2JIndex> iter = null;

          try
          {
             iter = P2OLookup.indexes(schemaname);
          }

          catch (SchemaException se)
          {
             LOG.log(Level.SEVERE, "", se);
          }

          return iter;
       }

       /**
        * Parse and return the element index within square brackets at the end of a field name.
        * The input is expected already to reference a zero-based index, so no adjustment is
        * performed.
        *
        * @param   name
        *          Field name.
        * @param   lpos
        *          Zero-based index of left bracket.
        *
        * @return  Parsed element index, if a valid number, else <code>null</code>.
        */
       private Long parseExtentIndex(String name, int lpos)
       {
          if (lpos < 0)
          {
             return null;
          }

          int rpos = name.lastIndexOf(']');
          String s = name.substring(lpos + 1, rpos);
          try
          {
             return Long.parseLong(s);
          }
          catch (NumberFormatException exc)
          {
             return null;
          }
       }

       /**
        * Provides the core user-functions exported to pattern engine rules for
        * Progress index selection calculation and schema index access.
        */
       public class IndexHelper
       {
          /**
           * Initialize data structures related to processing the specified record name.
           * This should be called upon encountering (walking) a new record phrase.
           *
           * @param    schemaname
           *           The fully qualified schema name of the record being processed.
           * @param    isFind
           *           Flag indicating whether record phrase is in a find statement.
           */
          public void pushRecordPhrase(String schemaname, boolean isFind)
          {
             WorkArea wa = context.get();

             // push existing record phrase data onto stack
             wa.stack.push(wa.rpData);

             // create a new record phrase data to work with
             wa.rpData = new RecordPhraseData(schemaname, isFind);
          }

          /**
           * Pop the previous record phrase data object from the stack and make it the current
           * record phrase data. This should be called when ascending back up to a record phrase
           * node.
           */
          public void popRecordPhrase()
          {
             WorkArea wa = context.get();

             // pop the previous record phrase data from stack
             wa.rpData = wa.stack.pop();
          }

          /**
           * Use the recorded index component matches to determine the implicitly
           * selected index for this record phrase.  This should be called
           * upon exiting (ascending back to) a record phrase node.
           * <p>
           * The following is the precedence order:
           * <ol>
           *    <li> A unique index with all components in equality matches.
           *    <li> A word index matched via contains.
           *    <li> The index with the most sequential leading equality matches.
           *    <li> The index with the most sequential leading range matches.
           *    <li> The index with the most sequential leading sort matches.
           *    <li> The primary index.
           * </ol>
           * <p>
           * See <a href="../schema/package.html">Schema Package Summary</a>
           * for complete details.
           *
           * @return   The name of the index selected or <code>""</code> (the
           *           empty string) in the case where there are no indexes
           *           defined for this table and thus there is no primary
           *           index to use as a default.
           */
          public String calcIndexSelection()
          {
             WorkArea wa = context.get();

             IndexInfo selectable = null;
             IndexInfo result     = null;

             wa.rpData.rangeBypass = false;

             do
             {
                // unique index match
                result = findUniqueIndexMatch();
                if (result != null) break;

                // word index match
                result = findWordIndexMatch();
                if (result != null) break;

                // restart with all possible indexes
                selectable = new IndexInfo(getFullIndexList(), true);

                // filter down by most sequential leading equality matches
                selectable = findLeadingIndexMatch(selectable);

                if (selectable.indexes.size() > 0)
                {
                   // exit if we found a single match
                   result = readSingleIndex(selectable);
                   if (result != null) break;

                   // have we already processed ranges sufficiently?
                   if (!wa.rpData.rangeBypass)
                   {
                      // filter down by first index component "begins" matches
                      selectable = findBeginsIndexMatch(selectable);

                      // exit if we found a single match
                      result = readSingleIndex(selectable);
                      if (result != null) break;

                      // filter down by first index component range matches
                      selectable = findRangeIndexMatch(selectable);

                      // exit if we found a single match
                      result = readSingleIndex(selectable);
                      if (result != null) break;
                   }

                   // most sequential leading sort matches
                   result = findSortIndexMatch(selectable);
                   if (result != null) break;
                }

                // primary index
                P2JIndex primary = getPrimaryIndex();
                if (primary != null)
                {
                   // default choice is the explicitly defined primary index
                   result = new IndexInfo(getIndex(primary.getName()), true);
                }
                else
                {
                   // the implicitly defined primary index (just recid)
                   result = new IndexInfo();
                }
             }
             while (false);

             return result.toString();
          }

          /**
           * Compares the given selected index with the stored sort criteria to
           * determine if the the complete sort criteria is an exact match or a
           * compatible subset match to the selected index.  In this case, this
           * method will return <code>true</code>.  Any incompatibility between
           * the selected index and the sort criteria will generate a return of
           * <code>false</code>.
           * <p>
           * All sort criteria (the list of index components that are sort
           * matches) must match in name, specificity (order of occurrance) and
           * sort direction.  If any of the sort matches does not exist in the
           * selected index, exists in a different sequential position or differs
           * in direction (ascending/descending), then this is not compatible.
           * <p>
           * If the selected index has more index components than the sort
           * criteria AND all the sort criteria are a leading sequential subset,
           * this is still an overlap.  At this time, this routine will consider
           * any gaps as incompatibilities (it is unknown if this is different
           * from the Progress compiler's approach).
           *
           * @param    selected
           *           The name of the index to compare against.
           *
           * @return   A value greater than <code>0</code> if the previously
           *           recorded sort criteria are compatible with all or a leading
           *           subset of the selected index in the direct order. A value
           *           less than <code>0</code> if the previously recorded sort
           *           criteria are compatible with all or a leading subset of the
           *           selected index, but in the reverse order. <code>0</code> in
           *           any other condition, including the case where there is a
           *           <code>null</code>, empty string or invalid index name
           *           provided as the selected index.
           */
          public int calcSortOverlap(String selected)
          {
             P2JIndex idx = getIndex(selected);

             if (idx == null)
             {
                return 0;
             }

             WorkArea wa = context.get();
             int num = leadingSortComponentsMatch(idx);

             if (num != 0 && num == wa.rpData.sortMatches.size())
             {
                return 1;
             }
             else if (num != 0 && -num == wa.rpData.sortMatches.size())
             {
                return -1;
             }
             else
             {
                return 0;
             }
          }

          /**
           * Converts the given index into an equivalent order by statement. If
           * no index is given, the empty string will be returned.
           *
           * @param    selected
           *           The name of the index from which to generate the order by
           *           clause.
           * @param    bufName
           *           Name of the Java variable which represents the buffer.
           * @param    reverse
           *           If <code>true</code> then the index should be taken in the
           *           reverse order.
           *
           * @return   The generated order by string or the empty string if
           *           the empty string or an invalid index name is provided as
           *           the selected index, and a set with the legacy name criteria.
           */
          public List<Object> calcOrderByIndex(String selected, String bufName, boolean reverse)
          throws SchemaException
          {
             StringBuilder result = new StringBuilder();
             String        field  = null;

             P2JIndex idx = getIndex(selected);
             Set<String> legacyNames = new HashSet<>();

             if (idx != null)
             {
                ArrayList<P2JIndexComponent> comps = idx.components();
                for (int i = 0; i < comps.size(); i++)
                {
                   P2JIndexComponent nextComp = comps.get(i);
                   if (result.length() > 0)
                   {
                      result.append(", ");
                   }

                   String sname = nextComp.getNormalizedName();
                   legacyNames.add(sname);
                   field = P2OLookup.javaPropertyName(sname, false);

                   result.append(bufName);
                   result.append(".");
                   result.append(field);
                   result.append(" ");
                   result.append((reverse != nextComp.isDescending()) ? "desc" : "asc");
                }
             }

             return Arrays.asList(result.toString(), legacyNames);
          }

          /**
           * Converts the sort criteria into an equivalent order by statement. If no sort matches
           * exist, the empty string will be returned.
           * <p>
           * The string keys of both the {@code criteria} map (or the internal map of sort matches,
           * if {@code criteria} is {@code null}) and the {@code bufNames} map will end with an array
           * dereferencing trailer (e.g., "[0]"), IF the associated sort component refers to a
           * specific element within an extent field.  This trailer already will be adjusted to assume
           * a 0-based array.
           *
           * @param    criteria
           *           An ordered list of the sort criteria with the key as the fully qualified schema
           *           name of the field and the value as a {@code Boolean} that specifies that the
           *           sort direction is descending if {@code true}, otherwise that field will have an
           *           ascending sort direction.  The list MUST be ordered in the same order (from
           *           least granular to most granular) of the original BY clauses. If {@code null},
           *           the previously recorded sort match criteria will be used.
           * @param    bufNames
           *           Map of fully qualified schema names of fields to the Java names of the buffers
           *           which they reference.
           *
           * @return   The generated order by string or the empty string if no sort criteria exist.
           */
          public String calcOrderByCriteria(LinkedHashMap<String, Boolean> criteria,
                                            Map<String, String> bufNames)
          throws SchemaException
          {
             StringBuilder result = new StringBuilder();
             String        field  = null;

             WorkArea wa = context.get();
             Map<String, Boolean> matches = (criteria == null ? wa.rpData.sortMatches : criteria);

             for (String critName : matches.keySet())
             {
                if (result.length() > 0)
                {
                   result.append(", ");
                }

                if (critName.isEmpty())
                {
                   // the criterion are [null] when they were 'moved' to client-side because they are
                   // an extent with non-literal (constant) index expression. Instead of adding it as
                   // a SQL sort, a addSortCriterion() will be generated in the source code to support
                   // this kind of sorting (eventually using a [PresortQuery] instead of a
                   // [PreselectQuery])
                   continue;
                }
                int pos = critName.lastIndexOf('[');
                String sname = (pos < 0 ? critName : critName.substring(0, pos));
                boolean useSubscript = (pos > 0 && !P2OLookup.isDenormalizedProperty(sname));

                result.append(bufNames.get(critName));
                result.append(".");
                Long elemIdx = useSubscript ? null : parseExtentIndex(critName, pos);
                field = P2OLookup.javaPropertyName(sname, false, elemIdx);
                result.append(field);
                if (useSubscript)
                {
                   result.append(critName.substring(pos));
                }
                Boolean descend = matches.get(critName);
                result.append(descend ? " desc" : " asc");
             }

             return result.toString();
          }

          /**
           * Merges the sort criteria and the selected index into an equivalent
           * order by statement. If no sort matches exist and the index is
           * <code>null</code>, the empty string will be returned.
           * <p>
           * Merging is performed in the following way: first, sort criteria are
           * emitted; then the sort criteria specified by index which are not
           * already present in the emitted criteria (into direct or reverse way)
           * are appended to the output.
           * <p>
           * The string keys of both the <code>criteria</code> map (or the
           * internal map of sort matches, if <code>criteria</code> is
           * <code>null</code>) and the <code>bufNames</code> map will end with
           * an array dereferencing trailer (e.g., "[0]"), IF the associated sort
           * component refers to a specific element within an extent field.  This
           * trailer already will be adjusted to assume a 0-based array.
           *
           * @param    criteria
           *           An ordered list of the sort criteria with the key as the
           *           fully qualified schema name of the field and the value
           *           as a <code>Boolean</code> that specifies that the sort
           *           direction is descending if <code>true</code>, otherwise
           *           that field will have an ascending sort direction.  The
           *           list MUST be ordered in the same order (from least
           *           granular to most granular) of the original BY clauses.
           *           If <code>null</code>, the previously recorded sort
           *           match criteria will be used.
           * @param    bufNames
           *           Map of fully qualified schema names of fields to the Java
           *           names of the buffers which they reference.
           * @param    selected
           *           The name of the index which is merged with the order by
           *           clause. Can be <code>null</code>.
           * @param    indexBufName
           *           Name of the Java variable which represents the buffer for
           *           the specified index.
           *
           * @return   The generated order by string or the empty string if
           *           no sort criteria exist and the index is <code>null</code>,
           *           and a set with the legacy name criteria.
           */
          public List<Object> mergeOrderByCriteria(LinkedHashMap<String, Boolean> criteria,
                                                   Map<String, String> bufNames,
                                                   String selected,
                                                   String indexBufName)
          throws SchemaException
          {
             WorkArea wa = context.get();
             LinkedHashMap<String, Boolean> byElements = (criteria == null) ? wa.rpData.sortMatches : criteria;
             Set<String> legacyNames = new HashSet<>();

             String byPart = calcOrderByCriteria(byElements, bufNames);
             StringBuilder result = new StringBuilder(byPart);

             P2JIndex idx = getIndex(selected);

             if (idx != null)
             {
                ArrayList<P2JIndexComponent> comps = idx.components();
                for (int i = 0; i < comps.size(); i++)
                {
                   P2JIndexComponent nextComp = comps.get(i);
                   String sname = nextComp.getNormalizedName();

                   if (byElements.get(sname) != null)
                   {
                      continue;
                   }

                   legacyNames.add(sname);
                   String field = P2OLookup.javaPropertyName(sname, false);

                   if (result.length() > 0)
                   {
                      result.append(", ");
                   }

                   result.append(indexBufName);
                   result.append(".");
                   result.append(field);
                   result.append(" ");
                   result.append(nextComp.isDescending() ? "desc" : "asc");
                }
             }

             return Arrays.asList(result.toString(), legacyNames);
          }

          /**
           * Compares the order of tables (only important in a multi-table query)
           * with the order of the sort criteria.  A jumble sort occurs (is
           * <code>true</code>) if the fields from two or more tables are
           * intermixed (fields from one table are processed both before AND
           * after a field or fields of another table) AND/OR are out of order
           * (the fields of a later occurring table are less granular sort
           * criteria than fields of an earlier occurring table).  If there is
           * only 1 table or if there are no sort criteria, jumble sorting will
           * be <code>false</code>.
           *
           * @param    buffers
           *           A list of the tables by node ID, in the order of
           *           occurrence.
           * @param    sortCriteria
           *           A list of the tables referenced by each sort criteria,
           *           in the order of occurrence.
           *
           * @return   <code>true</code> if jumble sorting is detected.
           */
          public boolean detectJumbleSort(LinkedList<Long> buffers,
                                          LinkedList<Long> sortCriteria)
          {
             boolean result = false;

             // quick out if this is not a multi-table query OR if there are no BY clauses
             if (buffers.size() > 1 && sortCriteria.size() > 0)
             {
                Set<Long>      processed = new HashSet<>();
                Iterator<Long> bufs      = buffers.iterator();
                Iterator<Long> bys       = sortCriteria.iterator();
                Long           bufid     = bufs.next();

                outer:
                while (bys.hasNext())
                {
                   Long byid = bys.next();

                   while (!byid.equals(bufid))
                   {
                      if (processed.contains(byid))
                      {
                         // this is a reference to a previous table, so this is
                         // either out of order or intermix
                         result = true;
                         break outer;
                      }

                      if (bufs.hasNext())
                      {
                         processed.add(bufid);
                         bufid = bufs.next();
                      }
                      else
                      {
                         // we are trying to reference a table that is
                         // different than the last table, this must be an
                         // intermix
                         result = true;
                         break outer;
                      }
                   }
                }
             }

             return result;
          }

          /**
           * Return the given named index for the current table.
           *
           * @param    name
           *           The name of the index to obtain.
           *
           * @return   The named index or <code>null</code> if there are no
           *           indexes explicitly defined for this table or if there are
           *           no matches.
           */
          public P2JIndex getIndex(String name)
          {
             WorkArea wa = context.get();
             Iterator<P2JIndex> iter = indexes(wa.rpData.schemaname);

             while (iter.hasNext())
             {
                P2JIndex result = iter.next();

                if (result.getName().equalsIgnoreCase(name))
                {
                   // we are done
                   return result;
                }
             }

             return null;
          }

          /**
           * Returns the name of the index which corresponds the given prefix.
           * If an index name completely matches the given prefix, it is returned.
           * If there is a single index which starts with the given prefix, it is
           * returned. If there are no indexes or more than one index that start
           * with the given prefix, <code>null</code> is returned.
           *
           * @param   prefix
           *          Prefix of the index name.
           *
           * @return  The name of the index which corresponds the given prefix or
           *          <code>null</code> if there is no corresponding index.
           */
          public String getIndexNameByPrefix(String prefix)
          {
             WorkArea wa = context.get();
             Iterator<P2JIndex> iter = indexes(wa.rpData.schemaname);
             String targetName = null;
             prefix = prefix.toLowerCase();

             while (iter.hasNext())
             {
                P2JIndex result  = iter.next();
                String indexName = result.getName().toLowerCase();

                if (indexName.startsWith(prefix))
                {
                   if (indexName.length() == prefix.length())
                   {
                      // we have complete match
                      return indexName;
                   }

                   if (targetName == null)
                   {
                      targetName = indexName;
                   }
                   else
                   {
                      // we have two or more matching indexes
                      return null;
                   }
                }
             }

             return targetName;
          }

          /**
           * Return the index set as primary for the current table.
           *
           * @return   The primary index or <code>null</code> if there are no
           *           indexes explicitly defined for this table (as long as
           *           there is at least 1 explicit index, there is always
           *           a primary index).
           */
          public P2JIndex getPrimaryIndex()
          {
             WorkArea wa = context.get();
             Iterator<P2JIndex> iter = indexes(wa.rpData.schemaname);

             while (iter.hasNext())
             {
                P2JIndex result = iter.next();

                if (result.isPrimary())
                {
                   // we are done
                   return result;
                }
             }

             return null;
          }

          /**
           * Tests the given field name to determine if it is the last component
           * of an index and is flagged for abbreviation support.
           *
           * @param    name
           *           The field name.
           *
           * @return   <code>true</code> if this field is the last index
           *           component of an index AND is flagged for abbreviation
           *           support.
           */
          public boolean isAbbreviatedField(String name)
          {
             WorkArea wa = context.get();
             Iterator<P2JIndex> iter = indexes(wa.rpData.schemaname);
             P2JIndex next = null;
             name = name.toLowerCase();

             while (iter.hasNext())
             {
                next = iter.next();
                ArrayList<P2JIndexComponent> comps = next.components();
                for (int i = 0; i < comps.size(); i++)
                {
                   P2JIndexComponent nextComp = comps.get(i);

                   if (nextComp.getNormalizedName().equals(name))
                   {
                      if (nextComp.isAbbreviated())
                      {
                         return true;
                      }
                   }
                }
             }

             return false;
          }

          /**
           * Add an equality match (for the given index component) to the list of
           * matches.
           *
           * @param    field
           *           The index component (field) name which was matched.
           */
          public void addEqualityMatch(String field)
          {
             context.get().rpData.equalityMatches.add(field);
          }

          /**
           * Add an begins match (for the given index component) to the list of
           * matches.
           *
           * @param    field
           *           The index component (field) name which was matched.
           */
          public void addBeginsMatch(String field)
          {
             context.get().rpData.beginsMatches.add(field);
          }

          /**
           * Add an range match (for the given index component) to the list of
           * matches.
           *
           * @param    field
           *           The index component (field) name which was matched.
           */
          public void addRangeMatch(String field)
          {
             context.get().rpData.rangeMatches.add(field);
          }

          /**
           * Add an sort match (for the given index component) to the list of
           * matches (and maintains the order of specificity based on the
           * order of calls to this method).
           *
           * @param    field
           *           The index component (field) name which was matched.
           * @param    descending
           *           <code>true</code> if the sort is a descending sort,
           *           <code>false</code> for an ascending sort.
           */
          public void addSortMatch(String field, Boolean descending)
          {
             context.get().rpData.sortMatches.put(field, descending);
          }

          /**
           * Sets the isSimpleWhereClause flag.
           *
           * @param    isSimpleWhereClasue
           *           Boolean value indicating that the where clause is a single field check.
           */
          public void chageSimpleWhereClause(boolean isSimpleWhereClasue)
          {
             context.get().rpData.isSimpleWhereClause = isSimpleWhereClasue;
          }

          /**
           * Add an word match (for the given index component) to the list of
           * matches.
           *
           * @param    field
           *           The index component (field) name which was matched.
           */
          public void addWordMatch(String field)
          {
             context.get().rpData.wordMatches.add(field);
          }

          /**
           * Creates a list of all possible indexes for the current table.
           *
           * @return   The list of all index objects (may be empty if no indexes
           *           exist for this table).
           */
          public List<P2JIndex> getFullIndexList()
          {
             List<P2JIndex>     all  = new ArrayList<>();
             Iterator<P2JIndex> iter = indexes(context.get().rpData.schemaname);

             while (iter.hasNext())
             {
                P2JIndex idx = iter.next();

                // remember the complete list in case we find no matches
                // word index is used for ordering only if there is a CONTAINS
                // operator for a corresponding field
                if (!idx.isWord())
                {
                   all.add(idx);
                }
             }

             return all;
          }

          /**
           * Get "WHOLE-INDEX," prefix for index information.
           *
           * @return see above.
           */
          public String getWholeIndexPrefix()
          {
             return com.goldencode.p2j.persist.IndexHelper.WHOLE_INDEX_PREFIX;
          }

          /**
           * Uses the stored equality match data and the list of all indexes for the current record to
           * determine if there is a unique index that would be matched (the index must be unique and
           * all index components must have equality matches).  Only the indexes with the most index
           * components are returned.  For example, if 2 unique indexes are selectable, the one with
           * more index components will be returned. In the case that more than one unique index with
           * the same number of components is selectable, multiple matches are disambiguated using
           * the {@link #breakTie} method (in descending order).
           *
           * @return   Index information object containing the single unique index selected or
           *           <code>null</code> if no unique indexes are selectable by this criteria.
           */
          private IndexInfo findUniqueIndexMatch()
          {
             WorkArea wa = context.get();

             List<P2JIndex>     matches = new ArrayList<>();
             Iterator<P2JIndex> iter    = indexes(wa.rpData.schemaname);
             int      max     = 0;

             while (iter.hasNext())
             {
                P2JIndex idx = iter.next();

                if (idx.isUnique() && allComponentsMatch(idx, wa.rpData.equalityMatches))
                {
                   int num = idx.size();

                   // only add this index if it has at least as many matches as
                   // the previous candidates
                   if (num >= max)
                   {
                      // if this is the best match so far, reset the list
                      if (num > max)
                      {
                         max = num;
                         matches.clear();
                      }

                      // add the match
                      matches.add(idx);
                   }
                }
             }

             return breakTie(new IndexInfo(matches), false);
          }

          /**
           * Uses the stored word match data and the list of indexes for the current record to
           * determine if there is a word index that would be matched.  In the case that more than
           * one index is selectable the {@link #breakTie} method (in ascending order) is used to
           * select a single result.
           *
           * @return   Index information object containing the single index that was matched or
           *           <code>null</code> if no indexes were selectable by this criteria.
           */
          private IndexInfo findWordIndexMatch()
          {
             WorkArea wa = context.get();

             List<P2JIndex>     matches = new ArrayList<>();
             Iterator<P2JIndex> iter    = indexes(wa.rpData.schemaname);

             while (iter.hasNext())
             {
                P2JIndex idx = iter.next();

                if (idx.isWord() && numComponentsMatch(idx, wa.rpData.wordMatches) > 0)
                {
                   matches.add(idx);
                }
             }

             return breakTie(new IndexInfo(matches), true);
          }

          /**
           * Uses the equality and range match data and the list of selectable indexes for the current
           * record to determine if there is an index that would be matched based on the largest
           * number of sequential leading index components.  More than one index may remain selectable
           * after this method is complete.
           * <p>
           * Only indexes that have sequential leading index components matching can be selectable by
           * this method.  The method first checks for equality matches and if multiple equivalent
           * equality match choices exist (multiple choices with the same number of leading sequential
           * equality matches), then a single subsequent field is checked for a range match.  This
           * provides a very specific/tightly coupled tie break process.
           * <p>
           * The selection process removes indexes from the selectable list only in the case where
           * matches occur.  This means that those selectable indexes will remain in the list and thus
           * the returned list cannot be empty unless there are no indexes at all for a given table.
           *
           * @param    selectable
           *           Index information object containing the list of index candidates for the
           *           current table. This may be equivalent to the full list of all possible indexes
           *           or it may be a subset if prior processing removed indexes form the selection
           *           list.
           *
           * @return   Index information object containing the list of remaining candidates that are
           *           selectable.
           */
          private IndexInfo findLeadingIndexMatch(IndexInfo selectable)
          {
             List<P2JIndex>     eMatches = new ArrayList<>();
             List<P2JIndex>     bMatches = new ArrayList<>();
             List<P2JIndex>     rMatches = new ArrayList<>();
             Iterator<P2JIndex> iter     = selectable.indexes.iterator();
             int                max      = 0;

             WorkArea wa = context.get();

             // check equality first
             while (iter.hasNext())
             {
                P2JIndex idx = iter.next();

                int num = leadingComponentsMatch(idx, 0, wa.rpData.equalityMatches);

                // is this a candidate?
                if (num > 0)
                {
                   // only add this index if it has at least as many matches as
                   // the previous candidates
                   if (num >= max)
                   {
                      // if this is the best match so far, reset the list
                      if (num > max)
                      {
                         max = num;
                         eMatches.clear();
                      }

                      // add the match
                      eMatches.add(idx);
                   }
                }
             }

             // if there is more than 1 match, check if there is a begins match
             // on the next field
             if (eMatches.size() > 1)
             {
                // bypass range matching down-stream
                wa.rpData.rangeBypass = true;

                iter = eMatches.iterator();

                while (iter.hasNext())
                {
                   P2JIndex idx = iter.next();

                   // only add a match if there are more fields than already
                   // found AND the next field has a range match
                   if (idx.size() > max &&
                       wa.rpData.beginsMatches.contains(idx.getComponent(max).getNormalizedName()))
                   {
                      // add the match
                      bMatches.add(idx);
                   }
                }

                // did we cut the list down further?
                if (bMatches.size() > 0)
                {
                   return new IndexInfo(bMatches);
                }
             }

             // the begins match check did not cut down the list further, if
             // there is more than 1 match, check if there is a range match on
             // the next field
             if (eMatches.size() > 1)
             {
                // bypass range matching down-stream
                wa.rpData.rangeBypass = true;

                iter = eMatches.iterator();

                while (iter.hasNext())
                {
                   P2JIndex idx = iter.next();

                   // only add a match if there are more fields than already found AND the next field has a range
                   // match
                   if (idx.size() > max &&
                       wa.rpData.rangeMatches.contains(idx.getComponent(max).getNormalizedName()))
                   {
                      // add the match
                      rMatches.add(idx);
                   }
                }

                // did we cut the list down further?
                if (rMatches.size() > 0)
                {
                   return new IndexInfo(rMatches);
                }
             }

             if (!wa.rpData.isFind && !wa.rpData.isSimpleWhereClause)
             {
                List<P2JIndex> eCopyMatches = new ArrayList<>(eMatches);
                boolean foundExactMatch = false;
                for (P2JIndex idx: eCopyMatches)
                {
                   int num = leadingComponentsMatch(idx, 0, wa.rpData.equalityMatches);

                   if (num == idx.size())
                   {
                      if (!foundExactMatch)
                      {
                         foundExactMatch = true;
                         eMatches.clear();
                      }
                      eMatches.add(idx);
                   }
                }
             }

             return (eMatches.size() > 0 ? new IndexInfo(eMatches) : selectable);
          }

          /**
           * Uses the "begins" match data and the list of selectable indexes for the current record to
           * determine if there is an index that would be matched.  More than one index may remain
           * selectable after this method is complete.
           * <p>
           * Only indexes that have the first (leading) index component matching can be selectable.
           * All other begins matches are ignored.  There is no concept of the "most" begins matches.
           * <p>
           * The selection process removes indexes from the selectable list only in the case where
           * matches occur.  This means that those selectable indexes will remain in the list and thus
           * the returned list cannot be empty unless there are no indexes at all for a given table.
           *
           * @param    selectable
           *           Index information object containing the list of index candidates for the
           *           current table. This may be equivalent to the full list of all possible indexes
           *           or it may be a subset if prior processing removed indexes form the selection
           *           list.
           *
           * @return   Index information object containing the list of remaining candidates that are
           *           selectable.
           */
          private IndexInfo findBeginsIndexMatch(IndexInfo selectable)
          {
             WorkArea wa = context.get();

             List<P2JIndex>     matches = new ArrayList<>();
             Iterator<P2JIndex> iter    = selectable.indexes.iterator();

             while (iter.hasNext())
             {
                P2JIndex idx = iter.next();

                String field = idx.getComponent(0).getNormalizedName();

                // if the first field is not an equality match but IS a begins match, then it is a candidate
                if (wa.rpData.beginsMatches.contains(field))
                {
                   // add the match
                   matches.add(idx);
                }
             }

             return (matches.size() > 0 ? new IndexInfo(matches) : selectable);
          }

          /**
           * Uses the range match data and the list of selectable indexes for the current record to
           * determine if there is an index that would be matched. More than one index may remain
           * selectable after this method is complete.
           * <p>
           * Only indexes that have the first (leading) index component matching can be selectable.
           * All other range matches are ignored.  There is no concept of the "most" range matches.
           * <p>
           * The selection process removes indexes from the selectable list only in the case where
           * matches occur.  This means that those selectable indexes will remain in the list and thus
           * the returned list cannot be empty unless there are no indexes at all for a given table.
           *
           * @param    selectable
           *           Index information object containing the list of index candidates for the
           *           current table. This may be equivalent to the full list of all possible indexes
           *           or it may be a subset if prior processing removed indexes form the selection
           *           list.
           *
           * @return   Index information object containing the list of remaining candidates that are
           *           selectable.
           */
          private IndexInfo findRangeIndexMatch(IndexInfo selectable)
          {
             WorkArea wa = context.get();

             List<P2JIndex>     matches = new ArrayList<>();
             Iterator<P2JIndex> iter    = selectable.indexes.iterator();

             while (iter.hasNext())
             {
                P2JIndex idx = iter.next();

                String field = idx.getComponent(0).getNormalizedName();

                // if the first field is not an equality match but IS a range
                // match, then it is a candidate
                if (wa.rpData.rangeMatches.contains(field))
                {
                   // add the match
                   matches.add(idx);
                }
             }

             return (matches.size() > 0 ? new IndexInfo(matches) : selectable);
          }

          /**
           * Uses the sort match data and the list of selectable indexes for the current record to
           * determine if there is an index that would be matched.  In the case that more than one
           * index is selectable the {@link #breakTie} method (in ascending order) is used to select
           * a single result.
           * <p>
           * Only indexes that have sequential leading index components matching can be selectable.
           * Sort matches must also agree on the direction of the sort for the given index component
           * (ascending/descending).
           * <p>
           * The selection process removes indexes from the selectable list only in the case where
           * matches occur.  This means that those selectable indexes will remain in the list and thus
           * the returned list cannot be empty unless there are no indexes at all for a given table.
           *
           * @param    selectable
           *           Index information object containing the list of index candidates for the
           *           current table. This may be equivalent to the full list of all possible indexes
           *           or it may be a subset if prior processing removed indexes form the selection
           *           list.
           *
           * @return   Index information object containing the single index that was matched or
           *           <code>null</code> if no indexes were selectable by this criteria.
           */
          private IndexInfo findSortIndexMatch(IndexInfo selectable)
          {
             WorkArea wa = context.get();

             if (wa.rpData.sortMatches.isEmpty())
             {
                return breakTie(selectable, true);
             }

             List<P2JIndex>     matches = new ArrayList<>();
             Iterator<P2JIndex> iter    = selectable.indexes.iterator();
             int                max     = 0;

             while (iter.hasNext())
             {
                P2JIndex idx = iter.next();

                int num = leadingSortComponentsMatch(idx);

                // is this a candidate?
                if (num != 0)
                {
                   // only add this index if it has at least as many matches as
                   // the previous candidates
                   if (Math.abs(num) >= Math.abs(max))
                   {
                      // if this is the best match so far, reset the list
                      if (Math.abs(num) > Math.abs(max))
                      {
                         max = num;
                         matches.clear();
                      }

                      // add the match
                      matches.add(idx);
                   }
                }
             }

             return matches.isEmpty() ||
                    !(wa.rpData.beginsMatches.isEmpty()   &&
                      wa.rpData.equalityMatches.isEmpty() &&
                      wa.rpData.rangeMatches.isEmpty()    &&
                      wa.rpData.wordMatches.isEmpty())
                   ? breakTie(matches.size() > 0
                                ? new IndexInfo(matches, selectable.wholeIndex)
                               : selectable,
                              true)
                   : new IndexInfo(matches.get(0), selectable.wholeIndex);
          }

          /**
           * Calculates if all index components of the given index are found in
           * the match set.
           *
           * @param    idx
           *           The index to test.
           * @param    matches
           *           The list of matches that were found.
           *
           * @return   <code>true</code> if all index components were matched.
           */
          private boolean allComponentsMatch(P2JIndex idx, Set<String> matches)
          {
             return (idx.size() == numComponentsMatch(idx, matches));
          }

          /**
           * Calculates how many index components of the given index are to be
           * found in the match set.
           *
           * @param    idx
           *           The index to test.
           * @param    matches
           *           The list of matches that were found.
           *
           * @return   The number of matches or 0 if there are no matches.
           */
          private int numComponentsMatch(P2JIndex idx, Set<String> matches)
          {
             ArrayList<P2JIndexComponent> comps = idx.components();
             int num = 0;
             for (int i = 0; i < comps.size(); i++)
             {
                P2JIndexComponent comp = comps.get(i);
                if (matches.contains(comp.getNormalizedName()))
                {
                   num++;
                }
             }

             return num;
          }

          /**
           * Calculates the number of sequential leading index components of the
           * given index are found in the match set starting at the specified
           * position in the index component list.
           *
           * @param    idx
           *           The index to test.
           * @param    start
           *           The 0-based position of the first component to compare.
           * @param    matches
           *           The list of matches that were found.
           *
           * @return   The number of sequential leading index components that
           *           were matched (may be 0 if no leading components were
           *           matched) starting at the given position.
           */
          private int leadingComponentsMatch(P2JIndex idx, int start, Set<String> matches)
          {
             int len = idx.size();
             int num = 0;

             for (int i = start; i < len; i++)
             {
                P2JIndexComponent comp = idx.getComponent(i);

                if (matches.contains(comp.getNormalizedName()))
                {
                   // this a matched component
                   num++;
                }
                else
                {
                   // the first missing component stops the search
                   break;
                }
             }

             return num;
          }

          /**
           * Calculates the number of sequential leading index components of the
           * given index are found in the sort match set (tests component name,
           * the specificity of the sort matches and the sort direction for
           * compatibility). The set of components can be matched into direct or
           * reverse way, e.g. reverse match for the "crit1 asc crit2 desc crit3
           * desc" is the "crit1 desc crit2 asc crit3 asc".
           *
           * @param    idx
           *           The index to test.
           *
           * @return   The absolute value of the returned value is the number of
           *           sequential leading index components that were matched. If
           *           the returned value is greater than <code>0</code> then
           *           components were matched in the direct way. If the returned
           *           value is less than <code>0</code> then components were
           *           matched in the reverse way. If the returned value is
           *           <code>0</code> then no leading components were matched.
           */
          private int leadingSortComponentsMatch(P2JIndex idx)
          {
             WorkArea wa = context.get();

             Iterator<String> sort = wa.rpData.sortMatches.keySet().iterator();
             int      num  = 0;
             boolean  tryReverseMatch = false;

             ArrayList<P2JIndexComponent> comps = idx.components();
             for (int i = 0; i < comps.size(); i++)
             {
                P2JIndexComponent comp = comps.get(i);

                if (sort.hasNext())
                {
                   String critName = sort.next();

                   if (critName.equals(comp.getNormalizedName()))
                   {
                      Boolean descend = wa.rpData.sortMatches.get(critName);

                      if (i == 0 && descend != comp.isDescending())
                      {
                         tryReverseMatch = true;
                      }

                      if (tryReverseMatch == (descend != comp.isDescending()))
                      {
                         // this a matched component
                         num++;
                      }
                      else
                      {
                         // this sort match is a different direction than the
                         // component of the index
                         break;
                      }
                   }
                   else
                   {
                      // further sort matches are not from this index or are not
                      // sequential leading fields of this index
                      break;
                   }
                }
                else
                {
                   // no more sort matches to check
                   break;
                }
             }

             return tryReverseMatch ? -num : num;
          }

          /**
           * If the given index information object has only a single entry and the <code>wholeIndex</code> flag
           * is <code>false</code>, return this object, otherwise return <code>null</code>. The
           * <code>wholeIndex</code> check is necessary in order to continue index selection if that flag is set
           * on <code>true</code>.
           *
           * @param    indexes
           *           Index information object.
           *
           * @return   Index information object passed as the input parameter or <code>null</code> if
           *           no indexes or more than 1 index are in the list of indexes or the <code>wholeIndex</code>
           *           flag is <code>true</code>.
           */
          private IndexInfo readSingleIndex(IndexInfo indexes)
          {
             if (indexes.indexes.size() != 1 || indexes.wholeIndex)
             {
                return null;
             }

             return indexes;
          }

          /**
           * Select one index from the given list of indexes that are selectable, if there are more
           * than 1 in the list.  If there is only 1 index in the list, this is returned.  The
           * precedence for the choice is if one of the indexes is the primary index, this is
           * returned, otherwise the first or last index name alphabetically is returned (the
           * ascending or descending order is based on the <code>ascend</code> parameter).
           *
           * @param    matches
           *           Index information object containing the list of indexes to select from.
           * @param    ascend
           *           <code>true</code> to compare lexicographically (ascending order) and
           *           <code>false</code> to compare in reverse lexicographical order (descending
           *           order).
           *
           * @return   Index information object containing the selected indexes or <code>null</code>
           *           if no indexes are in the list.
           */
          private IndexInfo breakTie(IndexInfo matches, boolean ascend)
          {
             String result  = null;
             String alpha   = null;
             String upper   = null;
             int    compare = 0;

             Iterator<P2JIndex> iter = matches.indexes.iterator();

             while (iter.hasNext())
             {
                P2JIndex idx = iter.next();

                if (idx.isPrimary())
                {
                   return new IndexInfo(getIndex(idx.getName()), matches.wholeIndex);
                }

                alpha = idx.getName();
                upper = alpha.toUpperCase();

                if (result != null)
                {
                   compare = result.toUpperCase().compareTo(upper);
                }

                // init if first time through, otherwise compare lexicographically
                // with the next name (we uppercase to get the same kind of
                // case-insensitive comparison that Progress does)
                if (result == null || (ascend ? compare > 0 : compare < 0))
                {
                   result = alpha;
                }
             }

             return result != null ? new IndexInfo(getIndex(result), matches.wholeIndex) : null;
          }

          /**
           * Class containing index information: list of indexes and the WHOLE-INDEX flag which
           * indicates if the index or indexes have bracketing or 4GL walks through the whole
           * index(es).
           */
          private class IndexInfo
          {
             /**
              * WHOLE-INDEX flag which is <code>true</code> if the index or indexes do not have
              * bracketing, i.e. 4GL walks through the whole index(es) and <code>false</code> if
              * the index or indexes have bracketing.
              */
             private boolean wholeIndex = false;

             /** List of indexes. */
             private final List<P2JIndex> indexes;

             /** Creates information object with empty list of indexes. */
             private IndexInfo()
             {
                indexes = new ArrayList<>(0);
             }

             /**
              * Creates information object with the given list of indexes and WHOLE-INDEX flag set to
              * <code>false</code>.
              *
              * @param indexes
              *        List of indexes.
              */
             private IndexInfo(List<P2JIndex> indexes)
             {
                this.indexes = indexes;
             }

             /**
              * Creates information object with the given list of indexes and WHOLE-INDEX flag set to
              * the specified value.
              *
              * @param indexes
              *        List of indexes.
              * @param wholeIndex
              *        WHOLE-INDEX flag (see {@link #wholeIndex}).
              */
             private IndexInfo(List<P2JIndex> indexes, boolean wholeIndex)
             {
                this.indexes = indexes;
                this.wholeIndex = wholeIndex;
             }

             /**
              * Creates information object with the list of indexes consisting of the single specified
              * index and WHOLE-INDEX flag set to the specified value.
              *
              * @param index
              *        A single index.
              * @param wholeIndex
              *        WHOLE-INDEX flag (see {@link #wholeIndex}).
              */
             private IndexInfo(P2JIndex index, boolean wholeIndex)
             {
                this.indexes = new ArrayList<>(1);
                this.indexes.add(index);
                this.wholeIndex = wholeIndex;
             }

             /**
              * Produces index information string in the same format as INDEX-INFORMATION does:
              * comma-separated list of index names prepended "WHOLE-INDEX," if WHOLE-INDEX flag is
              * <code>true</code>.
              *
              * @return see above.
              */
             public String toString()
             {
                StringBuilder sb = new StringBuilder();
                if (wholeIndex)
                {
                   sb.append(getWholeIndexPrefix());
                }

                for (int i = 0; i < indexes.size(); i++)
                {
                   if (i > 0)
                   {
                      sb.append(",");
                   }
                   sb.append(indexes.get(i).getName());
                }

                return sb.toString();
             }
          }
       }

       /**
        * Data needed to process index selection for a record phrase.
        */
       private static class RecordPhraseData
       {
          /** Schema name for the record being processed. */
          private final String schemaname;

          /** Flag indicating whether record phrase is in a find statement. */
          private final boolean isFind;

          /** Flag indicating whether where clause is a single field check */
          private boolean isSimpleWhereClause = false;

          /** Stores the list of the equality matches for the current record phrase. */
          private final Set<String> equalityMatches = new HashSet<>();

          /** Stores the list of the begins matches for the current record phrase. */
          private final Set<String> beginsMatches = new HashSet<>();

          /** Stores the list of the range matches for the current record phrase. */
          private final Set<String> rangeMatches = new HashSet<>();

          /** Sort matches for current record phrase in order of increasing specificity. */
          private final LinkedHashMap<String, Boolean> sortMatches = new LinkedHashMap<>();

          /** Stores the list of the word matches for the current record phrase. */
          private final Set<String> wordMatches = new HashSet<>();

          /** Flag to control whether range processing is necessary. */
          private boolean rangeBypass = false;

          /**
           * Constructor.
           *
           * @param    schemaname
           *           The fully qualified schema name of the record being processed.
           * @param    isFind
           *           Flag indicating whether record phrase is in a find statement.
           */
          RecordPhraseData(String schemaname, boolean isFind)
          {
             this.schemaname = schemaname;
             this.isFind = isFind;
          }
       }

       /**
        * Context local work area.
        */
       private static class WorkArea
       {
          /** Stack of record phrase data objects; first one is not used */
          private final Deque<RecordPhraseData> stack = new ArrayDeque<>();

          /** Current record phrase data */
          private RecordPhraseData rpData = new RecordPhraseData(null, false);
       }
    }