/*
* eXist Open Source Native XML Database
* Copyright (C) 2013 The eXist Project
* http://exist-db.org
*
* This program 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
* 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 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 St, Fifth Floor, Boston, MA 02110-1301 USA
*
* $Id$
*/
package org.exist.xquery.modules.range;
import org.exist.Namespaces;
import org.exist.collections.Collection;
import org.exist.dom.QName;
import org.exist.indexing.range.*;
import org.exist.storage.IndexSpec;
import org.exist.storage.NodePath;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.*;
import org.exist.xquery.value.*;
import java.util.*;
/**
* A pragma which checks if an XPath expression could be replaced with a range field lookup.
*
* @author wolf
*/
public class OptimizeFieldPragma extends Pragma {
public final static QName OPTIMIZE_RANGE_PRAGMA = new QName("optimize-field", Namespaces.EXIST_NS, "exist");
private final XQueryContext context;
private Expression rewritten = null;
private AnalyzeContextInfo contextInfo;
private int axis;
public OptimizeFieldPragma(QName qname, String contents, XQueryContext context) throws XPathException {
super(qname, contents);
this.context = context;
}
@Override
public void analyze(AnalyzeContextInfo contextInfo) throws XPathException {
super.analyze(contextInfo);
this.contextInfo = contextInfo;
}
@Override
public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException {
if (rewritten != null) {
rewritten.analyze(contextInfo);
return rewritten.eval(contextSequence, contextItem);
}
return null;
}
@Override
public void before(XQueryContext context, Expression expression, Sequence contextSequence) throws XPathException {
LocationStep locationStep = (LocationStep) expression;
if (locationStep.hasPredicates()) {
Expression parentExpr = locationStep.getParentExpression();
if (!(parentExpr instanceof RewritableExpression)) {
return;
}
final List<Predicate> preds = locationStep.getPredicates();
// get path of path expression before the predicates
NodePath contextPath = RangeQueryRewriter.toNodePath(RangeQueryRewriter.getPrecedingSteps(locationStep));
rewritten = tryRewriteToFields(locationStep, preds, contextPath, contextSequence);
axis = locationStep.getAxis();
}
}
@Override
public void after(XQueryContext context, Expression expression) throws XPathException {
}
private Expression tryRewriteToFields(LocationStep locationStep, List<Predicate> preds, NodePath contextPath, Sequence contextSequence) throws XPathException {
// without context path, we cannot rewrite the entire query
if (contextPath != null) {
List<Predicate> notOptimizable = new ArrayList<Predicate>(preds.size());
List<RangeIndexConfig> configs = getConfigurations(contextSequence);
// walk through the predicates attached to the current location step
// check if expression can be optimized
final Map<Predicate, List<Expression>> predicateArgs = new IdentityHashMap<Predicate, List<Expression>>(preds.size());
for (final Predicate pred : preds) {
List<Expression> args = null;
SequenceConstructor arg0 = null;
SequenceConstructor arg1 = null;
if (pred.getLength() != 1) {
// can only optimize predicates with one expression
notOptimizable.add(pred);
continue;
}
Expression innerExpr = pred.getExpression(0);
List<LocationStep> steps = RangeQueryRewriter.getStepsToOptimize(innerExpr);
if (steps == null) {
notOptimizable.add(pred);
continue;
}
// compute left hand path
NodePath innerPath = RangeQueryRewriter.toNodePath(steps);
if (innerPath == null) {
notOptimizable.add(pred);
continue;
}
NodePath path = new NodePath(contextPath);
path.append(innerPath);
if (path.length() > 0) {
// find all complex range index configurations matching the full path to the predicate expression
final List<ComplexRangeIndexConfigElement> rices = findConfigurations(path, configs);
// config with most conditions for path comes first
rices.sort(ComplexRangeIndexConfigElement.NUM_CONDITIONS_COMPARATOR);
if (rices.isEmpty()) {
notOptimizable.add(pred);
continue;
}
// found index configuration with sub-fields
ComplexRangeIndexConfigElement rice = null;
final List<Predicate> precedingPreds = preds.subList(0, preds.indexOf(pred));
final ArrayList<Predicate> matchedPreds = new ArrayList<Predicate>();
for (ComplexRangeIndexConfigElement testRice : rices) {
if (testRice.getNumberOfConditions() > 0) {
// find a config element where the conditions match preceding predicates
matchedPreds.clear();
for (Predicate precedingPred : precedingPreds ) {
if (testRice.findCondition(precedingPred)) {
matchedPreds.add(precedingPred);
}
}
if (matchedPreds.size() == testRice.getNumberOfConditions()) {
// all conditions matched
rice = testRice;
// if any preceding predicates found to be part of a condition for this config
// had been matched to another config before, remove them as is is the correct match
predicateArgs.keySet().removeAll(matchedPreds);
// also do not re-add them after optimizing
notOptimizable.removeAll(matchedPreds);
break;
}
} else {
// no conditional configs for this node path, take the first one found if any
rice = testRice;
}
}
if (rice != null && rice.getNodePath().match(contextPath)) {
// check for a matching sub-path and retrieve field information
RangeIndexConfigField field = rice.getField(path);
if (field != null) {
if (args == null) {
// initialize args
args = new ArrayList<Expression>(4);
arg0 = new SequenceConstructor(context);
args.add(arg0);
arg1 = new SequenceConstructor(context);
args.add(arg1);
}
// field is added to the sequence in first parameter
arg0.add(new LiteralValue(context, new StringValue(field.getName())));
// operator
arg1.add(new LiteralValue(context, new StringValue(RangeQueryRewriter.getOperator(innerExpr).toString())));
// append right hand expression as additional parameter
args.add(getKeyArg(innerExpr));
// store the collected arguments with a reference to the predicate
// so they can be removed if a better match is found (if the predicate happens to be
// one of the conditions for the following predicate
predicateArgs.put(pred, args);
} else {
notOptimizable.add(pred);
continue;
}
} else {
notOptimizable.add(pred);
continue;
}
} else {
notOptimizable.add(pred);
continue;
}
}
if (!predicateArgs.isEmpty()) {
// the entire filter expression can be replaced
// create range:field-equals function
FieldLookup func = new FieldLookup(context, FieldLookup.signatures[0]);
func.setFallback(locationStep);
func.setLocation(locationStep.getLine(), locationStep.getColumn());
if (predicateArgs.size() == 1) {
func.setArguments(predicateArgs.entrySet().iterator().next().getValue());
} else {
final List<Expression> mergedArgs = new ArrayList<Expression>(predicateArgs.size() * 4);
final SequenceConstructor arg0 = new SequenceConstructor(context);
mergedArgs.add(arg0);
final SequenceConstructor arg1 = new SequenceConstructor(context);
mergedArgs.add(arg1);
for (final List<Expression> args : predicateArgs.values()) {
arg0.add(args.get(0));
arg1.add(args.get(1));
mergedArgs.addAll(args.subList(2, args.size()));
}
func.setArguments(mergedArgs);
}
Expression optimizedExpr = new InternalFunctionCall(func);
if (notOptimizable.size() > 0) {
final FilteredExpression filtered = new FilteredExpression(context, optimizedExpr);
for (Predicate pred : notOptimizable) {
filtered.addPredicate(pred);
}
optimizedExpr = filtered;
}
return optimizedExpr;
}
}
return null;
}
private Expression getKeyArg(Expression expression) {
if (expression instanceof GeneralComparison) {
return ((GeneralComparison)expression).getRight();
} else if (expression instanceof InternalFunctionCall) {
InternalFunctionCall fcall = (InternalFunctionCall) expression;
Function function = fcall.getFunction();
if (function instanceof Lookup) {
return function.getArgument(1);
}
}
return null;
}
/**
* Find all complex configurations matching the path
*/
private List<ComplexRangeIndexConfigElement> findConfigurations(NodePath path, List<RangeIndexConfig> configs) {
ArrayList<ComplexRangeIndexConfigElement> rices = new ArrayList<ComplexRangeIndexConfigElement>();
for (RangeIndexConfig config : configs) {
List<ComplexRangeIndexConfigElement> foundRices = config.findAll(path);
if (rices.addAll(foundRices)) {
break;
}
}
return rices;
}
private List<RangeIndexConfig> getConfigurations(Sequence contextSequence) {
List<RangeIndexConfig> configs = new ArrayList<RangeIndexConfig>();
for (final Iterator<Collection> i = contextSequence.getCollectionIterator(); i.hasNext(); ) {
final Collection collection = i.next();
if (collection.getURI().startsWith(XmldbURI.SYSTEM_COLLECTION_URI)) {
continue;
}
IndexSpec idxConf = collection.getIndexConfiguration(context.getBroker());
if (idxConf != null) {
final RangeIndexConfig config = (RangeIndexConfig) idxConf.getCustomIndexSpec(RangeIndex.ID);
if (config != null) {
configs.add(config);
}
}
}
return configs;
}
}