/* * eXist Open Source Native XML Database * * Copyright (C) 2000-03, Wolfgang M. Meier (meier@ifs. tu- darmstadt. de) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public License * as published by the Free Software Foundation; either version 2 * 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 Library General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * $Id$ */ package org.exist.xquery; import org.exist.xquery.pragmas.Optimize; import org.exist.EXistException; import org.exist.collections.Collection; import org.exist.dom.ContextItem; import org.exist.dom.DocumentSet; import org.exist.dom.NewArrayNodeSet; import org.exist.dom.NodeProxy; import org.exist.dom.NodeSet; import org.exist.dom.QName; import org.exist.dom.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.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.Type; import java.text.Collator; import java.util.Iterator; import java.util.List; /** * A general XQuery/XPath2 comparison expression. * * @author wolf */ 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 int relation = Constants.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 int truncation = Constants.TRUNC_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 Expression 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; private int actualReturnType = Type.ITEM; private LocationStep contextStep = null; private QName contextQName = null; protected boolean optimizeSelf = false; private int axis = Constants.UNKNOWN_AXIS; private NodeSet preselectResult = null; private IndexFlags idxflags = new IndexFlags(); public GeneralComparison(XQueryContext context, int relation) { this(context, relation, Constants.TRUNC_NONE); } public GeneralComparison(XQueryContext context, int relation, int truncation) { super(context); this.relation = relation; } public GeneralComparison(XQueryContext context, Expression left, Expression right, int relation) { this(context, left, right, relation, Constants.TRUNC_NONE); } public GeneralComparison(XQueryContext context, Expression left, Expression right, int relation, int 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(); } }); if (contextInfo.getContextStep() != null && contextInfo.getContextStep() instanceof LocationStep) { ((LocationStep)contextInfo.getContextStep()).setUseDirectAttrSelect(false); } contextInfo.removeFlag(NEED_INDEX_INFO); List steps = BasicExpressionVisitor.findLocationSteps(getLeft()); if (!steps.isEmpty()) { LocationStep firstStep = (LocationStep) steps.get(0); LocationStep lastStep = (LocationStep) steps.get(steps.size() - 1); if (steps.size() == 1 && firstStep.getAxis() == Constants.SELF_AXIS) { Expression outerExpr = contextInfo.getContextStep(); if (outerExpr != null && outerExpr instanceof LocationStep) { LocationStep outerStep = (LocationStep) outerExpr; NodeTest test = outerStep.getTest(); if (!test.isWildcardTest() && test.getName() != null) { contextQName = new QName(test.getName()); if (outerStep.getAxis() == Constants.ATTRIBUTE_AXIS || outerStep.getAxis() == Constants.DESCENDANT_ATTRIBUTE_AXIS) contextQName.setNameType(ElementValue.ATTRIBUTE); contextStep = firstStep; axis = outerStep.getAxis(); optimizeSelf = true; } } } else { NodeTest test = lastStep.getTest(); if (!test.isWildcardTest() && test.getName() != null) { contextQName = new QName(test.getName()); if (lastStep.getAxis() == Constants.ATTRIBUTE_AXIS || lastStep.getAxis() == Constants.DESCENDANT_ATTRIBUTE_AXIS) contextQName.setNameType(ElementValue.ATTRIBUTE); contextStep = lastStep; axis = firstStep.getAxis(); if (axis == Constants.SELF_AXIS && steps.size() > 1) axis = ((LocationStep) steps.get(1)).getAxis(); } } } } public boolean canOptimize(Sequence contextSequence) { if (contextQName == null) return false; return Optimize.getQNameIndexType(context, contextSequence, contextQName) != Type.ITEM; } public boolean optimizeOnSelf() { return optimizeSelf; } 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() { // left expression returns node set if (Type.subTypeOf(getLeft().returnsType(), Type.NODE) && // and does not depend on the context item !Dependency.dependsOn(getLeft(), Dependency.CONTEXT_ITEM) && (!inWhereClause || !Dependency.dependsOn(getLeft(), Dependency.CONTEXT_VARS))) { return Dependency.CONTEXT_SET; } else { return Dependency.CONTEXT_SET + Dependency.CONTEXT_ITEM; } } public int getRelation() { return this.relation; } 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; long start = System.currentTimeMillis(); int indexType = Optimize.getQNameIndexType(context, contextSequence, contextQName); if (LOG.isTraceEnabled()) LOG.trace("Using QName index on type " + Type.getTypeName(indexType)); Sequence rightSeq = getRight().eval(contextSequence); if (rightSeq.getItemCount() > 1) preselectResult = new NewArrayNodeSet(); for (SequenceIterator itRightSeq = rightSeq.iterate(); itRightSeq.hasNext();) { //Get the index key Item key = itRightSeq.nextItem().atomize(); //if key has truncation, convert it to string if(truncation != Constants.TRUNC_NONE) { if (!Type.subTypeOf(key.getType(), Type.STRING)) { LOG.info("Truncated key. Converted from " + Type.getTypeName(key.getType()) + " to xs:string"); //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(XPathException xpe) { if (LOG.isTraceEnabled()) LOG.trace("Cannot convert key: " + Type.getTypeName(key.getType()) + " to required index type: " + 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; NodeSet contextSet = useContext ? contextSequence.toNodeSet() : null; if(truncation == Constants.TRUNC_NONE) { temp = context.getBroker().getValueIndex().find(relation, contextSequence.getDocumentSet(), contextSet, NodeSet.DESCENDANT, contextQName, (Indexable)key); hasUsedIndex = true; } else { try { temp = context.getBroker().getValueIndex().match(contextSequence.getDocumentSet(), contextSet, NodeSet.DESCENDANT, getRegexp(key.getStringValue()).toString(), contextQName, DBBroker.MATCH_REGEXP); hasUsedIndex = true; } catch (EXistException e) { throw new XPathException(this, "Error during index lookup: " + e.getMessage(), e); } } 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.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 { 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(); } } // can this result be cached? Don't cache if the result depends on local variables. 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 * @param contextItem * @return The Sequence resulting from the comparison * @throws XPathException */ 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 { 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 (SequenceIterator i2 = rs.iterate(); i2.hasNext();) { if (compareAtomic(collator, AtomicValue.EMPTY_VALUE, i2.nextItem().atomize())) { result = BooleanValue.TRUE; break; } } } else if (!ls.isEmpty()&& rs.isEmpty()) { for (SequenceIterator i1 = ls.iterate(); i1.hasNext();) { AtomicValue lv = i1.nextItem().atomize(); if (compareAtomic(collator, lv, AtomicValue.EMPTY_VALUE)) { result = BooleanValue.TRUE; break; } } } else if (ls.hasOne() && rs.hasOne()) { result = BooleanValue.valueOf(compareAtomic(collator, ls.itemAt(0).atomize(), rs.itemAt(0).atomize())); } else { for (SequenceIterator i1 = ls.iterate(); i1.hasNext();) { AtomicValue lv = i1.nextItem().atomize(); if (rs.isEmpty()) { if (compareAtomic(collator, lv, AtomicValue.EMPTY_VALUE)) { result = BooleanValue.TRUE; break; } } else if (rs.hasOne()) { if (compareAtomic(collator, lv, rs.itemAt(0).atomize())) { //return early if we are successful, continue otherwise result = BooleanValue.TRUE; break; } } else { for (SequenceIterator i2 = 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. */ 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"); long start = System.currentTimeMillis(); NodeSet result = new NewArrayNodeSet(); final Collator collator = getCollator(contextSequence); if (contextSequence != null && !contextSequence.isEmpty() && !contextSequence.getDocumentSet().contains(nodes.getDocumentSet())) { for (Iterator i1 = nodes.iterator(); i1.hasNext();) { NodeProxy item = (NodeProxy) i1.next(); ContextItem context = item.getContext(); if (context == null) throw new XPathException(this, "Internal error: context node missing"); AtomicValue lv = item.atomize(); do { Sequence rs = getRight().eval(context.getNode().toSequence()); for (SequenceIterator i2 = rs.iterate(); i2.hasNext();) { AtomicValue rv = i2.nextItem().atomize(); if (compareAtomic(collator, lv, rv)) result.add(item); } } while ((context = context.getNextDirect()) != null); } } else { for (Iterator i1 = nodes.iterator(); i1.hasNext();) { NodeProxy item = (NodeProxy) i1.next(); AtomicValue lv = item.atomize(); Sequence rs = getRight().eval(contextSequence); for (SequenceIterator i2 = rs.iterate(); i2.hasNext();) { 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. If that fails, check if we can use * the fulltext index to speed up the search. Otherwise, fall back to * {@link #nodeSetCompare(NodeSet, Sequence)}. */ 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"); long start = System.currentTimeMillis(); //get the NodeSet on the left Sequence leftSeq = getLeft().eval(contextSequence); if (!leftSeq.isPersistentSet()) return genericCompare(leftSeq, contextSequence, null); 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 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 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; if (contextSequence != null) { IndexFlags iflags = checkForQNameIndex(idxflags, context, contextSequence, contextQName); boolean indexFound = false; if (!iflags.indexOnQName) { // if contextQName != null and no index is defined on // contextQName, we don't need to scan other QName indexes // and can just use the generic range index indexFound = contextQName != null; // if there's a qname index on some collection, scan them as well if (iflags.partialIndexOnQName) { indexMixed = true; } else { // set contextQName to null so the index lookup below is not // restricted to that QName contextQName = null; } } if (!indexFound && contextQName == 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 (SequenceIterator itRightSeq = rightSeq.iterate(); itRightSeq.hasNext();) { //Get the index key Item key = itRightSeq.nextItem().atomize(); //if key has truncation, convert it to string if(truncation != Constants.TRUNC_NONE) { if (!Type.subTypeOf(key.getType(), Type.STRING)) { LOG.info("Truncated key. Converted from " + Type.getTypeName(key.getType()) + " to xs:string"); //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(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: " + Type.getTypeName(key.getType()) + " to required index type: " + 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()); if (Type.subTypeOf(key.getType(), indexType)) { if(truncation == Constants.TRUNC_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(relation, docs, nodes, NodeSet.ANCESTOR, (Indexable)key); else { ns = context.getBroker().getValueIndex().find(relation, docs, nodes, NodeSet.ANCESTOR, contextQName, (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; if (indexScan) ns = context.getBroker().getValueIndex().matchAll(docs, nodes, NodeSet.ANCESTOR, getRegexp(key.getStringValue()).toString(), DBBroker.MATCH_REGEXP, 0, true); else ns = context.getBroker().getValueIndex().match(docs, nodes, NodeSet.ANCESTOR, getRegexp(key.getStringValue()).toString(), contextQName, DBBroker.MATCH_REGEXP); hasUsedIndex = true; if (result == null) result = ns; else result = result.union(ns); } catch (EXistException e) { throw new XPathException(this, e.getMessage(), 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: " + Type.getTypeName(key.getType()) + ") whereas index is of type '" + 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 CharSequence getRegexp(String expr) { switch (truncation) { case Constants.TRUNC_LEFT : return new StringBuilder().append(expr).append('$'); case Constants.TRUNC_RIGHT : return new StringBuilder().append('^').append(expr); default : return expr; } } /** * Cast the atomic operands into a comparable type * and compare them. */ private boolean compareAtomic(Collator collator, AtomicValue lv, AtomicValue rv) throws XPathException { try { int ltype = lv.getType(); int rtype = rv.getType(); if (ltype == Type.UNTYPED_ATOMIC) { //If one of the atomic values is an instance of xdt:untypedAtomic //and the other is an instance of a numeric type, //then the xdt:untypedAtomic value is cast to the type xs:double. if (Type.subTypeOf(rtype, Type.NUMBER)) { //if(isEmptyString(lv)) // return false; lv = lv.convertTo(Type.DOUBLE); //If one of the atomic values is an instance of xdt:untypedAtomic //and the other is an instance of xdt:untypedAtomic or xs:string, //then the xdt:untypedAtomic value (or values) is (are) cast to the type xs:string. } else if (rtype == Type.UNTYPED_ATOMIC || rtype == Type.STRING) { lv = lv.convertTo(Type.STRING); //if (rtype == Type.UNTYPED_ATOMIC) //rv = rv.convertTo(Type.STRING); //If one of the atomic values is an instance of xdt:untypedAtomic //and the other is not an instance of xs:string, xdt:untypedAtomic, or any numeric type, //then the xdt:untypedAtomic value is cast to the dynamic type of the other value. } else lv = lv.convertTo(rtype); } if (rtype == Type.UNTYPED_ATOMIC) { //If one of the atomic values is an instance of xdt:untypedAtomic //and the other is an instance of a numeric type, //then the xdt:untypedAtomic value is cast to the type xs:double. if (Type.subTypeOf(ltype, Type.NUMBER)) { //if(isEmptyString(lv)) // return false; rv = rv.convertTo(Type.DOUBLE); //If one of the atomic values is an instance of xdt:untypedAtomic //and the other is an instance of xdt:untypedAtomic or xs:string, //then the xdt:untypedAtomic value (or values) is (are) cast to the type xs:string. } else if (ltype == Type.UNTYPED_ATOMIC || ltype == Type.STRING) { rv = rv.convertTo(Type.STRING); //if (ltype == Type.UNTYPED_ATOMIC) // lv = lv.convertTo(Type.STRING); //If one of the atomic values is an instance of xdt:untypedAtomic //and the other is not an instance of xs:string, xdt:untypedAtomic, or any numeric type, //then the xdt:untypedAtomic value is cast to the dynamic type of the other value. } else rv = rv.convertTo(ltype); } /* if (backwardsCompatible) { if (!"".equals(lv.getStringValue()) && !"".equals(rv.getStringValue())) { // in XPath 1.0 compatible mode, if one of the operands is a number, cast // both operands to xs:double if (Type.subTypeOf(ltype, Type.NUMBER) || Type.subTypeOf(rtype, Type.NUMBER)) { lv = lv.convertTo(Type.DOUBLE); rv = rv.convertTo(Type.DOUBLE); } } } */ // if truncation is set, we always do a string comparison if (truncation != Constants.TRUNC_NONE) { //TODO : log this ? lv = lv.convertTo(Type.STRING); } // System.out.println( // lv.getStringValue() + Constants.OPS[relation] + rv.getStringValue()); switch(truncation) { case Constants.TRUNC_RIGHT: return lv.startsWith(collator, rv); case Constants.TRUNC_LEFT: return lv.endsWith(collator, rv); case Constants.TRUNC_BOTH: return lv.contains(collator, rv); default: return lv.compareTo(collator, relation, rv); } } catch (XPathException e) { e.setLocation(e.getLine(), e.getColumn()); throw e; } } /** * @param lv * @return Whether or not <code>lv</code> is an empty string * @throws XPathException */ 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 == Constants.TRUNC_BOTH) { dumper.display("contains").display('('); getLeft().dump(dumper); dumper.display(", "); getRight().dump(dumper); dumper.display(")"); } else { getLeft().dump(dumper); dumper.display(' ').display(Constants.OPS[relation]).display(' '); getRight().dump(dumper); } } public String toString() { StringBuilder result = new StringBuilder(); if (truncation == Constants.TRUNC_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(Constants.OPS[relation]).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 Constants.GT : relation = Constants.LT; break; case Constants.LT : relation = Constants.GT; break; case Constants.LTEQ : relation = Constants.GTEQ; break; case Constants.GTEQ : relation = Constants.LTEQ; break; //What about Constants.EQ and Constants.NEQ ? Well, it seems to never be called } Expression right = getRight(); setRight(getLeft()); setLeft(right); } /** * Possibly switch operands to simplify execution */ protected void simplify() { //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.checkCardinality(Cardinality.MANY, getLeft().getCardinality())) && !(Cardinality.checkCardinality(Cardinality.MANY, getRight().getCardinality()))) switchOperands(); } protected Collator getCollator(Sequence contextSequence) throws XPathException { if(collationArg == null) return context.getDefaultCollator(); String collationURI = collationArg.eval(contextSequence).getStringValue(); return context.getCollator(collationURI); } public void setCollation(Expression collationArg) { this.collationArg = collationArg; } public static IndexFlags checkForQNameIndex(IndexFlags idxflags, XQueryContext context, Sequence contextSequence, QName contextQName) { idxflags.reset(contextQName != null); for (Iterator i = contextSequence.getCollectionIterator(); i.hasNext(); ) { Collection collection = (Collection) i.next(); if (collection.getURI().equalsInternal(XmldbURI.SYSTEM_COLLECTION_URI)) continue; IndexSpec idxcfg = collection.getIndexConfiguration(context.getBroker()); boolean hasIndex = contextQName != null && idxcfg.getIndexByQName(contextQName) != null; if (!idxflags.partialIndexOnQName && hasIndex) idxflags.partialIndexOnQName = true; if (idxflags.indexOnQName && !hasIndex) { idxflags.indexOnQName = false; if (LOG.isTraceEnabled()) LOG.trace("cannot use index on QName: " + contextQName + ". Collection " + collection.getURI() + " does not define an index"); } 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; } } }