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