DefaultEvaluationStrategy.java

/*******************************************************************************
 * Copyright (c) 2022 Eclipse RDF4J contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
package org.eclipse.rdf4j.query.algebra.evaluation.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;

import org.eclipse.rdf4j.collection.factory.api.CollectionFactory;
import org.eclipse.rdf4j.collection.factory.impl.DefaultCollectionFactory;
import org.eclipse.rdf4j.common.iteration.CloseableIteration;
import org.eclipse.rdf4j.common.iteration.DistinctIteration;
import org.eclipse.rdf4j.common.iteration.IterationWrapper;
import org.eclipse.rdf4j.common.iteration.LookAheadIteration;
import org.eclipse.rdf4j.common.iteration.ReducedIteration;
import org.eclipse.rdf4j.common.iteration.SingletonIteration;
import org.eclipse.rdf4j.common.transaction.QueryEvaluationMode;
import org.eclipse.rdf4j.model.BNode;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.base.CoreDatatype;
import org.eclipse.rdf4j.model.impl.BooleanLiteral;
import org.eclipse.rdf4j.model.util.Literals;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.query.Dataset;
import org.eclipse.rdf4j.query.MutableBindingSet;
import org.eclipse.rdf4j.query.QueryEvaluationException;
import org.eclipse.rdf4j.query.algebra.And;
import org.eclipse.rdf4j.query.algebra.ArbitraryLengthPath;
import org.eclipse.rdf4j.query.algebra.BNodeGenerator;
import org.eclipse.rdf4j.query.algebra.BinaryTupleOperator;
import org.eclipse.rdf4j.query.algebra.BinaryValueOperator;
import org.eclipse.rdf4j.query.algebra.BindingSetAssignment;
import org.eclipse.rdf4j.query.algebra.Bound;
import org.eclipse.rdf4j.query.algebra.Coalesce;
import org.eclipse.rdf4j.query.algebra.Compare;
import org.eclipse.rdf4j.query.algebra.CompareAll;
import org.eclipse.rdf4j.query.algebra.CompareAny;
import org.eclipse.rdf4j.query.algebra.Datatype;
import org.eclipse.rdf4j.query.algebra.DescribeOperator;
import org.eclipse.rdf4j.query.algebra.Difference;
import org.eclipse.rdf4j.query.algebra.Distinct;
import org.eclipse.rdf4j.query.algebra.EmptySet;
import org.eclipse.rdf4j.query.algebra.Exists;
import org.eclipse.rdf4j.query.algebra.Extension;
import org.eclipse.rdf4j.query.algebra.Filter;
import org.eclipse.rdf4j.query.algebra.FunctionCall;
import org.eclipse.rdf4j.query.algebra.Group;
import org.eclipse.rdf4j.query.algebra.IRIFunction;
import org.eclipse.rdf4j.query.algebra.If;
import org.eclipse.rdf4j.query.algebra.In;
import org.eclipse.rdf4j.query.algebra.Intersection;
import org.eclipse.rdf4j.query.algebra.IsBNode;
import org.eclipse.rdf4j.query.algebra.IsLiteral;
import org.eclipse.rdf4j.query.algebra.IsNumeric;
import org.eclipse.rdf4j.query.algebra.IsResource;
import org.eclipse.rdf4j.query.algebra.IsURI;
import org.eclipse.rdf4j.query.algebra.Join;
import org.eclipse.rdf4j.query.algebra.Label;
import org.eclipse.rdf4j.query.algebra.Lang;
import org.eclipse.rdf4j.query.algebra.LangMatches;
import org.eclipse.rdf4j.query.algebra.LeftJoin;
import org.eclipse.rdf4j.query.algebra.ListMemberOperator;
import org.eclipse.rdf4j.query.algebra.LocalName;
import org.eclipse.rdf4j.query.algebra.MathExpr;
import org.eclipse.rdf4j.query.algebra.MathExpr.MathOp;
import org.eclipse.rdf4j.query.algebra.MultiProjection;
import org.eclipse.rdf4j.query.algebra.Namespace;
import org.eclipse.rdf4j.query.algebra.Not;
import org.eclipse.rdf4j.query.algebra.Or;
import org.eclipse.rdf4j.query.algebra.Order;
import org.eclipse.rdf4j.query.algebra.Projection;
import org.eclipse.rdf4j.query.algebra.QueryModelNode;
import org.eclipse.rdf4j.query.algebra.QueryRoot;
import org.eclipse.rdf4j.query.algebra.Reduced;
import org.eclipse.rdf4j.query.algebra.Regex;
import org.eclipse.rdf4j.query.algebra.SameTerm;
import org.eclipse.rdf4j.query.algebra.Service;
import org.eclipse.rdf4j.query.algebra.SingletonSet;
import org.eclipse.rdf4j.query.algebra.Slice;
import org.eclipse.rdf4j.query.algebra.StatementPattern;
import org.eclipse.rdf4j.query.algebra.StatementPattern.Scope;
import org.eclipse.rdf4j.query.algebra.Str;
import org.eclipse.rdf4j.query.algebra.TripleRef;
import org.eclipse.rdf4j.query.algebra.TupleExpr;
import org.eclipse.rdf4j.query.algebra.TupleFunctionCall;
import org.eclipse.rdf4j.query.algebra.UnaryTupleOperator;
import org.eclipse.rdf4j.query.algebra.UnaryValueOperator;
import org.eclipse.rdf4j.query.algebra.Union;
import org.eclipse.rdf4j.query.algebra.ValueConstant;
import org.eclipse.rdf4j.query.algebra.ValueExpr;
import org.eclipse.rdf4j.query.algebra.ValueExprTripleRef;
import org.eclipse.rdf4j.query.algebra.Var;
import org.eclipse.rdf4j.query.algebra.ZeroLengthPath;
import org.eclipse.rdf4j.query.algebra.evaluation.EvaluationStrategy;
import org.eclipse.rdf4j.query.algebra.evaluation.QueryBindingSet;
import org.eclipse.rdf4j.query.algebra.evaluation.QueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.QueryOptimizer;
import org.eclipse.rdf4j.query.algebra.evaluation.QueryOptimizerPipeline;
import org.eclipse.rdf4j.query.algebra.evaluation.QueryValueEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.QueryValueEvaluationStep.ConstantQueryValueEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.RDFStarTripleSource;
import org.eclipse.rdf4j.query.algebra.evaluation.TripleSource;
import org.eclipse.rdf4j.query.algebra.evaluation.ValueExprEvaluationException;
import org.eclipse.rdf4j.query.algebra.evaluation.federation.FederatedService;
import org.eclipse.rdf4j.query.algebra.evaluation.federation.FederatedServiceResolver;
import org.eclipse.rdf4j.query.algebra.evaluation.federation.FederatedServiceResolverClient;
import org.eclipse.rdf4j.query.algebra.evaluation.function.Function;
import org.eclipse.rdf4j.query.algebra.evaluation.function.FunctionRegistry;
import org.eclipse.rdf4j.query.algebra.evaluation.function.TupleFunction;
import org.eclipse.rdf4j.query.algebra.evaluation.function.TupleFunctionRegistry;
import org.eclipse.rdf4j.query.algebra.evaluation.function.datetime.Now;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.BindingSetAssignmentQueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.IntersectionQueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.JoinQueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.LeftJoinQueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.MinusQueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.OrderQueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.ProjectionQueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.RdfStarQueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.RegexValueEvaluationStepSupplier;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.ReificationRdfStarQueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.ServiceQueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.SliceQueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.StatementPatternQueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.UnionQueryEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.ZeroLengthPathEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.values.AndValueEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.values.CompareAllQueryValueEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.values.CompareAnyValueEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.values.ExistsQueryValueEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.values.IfValueEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.values.InValueEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.values.ListMemberValueOperationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.values.OrValueEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.values.QueryValueEvaluationStepSupplier;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.values.ValueExprTripleRefEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.iterator.DescribeIteration;
import org.eclipse.rdf4j.query.algebra.evaluation.iterator.ExtensionIterator;
import org.eclipse.rdf4j.query.algebra.evaluation.iterator.FilterIterator;
import org.eclipse.rdf4j.query.algebra.evaluation.iterator.GroupIterator;
import org.eclipse.rdf4j.query.algebra.evaluation.iterator.MultiProjectionIterator;
import org.eclipse.rdf4j.query.algebra.evaluation.iterator.PathIteration;
import org.eclipse.rdf4j.query.algebra.evaluation.util.MathUtil;
import org.eclipse.rdf4j.query.algebra.evaluation.util.OrderComparator;
import org.eclipse.rdf4j.query.algebra.evaluation.util.QueryEvaluationUtil;
import org.eclipse.rdf4j.query.algebra.evaluation.util.QueryEvaluationUtility;
import org.eclipse.rdf4j.query.algebra.evaluation.util.ValueComparator;
import org.eclipse.rdf4j.query.algebra.evaluation.util.XMLDatatypeMathUtil;
import org.eclipse.rdf4j.query.impl.EmptyBindingSet;

import com.google.common.base.Stopwatch;

/**
 * Default SPARQL 1.1 Query Evaluation strategy, to evaluate one {@link TupleExpr} on the given {@link TripleSource},
 * optionally using the given {@link Dataset}. The behaviour of this strategy can be modified by setting the
 * {@link #setQueryEvaluationMode(QueryEvaluationMode) QueryEvaluationMode}, which determines if the SPARQL query is
 * evaluated using {@link QueryEvaluationMode#STRICT strict} compliance or {@link QueryEvaluationMode#STANDARD standard}
 * compliance.
 *
 * @author Jeen Broekstra
 * @author James Leigh
 * @author Arjohn Kampman
 * @author David Huynh
 * @author Andreas Schwarte
 */
public class DefaultEvaluationStrategy implements EvaluationStrategy, FederatedServiceResolverClient {

	protected final TripleSource tripleSource;

	protected final Dataset dataset;

	protected FederatedServiceResolver serviceResolver;

	// shared return value for successive calls of the NOW() function within the
	// same query. Will be reset upon each new query being evaluated. See
	// SES-869.
	private Literal sharedValueOfNow;

	private final long iterationCacheSyncThreshold;

	// track the results size that each node in the query plan produces during execution
	private boolean trackResultSize;

	// track the exeution time of each node in the plan
	private boolean trackTime;

	private UUID uuid;

	private QueryOptimizerPipeline pipeline;

	private final TupleFunctionRegistry tupleFuncRegistry;

	private QueryEvaluationMode queryEvaluationMode;

	private Supplier<CollectionFactory> collectionFactory = DefaultCollectionFactory::new;

	protected static CloseableIteration<BindingSet> evaluate(TupleFunction func,
			final List<Var> resultVars, final BindingSet bindings, ValueFactory valueFactory, Value... argValues)
			throws QueryEvaluationException {
		final CloseableIteration<? extends List<? extends Value>> iter = func
				.evaluate(valueFactory, argValues);
		return new LookAheadIteration<BindingSet>() {

			@Override
			public BindingSet getNextElement() throws QueryEvaluationException {
				QueryBindingSet resultBindings = null;
				while (resultBindings == null && iter.hasNext()) {
					resultBindings = new QueryBindingSet(bindings);
					List<? extends Value> values = iter.next();
					if (resultVars.size() != values.size()) {
						throw new QueryEvaluationException("Incorrect number of result vars: require " + values.size());
					}
					for (int i = 0; i < values.size(); i++) {
						Value result = values.get(i);
						Var resultVar = resultVars.get(i);
						Value varValue = resultVar.getValue();
						String varName = resultVar.getName();
						Value boundValue = bindings.getValue(varName);
						if ((varValue == null || result.equals(varValue))
								&& (boundValue == null || result.equals(boundValue))) {
							if (boundValue == null) { // if not already present
								resultBindings.addBinding(varName, result);
							}
						} else {
							resultBindings = null;
							break;
						}
					}
				}
				return resultBindings;
			}

			@Override
			protected void handleClose() throws QueryEvaluationException {
				iter.close();
			}
		};
	}

	public DefaultEvaluationStrategy(TripleSource tripleSource, FederatedServiceResolver serviceResolver) {
		this(tripleSource, null, serviceResolver);
	}

	public DefaultEvaluationStrategy(TripleSource tripleSource, Dataset dataset,
			FederatedServiceResolver serviceResolver) {
		this(tripleSource, dataset, serviceResolver, 0, new EvaluationStatistics());
	}

	public DefaultEvaluationStrategy(TripleSource tripleSource, Dataset dataset,
			FederatedServiceResolver serviceResolver, long iterationCacheSyncTreshold,
			EvaluationStatistics evaluationStatistics) {
		this(tripleSource, dataset, serviceResolver, iterationCacheSyncTreshold, evaluationStatistics, false);
	}

	public DefaultEvaluationStrategy(TripleSource tripleSource, Dataset dataset,
			FederatedServiceResolver serviceResolver, long iterationCacheSyncTreshold,
			EvaluationStatistics evaluationStatistics, boolean trackResultSize) {
		this(tripleSource, dataset, serviceResolver, iterationCacheSyncTreshold, evaluationStatistics, trackResultSize,
				TupleFunctionRegistry.getInstance());
	}

	public DefaultEvaluationStrategy(TripleSource tripleSource, Dataset dataset,
			FederatedServiceResolver serviceResolver, long iterationCacheSyncTreshold,
			EvaluationStatistics evaluationStatistics, boolean trackResultSize,
			TupleFunctionRegistry tupleFunctionRegistry) {
		this.tripleSource = tripleSource;
		this.dataset = dataset;
		this.serviceResolver = serviceResolver;
		this.iterationCacheSyncThreshold = iterationCacheSyncTreshold;
		this.pipeline = new org.eclipse.rdf4j.query.algebra.evaluation.optimizer.StandardQueryOptimizerPipeline(this,
				tripleSource, evaluationStatistics);
		this.trackResultSize = trackResultSize;
		this.tupleFuncRegistry = tupleFunctionRegistry;
		this.setQueryEvaluationMode(QueryEvaluationMode.STANDARD);
	}

	@Override
	public void setFederatedServiceResolver(FederatedServiceResolver resolver) {
		serviceResolver = resolver;
	}

	@Override
	public FederatedServiceResolver getFederatedServiceResolver() {
		return serviceResolver;
	}

	@Override
	public FederatedService getService(String serviceUrl) throws QueryEvaluationException {
		return serviceResolver.getService(serviceUrl);
	}

	@Override
	public void setOptimizerPipeline(QueryOptimizerPipeline pipeline) {
		Objects.requireNonNull(pipeline);
		this.pipeline = pipeline;
	}

	/**
	 * Execute the {@link QueryOptimizerPipeline} on the given {@link TupleExpr} to optimize its execution plan.
	 *
	 * @param expr                 the {@link TupleExpr} to optimize.
	 * @param evaluationStatistics this param is ignored!
	 * @param bindings             a-priori bindings supplied for the query, which can potentially be inlined.
	 * @return the optimized {@link TupleExpr}.
	 * @see #setOptimizerPipeline(QueryOptimizerPipeline)
	 * @since 3.0
	 */
	@Override
	public TupleExpr optimize(TupleExpr expr, EvaluationStatistics evaluationStatistics, BindingSet bindings) {

		for (QueryOptimizer optimizer : pipeline.getOptimizers()) {
			optimizer.optimize(expr, dataset, bindings);
		}
		return expr;
	}

	@Deprecated(forRemoval = true) // this method is still in use and I think that there is quite a lot of work left
	// before it can be removed
	@Override
	public CloseableIteration<BindingSet> evaluate(TupleExpr expr, BindingSet bindings)
			throws QueryEvaluationException {

		CloseableIteration<BindingSet> result = null;
		try {
			if (expr instanceof StatementPattern) {
				result = precompile(expr).evaluate(bindings);
			} else if (expr instanceof UnaryTupleOperator) {
				if (expr instanceof Projection) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof MultiProjection) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof Filter) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof Service) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof Slice) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof Extension) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof Distinct) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof Reduced) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof Group) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof Order) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof QueryRoot) {
					// new query, reset shared return value for successive calls of
					// NOW()
					this.sharedValueOfNow = null;
					result = evaluate(((UnaryTupleOperator) expr).getArg(), bindings);
				} else if (expr instanceof DescribeOperator) {
					result = precompile(expr).evaluate(bindings);
				} else {
					throw new QueryEvaluationException(
							"Unknown unary tuple operator type: " + ((UnaryTupleOperator) expr).getClass());
				}
			} else if (expr instanceof BinaryTupleOperator) {
				if (expr instanceof Join) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof LeftJoin) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof Union) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof Intersection) {
					result = precompile(expr).evaluate(bindings);
				} else if (expr instanceof Difference) {
					result = precompile(expr).evaluate(bindings);
				} else {
					throw new QueryEvaluationException(
							"Unsupported binary tuple operator type: " + ((BinaryTupleOperator) expr).getClass());
				}
			} else if (expr instanceof SingletonSet) {
				result = precompile(expr).evaluate(bindings);
			} else if (expr instanceof EmptySet) {
				result = QueryEvaluationStep.EMPTY_ITERATION;
			} else if (expr instanceof ZeroLengthPath) {
				result = precompile(expr).evaluate(bindings);
			} else if (expr instanceof ArbitraryLengthPath) {
				result = precompile(expr).evaluate(bindings);
			} else if (expr instanceof BindingSetAssignment) {
				result = precompile(expr).evaluate(bindings);
			} else if (expr instanceof TripleRef) {
				result = evaluate((TripleRef) expr, bindings);
			} else if (expr instanceof TupleFunctionCall) {
				if (getQueryEvaluationMode().compareTo(QueryEvaluationMode.STANDARD) < 0) {
					throw new QueryEvaluationException(
							"Tuple function call not supported in query evaluation mode " + getQueryEvaluationMode());
				}
				return evaluate(expr, bindings);
			} else if (expr == null) {
				throw new IllegalArgumentException("expr must not be null");
			} else {
				throw new QueryEvaluationException("Unsupported tuple expr type: " + expr.getClass());
			}

			if (trackTime) {
				// set resultsSizeActual to at least be 0 so we can track iterations that don't procude anything
				expr.setTotalTimeNanosActual(Math.max(0, expr.getTotalTimeNanosActual()));
				result = new TimedIterator(result, expr);
			}

			if (trackResultSize) {
				// set resultsSizeActual to at least be 0 so we can track iterations that don't procude anything
				expr.setResultSizeActual(Math.max(0, expr.getResultSizeActual()));
				result = new ResultSizeCountingIterator(result, expr);
			}
			return result;
		} catch (Throwable t) {
			if (result != null) {
				result.close();
			}
			throw t;
		}
	}

	@Override
	public QueryEvaluationStep precompile(TupleExpr expr) {
		QueryEvaluationContext context = new QueryEvaluationContext.Minimal(dataset, tripleSource.getValueFactory(),
				tripleSource.getComparator());
		if (expr instanceof QueryRoot) {
			String[] allVariables = ArrayBindingBasedQueryEvaluationContext
					.findAllVariablesUsedInQuery((QueryRoot) expr);
			context = new ArrayBindingBasedQueryEvaluationContext(context, allVariables, tripleSource.getComparator());
		}
		return precompile(expr, context);
	}

	@Override
	public QueryEvaluationStep precompile(TupleExpr expr, QueryEvaluationContext context) {
		QueryEvaluationStep ret;

		if (expr instanceof StatementPattern) {
			ret = prepare((StatementPattern) expr, context);
		} else if (expr instanceof UnaryTupleOperator) {
			ret = prepare((UnaryTupleOperator) expr, context);
		} else if (expr instanceof BinaryTupleOperator) {
			ret = prepare((BinaryTupleOperator) expr, context);
		} else if (expr instanceof SingletonSet) {
			ret = prepare((SingletonSet) expr, context);
		} else if (expr instanceof EmptySet) {
			ret = prepare((EmptySet) expr, context);
		} else if (expr instanceof ZeroLengthPath) {
			ret = prepare((ZeroLengthPath) expr, context);
		} else if (expr instanceof ArbitraryLengthPath) {
			ret = prepare((ArbitraryLengthPath) expr, context);
		} else if (expr instanceof BindingSetAssignment) {
			ret = prepare((BindingSetAssignment) expr, context);
		} else if (expr instanceof TripleRef) {
			ret = prepare((TripleRef) expr, context);
		} else if (expr instanceof TupleFunctionCall) {
			ret = prepare((TupleFunctionCall) expr, context);
		} else if (expr == null) {
			throw new IllegalArgumentException("expr must not be null");
		} else {
			throw new QueryEvaluationException("Unsupported tuple expr type: " + expr.getClass());
		}

		if (ret != null) {
			if (trackTime) {
				ret = trackTime(expr, ret);
			}
			if (trackResultSize) {
				ret = trackResultSize(expr, ret);
			}
			return ret;
		} else {
			return QueryEvaluationStep.minimal(this, expr);
		}
	}

	private QueryEvaluationStep trackResultSize(TupleExpr expr, QueryEvaluationStep qes) {
		return QueryEvaluationStep.wrap(qes, iter -> {
			expr.setResultSizeActual(Math.max(0, expr.getResultSizeActual()));
			return new ResultSizeCountingIterator(iter, expr);
		});
	}

	private QueryEvaluationStep trackTime(TupleExpr expr, QueryEvaluationStep qes) {
		return QueryEvaluationStep.wrap(qes, iter -> {
			expr.setTotalTimeNanosActual(Math.max(0, expr.getTotalTimeNanosActual()));
			return new TimedIterator(iter, expr);
		});
	}

	protected QueryEvaluationStep prepare(ArbitraryLengthPath alp, QueryEvaluationContext context)
			throws QueryEvaluationException {
		final Scope scope = alp.getScope();
		final Var subjectVar = alp.getSubjectVar();
		final TupleExpr pathExpression = alp.getPathExpression();
		final Var objVar = alp.getObjectVar();
		final Var contextVar = alp.getContextVar();
		final long minLength = alp.getMinLength();
		return bindings -> new PathIteration(DefaultEvaluationStrategy.this, scope, subjectVar, pathExpression, objVar,
				contextVar, minLength, bindings);
	}

	protected QueryEvaluationStep prepare(ZeroLengthPath zlp, QueryEvaluationContext context)
			throws QueryEvaluationException {

		final Var subjectVar = zlp.getSubjectVar();
		final Var objVar = zlp.getObjectVar();
		final Var contextVar = zlp.getContextVar();
		QueryValueEvaluationStep subPrep = precompile(subjectVar, context);
		QueryValueEvaluationStep objPrep = precompile(objVar, context);

		return new ZeroLengthPathEvaluationStep(subjectVar, objVar, contextVar, subPrep, objPrep, this, context);
	}

	protected QueryEvaluationStep prepare(Difference node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		return new MinusQueryEvaluationStep(precompile(node.getLeftArg(), context),
				precompile(node.getRightArg(), context));
	}

	protected QueryEvaluationStep prepare(Group node, QueryEvaluationContext context) throws QueryEvaluationException {
		return bindings -> new GroupIterator(DefaultEvaluationStrategy.this, node, bindings, context);
	}

	protected QueryEvaluationStep prepare(Intersection node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryEvaluationStep leftArg = precompile(node.getLeftArg(), context);
		QueryEvaluationStep rightArg = precompile(node.getRightArg(), context);

		return new IntersectionQueryEvaluationStep(leftArg, rightArg, getCollectionFactory());
	}

	protected QueryEvaluationStep prepare(Join node, QueryEvaluationContext context) throws QueryEvaluationException {
		return new JoinQueryEvaluationStep(this, node, context);
	}

	protected QueryEvaluationStep prepare(LeftJoin node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		return LeftJoinQueryEvaluationStep.supply(this, node, context);
	}

	protected QueryEvaluationStep prepare(MultiProjection node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryEvaluationStep arg = precompile(node.getArg(), context);
		return bindings -> {
			CloseableIteration<BindingSet> evaluate = null;
			try {
				evaluate = arg.evaluate(bindings);
				return new MultiProjectionIterator(node, evaluate, bindings);
			} catch (Throwable t) {
				if (evaluate != null) {
					evaluate.close();
				}
				throw t;
			}
		};
	}

	protected QueryEvaluationStep prepare(Projection node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryEvaluationStep temp = precompile(node.getArg(), context);
		return new ProjectionQueryEvaluationStep(node, temp, context);
	}

	protected QueryEvaluationStep prepare(QueryRoot node, QueryEvaluationContext context)
			throws QueryEvaluationException {

		QueryEvaluationStep arg = precompile(node.getArg(), context);
		return new QueryRootQueryEvaluationStep(arg, context);
	}

	protected QueryEvaluationStep prepare(StatementPattern node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		return new StatementPatternQueryEvaluationStep(node, context, tripleSource);
	}

	protected QueryEvaluationStep prepare(Union node, QueryEvaluationContext context) throws QueryEvaluationException {
		QueryEvaluationStep leftQes = precompile(node.getLeftArg(), context);
		QueryEvaluationStep rightQes = precompile(node.getRightArg(), context);

		return new UnionQueryEvaluationStep(leftQes, rightQes);
	}

	protected QueryEvaluationStep prepare(Slice node, QueryEvaluationContext context) throws QueryEvaluationException {
		QueryEvaluationStep arg = precompile(node.getArg(), context);
		return SliceQueryEvaluationStep.supply(node, arg);
	}

	protected QueryEvaluationStep prepare(Extension node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryEvaluationStep arg = precompile(node.getArg(), context);
		Consumer<MutableBindingSet> consumer = ExtensionIterator.buildLambdaToEvaluateTheExpressions(node, this,
				context);
		return new ExtensionQueryEvaluationStep(arg, consumer, context);
	}

	protected QueryEvaluationStep prepare(Service service, QueryEvaluationContext context)
			throws QueryEvaluationException {
		Var serviceRef = service.getServiceRef();
		return new ServiceQueryEvaluationStep(service, serviceRef, serviceResolver);
	}

	protected QueryEvaluationStep prepare(Filter node, QueryEvaluationContext context) throws QueryEvaluationException {

		return FilterIterator.supply(node, DefaultEvaluationStrategy.this, context);

	}

	protected QueryEvaluationStep prepare(Order node, QueryEvaluationContext context) throws QueryEvaluationException {
		ValueComparator vcmp = new ValueComparator();
		OrderComparator cmp = new OrderComparator(this, node, vcmp, context);
		boolean reduced = isReducedOrDistinct(node);
		long limit = getLimit(node);
		QueryEvaluationStep preparedArg = precompile(node.getArg(), context);
		return new OrderQueryEvaluationStep(cmp, limit, reduced, preparedArg, iterationCacheSyncThreshold);
	}

	protected QueryEvaluationStep prepare(BindingSetAssignment node, QueryEvaluationContext context)
			throws QueryEvaluationException {

		return new BindingSetAssignmentQueryEvaluationStep(node, context);
	}

	private final class QueryRootQueryEvaluationStep implements QueryEvaluationStep {
		private final QueryEvaluationStep arg;
		private final QueryEvaluationContext context;

		private QueryRootQueryEvaluationStep(QueryEvaluationStep arg, QueryEvaluationContext context) {
			this.arg = arg;
			this.context = context;
		}

		@Override
		public CloseableIteration<BindingSet> evaluate(BindingSet bs) {
			// TODO fix the sharing of the now element to be safe
			DefaultEvaluationStrategy.this.sharedValueOfNow = null;
			CloseableIteration<BindingSet> evaluate = null;
			try {
				evaluate = arg.evaluate(bs);
				var eval = evaluate;

				CloseableIteration<BindingSet> closeContext = new CloseableIteration<>() {

					@Override
					public boolean hasNext() throws QueryEvaluationException {
						return eval.hasNext();
					}

					@Override
					public BindingSet next() throws QueryEvaluationException {
						return eval.next();
					}

					@Override
					public void remove() throws QueryEvaluationException {
						eval.remove();

					}

					@Override
					public void close() throws QueryEvaluationException {
						eval.close();
					}
				};
				return closeContext;
			} catch (Throwable t) {
				if (evaluate != null) {
					evaluate.close();
				}
				throw t;
			}
		}

	}

	protected QueryEvaluationStep prepare(DescribeOperator node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryEvaluationStep child = precompile(node.getArg(), context);
		return bs -> {
			CloseableIteration<BindingSet> evaluate = null;

			try {
				evaluate = child.evaluate(bs);
				return new DescribeIteration(evaluate, DefaultEvaluationStrategy.this, node.getBindingNames(), bs);
			} catch (Throwable t) {
				if (evaluate != null) {
					evaluate.close();
				}
				throw t;
			}

		};
	}

	protected QueryEvaluationStep prepare(Distinct node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		final QueryEvaluationStep child = precompile(node.getArg(), context);
		final CollectionFactory cf = this.getCollectionFactory().get();
		return bindings -> {
			final CloseableIteration<BindingSet> evaluate = child.evaluate(bindings);
			return new DistinctIteration<BindingSet>(evaluate, cf.createSetOfBindingSets()) {

				@Override
				protected void handleClose() throws QueryEvaluationException {
					try {
						cf.close();
					} finally {
						super.handleClose();
					}
				}
			};
		};
	}

	protected QueryEvaluationStep prepare(Reduced node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryEvaluationStep arg = precompile(node.getArg(), context);
		return bindings -> new ReducedIteration<>(arg.evaluate(bindings));
	}

	protected QueryEvaluationStep prepare(TupleFunctionCall expr, QueryEvaluationContext context)
			throws QueryEvaluationException {
		TupleFunction func = tupleFuncRegistry.get(expr.getURI())
				.orElseThrow(() -> new QueryEvaluationException("Unknown tuple function '" + expr.getURI() + "'"));

		List<ValueExpr> args = expr.getArgs();
		QueryValueEvaluationStep[] argEpresions = new QueryValueEvaluationStep[args.size()];
		for (int i = 0; i < args.size(); i++) {
			argEpresions[i] = precompile(args.get(i), context);
		}

		return bindings -> {
			Value[] argValues = new Value[args.size()];
			for (int i = 0; i < args.size(); i++) {
				argValues[i] = argEpresions[i].evaluate(bindings);
			}

			return evaluate(func, expr.getResultVars(), bindings,
					tripleSource.getValueFactory(), argValues);
		};
	}

	public static Value getVarValue(Var var, BindingSet bindings) {
		if (var == null) {
			return null;
		} else if (var.hasValue()) {
			return var.getValue();
		} else {
			return bindings.getValue(var.getName());
		}
	}

	protected QueryEvaluationStep prepare(UnaryTupleOperator expr, QueryEvaluationContext context)
			throws QueryEvaluationException {
		if (expr instanceof Projection) {
			return prepare((Projection) expr, context);
		} else if (expr instanceof MultiProjection) {
			return prepare((MultiProjection) expr, context);
		} else if (expr instanceof Filter) {
			return prepare((Filter) expr, context);
		} else if (expr instanceof Service) {
			return prepare((Service) expr, context);
		} else if (expr instanceof Slice) {
			return prepare((Slice) expr, context);
		} else if (expr instanceof Extension) {
			return prepare((Extension) expr, context);
		} else if (expr instanceof Distinct) {
			return prepare((Distinct) expr, context);
		} else if (expr instanceof Reduced) {
			return prepare((Reduced) expr, context);
		} else if (expr instanceof Group) {
			return prepare((Group) expr, context);
		} else if (expr instanceof Order) {
			return prepare((Order) expr, context);
		} else if (expr instanceof QueryRoot) {
			// new query, reset shared return value for successive calls of
			// NOW()
			this.sharedValueOfNow = null;
			return precompile(expr.getArg(), context);
		} else if (expr instanceof DescribeOperator) {
			return prepare((DescribeOperator) expr, context);
		} else if (expr == null) {
			throw new IllegalArgumentException("expr must not be null");
		} else {
			throw new QueryEvaluationException("Unknown unary tuple operator type: " + expr.getClass());
		}
	}

	protected QueryEvaluationStep prepare(BinaryTupleOperator expr, QueryEvaluationContext context)
			throws QueryEvaluationException {
		if (expr instanceof Join) {
			return prepare((Join) expr, context);
		} else if (expr instanceof LeftJoin) {
			return prepare((LeftJoin) expr, context);
		} else if (expr instanceof Union) {
			return prepare((Union) expr, context);
		} else if (expr instanceof Intersection) {
			return prepare((Intersection) expr, context);
		} else if (expr instanceof Difference) {
			return prepare((Difference) expr, context);
		} else if (expr == null) {
			throw new IllegalArgumentException("expr must not be null");
		} else {
			throw new QueryEvaluationException("Unsupported binary tuple operator type: " + expr.getClass());
		}
	}

	protected QueryEvaluationStep prepare(SingletonSet singletonSet, QueryEvaluationContext context)
			throws QueryEvaluationException {
		return SingletonIteration::new;

	}

	protected QueryEvaluationStep prepare(EmptySet emptySet, QueryEvaluationContext context)
			throws QueryEvaluationException {
		return bindings -> QueryEvaluationStep.EMPTY_ITERATION;
	}

	@Override
	public QueryValueEvaluationStep precompile(ValueExpr expr,
			QueryEvaluationContext context)
			throws QueryEvaluationException {
		if (expr instanceof Var) {
			return prepare((Var) expr, context);
		} else if (expr instanceof ValueConstant) {
			return prepare((ValueConstant) expr, context);
		} else if (expr instanceof BNodeGenerator) {
			return prepare((BNodeGenerator) expr, context);
		} else if (expr instanceof Bound) {
			return prepare((Bound) expr, context);
//			return new QueryValueEvaluationStep.Minimal(this, expr);
		} else if (expr instanceof Str) {
			return prepare((Str) expr, context);
		} else if (expr instanceof Label) {
			return prepare((Label) expr, context);
		} else if (expr instanceof Lang) {
			return prepare((Lang) expr, context);
		} else if (expr instanceof LangMatches) {
			return prepare((LangMatches) expr, context);
		} else if (expr instanceof Datatype) {
			return prepare((Datatype) expr, context);
		} else if (expr instanceof Namespace) {
			return prepare((Namespace) expr, context);
		} else if (expr instanceof LocalName) {
			return prepare((LocalName) expr, context);
		} else if (expr instanceof IsResource) {
			return prepare((IsResource) expr, context);
		} else if (expr instanceof IsURI) {
			return prepare((IsURI) expr, context);
		} else if (expr instanceof IsBNode) {
			return prepare((IsBNode) expr, context);
		} else if (expr instanceof IsLiteral) {
			return prepare((IsLiteral) expr, context);
		} else if (expr instanceof IsNumeric) {
			return prepare((IsNumeric) expr, context);
		} else if (expr instanceof IRIFunction) {
			return prepare((IRIFunction) expr, context);
		} else if (expr instanceof Regex) {
			return prepare((Regex) expr, context);
		} else if (expr instanceof Coalesce) {
			return prepare((Coalesce) expr, context);
		} else if (expr instanceof FunctionCall) {
			return prepare((FunctionCall) expr, context);
		} else if (expr instanceof And) {
			return prepare((And) expr, context);
		} else if (expr instanceof Or) {
			return prepare((Or) expr, context);
		} else if (expr instanceof Not) {
			return prepare((Not) expr, context);
		} else if (expr instanceof SameTerm) {
			return prepare((SameTerm) expr, context);
		} else if (expr instanceof Compare) {
			return prepare((Compare) expr, context);
		} else if (expr instanceof MathExpr) {
			return prepare((MathExpr) expr, context);
		} else if (expr instanceof In) {
			return prepare((In) expr, context);
		} else if (expr instanceof CompareAny) {
			return prepare((CompareAny) expr, context);
		} else if (expr instanceof CompareAll) {
			return prepare((CompareAll) expr, context);
		} else if (expr instanceof Exists) {
			return prepare((Exists) expr, context);
		} else if (expr instanceof If) {
			return prepare((If) expr, context);
		} else if (expr instanceof ListMemberOperator) {
			return prepare((ListMemberOperator) expr, context);
		} else if (expr instanceof ValueExprTripleRef) {
			return prepare((ValueExprTripleRef) expr, context);
		} else if (expr == null) {
			throw new IllegalArgumentException("expr must not be null");
		} else {
			throw new QueryEvaluationException("Unsupported value expr type: " + expr.getClass());
		}
	}

	@Deprecated(forRemoval = true)
	@Override
	public Value evaluate(ValueExpr expr, BindingSet bindings)
			throws QueryEvaluationException {
		return precompile(expr,
				new QueryEvaluationContext.Minimal(DefaultEvaluationStrategy.this.sharedValueOfNow, dataset,
						tripleSource.getComparator()))
				.evaluate(bindings);
	}

	protected QueryValueEvaluationStep prepare(Var var, QueryEvaluationContext context)
			throws QueryEvaluationException {

		Value value = var.getValue();

		if (value != null) {
			return new ConstantQueryValueEvaluationStep(value);
		} else {
			java.util.function.Function<BindingSet, Value> getValue = context.getValue(var.getName());
			Predicate<BindingSet> hasValue = context.hasBinding(var.getName());
			return bindings -> {
				if (hasValue.test(bindings)) {
					return getValue.apply(bindings);
				} else {
					throw new ValueExprEvaluationException();
				}
			};
		}

	}

	protected QueryValueEvaluationStep prepare(ValueConstant valueConstant, QueryEvaluationContext context)
			throws QueryEvaluationException {
		return new ConstantQueryValueEvaluationStep(valueConstant);
	}

	protected QueryValueEvaluationStep prepare(BNodeGenerator node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		ValueFactory vf = tripleSource.getValueFactory();
		ValueExpr nodeIdExpr = node.getNodeIdExpr();
		if (nodeIdExpr != null) {
			QueryValueEvaluationStep nodeVes = precompile(nodeIdExpr, context);
			return QueryValueEvaluationStepSupplier.bnode(nodeVes, vf);
		} else {
			return new QueryValueEvaluationStep.ApplyFunctionForEachBinding(bs -> vf.createBNode());
		}
	}

	protected QueryValueEvaluationStep prepare(Bound node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		try {
			QueryValueEvaluationStep arg = precompile(node.getArg(), context);
			return QueryValueEvaluationStepSupplier.prepareBound(arg, context);
		} catch (QueryEvaluationException e) {
			return new QueryValueEvaluationStep.ConstantQueryValueEvaluationStep(BooleanLiteral.FALSE);
		}
	}

	protected QueryValueEvaluationStep prepare(Str node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		ValueFactory valueFactory = tripleSource.getValueFactory();
		return QueryValueEvaluationStepSupplier.prepareStr(arg, valueFactory);
	}

	protected QueryValueEvaluationStep prepare(Label node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		return QueryValueEvaluationStepSupplier.prepareLabel(arg, tripleSource.getValueFactory());
	}

	protected QueryValueEvaluationStep prepare(Lang node, QueryEvaluationContext context) {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		return QueryValueEvaluationStepSupplier.prepareLang(arg, tripleSource.getValueFactory());
	}

	protected QueryValueEvaluationStep prepare(Datatype node, QueryEvaluationContext context) {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		return QueryValueEvaluationStepSupplier.prepareDatatype(arg, context);
	}

	protected QueryValueEvaluationStep prepare(Namespace node, QueryEvaluationContext context) {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		return QueryValueEvaluationStepSupplier.prepareNamespace(arg, tripleSource.getValueFactory());
	}

	protected QueryValueEvaluationStep prepare(LocalName node, QueryEvaluationContext context) {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		return QueryValueEvaluationStepSupplier.prepareLocalName(arg, tripleSource.getValueFactory());
	}

	protected QueryValueEvaluationStep prepare(IsResource node, QueryEvaluationContext context) {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		return QueryValueEvaluationStepSupplier.prepareIs(arg, v -> v instanceof Resource);
	}

	protected QueryValueEvaluationStep prepare(IsURI node, QueryEvaluationContext context) {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		return QueryValueEvaluationStepSupplier.prepareIs(arg, v -> v instanceof IRI);
	}

	protected QueryValueEvaluationStep prepare(IsBNode node, QueryEvaluationContext context) {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		return QueryValueEvaluationStepSupplier.prepareIs(arg, v -> v instanceof BNode);
	}

	protected QueryValueEvaluationStep prepare(IsLiteral node, QueryEvaluationContext context) {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		return QueryValueEvaluationStepSupplier.prepareIs(arg, v -> v instanceof Literal);
	}

	protected QueryValueEvaluationStep prepare(IsNumeric node, QueryEvaluationContext context) {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		return QueryValueEvaluationStepSupplier.prepareIs(arg, DefaultEvaluationStrategy::isNumeric);
	}

	private static boolean isNumeric(Value argValue) {
		if (argValue instanceof Literal) {
			Literal lit = (Literal) argValue;
			CoreDatatype.XSD datatype = lit.getCoreDatatype().asXSDDatatypeOrNull();
			return datatype != null && datatype.isNumericDatatype();
		} else {
			return false;
		}
	}

	protected QueryValueEvaluationStep prepare(IRIFunction node, QueryEvaluationContext context) {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		return QueryValueEvaluationStepSupplier.prepareIriFunction(node, arg, tripleSource.getValueFactory());
	}

	/**
	 * Determines whether the two operands match according to the <code>regex</code> operator.
	 *
	 * @return <var>true</var> if the operands match according to the <var>regex</var> operator, <var>false</var>
	 *         otherwise.
	 */
	@Deprecated(forRemoval = true)
	public Value evaluate(Regex node, BindingSet bindings)
			throws QueryEvaluationException {
		return prepare(node,
				new QueryEvaluationContext.Minimal(sharedValueOfNow, dataset, tripleSource.getComparator()))
				.evaluate(bindings);
	}

	/**
	 * Determines whether the two operands match according to the <code>regex</code> operator.
	 *
	 * @return <var>true</var> if the operands match according to the <var>regex</var> operator, <var>false</var>
	 *         otherwise.
	 */
	protected QueryValueEvaluationStep prepare(Regex node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		return RegexValueEvaluationStepSupplier.make(this, node, context);
	}

	protected QueryValueEvaluationStep prepare(LangMatches node, QueryEvaluationContext context) {
		return supplyBinaryValueEvaluation(node, this::evaluateLangMatch, context);
	}

	private Value evaluateLangMatch(Value langTagValue, Value langRangeValue) {
		if (QueryEvaluationUtility.isSimpleLiteral(langTagValue)
				&& QueryEvaluationUtility.isSimpleLiteral(langRangeValue)) {
			String langTag = ((Literal) langTagValue).getLabel();
			String langRange = ((Literal) langRangeValue).getLabel();

			boolean result = Literals.langMatches(langTag, langRange);

			return BooleanLiteral.valueOf(result);
		}

		throw new ValueExprEvaluationException();
	}

	public QueryValueEvaluationStep prepare(FunctionCall node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		Function function = FunctionRegistry.getInstance()
				.get(node.getURI())
				.orElseThrow(() -> new QueryEvaluationException("Unknown function '" + node.getURI() + "'"));

		// the NOW function is a special case as it needs to keep a shared
		// return
		// value for the duration of the query.
		if (function instanceof Now) {
			return prepare((Now) function, context);
		}

		List<ValueExpr> args = node.getArgs();

		QueryValueEvaluationStep[] argSteps = new QueryValueEvaluationStep[args.size()];

		boolean allConstant = determineIfFunctionCallWillBeAConstant(context, function, args, argSteps);
		if (allConstant) {
			try {
				Value[] argValues = evaluateAllArguments(args, argSteps, EmptyBindingSet.getInstance());
				Value res = function.evaluate(tripleSource, argValues);
				return new QueryValueEvaluationStep.ConstantQueryValueEvaluationStep(res);
			} catch (QueryEvaluationException ex) {
				return new QueryValueEvaluationStep.Fail("Constant failure: " + ex.getMessage());
			}
		} else {
			return bindings -> {
				Value[] argValues = evaluateAllArguments(args, argSteps, bindings);
				return function.evaluate(tripleSource, argValues);
			};
		}
	}

	/**
	 * If all input is constant normally the function call output will be constant as well.
	 *
	 * @param context  used to precompile arguments of the function
	 * @param function that might be constant
	 * @param args     that the function must evaluate
	 * @param argSteps side effect this array is filled
	 * @return if this function resolves to a constant value
	 */
	private boolean determineIfFunctionCallWillBeAConstant(QueryEvaluationContext context, Function function,
			List<ValueExpr> args, QueryValueEvaluationStep[] argSteps) {
		boolean allConstant = true;
		if (function.mustReturnDifferentResult()) {
			allConstant = false;
			for (int i = 0; i < args.size(); i++) {
				argSteps[i] = precompile(args.get(i), context);
			}
		} else {
			for (int i = 0; i < args.size(); i++) {
				argSteps[i] = precompile(args.get(i), context);
				if (!argSteps[i].isConstant()) {
					allConstant = false;
				}
			}
		}
		return allConstant;
	}

	private Value[] evaluateAllArguments(List<ValueExpr> args, QueryValueEvaluationStep[] argSteps,
			BindingSet bindings) {
		Value[] argValues = new Value[argSteps.length];
		for (int i = 0; i < args.size(); i++) {
			argValues[i] = argSteps[i].evaluate(bindings);
		}
		return argValues;
	}

	private QueryValueEvaluationStep prepare(And node, QueryEvaluationContext context) throws QueryEvaluationException {
		QueryValueEvaluationStep leftStep = precompile(node.getLeftArg(), context);
		QueryValueEvaluationStep rightStep = precompile(node.getRightArg(), context);

		return AndValueEvaluationStep.supply(leftStep, rightStep);
	}

	protected QueryValueEvaluationStep prepare(Or node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryValueEvaluationStep leftArg = null;
		QueryValueEvaluationStep rightArg = null;
		try {
			try {
				leftArg = precompile(node.getLeftArg(), context);
			} catch (ValueExprEvaluationException e) {
				// leftArg would always be false in this case so no need to evaluate it.
				return precompile(node.getRightArg(), context);
			}
			rightArg = precompile(node.getRightArg(), context);
		} catch (ValueExprEvaluationException e) {
			// Both failed to compile so we know we will always throw an exception
			return new QueryValueEvaluationStep.Fail("Value Expressions in OR both failed to prepare/precompile");
		}
		return new OrValueEvaluationStep(leftArg, rightArg);
	}

	protected QueryValueEvaluationStep prepare(Not node, QueryEvaluationContext context) {
		return supplyUnaryValueEvaluation(node,
				v -> BooleanLiteral.valueOf(!QueryEvaluationUtil.getEffectiveBooleanValue(v)), context);
	}

	/**
	 * During the execution of a single query NOW() should always return the same result and is in practical terms a
	 * constant during evaluation.
	 *
	 * @param node    that represent the NOW() function
	 * @param context that holds the shared now() of the query invocation
	 * @return a constant value evaluation step
	 */
	protected QueryValueEvaluationStep prepare(Now node, QueryEvaluationContext context) {
		return new QueryValueEvaluationStep.ConstantQueryValueEvaluationStep(context.getNow());
	}

	protected QueryValueEvaluationStep prepare(SameTerm node, QueryEvaluationContext context) {
		return supplyBinaryValueEvaluation(node,
				(leftVal, rightVal) -> BooleanLiteral.valueOf(leftVal != null && leftVal.equals(rightVal)), context);
	}

	protected QueryValueEvaluationStep prepare(Coalesce node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		List<ValueExpr> args = node.getArguments();
		List<QueryValueEvaluationStep> compiledArgs = new ArrayList<>(args.size());
		for (ValueExpr arg : args) {
			try {
				compiledArgs.add(precompile(arg, context));
			} catch (QueryEvaluationException e) {
				// no need to add as it would have been ignored anyway.
			}
		}

		return bindings -> {

			for (QueryValueEvaluationStep expr : compiledArgs) {
				try {
					// return first result that does not produce an error on
					// evaluation.
					return expr.evaluate(bindings);

				} catch (QueryEvaluationException ignored) {
				}
			}

			throw new ValueExprEvaluationException(
					"COALESCE arguments do not evaluate to a value: " + node.getSignature());
		};
	}

	protected QueryValueEvaluationStep prepare(Compare node, QueryEvaluationContext context) {
		boolean strict = QueryEvaluationMode.STRICT == getQueryEvaluationMode();
		return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
				.valueOf(QueryEvaluationUtil.compare(leftVal, rightVal, node.getOperator(), strict)), context);
	}

	private BiFunction<Value, Value, Value> mathOperationApplier(MathExpr node,
			QueryEvaluationMode queryEvaluationMode, ValueFactory vf) {
		final MathOp operator = node.getOperator();
		switch (queryEvaluationMode) {
		case STRICT: {
			return (l, r) -> {
				if (l instanceof Literal && r instanceof Literal) {
					return MathUtil.compute((Literal) l, (Literal) r, operator);
				} else {
					throw new ValueExprEvaluationException("Both arguments must be literals");
				}
			};
		}
		case STANDARD:
		default:

			return (l, r) -> {
				if (l instanceof Literal && r instanceof Literal) {
					return XMLDatatypeMathUtil.compute((Literal) l, (Literal) r, operator, vf);
				} else {
					throw new ValueExprEvaluationException("Both arguments must be literals");
				}
			};
		}
	}

	protected QueryValueEvaluationStep prepare(MathExpr node, QueryEvaluationContext context) {
		final BiFunction<Value, Value, Value> mathOperationApplier = mathOperationApplier(node,
				getQueryEvaluationMode(),
				tripleSource.getValueFactory());
		return supplyBinaryValueEvaluation(node, mathOperationApplier, context);
	}

	protected QueryValueEvaluationStep prepare(If node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryValueEvaluationStep condition;
		try {
			condition = precompile(node.getCondition(), context);
		} catch (ValueExprEvaluationException e) {
			// in case of type error, if-construction should result in empty
			// binding.
			return new QueryValueEvaluationStep.ApplyFunctionForEachBinding(bs -> null);
		}
		QueryValueEvaluationStep result = precompile(node.getResult(), context);
		QueryValueEvaluationStep alternative = precompile(node.getAlternative(), context);
		return new IfValueEvaluationStep(result, condition, alternative);
	}

	protected QueryValueEvaluationStep prepare(In node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryValueEvaluationStep left = precompile(node.getArg(), context);
		QueryEvaluationStep subquery = precompile(node.getSubQuery(), context);
		return new InValueEvaluationStep(node, subquery, left);
	}

	protected QueryValueEvaluationStep prepare(ListMemberOperator node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		List<ValueExpr> args = node.getArguments();
		List<QueryValueEvaluationStep> compiledArgs = new ArrayList<>(args.size());
		for (ValueExpr arg : args) {
			try {
				compiledArgs.add(precompile(arg, context));
			} catch (ValueExprEvaluationException e) {
				compiledArgs.add(new QueryValueEvaluationStep.Fail(""));
			}
		}
		return new ListMemberValueOperationStep(compiledArgs);
	}

	protected QueryValueEvaluationStep prepare(CompareAny node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		QueryEvaluationStep subquery = precompile(node.getSubQuery(), context);
		return new CompareAnyValueEvaluationStep(arg, node, subquery, context);
	}

	protected QueryValueEvaluationStep prepare(CompareAll node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryValueEvaluationStep arg = precompile(node.getArg(), context);
		QueryEvaluationStep subquery = precompile(node.getSubQuery(), context);
		return new CompareAllQueryValueEvaluationStep(arg, node, subquery, context);
	}

	protected QueryValueEvaluationStep prepare(Exists node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryEvaluationStep subquery = precompile(node.getSubQuery(), context);
		return new ExistsQueryValueEvaluationStep(subquery);
	}

	@Override
	public boolean isTrue(ValueExpr expr, BindingSet bindings) throws QueryEvaluationException {
		Value value = evaluate(expr, bindings);
		return QueryEvaluationUtility.getEffectiveBooleanValue(value).orElse(false);
	}

	public boolean isTrue(QueryValueEvaluationStep expr, BindingSet bindings) throws QueryEvaluationException {
		Value value = expr.evaluate(bindings);
		return QueryEvaluationUtility.getEffectiveBooleanValue(value).orElse(false);
	}

	protected boolean isReducedOrDistinct(QueryModelNode node) {
		QueryModelNode parent = node.getParentNode();
		if (parent instanceof Slice) {
			return isReducedOrDistinct(parent);
		}
		return parent instanceof Distinct || parent instanceof Reduced;
	}

	/**
	 * Returns the limit of the current variable bindings before any further projection.
	 */
	protected long getLimit(QueryModelNode node) {
		long offset = 0;
		if (node instanceof Slice) {
			Slice slice = (Slice) node;
			if (slice.hasOffset() && slice.hasLimit()) {
				return slice.getOffset() + slice.getLimit();
			} else if (slice.hasLimit()) {
				return slice.getLimit();
			} else if (slice.hasOffset()) {
				offset = slice.getOffset();
			}
		}
		QueryModelNode parent = node.getParentNode();
		if (parent instanceof Distinct || parent instanceof Reduced || parent instanceof Slice) {
			long limit = getLimit(parent);
			if (offset > 0L && limit < Long.MAX_VALUE) {
				return offset + limit;
			} else {
				return limit;
			}
		}
		return Long.MAX_VALUE;
	}

	protected QueryValueEvaluationStep prepare(ValueExprTripleRef node, QueryEvaluationContext context)
			throws QueryEvaluationException {
		QueryValueEvaluationStep subject = precompile(node.getSubjectVar(), context);
		QueryValueEvaluationStep predicate = precompile(node.getPredicateVar(), context);
		QueryValueEvaluationStep object = precompile(node.getObjectVar(), context);
		ValueFactory valueFactory = tripleSource.getValueFactory();
		return new ValueExprTripleRefEvaluationStep(subject, valueFactory, predicate, object);

	}

	/**
	 * evaluates a TripleRef node returning bindingsets from the matched Triple nodes in the dataset (or explore
	 * standard reification)
	 *
	 * @param ref      to evaluate
	 * @param bindings with the solutions
	 * @return iteration over the solutions
	 */
	public CloseableIteration<BindingSet> evaluate(TripleRef ref, BindingSet bindings) {
		return precompile(ref).evaluate(bindings);
	}

	protected QueryEvaluationStep prepare(TripleRef ref, QueryEvaluationContext context) {
		// Naive implementation that walks over all statements matching (x rdf:type rdf:Statement)
		// and filter those that do not match the bindings for subject, predicate and object vars (if bound)
		final org.eclipse.rdf4j.query.algebra.Var subjVar = ref.getSubjectVar();
		final org.eclipse.rdf4j.query.algebra.Var predVar = ref.getPredicateVar();
		final org.eclipse.rdf4j.query.algebra.Var objVar = ref.getObjectVar();
		final org.eclipse.rdf4j.query.algebra.Var extVar = ref.getExprVar();
		// whether the TripleSouce support access to RDF star
		final boolean sourceSupportsRdfStar = tripleSource instanceof RDFStarTripleSource;
		if (sourceSupportsRdfStar) {
			return new RdfStarQueryEvaluationStep(subjVar, predVar, objVar, extVar, (RDFStarTripleSource) tripleSource,
					context);
		} else {
			return new ReificationRdfStarQueryEvaluationStep(subjVar, predVar, objVar, extVar, tripleSource, context);
		}
	}

	/**
	 * This class wraps an iterator and increments the "resultSizeActual" of the query model node that the iterator
	 * represents. This means we can track the number of tuples that have been retrieved from this node.
	 */
	private static class ResultSizeCountingIterator extends IterationWrapper<BindingSet> {

		CloseableIteration<BindingSet> iterator;
		QueryModelNode queryModelNode;

		public ResultSizeCountingIterator(CloseableIteration<BindingSet> iterator,
				QueryModelNode queryModelNode) {
			super(iterator);
			this.iterator = iterator;
			this.queryModelNode = queryModelNode;
		}

		@Override
		public boolean hasNext() throws QueryEvaluationException {
			return iterator.hasNext();
		}

		@Override
		public BindingSet next() throws QueryEvaluationException {
			queryModelNode.setResultSizeActual(queryModelNode.getResultSizeActual() + 1);
			return iterator.next();
		}

	}

	/**
	 * This class wraps an iterator and tracks the time used to execute next() and hasNext()
	 */
	private static class TimedIterator extends IterationWrapper<BindingSet> {

		CloseableIteration<BindingSet> iterator;
		QueryModelNode queryModelNode;
		Stopwatch stopwatch = Stopwatch.createStarted();

		public TimedIterator(CloseableIteration<BindingSet> iterator,
				QueryModelNode queryModelNode) {
			super(iterator);
			this.iterator = iterator;
			this.queryModelNode = queryModelNode;
			stopwatch.stop();
		}

		@Override
		public BindingSet next() throws QueryEvaluationException {
			stopwatch.start();
			BindingSet next = iterator.next();
			stopwatch.stop();
			return next;
		}

		@Override
		public boolean hasNext() throws QueryEvaluationException {
			stopwatch.start();
			boolean hasNext = super.hasNext();
			stopwatch.stop();
			return hasNext;
		}

		@Override
		protected void handleClose() throws QueryEvaluationException {
			try {
				long totalTimeNanosActual = queryModelNode.getTotalTimeNanosActual();
				queryModelNode
						.setTotalTimeNanosActual((totalTimeNanosActual + stopwatch.elapsed(TimeUnit.NANOSECONDS)));
			} finally {
				super.handleClose();

			}
		}
	}

	@Override
	public void setTrackResultSize(boolean trackResultSize) {
		this.trackResultSize = trackResultSize;
	}

	@Override
	public void setTrackTime(boolean trackTime) {
		this.trackTime = trackTime;
	}

	/**
	 * Supply a QueryValueEvalationStep that will invoke the function (operator passed in). It will try to optimise
	 * constant argument to be called only once per query run,
	 *
	 * @param node      the node to evaluate
	 * @param operation the function that wraps the operator.
	 * @param context   in which the query is running.
	 * @return a potential constant evaluation step.
	 */
	protected QueryValueEvaluationStep supplyBinaryValueEvaluation(BinaryValueOperator node,
			BiFunction<Value, Value, Value> operation, QueryEvaluationContext context) {
		QueryValueEvaluationStep leftStep = precompile(node.getLeftArg(), context);
		QueryValueEvaluationStep rightStep = precompile(node.getRightArg(), context);
		if (leftStep.isConstant() && rightStep.isConstant()) {
			Value leftVal = leftStep.evaluate(EmptyBindingSet.getInstance());
			Value rightVal = rightStep.evaluate(EmptyBindingSet.getInstance());
			Value value = operation.apply(leftVal, rightVal);
			return new QueryValueEvaluationStep.ConstantQueryValueEvaluationStep(value);
		} else if (leftStep.isConstant()) {
			Value leftVal = leftStep.evaluate(EmptyBindingSet.getInstance());
			return bindings -> {
				Value rightVal = rightStep.evaluate(bindings);
				return operation.apply(leftVal, rightVal);
			};
		} else if (rightStep.isConstant()) {
			Value rightVal = rightStep.evaluate(EmptyBindingSet.getInstance());
			return bindings -> {
				Value leftVal = leftStep.evaluate(bindings);
				Value result = operation.apply(leftVal, rightVal);
				return result;
			};
		} else {
			return bindings -> {
				Value leftVal = leftStep.evaluate(bindings);
				Value rightVal = rightStep.evaluate(bindings);
				return operation.apply(leftVal, rightVal);
			};
		}
	}

	/**
	 * Return a QueryEvaluationStep that applies constant propegation.
	 *
	 * @param node      that will be evaluated/prepared
	 * @param operation the task to be done
	 * @param context   in which the evaluation takes place
	 * @return a potentially constant step
	 */
	protected QueryValueEvaluationStep supplyUnaryValueEvaluation(UnaryValueOperator node,
			java.util.function.Function<Value, Value> operation, QueryEvaluationContext context) {
		QueryValueEvaluationStep argStep = precompile(node.getArg(), context);
		if (argStep.isConstant()) {
			Value argValue = argStep.evaluate(EmptyBindingSet.getInstance());

			return new QueryValueEvaluationStep.ConstantQueryValueEvaluationStep(operation.apply(argValue));
		} else {
			return bindings -> {
				Value argValue = argStep.evaluate(bindings);
				return operation.apply(argValue);
			};
		}
	}

	/**
	 * @return the queryEvaluationMode
	 */
	public QueryEvaluationMode getQueryEvaluationMode() {
		return queryEvaluationMode;
	}

	/**
	 * @param queryEvaluationMode the queryEvaluationMode to set
	 */
	public void setQueryEvaluationMode(QueryEvaluationMode queryEvaluationMode) {
		this.queryEvaluationMode = Objects.requireNonNull(queryEvaluationMode);
	}

	@Override
	public Supplier<CollectionFactory> getCollectionFactory() {
		return collectionFactory;
	}

	@Override
	public void setCollectionFactory(Supplier<CollectionFactory> cf) {
		this.collectionFactory = cf;
	}
}