IndexHelper.java

/*
** Module   : IndexHelper.java
** Abstract : Helper object to manage database index information
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20080513   @38618 Created initial version. Helper object to manage
**                           database index information.
** 002 ECF 20080606   @38644 Support API change in DatabaseManager.
** 003 ECF 20080610   @38680 Fixed lookup of index by sort phrase. We were not
**                           properly handling primary key column in indexes.
**                           Added debug logging.
** 004 SVL 20080730   @39224 Sorters are stored by sort clauses, not by
**                           indexes. 
** 005 CA  20080819   @39461 Support API change in DMOSorter.
** 006 ECF 20090508   @42125 Added maps of index names to property lists.
**                           Needed for validation improvements.
** 007 ECF 20090513   @42166 Fixed getIndexesForProperty(). Must not return
**                           null.
** 008 ECF 20090604   @42588 Fixed getIndexesForEntity(). Was not taking
**                           uniqueness flag into account properly when
**                           caching results.
** 009 ECF 20090707   @43100 Fixed lookupIndexesForDMO(). Instead of omitting
**                           merely the index containing only the primary key,
**                           we were omitting any index containing the primary key.
** 010 ECF 20131028          Import change.
** 011 VMN 20131128          Added map of entity to primary index and method getPrimaryIndex().
** 012 VMN 20140423          Added enhance schema name conversion support for hint "escape"
**                           attribute.
** 013 OM  20140509          Replaced Apache logging. Applied latest coding-style.
**                           Added intermediary implementation of getLegacyIndexName().
** 014 SVL 20140602          Added WHOLE_INDEX_PREFIX.
** 015 OM  20140617          Removed getLegacyIndexName() as it was a duplicate of 
**                           TableMapper.getLegacyIndexName().
** 016 ECF 20150201          Fixed lookupIndexForSort to properly match a non-unique index which
**                           has had the table's primary key appended to its components.
** 017 ECF 20150801          Minor performance improvements. Some uses of String.intern() were
**                           redundant with those of clients of this class.
** 018 ECF 20160517          Reimplemented getPrimaryIndex to return null instead of throwing an
**                           exception if no primary index is found.
** 019 OM  20190226          Workaround for extent fields in word indexes.
** 020 ECF 20190814          Do not include word index in DMO index map.
**     OM  20190829          Case sensitive index name lookup.
** 021 OM  20191031          Avoid NPE in some cases DTZ index components.
** 022 ECF 20200906          New ORM implementation.
** 023 OM  20200919          Improved access to dmo metadata.
**     OM  20200924          P2JIndexComponent carries multiple information to avoid map lookups for them.
**     OM  20201008          Detected direction of used index and stored for further access.
**     OM  20201012          Force use locally cached meta information instead of map lookup.
**     ECF 20210506          Use RecordBuffer.getDmoInfo() getter instead of direct field access to
**                           prevent NPE in proxy case.
**     SVL 20230110          P2JIndex.components() provides direct access to the components array in order to
**                           improve performance.
** 024 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 025 CA  20240608          If a non-unique index is found for DMOSorter, force it to be unique by adding  
**                           the primary key as last component, using ascendent direction.  This is required
**                           because when comparing a dirty record and a query found record, the order must
**                           be on the primary key via pk, if the index is non-unique and both records are the
**                           same otherwise.
** 025 AL2 20240719          DMO sorter (used by dirty DMO logic) should take the recid in consideration.
** 026 LS  20250310          Reduced the level of the logger to INFO In lookupIndexForSort() when the
**                           direction of a component is not consistent with the first one.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.persist;

import java.util.*;
import java.util.logging.*;
import com.goldencode.p2j.persist.annotation.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import org.apache.commons.lang3.tuple.*;

/**
 * Helper object to manage database index information.  One instance of this
 * class exists for each permanent, primary database.  Provides methods to
 * retrieve:
 * <ul>
 *   <li>all indexed properties for a DMO type
 *   <li>all index names for the table backing a DMO type
 *   <li>all indexes in which the column backing a DMO property participates
 *   <li>the index name associated with an index-driven query's sort phrase
 *   <li>a {@link DMOSorter} instance appropriate to an index-driven query's
 *       sort phrase
 * </ul>
 * <p>
 * It is anticipated that the lists of index names returned by some of the
 * methods in this class will be used for purposes of serialization of
 * index resources via a {@link LockManager}.  Therefore, unless otherwise
 * noted, these lists will be sorted in ascending, alphabetical order, to
 * help prevent deadlocks in situations where all such indexes will be locked
 * by a particular context.  That is, if all contexts acquiring locks on these
 * indexes are careful to do so in the same order, deadlocks should not occur.
 */
public final class IndexHelper
{
   /** Prefix for index information string. */
   public static final String WHOLE_INDEX_PREFIX = "WHOLE-INDEX,";
   
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(IndexHelper.class);
   
   /** Is FINE level logging allowed? */
   private static final boolean LOG_FINE = LOG.isLoggable(Level.FINE);
   
   /** Cache of instances, mapped by database */
   private static final Map<Database, IndexHelper> cache = new HashMap<>();
   
   /** Primary (non-dirty) database with which this helper is associated */
   private final Database database;
   
   /** Map of sort clauses to pair of index names and their direction ({@code false} means reversed). */
   private final Map<SortKey, Pair<String, Boolean>> indexBySort = new HashMap<>();
   
   /** Map of property names to lists of indexes containing them */
   private final Map<PropertyKey, List<String>> indexesByProperty = new HashMap<>();
   
   /** Map of sort clauses to <code>DMOSorter</code> objects */
   private final Map<SortKey, DMOSorter> sorters = new HashMap<>();
   
   /** Map of entity keys to lists of all index names */
   private final Map<EntityIndexKey, List<String>> indexesByEntity = new HashMap<>();
   
   /** Map of entity to primary index */
   private final Map<String, P2JIndex> primaryIndexByEntity = new HashMap<>();
   
   /** Map of entity keys to maps of index names to index property names */
   private final Map<EntityIndexKey, Map<String, List<String>>> indexMapsByEntity = new HashMap<>();
   
   /** Map of entity keys to indexed properties */
   private final Map<EntityIndexKey, Set<String>> propsByEntity = new HashMap<>();
   
   /**
    * Private constructor.  Instances of this class are created only by the
    * public factory method {@link #get(com.goldencode.p2j.persist.Database)}.
    * 
    * @param   database
    *          Database for which index information is needed.
    */
   private IndexHelper(Database database)
   {
      this.database = database;
   }
   
   /**
    * Get an instance of this class for the given database.  If an instance
    * does not exist in the shared cache, one is created and stored for future
    * requests.
    * 
    * @param   database
    *          Database for which index information is needed.
    * 
    * @return  Instance of this class for the given database.
    */
   public static IndexHelper get(Database database)
   {
      IndexHelper helper = null;
      
      synchronized (cache)
      {
         helper = cache.get(database);
         if (helper == null)
         {
            helper = new IndexHelper(database);
            cache.put(database, helper);
         }
      }
      
      return helper;
   }
   
   /**
    * Retrieve a list of the names of all indexes in which the database
    * column associated with the given DMO property participates.
    * 
    * @param   entity
    *          A DMO entity name which represents a database table.
    * @param   property
    *          A DMO property which represents a database column.
    * 
    * @return  A list of index names in ascending alphabetical order.  The
    *          list will not contain duplicates, but it may be empty.  It will
    *          never be <code>null</code>.
    *          
    * @throws  PersistenceException
    *          if there is any error querying index metadata from the JDBC
    *          driver of the associated database.
    */
   public List<String> getIndexesForProperty(String entity, String property)
   throws PersistenceException
   {
      List<String> indexes = null;
      property = property.intern();
      PropertyKey key = new PropertyKey(entity, property);
      
      synchronized (indexesByProperty)
      {
         indexes = indexesByProperty.get(key);
      }
      
      if (indexes == null)
      {
         indexes = lookupIndexesForProperty(entity, property);
         if (indexes == null)
         {
            indexes = Collections.emptyList();
         }
         else
         {
            Collections.sort(indexes);
            indexes = Collections.unmodifiableList(indexes);
         }
         
         synchronized (indexesByProperty)
         {
            List<String> old = indexesByProperty.put(key, indexes);
            if (old != null)
            {
               indexes = old;
               indexesByProperty.put(key, old);
            }
         }
      }
      
      return indexes;
   }
   
   /**
    * Given a sort phrase from an index-driven query, return the name of the
    * index represented by the sort phrase.
    * 
    * @param   buffer
    *          Record buffer which contains information about the DMO entity
    *          associated with the sort phrase.
    * @param   sort
    *          A sort phrase which includes one or more DMO property
    *          references, each qualified by a DMO alias and followed by a
    *          keyword indicating sort direction.
    * 
    * @return  Name of the index associated with the given sort phrase, or
    *          <code>null</code> if no matching index can be found.
    * 
    * @throws  PersistenceException
    *          if there is any error querying index metadata from the JDBC
    *          driver of the associated database.
    */
   public String getIndexForSort(RecordBuffer buffer, String sort)
   throws PersistenceException
   {
      String index = null;
      String entity = buffer.getEntityName();
      SortKey key = new SortKey(entity, sort);
      Pair<String, Boolean> indexAndDir;
      
      synchronized (indexBySort)
      {
         indexAndDir = indexBySort.get(key);
         if (indexAndDir == null)
         {
            indexAndDir = lookupIndexForSort(buffer, sort);
         }
      }
      
      index = indexAndDir.getLeft();
      if ("".equals(index))
      {
         index = null;
      }
      
      return index;
   }
   
   /**
    * Retrieve primary index of given database table.
    *
    * @param   entity
    *          A DMO entity name which represents a database table.
    *
    * @return  Primary index.
    *
    * @throws  PersistenceException
    *          if there is any error in retrieving primary index or more than one primary index
    *          is found.
    */
   public P2JIndex getPrimaryIndex(String entity)
   throws PersistenceException
   {
      P2JIndex index;
      
      synchronized (primaryIndexByEntity)
      {
         index = primaryIndexByEntity.get(entity);
         
         // nulls are permitted and mean the entity has no primary index
         if (index == null && primaryIndexByEntity.containsKey(entity))
         {
            return null;
         }
      }
      
      if (index == null)
      {
         String indexName = null;
         DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(entity, null);
         Class<? extends DataModelObject> dmoIface = dmoInfo.getAnnotatedInterface();
         Indices lIndexes = dmoIface.getAnnotation(Indices.class);
         if (lIndexes != null)
         {
            for (Index lIndex : lIndexes.value())
            {
               if (lIndex.primary())
               {
                  if (indexName == null)
                  {
                     indexName = lIndex.name();
                  }
                  else
                  {
                     throw new PersistenceException("Table '" + entity +
                                                    "' has two primary indexes: '" +
                                                    indexName + "' and '" +
                                                    lIndex.name() +"'");
                  }
               }
            }
         }
         
         if (indexName != null)
         {
            Iterator<P2JIndex> iter = dmoInfo.getDatabaseIndexes();
            while (iter.hasNext())
            {
               P2JIndex ind = iter.next();
               if (indexName.equals(ind.getName()))
               {
                  index = ind;
                  break;
               }
            }
         }
         
         synchronized (primaryIndexByEntity)
         {
            // cache index, even if none found (i.e., null)
            primaryIndexByEntity.putIfAbsent(entity, index);
         }
      }
      
      return index;
   }
   
   /**
    * Retrieve a {@link DMOSorter} instance which is able to sort DMO instances according to the sort criteria
    * specified by the given sort phrase.
    * 
    * @param   buffer
    *          Record buffer which contains information about the DMO entity associated with the sort phrase.
    * @param   sort
    *          A sort phrase which includes one or more DMO property references, each qualified by a DMO alias
    *          and followed by a keyword indicating sort direction.
    * 
    * @return  DMO sorter object which can sort DMOs according to the given index.
    *
    * @throws  PersistenceException
    *          if there is any error querying index metadata from the JDBC driver of the associated database.
    */
   public DMOSorter getSorterForSortPhrase(RecordBuffer buffer, String sort)
   throws PersistenceException
   {
      Pair<String, Boolean> indexAndDir;
      SortKey key = new SortKey(buffer.getEntityName(), sort);
      
      synchronized (indexBySort)
      {
         indexAndDir = indexBySort.get(key);
         if (indexAndDir == null)
         {
            // this will add an appropriate sorter to the sorters map, if possible
            lookupIndexForSort(buffer, sort);
         }
      }
      
      return sorters.get(key);
   }
   
   /**
    * Obtain the direction of the index used to sort the buffer by a specified sort phrase.
    *
    * @param   buffer
    *          Record buffer which contains information about the DMO entity associated with the sort phrase.
    * @param   sort
    *          A sort phrase which includes one or more DMO property references, each qualified by a DMO alias
    *          and followed by a keyword indicating sort direction.
    *
    * @return  {@code true} if the direction is the same as the index, {@code false} if the direction is
    *          inversed (I.e. the result set is reversed compared to the index) and {@code null} if the sort
    *          phrase does not benefit from any index, or the sort phrase was not processed yet for this
    *          buffer.
    */
   public Boolean getIndexDirection(RecordBuffer buffer, String sort)
   {
      SortKey key = new SortKey(buffer.getEntityName(), sort);
      
      synchronized (indexBySort)
      {
         Pair<String, Boolean> indexAndDir = indexBySort.get(key);
         return indexAndDir == null ? null : indexAndDir.getRight();
      }
   }
   
   /**
    * Retrieve a list of all index names associated with the table represented
    * by the given DMO entity name, or the sublist which represents unique
    * indexes only.
    * 
    * @param   entity
    *          A DMO entity name which represents a database table.
    * @param   unique
    *          If <code>true</code>, only unique indexes are returned;  if
    *          <code>false</code>, all indexes are returned.
    * 
    * @return  The names of all or only unique indexes for the given entity,
    *          sorted in ascending alphabetical order.  The list will not
    *          contain duplicates, but it may be empty.  It will never be
    *          <code>null</code>.
    *          
    * @throws  PersistenceException
    *          if there is any error querying index metadata from the JDBC
    *          driver of the associated database.
    */
   public List<String> getIndexesForEntity(String entity, boolean unique)
   throws PersistenceException
   {
      List<String> list = null;
      EntityIndexKey key = new EntityIndexKey(entity, unique);
      
      synchronized (indexesByEntity)
      {
         list = indexesByEntity.get(key);
      }
      
      if (list == null)
      {
         list = lookupIndexesForDMO(entity, unique);
         if (list != null)
         {
            Collections.sort(list);
            list = Collections.unmodifiableList(list);
         }
         else
         {
            list = Collections.emptyList();
         }
         
         synchronized (indexesByEntity)
         {
            List<String> old = indexesByEntity.put(key, list);
            if (old != null)
            {
               indexesByEntity.put(key, old);
            }
         }
      }
      
      return list;
   }
   
   /**
    * Retrieve a mapping of index names to their component property names, for
    * indexes associated with the table represented by the given DMO entity
    * name.
    * 
    * @param   entity
    *          A DMO entity name which represents a database table.
    * @param   unique
    *          If <code>true</code>, only unique indexes are returned;  if
    *          <code>false</code>, only non-unique indexes are returned.
    * 
    * @return  A mapping of index names to ordered lists of the names of their
    *          component properties, for the given DMO entity.  The map
    *          represents only unique indexes, or only non-unique indexes,
    *          depending upon the <code>unique</code> parameter.  The map may
    *          be empty, but it will never by <code>null</code>.  The property
    *          names will not be wrapped in any functions.  The map will
    *          iterate in ascending alphabetical order by index name.
    *          
    * @throws  PersistenceException
    *          if there is any error querying index metadata from the JDBC
    *          driver of the associated database.
    */
   public Map<String, List<String>> getIndexMapForEntity(String entity, boolean unique)
   throws PersistenceException
   {
      Map<String, List<String>> map = null;
      EntityIndexKey key = new EntityIndexKey(entity, unique);
      
      synchronized (indexMapsByEntity)
      {
         map = indexMapsByEntity.get(key);
      }
      
      if (map == null)
      {
         map = Collections.unmodifiableMap(lookupIndexMapForDMO(entity, unique));
         
         synchronized (indexMapsByEntity)
         {
            Map<String, List<String>> old = indexMapsByEntity.put(key, map);
            if (old != null)
            {
               indexMapsByEntity.put(key, old);
            }
         }
      }
      
      return map;
   }
   
   /**
    * Retrieve the set of all property names for a particular DMO entity,
    * which represent database columns that participate in an index on the
    * entity's backing table, or the subset which represent database columns
    * that participate in a unique index.
    * <p>
    * Note:  the returned set will <em>not</em> include the reserved primary
    * key property <code>id</code>.
    * 
    * @param   entity
    *          A DMO entity name which represents a database table.
    * @param   unique
    *          If <code>true</code>, only properties associated with unique
    *          indexes are returned;  if <code>false</code>, all indexed
    *          properties are returned.
    * 
    * @return  Set of indexed property names.  The set may be empty but it
    *          will never be <code>null</code>.
    * 
    * @throws  PersistenceException
    *          if there is any error querying index metadata from the JDBC
    *          driver of the associated database.
    */
   public Set<String> getIndexedProperties(String entity, boolean unique)
   throws PersistenceException
   {
      Set<String> set = null;
      EntityIndexKey key = new EntityIndexKey(entity, unique);
      
      synchronized (propsByEntity)
      {
         set = propsByEntity.get(key);
      }
      
      if (set == null)
      {
         set = lookupIndexedProperties(entity, unique);
         if (set != null)
         {
            set = Collections.unmodifiableSet(set);
         }
         else
         {
            set = Collections.emptySet();
         }
         
         synchronized (propsByEntity)
         {
            Set<String> old = propsByEntity.put(key, set);
            if (old != null)
            {
               propsByEntity.put(key, old);
            }
         }
      }
      
      return set;
   }
   
   /**
    * Collect a list of all index names associated with the table represented
    * by the given DMO entity name.
    * 
    * @param   entity
    *          A DMO entity name which represents a database table.
    * @param   unique
    *          If <code>true</code>, only unique indexes are returned;  if
    *          <code>false</code>, all indexes are returned.
    * 
    * @return  The names of all indexes for the given entity, in no particular
    *          order, or <code>null</code> if none are found.
    *          
    * @throws  PersistenceException
    *          if there is any error querying index metadata from the JDBC
    *          driver of the associated database.
    */
   private List<String> lookupIndexesForDMO(String entity, boolean unique)
   throws PersistenceException
   {
      ArrayList<String> list = null;
      Iterator<P2JIndex> iter = DmoMetadataManager.getDmoInfo(entity, null).getDatabaseIndexes();
      
      outer:
      while (iter.hasNext())
      {
         P2JIndex index = iter.next();
         
         if (unique && !index.isUnique())
         {
            continue;
         }
         
         // Special case:  omit index on (only) primary key.
         ArrayList<P2JIndexComponent> comps = index.components();
         for (int i = 0; i < comps.size(); i++)
         {
            P2JIndexComponent comp = comps.get(i);
            String compName = comp.getPropertyName();
            
            if (Session.PK.equals(compName))
            {
               continue outer;
            }
            else
            {
               break;
            }
         }
         
         if (list == null)
         {
            list = new ArrayList<>();
         }
         
         list.add(index.getName());
      }
      
      if (list != null)
      {
         list.trimToSize();
      }
      
      if (LOG_FINE)
      {
         LOG.log(Level.FINE,
                 "Indexes " + (unique ? "[unique]" : "[all]") + " for entity '" + entity + "':  " + list);
      }
      
      return list;
   }
   
   /**
    * Generate a mapping of index names to their component property names, for indexes associated
    * with the table represented by the given DMO entity name.
    * 
    * @param   entity
    *          A DMO entity name which represents a database table.
    * @param   unique
    *          If {@code true}, only unique indexes are returned;
    *          if {@code false}, only non-unique indexes are returned.
    * 
    * @return  A mapping of index names to ordered lists of the names of their component
    *          properties, for the given DMO entity. The map represents only unique indexes, or
    *          only non-unique indexes, depending upon the {@code unique} parameter. The map may
    *          be empty, but it will never by {@code null}. The property names will not be wrapped
    *          in any functions. The map will iterate in ascending alphabetical order by index
    *          name.
    *          
    * @throws  PersistenceException
    *          if there is any error querying index metadata from the JDBC driver of the
    *          associated database.
    */
   private Map<String, List<String>> lookupIndexMapForDMO(String entity, boolean unique)
   throws PersistenceException
   {
      Map<String, List<String>> map = new TreeMap<>(); // may be empty, but never null
      ArrayList<String> props = null;
      Iterator<P2JIndex> iter = DmoMetadataManager.getDmoInfo(entity, null).getDatabaseIndexes();
      if (iter == null)
      {
         return map;
      }
      while (iter.hasNext())
      {
         P2JIndex index = iter.next();
         
         if (index.isWord())
         {
            continue;
         }
         
         if (index.isUnique())
         {
            if (!unique)
            {
               continue;
            }
         }
         else if (unique)
         {
            continue;
         }
         
         props = new ArrayList<>();
         ArrayList<P2JIndexComponent> comps = index.components();
         for (int i = 0; i < comps.size(); i++)
         {
            P2JIndexComponent comp = comps.get(i);
            String propName = comp.getPropertyName();
            
            if (Session.PK.equals(propName))
            {
               continue;
            }
            
            props.add(propName);
         }
         
         if (!props.isEmpty())
         {
            props.trimToSize();
            map.put(index.getName(), props);
         }
      }
      
      if (LOG_FINE)
      {
         LOG.log(Level.FINE,
                 "Indexes " + (unique ? "[unique]" : "[non-unique]") +
                 " for entity '" + entity + "':  " + map);
      }
      
      return map;
   }
   
   /**
    * Collect a list of the names of all indexes in which the database column associated with the
    * given DMO property participates.
    * 
    * @param   entity
    *          A DMO entity name which represents a database table.
    * @param   property
    *          A DMO property which represents a database column.
    * 
    * @return  A list of index names in no particular order, or {@code null} if none are found.
    *          
    * @throws  PersistenceException
    *          if there is any error querying index metadata from the JDBC driver of the
    *          associated database.
    */
   private List<String> lookupIndexesForProperty(String entity, String property)
   throws PersistenceException
   {
      ArrayList<String> list = null;
      Iterator<P2JIndex> iter = DmoMetadataManager.getDmoInfo(entity, null).getDatabaseIndexes();
      if (iter == null)
      {
         return null;
      }
      
      while (iter.hasNext())
      {
         P2JIndex index = iter.next();
         ArrayList<P2JIndexComponent> comps = index.components();
         for (int i = 0; i < comps.size(); i++)
         {
            P2JIndexComponent next = comps.get(i);
            String propName = next.getPropertyName();
            
            if (propName != null && propName.equals(property))
            {
               if (list == null)
               {
                  list = new ArrayList<>();
               }
               list.add(index.getName());
               
               break;
            }
         }
      }
      
      if (list != null)
      {
         list.trimToSize();
      }
      
      if (LOG_FINE)
      {
         LOG.log(Level.FINE, "Indexes for '" + entity + ":" + property + "':  " + list);
      }
      
      return list;
   }
   
   /**
    * Given a sort phrase from an index-driven query, find the name of the index represented by 
    * the sort phrase. If a matching index is found, a {@link DMOSorter} instance is created and
    * cached by the sort clause, for later retrieval using the
    * {@link #getSorterForSortPhrase(RecordBuffer, String)} method.
    * 
    * @param   buffer
    *          Record buffer which contains information about the DMO entity associated with the
    *          sort phrase.
    * @param   sort
    *          A sort phrase which includes one or more DMO property references, each qualified by
    *          a DMO alias and followed by a keyword indicating sort direction.
    * 
    * @return  Name of the index associated with the given sort phrase, or {@code null} if no
    *          matching index can be found.
    * 
    * @throws  PersistenceException
    *          if there is any error querying index metadata from the JDBC driver of the
    *          associated database.
    */
   private Pair<String, Boolean> lookupIndexForSort(RecordBuffer buffer, String sort)
   throws PersistenceException
   {
      Pair<String, Boolean> ret = null;
      String indexName = null;
      Boolean direction = null;
      int badDirection = -1;
      SortKey key = new SortKey(buffer.getEntityName(), sort);
      DmoMeta dmoInfo = buffer.getDmoInfo();
      Iterator<P2JIndex> iter = dmoInfo.getDatabaseIndexes();
      Class<? extends DataModelObject> dmoIface = dmoInfo.getAnnotatedInterface();
      
      if (iter.hasNext())
      {
         List<SortCriterion> scList = SortCriterion.parse(sort, buffer, false);
         
         while (iter.hasNext())
         {
            boolean match = true;
            direction = null;
            int i = 0;
            badDirection = -1;
            P2JIndex index = iter.next();
            Iterator<P2JIndexComponent> comps = index.components(true);
            Iterator<SortCriterion> crits = scList.iterator();
            while (true) // loop iterators, exit only by breaks
            {
               boolean moreComps = comps.hasNext();
               boolean moreCrits = crits.hasNext();
               
               if (moreComps != moreCrits)
               {
                  match = false;
                  break;
               }
               
               if (!moreComps) // moreCrits and moreComps are the same 
               {
                  break;
               }
               
               P2JIndexComponent next = comps.next();
               String propName = next.getPropertyName();
               SortCriterion crit = crits.next();
               String critName = crit.getUnqualifiedName();
               
               if (!critName.equals(propName))
               {
                  match = false;
                  break;
               }
               
               i++;
               boolean sameDir = next.isDescending() != crit.isAscending();
               if (direction == null)
               {
                  // detect index direction (whether the order given by sort is the same as the index)
                  direction = sameDir; 
               }
               else
               {
                  // just test whether the direction is kept and the index is a match (even if completely
                  // reversed). If a discrepancy is detected a warning is issued, but the index is accepted
                  // TODO: in the latter case there is probably a programming error. The index will not be
                  //       matched server-side and the query will be slow!
                  if (direction != sameDir && badDirection == -1)
                  {
                     badDirection = i;
                  }
               }
            }
            
            if (match)
            {
               indexName = index.getName().intern();
               ret = Pair.of(indexName, direction);
               sorters.put(key, new DMOSorter(buffer.getDatabase(), 
                                              dmoIface, 
                                              SortCriterion.parse(sort, buffer, true)));
               
               if (badDirection != -1 && LOG.isLoggable(Level.INFO))
               {
                  LOG.log(Level.INFO,
                          "The sort properties (" + sort + ") match the index components (" + index +
                          ") but the direction of " + badDirection + " component is not consistent with " +
                          "the first one.  The index will NOT be used!");
               }
               break;
            }
         }
      }
      
      if (LOG_FINE)
      {
         LOG.log(Level.FINE,
                 "Index for sort phrase '" + sort + "':  " +
                 (indexName != null ? indexName : "?") + " [" + buffer.getTable() + "]");
      }
      
      if (indexName == null)
      {
         ret = Pair.of("", true);
      }
      
      indexBySort.put(key, ret);
      return ret;
   }
   
   /**
    * Collect the set of all indexed property names associated with the table
    * represented by the given DMO entity name.
    * <p>
    * Note:  the returned set will <em>not</em> include the reserved primary
    * key property {@code id}.
    * 
    * @param   entity
    *          A DMO entity name which represents a database table.
    * @param   unique
    *          If {@code true}, only properties associated with unique indexes are returned;
    *          if {@code false}, all indexed properties are returned.
    * 
    * @return  The names of all indexes for the given entity, or {@code null} if none are found.
    *          
    * @throws  PersistenceException
    *          if there is any error querying index metadata from the JDBC driver of the
    *          associated database.
    */
   private Set<String> lookupIndexedProperties(String entity, boolean unique)
   throws PersistenceException
   {
      Set<String> union = null;
      DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(entity, null);
      Iterator<P2JIndex> iter = dmoInfo.getDatabaseIndexes();
      Map<String, String> mappedCols = DatabaseManager.getColumnToPropertyMap(
            database, dmoInfo.getImplementationClass(), null, true);
      
      if (iter == null)
      {
         return null;
      }
      
      while (iter.hasNext())
      {
         P2JIndex index = iter.next();
         
         if (unique && !index.isUnique())
         {
            continue;
         }

         ArrayList<P2JIndexComponent> comps = index.components();
         for (int i = 0; i < comps.size(); i++)
         {
            P2JIndexComponent next = comps.get(i);
            String colName = next.getPropertyName();
            if (Session.PK.equals(colName))
            {
               continue;
            }
            
            String propName = mappedCols.get(colName);
            // detect possible DTZs; TODO: I do not like this approach (mappedCols), use the metadata instead
            if (propName == null)
            {
               propName = mappedCols.get(colName + "_timestamp");
            }
            
            if (propName == null)
            {
               if (index.isWord())
               {
                  // we just skip these properties (EXTENTS in a WORD-INDEX) for now
                  UnimplementedFeature.todo("Need full support for WORD-INDEX.");
                  continue;
               }
               else
               {
                  throw new NullPointerException("Cannot map column '" + colName + 
                                                 "' to a property in entity '" + entity + "'");
               }
            }
            
            if (union == null)
            {
               union = new HashSet<>();
            }
            
            union.add(propName);
         }
      }
      
      if (LOG_FINE)
      {
         LOG.log(Level.FINE,
                 "Indexed properties " + (unique ? "[unique]" : "[all]") +
                 " for entity '" + entity + "':  " + union);
      }
      
      return union;
   }
   
   /**
    * A hashable key which encapsulates a DMO entity and property name.
    */
   private static class PropertyKey
   {
      /** DMO entity name */
      private final String entity;
      
      /** DMO property name */
      private final String property;
      
      /**
       * Constructor.
       * 
       * @param   entity
       *          DMO entity name.
       * @param   property
       *          DMO property name.
       */
      PropertyKey(String entity, String property)
      {
         this.entity = entity;
         this.property = property;
      }
      
      /**
       * Override the default implementation.  Consistent with {@link
       * #equals(Object)}.
       * 
       * @return  Hash code.
       * 
       * @see     java.lang.Object#hashCode()
       */
      public int hashCode()
      {
         int result = 17;
         result = 37 * result + entity.hashCode();
         result = 37 * result + property.hashCode();
         
         return result;
      }
      
      /**
       * Override the default implementation.
       * 
       * @param   o
       *          Another instance of this class to compare with this one.
       * 
       * @return  <code>true</code> if <code>o</code> is equivalent to this
       *          object, else <code>false</code>.
       *          
       * @see     java.lang.Object#equals(java.lang.Object)
       */
      public boolean equals(Object o)
      {
         if (!(o instanceof PropertyKey))
         {
            return false;
         }
         
         PropertyKey that = (PropertyKey) o;
         
         return (entity.equals(that.entity) &&
                 property.equals(that.property));
      }
   }
   
   /**
    * A hashable key which encapsulates a DMO entity and a sort phrase.
    */
   private static class SortKey
   {
      /** DMO entity name */
      private final String entity;
      
      /** Sort phrase */
      private final String sort;
      
      /**
       * Constructor.
       * 
       * @param   entity
       *          DMO entity name.
       * @param   sort
       *          Sort phrase.
       */
      SortKey(String entity, String sort)
      {
         this.entity = entity;
         this.sort = sort;
      }
      
      /**
       * Override the default implementation.  Consistent with {@link #equals(Object)}.
       * 
       * @return  Hash code.
       * 
       * @see     java.lang.Object#hashCode()
       */
      public int hashCode()
      {
         int result = 17;
         result = 37 * result + entity.hashCode();
         result = 37 * result + (sort == null ? 0 : sort.hashCode());
         
         return result;
      }
      
      /**
       * Override the default implementation.
       * 
       * @param   o
       *          Another instance of this class to compare with this one.
       * 
       * @return  <code>true</code> if <code>o</code> is equivalent to this
       *          object, else <code>false</code>.
       *          
       * @see     java.lang.Object#equals(java.lang.Object)
       */
      public boolean equals(Object o)
      {
         if (!(o instanceof SortKey))
         {
            return false;
         }
         
         SortKey that = (SortKey) o;
         
         return entity.equals(that.entity) && Objects.equals(this.sort, that.sort);
      }
      
      /**
       * Provide a string representation of this object's state.
       * 
       * @return  String representation of this object.
       * 
       * @see java.lang.Object#toString()
       */
      public String toString()
      {
         return "SortKey [" + entity + ":" + sort + "]";
      }
   }
   
   /**
    * A hashable key which encapsulates a DMO entity and a boolean indicating
    * unique vs. non-unique indexes.
    */
   private static class EntityIndexKey
   {
      /** DMO entity name */
      private final String entity;
      
      /** Sort phrase */
      private final boolean unique;
      
      /**
       * Constructor.
       * 
       * @param   entity
       *          DMO entity name.
       * @param   unique
       *          Uniqueness flag.
       */
      EntityIndexKey(String entity, boolean unique)
      {
         this.entity = entity;
         this.unique = unique;
      }
      
      /**
       * Override the default implementation.  Consistent with {@link
       * #equals(Object)}.
       * 
       * @return  Hash code.
       * 
       * @see     java.lang.Object#hashCode()
       */
      public int hashCode()
      {
         int result = 17;
         result = 37 * result + entity.hashCode();
         result = 37 * result + (unique ? 1 : 0);
         
         return result;
      }
      
      /**
       * Override the default implementation.
       * 
       * @param   o
       *          Another instance of this class to compare with this one.
       * 
       * @return  <code>true</code> if <code>o</code> is equivalent to this
       *          object, else <code>false</code>.
       *          
       * @see     java.lang.Object#equals(java.lang.Object)
       */
      public boolean equals(Object o)
      {
         if (!(o instanceof EntityIndexKey))
         {
            return false;
         }
         
         EntityIndexKey that = (EntityIndexKey) o;
         
         return (entity.equals(that.entity) && unique == that.unique);
      }
   }
}