org.exist.xquery.GeneralComparison Maven / Gradle / Ivy
/*
* eXist-db Open Source Native XML Database
* Copyright (C) 2001 The eXist-db Authors
*
* [email protected]
* http://www.exist-db.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.exist.xquery;
import com.ibm.icu.text.Collator;
import org.exist.EXistException;
import org.exist.collections.Collection;
import org.exist.dom.persistent.ContextItem;
import org.exist.dom.persistent.DocumentSet;
import org.exist.dom.persistent.NewArrayNodeSet;
import org.exist.dom.persistent.NodeProxy;
import org.exist.dom.persistent.NodeSet;
import org.exist.dom.QName;
import org.exist.dom.persistent.VirtualNodeSet;
import org.exist.storage.DBBroker;
import org.exist.storage.ElementValue;
import org.exist.storage.IndexSpec;
import org.exist.storage.Indexable;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.Constants.Comparison;
import org.exist.xquery.Constants.StringTruncationOperator;
import org.exist.xquery.pragmas.Optimize;
import org.exist.xquery.util.ExpressionDumper;
import org.exist.xquery.value.AtomicValue;
import org.exist.xquery.value.BooleanValue;
import org.exist.xquery.value.Item;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.SequenceIterator;
import org.exist.xquery.value.StringValue;
import org.exist.xquery.value.Type;
import java.util.Iterator;
import java.util.List;
/**
* A general XQuery/XPath2 comparison expression.
*
* @author wolf
* @author [email protected]
* @author Adam Retter
*/
public class GeneralComparison extends BinaryOp implements Optimizable, IndexUseReporter
{
/** The type of operator used for the comparison, i.e. =, !=, <, > ... One of the constants declared in class {@link Constants}. */
protected Comparison relation = Comparison.EQ;
/**
* Truncation flags: when comparing with a string value, the search string may be truncated with a single * wildcard. See the constants declared
* in class {@link Constants}.
*
* The standard functions starts-with, ends-with and contains are transformed into a general comparison with wildcard. Hence the need to
* consider wildcards here.
*/
protected StringTruncationOperator truncation = StringTruncationOperator.NONE;
/** The class might cache the entire results of a previous execution. */
protected CachedResult cached = null;
/** Extra argument (to standard functions starts-with/contains etc.) to indicate the collation to be used for string comparisons. */
protected Object collationArg = null;
/** Set to true if this expression is called within the where clause of a FLWOR expression. */
protected boolean inWhereClause = false;
protected boolean invalidNodeEvaluation = false;
protected int rightOpDeps;
private boolean hasUsedIndex = false;
@SuppressWarnings( "unused" )
private int actualReturnType = Type.ITEM;
private LocationStep contextStep = null;
private QName contextQName = null;
protected boolean optimizeSelf = false;
protected boolean optimizeChild = false;
private int axis = Constants.UNKNOWN_AXIS;
private NodeSet preselectResult = null;
private IndexFlags idxflags = new IndexFlags();
public GeneralComparison( XQueryContext context, Comparison relation )
{
this( context, relation, StringTruncationOperator.NONE );
}
public GeneralComparison( XQueryContext context, Comparison relation, StringTruncationOperator truncation )
{
super( context );
this.relation = relation;
}
public GeneralComparison( XQueryContext context, Expression left, Expression right, Comparison relation )
{
this( context, left, right, relation, StringTruncationOperator.NONE );
}
public GeneralComparison( XQueryContext context, Expression left, Expression right, Comparison relation, final StringTruncationOperator truncation )
{
super( context );
boolean didLeftSimplification = false;
boolean didRightSimplification = false;
this.relation = relation;
this.truncation = truncation;
if( ( left instanceof PathExpr ) && ( ( ( PathExpr )left ).getLength() == 1 ) ) {
left = ( ( PathExpr )left ).getExpression( 0 );
didLeftSimplification = true;
}
add( left );
if( ( right instanceof PathExpr ) && ( ( ( PathExpr )right ).getLength() == 1 ) ) {
right = ( ( PathExpr )right ).getExpression( 0 );
didRightSimplification = true;
}
add( right );
//TODO : should we also use simplify() here ? -pb
if( didLeftSimplification ) {
context.getProfiler().message( this, Profiler.OPTIMIZATIONS, "OPTIMIZATION", "Marked left argument as a child expression" );
}
if( didRightSimplification ) {
context.getProfiler().message( this, Profiler.OPTIMIZATIONS, "OPTIMIZATION", "Marked right argument as a child expression" );
}
}
/* (non-Javadoc)
* @see org.exist.xquery.BinaryOp#analyze(org.exist.xquery.AnalyzeContextInfo)
*/
public void analyze( AnalyzeContextInfo contextInfo ) throws XPathException
{
contextInfo.addFlag( NEED_INDEX_INFO );
contextInfo.setParent( this );
super.analyze( contextInfo );
inWhereClause = ( contextInfo.getFlags() & IN_WHERE_CLAUSE ) != 0;
//Ugly workaround for the polysemy of "." which is expanded as self::node() even when it is not relevant
// (1)[.= 1] works...
invalidNodeEvaluation = false;
if( !Type.subTypeOf( contextInfo.getStaticType(), Type.NODE ) ) {
invalidNodeEvaluation = ( getLeft() instanceof LocationStep ) && ( ( ( LocationStep )getLeft() ).axis == Constants.SELF_AXIS );
}
//Unfortunately, we lose the possibility to make a nodeset optimization
//(we still don't know anything about the contextSequence that will be processed)
// check if the right-hand operand is a simple cast expression
// if yes, use the dependencies of the casted expression to compute
// optimizations
rightOpDeps = getRight().getDependencies();
getRight().accept( new BasicExpressionVisitor() {
public void visitCastExpr( CastExpression expression )
{
if( LOG.isTraceEnabled() ) {
LOG.debug( "Right operand is a cast expression" );
}
rightOpDeps = expression.getInnerExpression().getDependencies();
}
} );
contextInfo.removeFlag( NEED_INDEX_INFO );
final List steps = BasicExpressionVisitor.findLocationSteps( getLeft() );
if( !steps.isEmpty() ) {
LocationStep firstStep = steps.get( 0 );
LocationStep lastStep = steps.get( steps.size() - 1 );
if( firstStep != null && steps.size() == 1 && firstStep.getAxis() == Constants.SELF_AXIS) {
final Expression outerExpr = contextInfo.getContextStep();
if( ( outerExpr != null ) && ( outerExpr instanceof LocationStep ) ) {
final LocationStep outerStep = ( LocationStep )outerExpr;
final NodeTest test = outerStep.getTest();
if( !test.isWildcardTest() && ( test.getName() != null ) ) {
if( ( outerStep.getAxis() == Constants.ATTRIBUTE_AXIS ) || ( outerStep.getAxis() == Constants.DESCENDANT_ATTRIBUTE_AXIS ) ) {
contextQName = new QName(test.getName(), ElementValue.ATTRIBUTE);
} else {
contextQName = new QName(test.getName());
}
contextStep = firstStep;
axis = outerStep.getAxis();
optimizeSelf = true;
}
}
} else if (firstStep != null && lastStep != null) {
final NodeTest test = lastStep.getTest();
if( !test.isWildcardTest() && ( test.getName() != null ) ) {
if( ( lastStep.getAxis() == Constants.ATTRIBUTE_AXIS ) || ( lastStep.getAxis() == Constants.DESCENDANT_ATTRIBUTE_AXIS ) ) {
contextQName = new QName( test.getName(), ElementValue.ATTRIBUTE );
} else {
contextQName = new QName( test.getName() );
}
contextStep = lastStep;
axis = firstStep.getAxis();
if( ( axis == Constants.SELF_AXIS ) && ( steps.size() > 1 ) ) {
if (steps.get(1) != null) {
axis = steps.get( 1 ).getAxis();
} else {
contextQName = null;
contextStep = null;
axis = Constants.UNKNOWN_AXIS;
optimizeChild = false;
}
}
optimizeChild = ( steps.size() == 1 ) && ( ( axis == Constants.CHILD_AXIS ) || ( axis == Constants.ATTRIBUTE_AXIS ) );
}
}
}
}
public boolean canOptimize( Sequence contextSequence )
{
if( contextQName == null ) {
return( false );
}
return( Optimize.getQNameIndexType( context, contextSequence, contextQName ) != Type.ITEM );
}
public boolean optimizeOnSelf()
{
return( optimizeSelf );
}
public boolean optimizeOnChild()
{
return( optimizeChild );
}
public int getOptimizeAxis()
{
return( axis );
}
/* (non-Javadoc)
* @see org.exist.xquery.BinaryOp#returnsType()
*/
public int returnsType()
{
if( inPredicate && ( !Dependency.dependsOn( this, Dependency.CONTEXT_ITEM ) ) ) {
return( getLeft().returnsType() );
}
// In all other cases, we return boolean
return( Type.BOOLEAN );
}
/* (non-Javadoc)
* @see org.exist.xquery.AbstractExpression#getDependencies()
*/
public int getDependencies()
{
final Expression left = getLeft();
// variable dependencies should be reported to caller, so remember them here
final int deps = left.getDependencies() & Dependency.VARS;
// left expression returns node set
if( Type.subTypeOf( left.returnsType(), Type.NODE ) &&
// and does not depend on the context item
!Dependency.dependsOn( left, Dependency.CONTEXT_ITEM ) && ( !inWhereClause || !Dependency.dependsOn( left, Dependency.CONTEXT_VARS ) ) ) {
return( deps + Dependency.CONTEXT_SET );
} else {
return ( deps + Dependency.CONTEXT_SET + Dependency.CONTEXT_ITEM );
}
}
public Comparison getRelation()
{
return( this.relation );
}
public StringTruncationOperator getTruncation() {
return truncation;
}
public NodeSet preSelect( Sequence contextSequence, boolean useContext ) throws XPathException
{
// the expression can be called multiple times, so we need to clear the previous preselectResult
preselectResult = null;
final long start = System.currentTimeMillis();
final int indexType = Optimize.getQNameIndexType( context, contextSequence, contextQName );
if( LOG.isTraceEnabled() ) {
LOG.trace("Using QName index on type {}", Type.getTypeName(indexType));
}
final Sequence rightSeq = getRight().eval( contextSequence );
// if the right hand sequence has more than one item, we need to merge them
// into preselectResult
if (rightSeq.getItemCount() > 1)
{preselectResult = new NewArrayNodeSet();}
// Iterate through each item in the right-hand sequence
for( final SequenceIterator itRightSeq = Atomize.atomize(rightSeq).iterate(); itRightSeq.hasNext(); ) {
//Get the index key
Item key = itRightSeq.nextItem();
//if key has truncation, convert it to string
if( truncation != StringTruncationOperator.NONE ) {
if( !Type.subTypeOf( key.getType(), Type.STRING ) ) {
LOG.info("Truncated key. Converted from {} to xs:string", Type.getTypeName(key.getType()));
//truncation is only possible on strings
key = key.convertTo( Type.STRING );
}
}
//else if key is not the same type as the index
//TODO : use Type.isSubType() ??? -pb
else if( key.getType() != indexType ) {
//try to convert the key to the index type
try {
key = key.convertTo( indexType );
}
catch( final XPathException xpe ) {
if( LOG.isTraceEnabled() ) {
LOG.trace("Cannot convert key: {} to required index type: {}", Type.getTypeName(key.getType()), Type.getTypeName(indexType));
}
throw( new XPathException( this, "Cannot convert key to required index type" ) );
}
}
// If key implements org.exist.storage.Indexable, we can use the index
if( key instanceof Indexable ) {
if( LOG.isTraceEnabled() ) {
LOG.trace("Using QName range index for key: {}", key.getStringValue());
}
NodeSet temp;
final NodeSet contextSet = useContext ? contextSequence.toNodeSet() : null;
final Collator collator = ( ( collationArg != null ) ? getCollator( contextSequence ) : null );
if( truncation == StringTruncationOperator.NONE ) {
temp = context.getBroker().getValueIndex().find(context.getWatchDog(), relation, contextSequence.getDocumentSet(), contextSet, NodeSet.DESCENDANT, contextQName, ( Indexable )key);
hasUsedIndex = true;
} else {
try {
final String matchString = key.getStringValue();
final int matchType = getMatchType( truncation );
temp = context.getBroker().getValueIndex().match(context.getWatchDog(), contextSequence.getDocumentSet(), contextSet, NodeSet.DESCENDANT, matchString, contextQName, matchType, collator, truncation );
hasUsedIndex = true;
}
catch( final EXistException e ) {
throw( new XPathException( this, "Error during index lookup: " + e.getMessage(), e ) );
}
}
// if the right-hand sequence has more than one item,
// merge the result of the iteration into preselectResult,
// else replace it.
if( preselectResult == null ) {
preselectResult = temp;
} else {
preselectResult.addAll(temp);
}
}
}
if( context.getProfiler().traceFunctions() ) {
context.getProfiler().traceIndexUsage( context, PerformanceStats.RANGE_IDX_TYPE, this, PerformanceStats.OPTIMIZED_INDEX, System.currentTimeMillis() - start );
}
return( ( preselectResult == null ) ? NodeSet.EMPTY_SET : preselectResult );
}
/* (non-Javadoc)
* @see org.exist.xquery.Expression#eval(org.exist.xquery.StaticContext, org.exist.dom.persistent.DocumentSet, org.exist.xquery.value.Sequence, org.exist.xquery.value.Item)
*/
public Sequence eval( Sequence contextSequence, Item contextItem ) throws XPathException
{
if( context.getProfiler().isEnabled() ) {
context.getProfiler().start( this );
context.getProfiler().message( this, Profiler.DEPENDENCIES, "DEPENDENCIES", Dependency.getDependenciesName( this.getDependencies() ) );
if( contextSequence != null ) {
context.getProfiler().message( this, Profiler.START_SEQUENCES, "CONTEXT SEQUENCE", contextSequence );
}
if( contextItem != null ) {
context.getProfiler().message( this, Profiler.START_SEQUENCES, "CONTEXT ITEM", contextItem.toSequence() );
}
}
Sequence result;
// if the context sequence hasn't changed we can return a cached result
if( ( cached != null ) && cached.isValid( contextSequence, contextItem ) ) {
LOG.debug( "Using cached results" );
if( context.getProfiler().isEnabled() ) {
context.getProfiler().message( this, Profiler.OPTIMIZATIONS, "OPTIMIZATION", "Returned cached result" );
}
result = cached.getResult();
} else {
// if we were optimizing and the preselect did not return anything,
// we won't have any matches and can return
if( ( preselectResult != null ) && preselectResult.isEmpty() ) {
result = Sequence.EMPTY_SEQUENCE;
} else {
if( ( contextStep == null ) || ( preselectResult == null ) ) {
/*
* If we are inside a predicate and one of the arguments is a node set,
* we try to speed up the query by returning nodes from the context set.
* This works only inside a predicate. The node set will always be the left
* operand.
*/
if( inPredicate && !invalidNodeEvaluation && !Dependency.dependsOn( this, Dependency.CONTEXT_ITEM ) && Type.subTypeOf( getLeft().returnsType(), Type.NODE ) && ( ( contextSequence == null ) || contextSequence.isPersistentSet() ) ) {
if( contextItem != null ) {
contextSequence = contextItem.toSequence();
}
if( ( !Dependency.dependsOn( rightOpDeps, Dependency.CONTEXT_ITEM ) ) ) {
result = quickNodeSetCompare( contextSequence );
} else {
final NodeSet nodes = ( NodeSet )getLeft().eval( contextSequence );
result = nodeSetCompare( nodes, contextSequence );
}
} else {
result = genericCompare( contextSequence, contextItem );
}
} else {
contextStep.setPreloadedData( preselectResult.getDocumentSet(), preselectResult );
result = getLeft().eval( contextSequence ).toNodeSet();
// the expression can be called multiple times, so we need to clear the previous preselectResult
preselectResult = null;
}
}
// can this result be cached? Don't cache if the result depends on local variables.
final boolean canCache = ( contextSequence != null ) && contextSequence.isCacheable() && !Dependency.dependsOn( getLeft(), Dependency.CONTEXT_ITEM ) && !Dependency.dependsOn( getRight(), Dependency.CONTEXT_ITEM ) && !Dependency.dependsOnVar( getLeft() ) && !Dependency.dependsOnVar( getRight() );
if( canCache ) {
cached = new CachedResult( contextSequence, contextItem, result );
}
}
if( context.getProfiler().isEnabled() ) {
context.getProfiler().end( this, "", result );
}
actualReturnType = result.getItemType();
return( result );
}
/**
* Generic, slow implementation. Applied if none of the possible optimizations can be used.
*
* @param contextSequence the context sequence
* @param contextItem optional context item
*
* @return The Sequence resulting from the comparison
*
* @throws XPathException in case of dynamic error
*/
protected Sequence genericCompare( Sequence contextSequence, Item contextItem ) throws XPathException
{
if( context.getProfiler().isEnabled() ) {
context.getProfiler().message( this, Profiler.OPTIMIZATION_FLAGS, "OPTIMIZATION CHOICE", "genericCompare" );
}
final Sequence ls = getLeft().eval( contextSequence, contextItem );
return( genericCompare( ls, contextSequence, contextItem ) );
}
protected Sequence genericCompare( Sequence ls, Sequence contextSequence, Item contextItem ) throws XPathException
{
final long start = System.currentTimeMillis();
final Sequence rs = getRight().eval( contextSequence, contextItem );
final Collator collator = getCollator( contextSequence );
Sequence result = BooleanValue.FALSE;
if( ls.isEmpty() && rs.isEmpty() ) {
result = BooleanValue.valueOf( compareAtomic( collator, AtomicValue.EMPTY_VALUE, AtomicValue.EMPTY_VALUE ) );
} else if( ls.isEmpty() && !rs.isEmpty() ) {
for( final SequenceIterator i2 = Atomize.atomize(rs).iterate(); i2.hasNext(); ) {
if( compareAtomic( collator, AtomicValue.EMPTY_VALUE, i2.nextItem().atomize() ) ) {
result = BooleanValue.TRUE;
break;
}
}
} else if( !ls.isEmpty() && rs.isEmpty() ) {
for( final SequenceIterator i1 = Atomize.atomize(ls).iterate(); i1.hasNext(); ) {
final AtomicValue lv = i1.nextItem().atomize();
if( compareAtomic( collator, lv, AtomicValue.EMPTY_VALUE ) ) {
result = BooleanValue.TRUE;
break;
}
}
} else if( ls.hasOne() && rs.hasOne() && ls.itemAt(0).getType() != Type.ARRAY && rs.itemAt(0).getType() != Type.ARRAY) {
result = BooleanValue.valueOf( compareAtomic( collator, ls.itemAt( 0 ).atomize(), rs.itemAt( 0 ).atomize() ) );
} else {
for( final SequenceIterator i1 = Atomize.atomize(ls).iterate(); i1.hasNext(); ) {
final AtomicValue lv = i1.nextItem().atomize();
if( rs.isEmpty() ) {
if( compareAtomic( collator, lv, AtomicValue.EMPTY_VALUE ) ) {
result = BooleanValue.TRUE;
break;
}
} else if( rs.hasOne() && rs.itemAt(0).getType() != Type.ARRAY) {
if( compareAtomic( collator, lv, rs.itemAt( 0 ).atomize() ) ) {
//return early if we are successful, continue otherwise
result = BooleanValue.TRUE;
break;
}
} else {
for( final SequenceIterator i2 = Atomize.atomize(rs).iterate(); i2.hasNext(); ) {
if( compareAtomic( collator, lv, i2.nextItem().atomize() ) ) {
result = BooleanValue.TRUE;
break;
}
}
}
}
}
if( context.getProfiler().traceFunctions() ) {
context.getProfiler().traceIndexUsage( context, PerformanceStats.RANGE_IDX_TYPE, this, PerformanceStats.NO_INDEX, System.currentTimeMillis() - start );
}
return( result );
}
/**
* Optimized implementation, which can be applied if the left operand returns a node set. In this case, the left expression is executed first. All
* matching context nodes are then passed to the right expression.
*
* @param nodes DOCUMENT ME!
* @param contextSequence DOCUMENT ME!
*
* @return DOCUMENT ME!
*
* @throws XPathException DOCUMENT ME!
*/
protected Sequence nodeSetCompare( NodeSet nodes, Sequence contextSequence ) throws XPathException
{
if( context.getProfiler().isEnabled() ) {
context.getProfiler().message( this, Profiler.OPTIMIZATION_FLAGS, "OPTIMIZATION CHOICE", "nodeSetCompare" );
}
if( LOG.isTraceEnabled() ) {
LOG.trace( "No index: fall back to nodeSetCompare" );
}
final long start = System.currentTimeMillis();
final NodeSet result = new NewArrayNodeSet();
final Collator collator = getCollator( contextSequence );
if( ( contextSequence != null ) && !contextSequence.isEmpty() && !contextSequence.getDocumentSet().contains( nodes.getDocumentSet() ) ) {
for( final NodeProxy item : nodes ) {
ContextItem context = item.getContext();
if( context == null ) {
throw( new XPathException( this, "Internal error: context node missing" ) );
}
final AtomicValue lv = item.atomize();
do {
final Sequence rs = getRight().eval( context.getNode().toSequence() );
for( final SequenceIterator i2 = Atomize.atomize(rs).iterate(); i2.hasNext(); ) {
final AtomicValue rv = i2.nextItem().atomize();
if( compareAtomic( collator, lv, rv ) ) {
result.add( item );
}
}
} while( ( context = context.getNextDirect() ) != null );
}
} else {
for( final NodeProxy item : nodes ) {
final AtomicValue lv = item.atomize();
final Sequence rs = getRight().eval( contextSequence );
for( final SequenceIterator i2 = Atomize.atomize(rs).iterate(); i2.hasNext(); ) {
final AtomicValue rv = i2.nextItem().atomize();
if( compareAtomic( collator, lv, rv ) ) {
result.add( item );
}
}
}
}
if( context.getProfiler().traceFunctions() ) {
context.getProfiler().traceIndexUsage( context, PerformanceStats.RANGE_IDX_TYPE, this, PerformanceStats.NO_INDEX, System.currentTimeMillis() - start );
}
return( result );
}
/**
* Optimized implementation: first checks if a range index is defined on the nodes in the left argument.
* Otherwise, fall back to {@link #nodeSetCompare(NodeSet, Sequence)}.
*
* @param contextSequence DOCUMENT ME!
*
* @return DOCUMENT ME!
*
* @throws XPathException DOCUMENT ME!
*/
protected Sequence quickNodeSetCompare( Sequence contextSequence ) throws XPathException
{
/* TODO think about optimising fallback to NodeSetCompare() in the for loop!!!
* At the moment when we fallback to NodeSetCompare() we are in effect throwing away any nodes
* we have already processed in quickNodeSetCompare() and reprocessing all the nodes in NodeSetCompare().
* Instead - Could we create a NodeCompare() (based on NodeSetCompare() code) to only compare a single node and then union the result?
* - deliriumsky
*/
if( context.getProfiler().isEnabled() ) {
context.getProfiler().message( this, Profiler.OPTIMIZATION_FLAGS, "OPTIMIZATION CHOICE", "quickNodeSetCompare" );
}
final long start = System.currentTimeMillis();
//get the NodeSet on the left
final Sequence leftSeq = getLeft().eval( contextSequence );
if( !leftSeq.isPersistentSet() ) {
return( genericCompare( leftSeq, contextSequence, null ) );
}
final NodeSet nodes = leftSeq.isEmpty() ? NodeSet.EMPTY_SET : ( NodeSet )leftSeq;
//nothing on the left, so nothing to do
if( !( nodes instanceof VirtualNodeSet ) && nodes.isEmpty() ) {
//Well, we might discuss this one ;-)
hasUsedIndex = true;
return( Sequence.EMPTY_SEQUENCE );
}
//get the Sequence on the right
final Sequence rightSeq = getRight().eval( contextSequence );
//nothing on the right, so nothing to do
if( rightSeq.isEmpty() ) {
//Well, we might discuss this one ;-)
hasUsedIndex = true;
return( Sequence.EMPTY_SEQUENCE );
}
//get the type of a possible index
final int indexType = nodes.getIndexType();
//See if we have a range index defined on the nodes in this sequence
//remember that Type.ITEM means... no index ;-)
if( indexType != Type.ITEM ) {
if( LOG.isTraceEnabled() ) {
LOG.trace("found an index of type: {}", Type.getTypeName(indexType));
}
boolean indexScan = false;
boolean indexMixed = false;
QName myContextQName = contextQName;
if( contextSequence != null ) {
final IndexFlags iflags = checkForQNameIndex( idxflags, context, contextSequence, myContextQName );
boolean indexFound = false;
if( !iflags.indexOnQName ) {
// if myContextQName != null and no index is defined on
// myContextQName, we don't need to scan other QName indexes
// and can just use the generic range index
indexFound = myContextQName != null;
if (iflags.partialIndexOnQName) {
indexMixed = true;
} else {
// set myContextQName to null so the index lookup below is not
// restricted to that QName
myContextQName = null;
}
}
if( !indexFound && ( myContextQName == null ) ) {
// if there are some indexes defined on a qname,
// we need to check them all
if( iflags.hasIndexOnQNames ) {
indexScan = true;
}
// else use range index defined on path by default
}
} else {
return( nodeSetCompare( nodes, contextSequence ) );
}
//Get the documents from the node set
final DocumentSet docs = nodes.getDocumentSet();
//Holds the result
NodeSet result = null;
//Iterate through the right hand sequence
for( final SequenceIterator itRightSeq = Atomize.atomize(rightSeq).iterate(); itRightSeq.hasNext(); ) {
//Get the index key
Item key = itRightSeq.nextItem();
//if key has truncation, convert it to string
if( truncation != StringTruncationOperator.NONE ) {
if( !Type.subTypeOf( key.getType(), Type.STRING ) ) {
LOG.info("Truncated key. Converted from {} to xs:string", Type.getTypeName(key.getType()));
//truncation is only possible on strings
key = key.convertTo( Type.STRING );
}
}
//else if key is not the same type as the index
//TODO : use Type.isSubType() ??? -pb
else if( key.getType() != indexType ) {
//try to convert the key to the index type
try {
key = key.convertTo( indexType );
}
catch( final XPathException xpe ) {
//TODO : rethrow the exception ? -pb
//Could not convert the key to a suitable type for the index, fallback to nodeSetCompare()
if( context.getProfiler().isEnabled() ) {
context.getProfiler().message( this, Profiler.OPTIMIZATION_FLAGS, "OPTIMIZATION FALLBACK", "Falling back to nodeSetCompare (" + xpe.getMessage() + ")" );
}
if( LOG.isTraceEnabled() ) {
LOG.trace("Cannot convert key: {} to required index type: {}", Type.getTypeName(key.getType()), Type.getTypeName(indexType));
}
return( nodeSetCompare( nodes, contextSequence ) );
}
}
// If key implements org.exist.storage.Indexable, we can use the index
if( key instanceof Indexable ) {
if( LOG.isTraceEnabled() ) {
LOG.trace("Checking if range index can be used for key: {}", key.getStringValue());
}
final Collator collator = ( ( collationArg != null ) ? getCollator( contextSequence ) : null );
if( Type.subTypeOf( key.getType(), indexType ) ) {
if( truncation == StringTruncationOperator.NONE ) {
if( LOG.isTraceEnabled() ) {
LOG.trace("Using range index for key: {}", key.getStringValue());
}
//key without truncation, find key
context.getProfiler().message( this, Profiler.OPTIMIZATIONS, "OPTIMIZATION", "Using value index '" + context.getBroker().getValueIndex().toString() + "' to find key '" + Type.getTypeName( key.getType() ) + "(" + key.getStringValue() + ")'" );
NodeSet ns;
if( indexScan ) {
ns = context.getBroker().getValueIndex().findAll( context.getWatchDog(), relation, docs, nodes, NodeSet.ANCESTOR, ( Indexable )key);
} else {
ns = context.getBroker().getValueIndex().find( context.getWatchDog(), relation, docs, nodes, NodeSet.ANCESTOR, myContextQName,
( Indexable )key, indexMixed );
}
hasUsedIndex = true;
if( result == null ) {
result = ns;
} else {
result = result.union( ns );
}
} else {
//key with truncation, match key
if( LOG.isTraceEnabled() ) {
context.getProfiler().message( this, Profiler.OPTIMIZATIONS, "OPTIMIZATION", "Using value index '" + context.getBroker().getValueIndex().toString() + "' to match key '" + Type.getTypeName( key.getType() ) + "(" + key.getStringValue() + ")'" );
}
if( LOG.isTraceEnabled() ) {
LOG.trace("Using range index for key: {}", key.getStringValue());
}
try {
NodeSet ns;
final String matchString = key.getStringValue();
final int matchType = getMatchType( truncation );
if( indexScan ) {
ns = context.getBroker().getValueIndex().matchAll( context.getWatchDog(), docs, nodes, NodeSet.ANCESTOR, matchString, matchType, 0, true, collator, truncation );
} else {
ns = context.getBroker().getValueIndex().match( context.getWatchDog(), docs, nodes, NodeSet.ANCESTOR, matchString, myContextQName, matchType, collator, truncation );
}
hasUsedIndex = true;
if( result == null ) {
result = ns;
} else {
result = result.union( ns );
}
}
catch( final EXistException e ) {
throw( new XPathException( this, e ) );
}
}
} else {
//our key does is not of the correct type
if( context.getProfiler().isEnabled() ) {
context.getProfiler().message( this, Profiler.OPTIMIZATION_FLAGS, "OPTIMIZATION FALLBACK", "Falling back to nodeSetCompare (key is of type: " + Type.getTypeName( key.getType() ) + ") whereas index is of type '" + Type.getTypeName( indexType ) + "'" );
}
if( LOG.isTraceEnabled() ) {
LOG.trace("Cannot use range index: key is of type: {}) whereas index is of type '{}", Type.getTypeName(key.getType()), Type.getTypeName(indexType));
}
return( nodeSetCompare( nodes, contextSequence ) );
}
} else {
//our key does not implement org.exist.storage.Indexable
if( context.getProfiler().isEnabled() ) {
context.getProfiler().message( this, Profiler.OPTIMIZATION_FLAGS, "OPTIMIZATION FALLBACK", "Falling back to nodeSetCompare (key is not an indexable type: " + key.getClass().getName() );
}
if( LOG.isTraceEnabled() ) {
LOG.trace("Cannot use key which is of type '{}", key.getClass().getName());
}
return( nodeSetCompare( nodes, contextSequence ) );
}
}
if( context.getProfiler().traceFunctions() ) {
context.getProfiler().traceIndexUsage( context, PerformanceStats.RANGE_IDX_TYPE, this, PerformanceStats.BASIC_INDEX, System.currentTimeMillis() - start );
}
return( result );
} else {
if( LOG.isTraceEnabled() ) {
LOG.trace("No suitable index found for key: {}", rightSeq.getStringValue());
}
//no range index defined on the nodes in this sequence, so fallback to nodeSetCompare
if( context.getProfiler().isEnabled() ) {
context.getProfiler().message( this, Profiler.OPTIMIZATION_FLAGS, "OPTIMIZATION FALLBACK", "falling back to nodeSetCompare (no index available)" );
}
return( nodeSetCompare( nodes, contextSequence ) );
}
}
private int getMatchType( StringTruncationOperator truncation ) throws XPathException
{
int matchType;
// Figure out what type of matching we need to do.
switch( truncation ) {
case RIGHT: {
matchType = DBBroker.MATCH_STARTSWITH;
break;
}
case LEFT: {
matchType = DBBroker.MATCH_ENDSWITH;
break;
}
case BOTH: {
matchType = DBBroker.MATCH_CONTAINS;
break;
}
case EQUALS: {
matchType = DBBroker.MATCH_EXACT;
break;
}
default: {
// We should never get here!
LOG.error("Invalid truncation type: {}", truncation);
throw( new XPathException( this, "Invalid truncation type: " + truncation ) );
}
}
return( matchType );
}
private CharSequence getRegexp( String expr )
{
switch( truncation ) {
case LEFT: {
return( new StringBuilder().append( expr ).append( '$' ) );
}
case RIGHT: {
return( new StringBuilder().append( '^' ).append( expr ) );
}
default: {
return( expr );
}
}
}
private AtomicValue convertForGeneralComparison(final AtomicValue value, final int thisType, final int otherType)
throws XPathException {
if (thisType == Type.UNTYPED_ATOMIC) {
try {
/*
If both atomic values are instances of xs:untypedAtomic,
then the values are cast to the type xs:string.
*/
if (otherType == Type.UNTYPED_ATOMIC) {
return value.convertTo(Type.STRING);
}
// it is cast to a type depending on the other value's dynamic type T
/*
i. If T is a numeric type or is derived from a numeric type,
then V is cast to xs:double.
*/
if (Type.subTypeOfUnion(otherType, Type.NUMBER)) {
return value.convertTo(Type.DOUBLE);
}
/*
ii. If T is xs:dayTimeDuration or is derived from xs:dayTimeDuration,
then V is cast to xs:dayTimeDuration.
*/
if (Type.subTypeOf(otherType, Type.DAY_TIME_DURATION)) {
return value.convertTo(Type.DAY_TIME_DURATION);
}
/*
iii. If T is xs:yearMonthDuration or is derived from xs:yearMonthDuration,
then V is cast to xs:yearMonthDuration.
*/
if (Type.subTypeOf(otherType, Type.YEAR_MONTH_DURATION)) {
return value.convertTo(Type.YEAR_MONTH_DURATION);
}
/*
iv. In all other cases, V is cast to the primitive base type of T.
*/
return value.convertTo(otherType);
} catch (XPathException e) {
if (e.getErrorCode() != ErrorCodes.FORG0001) {
e = new XPathException(ErrorCodes.FORG0001, e.getMessage(), e);
}
throw e;
}
}
return value;
}
private AtomicValue convertForValueComparison(final AtomicValue value, final int thisType, final int otherType)
throws XPathException {
/*
if the two operands are instances of different primitive types then:
*/
if (Type.primitiveTypeOf(thisType) != Type.primitiveTypeOf(otherType)) {
/*
a. If each operand is an instance of one of the types xs:string or xs:anyURI,
then both operands are cast to type xs:string.
*/
if ((Type.subTypeOf(thisType, Type.STRING) || thisType == Type.ANY_URI)
&& (Type.subTypeOf(otherType, Type.STRING) || otherType ==Type.ANY_URI)) {
return value.convertTo(Type.STRING);
}
/*
b. If each operand is an instance of one of the types xs:decimal or xs:float,
then both operands are cast to type xs:float.
*/
if ((Type.subTypeOf(thisType, Type.DECIMAL) || thisType == Type.FLOAT)
&& (Type.subTypeOf(otherType, Type.DECIMAL) || otherType == Type.FLOAT)) {
return value.convertTo(Type.FLOAT);
}
/*
* c. If each operand is an instance of one of the types xs:decimal, xs:float, or xs:double,
* then both operands are cast to type xs:double.
*/
if ((Type.subTypeOf(thisType, Type.DECIMAL) || thisType == Type.FLOAT || thisType == Type.DOUBLE)
&& (Type.subTypeOf(otherType, Type.DECIMAL) || otherType == Type.FLOAT || otherType == Type.DOUBLE)) {
return value.convertTo(Type.DOUBLE);
}
/*
* d. Otherwise, a type error is raised [err:XPTY0004].
*/
throw new XPathException(ErrorCodes.XPTY0004, "Incompatible primitive types");
}
return value;
}
/**
* Cast the atomic operands into a comparable type and compare them.
*
* @param collator the collator to use for comparisons
* @param lv left-hand-side value of comparison
* @param rv right-hand-side value of comparison
*
* @return true if the comparison holds, false otherwise
*
* @throws XPathException if an error occurs during the comparison
*/
private boolean compareAtomic(final Collator collator, AtomicValue lv, AtomicValue rv) throws XPathException {
// get types locally as convertForCompareAtomic may change the types of the AtomicValue itself
int ltype = lv.getType();
int rtype = rv.getType();
lv = convertForGeneralComparison(lv, ltype, rtype);
rv = convertForGeneralComparison(rv, rtype, ltype);
// if truncation is set, we always do a string comparison
if (truncation != StringTruncationOperator.NONE) {
lv = lv.convertTo(Type.STRING);
}
switch (truncation) {
case RIGHT:
return lv.startsWith(collator, rv);
case LEFT:
return lv.endsWith(collator, rv);
case BOTH:
return lv.contains(collator, rv);
}
/*
* If an atomized operand is an empty sequence, the result of the value comparison is an empty sequence
* and the implementation need not evaluate the other operand or apply the operator.
*/
if (lv.isEmpty() || rv.isEmpty()) {
return false;
}
// get types locally as convertForValueComparison may change the types of the AtomicValue itself
ltype = lv.getType();
rtype = rv.getType();
lv = convertForValueComparison(lv, ltype, rtype);
rv = convertForValueComparison(rv, rtype, ltype);
return lv.compareTo(collator, relation, rv);
}
/**
* DOCUMENT ME!
*
* @param lv
*
* @return Whether or not lv
is an empty string
*
* @throws XPathException
*/
@SuppressWarnings( "unused" )
private static boolean isEmptyString( AtomicValue lv ) throws XPathException
{
if( Type.subTypeOf( lv.getType(), Type.STRING ) || ( lv.getType() == Type.ATOMIC ) ) {
if( lv.getStringValue().length() == 0 ) {
return( true );
}
}
return( false );
}
public boolean hasUsedIndex()
{
return( hasUsedIndex );
}
/* (non-Javadoc)
* @see org.exist.xquery.PathExpr#dump(org.exist.xquery.util.ExpressionDumper)
*/
public void dump( ExpressionDumper dumper )
{
if( truncation == StringTruncationOperator.BOTH ) {
dumper.display( "contains" ).display( '(' );
getLeft().dump( dumper );
dumper.display( ", " );
getRight().dump( dumper );
dumper.display( ")" );
} else {
getLeft().dump( dumper );
dumper.display( ' ' ).display( relation.generalComparisonSymbol ).display( ' ' );
getRight().dump( dumper );
}
}
public String toString()
{
final StringBuilder result = new StringBuilder();
if( truncation == StringTruncationOperator.BOTH ) {
result.append( "contains" ).append( '(' );
result.append( getLeft().toString() );
result.append( ", " );
result.append( getRight().toString() );
result.append( ")" );
} else {
result.append( getLeft().toString() );
result.append( ' ' ).append( relation.generalComparisonSymbol ).append( ' ' );
result.append( getRight().toString() );
}
return( result.toString() );
}
protected void switchOperands()
{
context.getProfiler().message( this, Profiler.OPTIMIZATIONS, "OPTIMIZATION", "Switching operands" );
//Invert relation
switch(relation) {
case GT:
relation = Comparison.LT;
break;
case LT:
relation = Comparison.GT;
break;
case LTEQ:
relation = Comparison.GTEQ;
break;
case GTEQ:
relation = Comparison.LTEQ;
break;
//What about Comparison.EQand Comparison.NEQ? Well, it seems to never be called
}
final Expression right = getRight();
setRight(getLeft());
setLeft(right);
}
/**
* Possibly switch operands to simplify execution.
*/
protected void simplifyOperands()
{
//Prefer nodes at the left hand
if( ( !Type.subTypeOf( getLeft().returnsType(), Type.NODE ) ) && Type.subTypeOf( getRight().returnsType(), Type.NODE ) ) {
switchOperands();
}
//Prefer fewer items at the left hand
else if (Cardinality._MANY.isSuperCardinalityOrEqualOf(getLeft().getCardinality())
&& !Cardinality._MANY.isSuperCardinalityOrEqualOf(getRight().getCardinality())) {
switchOperands();
}
}
protected Collator getCollator( Sequence contextSequence ) throws XPathException
{
if( collationArg == null ) {
return( context.getDefaultCollator() );
}
String collationURI;
if( collationArg instanceof Expression ) {
collationURI = ( ( Expression )collationArg ).eval( contextSequence ).getStringValue();
} else if( collationArg instanceof StringValue ) {
collationURI = ( ( StringValue )collationArg ).getStringValue();
} else {
return( context.getDefaultCollator() );
}
return( context.getCollator( collationURI ) );
}
public void setCollation( Object collationArg )
{
this.collationArg = collationArg;
}
public final static IndexFlags checkForQNameIndex( IndexFlags idxflags, XQueryContext context, Sequence contextSequence, QName contextQName )
{
idxflags.reset( contextQName != null );
for( final Iterator i = contextSequence.getCollectionIterator(); i.hasNext(); ) {
final Collection collection = i.next();
if( collection.getURI().equalsInternal( XmldbURI.SYSTEM_COLLECTION_URI ) ) {
continue;
}
final IndexSpec idxcfg = collection.getIndexConfiguration( context.getBroker() );
if( idxflags.indexOnQName && ( idxcfg.getIndexByQName( contextQName ) == null ) ) {
idxflags.indexOnQName = false;
if( LOG.isTraceEnabled() ) {
LOG.trace("cannot use index on QName: {}. Collection {} does not define an index", contextQName, collection.getURI());
}
}
if( !idxflags.hasIndexOnQNames && idxcfg.hasIndexesByQName() ) {
idxflags.hasIndexOnQNames = true;
}
if( !idxflags.hasIndexOnPaths && idxcfg.hasIndexesByPath() ) {
idxflags.hasIndexOnPaths = true;
}
}
return( idxflags );
}
/* (non-Javadoc)
* @see org.exist.xquery.PathExpr#resetState()
*/
public void resetState( boolean postOptimization )
{
super.resetState( postOptimization );
getLeft().resetState( postOptimization );
getRight().resetState( postOptimization );
if( !postOptimization ) {
cached = null;
preselectResult = null;
hasUsedIndex = false;
}
}
public void accept( ExpressionVisitor visitor )
{
visitor.visitGeneralComparison( this );
}
public final static class IndexFlags
{
public boolean indexOnQName = true;
public boolean partialIndexOnQName = false;
public boolean hasIndexOnPaths = false;
public boolean hasIndexOnQNames = false;
public boolean indexOnQName()
{
return( indexOnQName );
}
public boolean hasIndexOnPaths()
{
return( hasIndexOnPaths );
}
public boolean hasIndexOnQNames()
{
return( hasIndexOnQNames );
}
public void reset( boolean indexOnQName )
{
this.indexOnQName = indexOnQName;
this.partialIndexOnQName = false;
this.hasIndexOnPaths = false;
this.hasIndexOnQNames = false;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy