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