All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.modeshape.jcr.query.engine.ScanningQueryEngine Maven / Gradle / Ivy

There is a newer version: 5.4.1.Final
Show newest version
/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.modeshape.jcr.query.engine;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.regex.Pattern;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.query.qom.BindVariableValue;
import javax.jcr.query.qom.StaticOperand;
import org.modeshape.common.SystemFailureException;
import org.modeshape.common.annotation.Immutable;
import org.modeshape.common.annotation.ThreadSafe;
import org.modeshape.common.collection.ArrayListMultimap;
import org.modeshape.common.collection.Multimap;
import org.modeshape.common.collection.Problems;
import org.modeshape.common.logging.Logger;
import org.modeshape.common.util.CheckArg;
import org.modeshape.jcr.ExecutionContext;
import org.modeshape.jcr.GraphI18n;
import org.modeshape.jcr.JcrLexicon;
import org.modeshape.jcr.ModeShapeLexicon;
import org.modeshape.jcr.NodeTypes;
import org.modeshape.jcr.RepositoryIndexes;
import org.modeshape.jcr.api.query.QueryCancelledException;
import org.modeshape.jcr.api.query.qom.Operator;
import org.modeshape.jcr.cache.CachedNode;
import org.modeshape.jcr.cache.CachedNodeSupplier;
import org.modeshape.jcr.cache.NodeCache;
import org.modeshape.jcr.cache.NodeKey;
import org.modeshape.jcr.cache.PropertyTypeUtil;
import org.modeshape.jcr.cache.RepositoryCache;
import org.modeshape.jcr.query.BufferManager;
import org.modeshape.jcr.query.NodeSequence;
import org.modeshape.jcr.query.NodeSequence.Batch;
import org.modeshape.jcr.query.NodeSequence.RowAccessor;
import org.modeshape.jcr.query.NodeSequence.RowFilter;
import org.modeshape.jcr.query.QueryContext;
import org.modeshape.jcr.query.QueryEngine;
import org.modeshape.jcr.query.QueryEngineBuilder;
import org.modeshape.jcr.query.QueryResults;
import org.modeshape.jcr.query.QueryResults.Columns;
import org.modeshape.jcr.query.QueryResults.Statistics;
import org.modeshape.jcr.query.RowExtractors;
import org.modeshape.jcr.query.RowExtractors.ExtractFromRow;
import org.modeshape.jcr.query.engine.process.DependentQuery;
import org.modeshape.jcr.query.engine.process.DistinctSequence;
import org.modeshape.jcr.query.engine.process.ExceptSequence;
import org.modeshape.jcr.query.engine.process.HashJoinSequence;
import org.modeshape.jcr.query.engine.process.IntersectSequence;
import org.modeshape.jcr.query.engine.process.JoinSequence.Range;
import org.modeshape.jcr.query.engine.process.JoinSequence.RangeProducer;
import org.modeshape.jcr.query.engine.process.SortingSequence;
import org.modeshape.jcr.query.model.And;
import org.modeshape.jcr.query.model.ArithmeticOperand;
import org.modeshape.jcr.query.model.Between;
import org.modeshape.jcr.query.model.BindVariableName;
import org.modeshape.jcr.query.model.ChildNode;
import org.modeshape.jcr.query.model.ChildNodeJoinCondition;
import org.modeshape.jcr.query.model.Column;
import org.modeshape.jcr.query.model.Comparison;
import org.modeshape.jcr.query.model.Constraint;
import org.modeshape.jcr.query.model.DescendantNode;
import org.modeshape.jcr.query.model.DescendantNodeJoinCondition;
import org.modeshape.jcr.query.model.DynamicOperand;
import org.modeshape.jcr.query.model.EquiJoinCondition;
import org.modeshape.jcr.query.model.FullTextSearch;
import org.modeshape.jcr.query.model.FullTextSearchScore;
import org.modeshape.jcr.query.model.JoinCondition;
import org.modeshape.jcr.query.model.JoinType;
import org.modeshape.jcr.query.model.Length;
import org.modeshape.jcr.query.model.Limit;
import org.modeshape.jcr.query.model.Literal;
import org.modeshape.jcr.query.model.LiteralValue;
import org.modeshape.jcr.query.model.LowerCase;
import org.modeshape.jcr.query.model.NodeDepth;
import org.modeshape.jcr.query.model.NodeId;
import org.modeshape.jcr.query.model.NodeLocalName;
import org.modeshape.jcr.query.model.NodeName;
import org.modeshape.jcr.query.model.NodePath;
import org.modeshape.jcr.query.model.Not;
import org.modeshape.jcr.query.model.NullOrder;
import org.modeshape.jcr.query.model.Or;
import org.modeshape.jcr.query.model.Ordering;
import org.modeshape.jcr.query.model.PropertyExistence;
import org.modeshape.jcr.query.model.PropertyValue;
import org.modeshape.jcr.query.model.QueryCommand;
import org.modeshape.jcr.query.model.ReferenceValue;
import org.modeshape.jcr.query.model.Relike;
import org.modeshape.jcr.query.model.SameNode;
import org.modeshape.jcr.query.model.SameNodeJoinCondition;
import org.modeshape.jcr.query.model.SelectorName;
import org.modeshape.jcr.query.model.SetCriteria;
import org.modeshape.jcr.query.model.SetQuery.Operation;
import org.modeshape.jcr.query.model.TypeSystem;
import org.modeshape.jcr.query.model.TypeSystem.TypeFactory;
import org.modeshape.jcr.query.model.UpperCase;
import org.modeshape.jcr.query.model.Visitors;
import org.modeshape.jcr.query.optimize.Optimizer;
import org.modeshape.jcr.query.optimize.RuleBasedOptimizer;
import org.modeshape.jcr.query.plan.JoinAlgorithm;
import org.modeshape.jcr.query.plan.PlanHints;
import org.modeshape.jcr.query.plan.PlanNode;
import org.modeshape.jcr.query.plan.PlanNode.Property;
import org.modeshape.jcr.query.plan.PlanNode.Traversal;
import org.modeshape.jcr.query.plan.PlanNode.Type;
import org.modeshape.jcr.query.plan.Planner;
import org.modeshape.jcr.query.validate.Schemata;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.NameFactory;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.PathFactory;
import org.modeshape.jcr.value.PropertyType;
import org.modeshape.jcr.value.StringFactory;
import org.modeshape.jcr.value.binary.BinaryStore;

/**
 * A {@link QueryEngine} implementation that always scans all nodes in the workspace(s) and filtering out any node that does not
 * satisfy the criteria. This scanning is not very efficient and can result in slow queries, especially when the repository is
 * quite large or when the number of nodes that satisfies the query's criteria is a small fraction of all possible nodes in the
 * workspace(s).
 * 

* However, this fully-functional QueryEngine implementation is designed to be subclassed when the nodes for a particular source * (and optionally criteria) can be found more quickly. In such cases, the subclass should override the * {@link #createNodeSequenceForSource(QueryCommand, QueryContext, PlanNode, Columns, QuerySources)} method or the * {@link #createNodeSequenceForSource(QueryCommand, QueryContext, PlanNode, IndexPlan, Columns, QuerySources)} method and return * a NodeSequence that contains only the applicable nodes. *

*/ @Immutable public class ScanningQueryEngine implements org.modeshape.jcr.query.QueryEngine { /** We don't use the standard logging convention here; we want clients to easily configure logging for the indexes */ protected static final Logger LOGGER = Logger.getLogger("org.modeshape.jcr.query"); protected static final Set PSEUDO_COLUMN_NAMES; static { Set names = new HashSet<>(); names.add(JcrLexicon.NAME); names.add(JcrLexicon.PATH); names.add(JcrLexicon.SCORE); names.add(JcrLexicon.UUID); names.add(ModeShapeLexicon.LOCALNAME); names.add(ModeShapeLexicon.DEPTH); names.add(ModeShapeLexicon.ID); PSEUDO_COLUMN_NAMES = Collections.unmodifiableSet(names); } public static class Builder extends QueryEngineBuilder { @Override public QueryEngine build() { return new ScanningQueryEngine(context(), repositoryName(), planner(), optimizer()); } @Override protected Optimizer defaultOptimizer() { return new RuleBasedOptimizer(); } } public static Builder builder() { return new Builder(); } protected final String repositoryName; protected final Planner planner; protected final Optimizer optimizer; public ScanningQueryEngine( ExecutionContext context, String repositoryName, Planner planner, Optimizer optimizer ) { assert planner != null; assert optimizer != null; this.repositoryName = repositoryName; this.planner = planner; this.optimizer = optimizer; } /** * Execute the supplied query by planning, optimizing, and then processing it. * * @param queryContext the context in which the query should be executed; same instance as returned by * {@link #createQueryContext} * @param query the query that is to be executed * @return the query results; never null * @throws IllegalArgumentException if the context or query references are null * @throws QueryCancelledException if the query was cancelled * @throws RepositoryException if there was a problem executing the query */ @Override public QueryResults execute( final QueryContext queryContext, QueryCommand query ) throws QueryCancelledException, RepositoryException { CheckArg.isNotNull(queryContext, "queryContext"); CheckArg.isNotNull(query, "query"); final ScanQueryContext context = (ScanQueryContext)queryContext; checkCancelled(context); // Validate that all of the referenced variables have been provided ... Visitors.visitAll(query, new Visitors.AbstractVisitor() { @Override public void visit( BindVariableName obj ) { if (!context.getVariables().keySet().contains(obj.getBindVariableName())) { context.getProblems().addError(GraphI18n.missingVariableValue, obj.getBindVariableName()); } } }); boolean trace = LOGGER.isTraceEnabled(); if (trace) { LOGGER.trace("Beginning to process query {3} against workspace(s) {0} in '{1}' repository: {2}", context.getWorkspaceNames(), repositoryName, query, context.id()); } // Create the canonical plan ... long start = System.nanoTime(); PlanNode plan = planner.createPlan(context, query); long duration = Math.abs(System.nanoTime() - start); Statistics stats = new Statistics(duration); final String workspaceName = context.getWorkspaceNames().iterator().next(); if (trace) { LOGGER.trace("Computed canonical query plan for query {0}: {1}", context.id(), plan); } checkCancelled(context); Columns resultColumns = null; if (!context.getProblems().hasErrors()) { // Optimize the plan ... start = System.nanoTime(); PlanNode optimizedPlan = optimizer.optimize(context, plan); duration = Math.abs(System.nanoTime() - start); stats = stats.withOptimizationTime(duration); if (trace) { LOGGER.trace("Computed optimized query plan for query {0}:\n{1}", context.id(), optimizedPlan); } // Find the query result columns ... start = System.nanoTime(); // Determine the Columns object for specific nodes in the plan, and store them in the context ... optimizedPlan.apply(Traversal.POST_ORDER, new PlanNode.Operation() { @Override public void apply( PlanNode node ) { Columns columns = null; switch (node.getType()) { case PROJECT: case SOURCE: columns = determineProjectedColumns(node, context); assert columns != null; break; case JOIN: Columns leftColumns = context.columnsFor(node.getFirstChild()); Columns rightColumns = context.columnsFor(node.getLastChild()); columns = leftColumns.with(rightColumns); assert columns != null; break; case DEPENDENT_QUERY: columns = context.columnsFor(node.getLastChild()); assert columns != null; break; case SET_OPERATION: leftColumns = context.columnsFor(node.getFirstChild()); rightColumns = context.columnsFor(node.getLastChild()); assert leftColumns.isUnionCompatible(rightColumns); columns = leftColumns; assert columns != null; break; case INDEX: // do nothing with indexes ... break; default: assert node.getChildCount() == 1; columns = context.columnsFor(node.getFirstChild()); assert columns != null; break; } if (columns != null) { context.addColumnsFor(node, columns); } } }); resultColumns = context.columnsFor(optimizedPlan); assert resultColumns != null; duration = Math.abs(System.nanoTime() - start); stats = stats.withResultsFormulationTime(duration); if (trace) { LOGGER.trace("Computed output columns for query {0}: {1}", context.id(), resultColumns); } if (!context.getProblems().hasErrors()) { checkCancelled(context); // Execute the plan ... try { start = System.nanoTime(); if (trace) { LOGGER.trace("Start executing query {0}", context.id()); } QueryResults results = executeOptimizedQuery(context, query, stats, optimizedPlan); if (trace) { LOGGER.trace("Stopped executing query {0}: {1}", context.id(), stats); } return results; } finally { duration = Math.abs(System.nanoTime() - start); stats = stats.withExecutionTime(duration); } } } // Check whether the query was cancelled during execution ... checkCancelled(context); if (resultColumns == null) resultColumns = ResultColumns.EMPTY; // There were problems somewhere ... int width = resultColumns.getColumns().size(); CachedNodeSupplier cachedNodes = context.getNodeCache(workspaceName); return new Results(resultColumns, stats, NodeSequence.emptySequence(width), cachedNodes, context.getProblems(), null); } /** * Compute the columns that are defined in the supplied {@link PlanNode plan node}. If the supplied plan node is not a * {@link Type#PROJECT project node}, the method finds the first PROJECT node below the given node. * * @param optimizedPlan the optimized plan node in a query plan; may not be null * @param context the query context; may not be null * @return the representation of the projected columns; never null */ protected Columns determineProjectedColumns( PlanNode optimizedPlan, final ScanQueryContext context ) { final PlanHints hints = context.getHints(); // Look for which columns to include in the results; this will be defined by the highest PROJECT node ... PlanNode project = optimizedPlan; if (project.getType() != Type.PROJECT) { project = optimizedPlan.findAtOrBelow(Traversal.LEVEL_ORDER, Type.PROJECT); } if (project != null) { List columns = project.getPropertyAsList(Property.PROJECT_COLUMNS, Column.class); List columnTypes = project.getPropertyAsList(Property.PROJECT_COLUMN_TYPES, String.class); // Determine whether to include the full-text search scores in the results ... boolean includeFullTextSearchScores = hints.hasFullTextSearch; if (!includeFullTextSearchScores) { for (PlanNode select : optimizedPlan.findAllAtOrBelow(Type.SELECT)) { Constraint constraint = select.getProperty(Property.SELECT_CRITERIA, Constraint.class); if (QueryUtil.includeFullTextScores(constraint)) { includeFullTextSearchScores = true; break; } } } // The projected columns may not include all of the selectors from the child of the PROJECT node. // So, we need to figure out the selector indexes based upon the ResultColumn for the child ... Columns childColumns = context.columnsFor(project.getFirstChild()); return new ResultColumns(columns, columnTypes, includeFullTextSearchScores, childColumns); } // Look for a SOURCE ... if (optimizedPlan.getType() == Type.SOURCE) { PlanNode source = optimizedPlan; List schemataColumns = source.getPropertyAsList(Property.SOURCE_COLUMNS, Schemata.Column.class); List columns = new ArrayList<>(schemataColumns.size()); List columnTypes = new ArrayList<>(schemataColumns.size()); SelectorName selector = source.getSelectors().iterator().next(); for (Schemata.Column schemataColumn : schemataColumns) { Column column = new Column(selector, schemataColumn.getName(), schemataColumn.getName()); columns.add(column); columnTypes.add(schemataColumn.getPropertyTypeName()); } return new ResultColumns(columns, columnTypes, hints.hasFullTextSearch, null); } return ResultColumns.EMPTY; } private void checkCancelled( QueryContext context ) throws QueryCancelledException { if (context.isCancelled()) { throw new QueryCancelledException(); } } @Override public void shutdown() { // nothing to do } @Override public QueryContext createQueryContext( ExecutionContext context, RepositoryCache repositoryCache, Set workspaceNames, Map overriddenNodeCachesByWorkspaceName, Schemata schemata, RepositoryIndexes indexDefns, NodeTypes nodeTypes, BufferManager bufferManager, PlanHints hints, Map variables ) { return new ScanQueryContext(context, repositoryCache, workspaceNames, overriddenNodeCachesByWorkspaceName, schemata, indexDefns, nodeTypes, bufferManager, hints, null, variables, new HashMap()); } /** * Execute the optimized query defined by the supplied {@link PlanNode plan node}. * * @param context the context in which the query is to be executed; may not be null * @param command the original query; may not be null * @param statistics the statistics for the current query execution * @param plan the optimized plan for the query; may not be null * @return the query results; never null but possibly empty */ protected QueryResults executeOptimizedQuery( final ScanQueryContext context, QueryCommand command, Statistics statistics, PlanNode plan ) { long nanos = System.nanoTime(); Columns columns = null; NodeSequence rows = null; final String workspaceName = context.getWorkspaceNames().iterator().next(); try { // Find the topmost PROJECT node and build the Columns ... PlanNode project = plan.findAtOrBelow(Type.PROJECT); assert project != null; columns = context.columnsFor(plan); assert columns != null; boolean trace = LOGGER.isTraceEnabled(); if (context.getHints().planOnly) { if (trace) { LOGGER.trace("Request for only query plan when executing query {0}", context.id()); } rows = NodeSequence.emptySequence(columns.getColumns().size()); } else { boolean includeSystemContent = context.getHints().includeSystemContent; final QuerySources sources = new QuerySources(context.getRepositoryCache(), context.getNodeTypes(), workspaceName, includeSystemContent); rows = createNodeSequence(command, context, plan, columns, sources); long nanos2 = System.nanoTime(); statistics = statistics.withResultsFormulationTime(Math.abs(nanos2 - nanos)); nanos = nanos2; if (rows == null) { // There must have been an error or was cancelled ... assert context.getProblems().hasErrors() || context.isCancelled(); rows = NodeSequence.emptySequence(columns.getColumns().size()); } if (trace) { LOGGER.trace("The execution function for {0}: {1}", context.id(), rows); } } } finally { statistics = statistics.withExecutionTime(Math.abs(System.nanoTime() - nanos)); } final String planDesc = context.getHints().showPlan ? plan.getString() : null; CachedNodeSupplier cachedNodes = context.getNodeCache(workspaceName); return new Results(columns, statistics, rows, cachedNodes, context.getProblems(), planDesc); } /** * Create a node sequence containing the results of the original query as defined by the supplied plan. * * @param originalQuery the original query command; may not be null * @param context the context in which the query is to be executed; may not be null * @param plan the optimized plan for the query; may not be null * @param columns the result column definition; may not be null * @param sources the query sources for the repository; may not be null * @return the sequence of results; null only if the type of plan is not understood */ protected NodeSequence createNodeSequence( QueryCommand originalQuery, ScanQueryContext context, PlanNode plan, Columns columns, QuerySources sources ) { NodeSequence rows = null; final String workspaceName = sources.getWorkspaceName(); final NodeCache cache = context.getNodeCache(workspaceName); final TypeSystem types = context.getTypeSystem(); final BufferManager bufferManager = context.getBufferManager(); switch (plan.getType()) { case ACCESS: // If the ACCESS node is known to never have results ... if (plan.hasProperty(Property.ACCESS_NO_RESULTS)) { rows = NodeSequence.emptySequence(columns.getColumns().size()); } else { // Create the sequence for the plan node under the the ACCESS node ... assert plan.getChildCount() == 1; rows = createNodeSequence(originalQuery, context, plan.getFirstChild(), columns, sources); } break; case DEPENDENT_QUERY: assert plan.getChildCount() == 2; // Create the independent query from the left ... PlanNode indepPlan = plan.getFirstChild(); Columns indepColumns = context.columnsFor(indepPlan); String variableName = indepPlan.getProperty(Property.VARIABLE_NAME, String.class); NodeSequence independent = createNodeSequence(originalQuery, context, indepPlan, indepColumns, sources); // Create an extractor to get the value specified in the columns ... Column column = indepColumns.getColumns().get(0); boolean allowMultiValued = false; String typeName = indepColumns.getColumnTypeForProperty(column.getSelectorName(), column.getPropertyName()); TypeFactory type = context.getTypeSystem().getTypeFactory(typeName); ExtractFromRow indepExtractor = createExtractFromRow(column.getSelectorName(), column.getPropertyName(), context, indepColumns, sources, type, allowMultiValued); // Create the sequence for the dependent query ... PlanNode depPlan = plan.getLastChild(); Columns depColumns = context.columnsFor(depPlan); NodeSequence dependent = createNodeSequence(originalQuery, context, depPlan, depColumns, sources); // now create the dependent query ... rows = new DependentQuery(independent, indepExtractor, type, dependent, variableName, context.getVariables()); break; case DUP_REMOVE: assert plan.getChildCount() == 1; if (plan.getFirstChild().getType() == Type.SORT) { // There is a SORT below this DUP_REMOVE, and we can do that in one fell swoop with the sort ... rows = createNodeSequence(originalQuery, context, plan.getFirstChild(), columns, sources); } else { // Create the sequence for the plan node under the DUP_REMOVE ... rows = createNodeSequence(originalQuery, context, plan.getFirstChild(), columns, sources); if (!rows.isEmpty() && !(rows instanceof DistinctSequence)) { // Wrap that with a sequence that removes duplicates ... boolean useHeap = false; rows = new DistinctSequence(rows, context.getTypeSystem(), context.getBufferManager(), useHeap); } } break; case GROUP: throw new UnsupportedOperationException(); case JOIN: // Create the components under the JOIN ... assert plan.getChildCount() == 2; PlanNode leftPlan = plan.getFirstChild(); PlanNode rightPlan = plan.getLastChild(); // Define the columns for each side, taken from the supplied columns ... Columns leftColumns = context.columnsFor(leftPlan); Columns rightColumns = context.columnsFor(rightPlan); // Query context for the join (must remove isExists condition). ScanQueryContext joinQueryContext = context; if (context.getHints().isExistsQuery) { // must not push down a LIMIT 1 condition to joins. PlanHints joinPlanHints = context.getHints().clone(); joinPlanHints.isExistsQuery = false; joinQueryContext = context.with(joinPlanHints); } NodeSequence left = createNodeSequence(originalQuery, joinQueryContext, leftPlan, leftColumns, sources); NodeSequence right = createNodeSequence(originalQuery, joinQueryContext, rightPlan, rightColumns, sources); // Figure out the join algorithm ... JoinAlgorithm algorithm = plan.getProperty(Property.JOIN_ALGORITHM, JoinAlgorithm.class); JoinType joinType = plan.getProperty(Property.JOIN_TYPE, JoinType.class); JoinCondition joinCondition = plan.getProperty(Property.JOIN_CONDITION, JoinCondition.class); boolean pack = false; boolean useHeap = false; if (0 >= right.getRowCount() && right.getRowCount() < 100) useHeap = true; ExtractFromRow leftExtractor = null; ExtractFromRow rightExtractor = null; RangeProducer rangeProducer = null; switch (algorithm) { case NESTED_LOOP: // rows = new NestedLoopJoinComponent(context, left, right, joinCondition, joinType); // break; case MERGE: if (joinCondition instanceof SameNodeJoinCondition) { SameNodeJoinCondition condition = (SameNodeJoinCondition)joinCondition; // check if the JOIN was not reversed by an optimization boolean joinReversed = !leftColumns.getSelectorNames().contains(condition.getSelector1Name()); int leftIndex; int rightIndex; if (joinReversed) { // figure out the row indexes for the different selectors ... leftIndex = leftColumns.getSelectorIndex(condition.getSelector2Name()); rightIndex = rightColumns.getSelectorIndex(condition.getSelector1Name()); } else { leftIndex = leftColumns.getSelectorIndex(condition.getSelector1Name()); rightIndex = rightColumns.getSelectorIndex(condition.getSelector2Name()); } String relativePath = condition.getSelector2Path(); if (relativePath != null) { // Get extractors that will get the path of the nodes ... PathFactory pathFactory = context.getExecutionContext().getValueFactories().getPathFactory(); Path relPath = pathFactory.create(relativePath); if (joinReversed) { leftExtractor = RowExtractors.extractRelativePath(leftIndex, relPath, cache, types); rightExtractor = RowExtractors.extractPath(rightIndex, cache, types); } else { leftExtractor = RowExtractors.extractPath(leftIndex, cache, types); rightExtractor = RowExtractors.extractRelativePath(rightIndex, relPath, cache, types); } } else { // The nodes must be the same node ... leftExtractor = RowExtractors.extractNodeKey(leftIndex, cache, types); rightExtractor = RowExtractors.extractNodeKey(rightIndex, cache, types); } } else if (joinCondition instanceof ChildNodeJoinCondition) { ChildNodeJoinCondition condition = (ChildNodeJoinCondition)joinCondition; assert leftColumns.getSelectorNames().contains(condition.getParentSelectorName()); int leftIndex = leftColumns.getSelectorIndex(condition.getParentSelectorName()); int rightIndex = rightColumns.getSelectorIndex(condition.getChildSelectorName()); leftExtractor = RowExtractors.extractNodeKey(leftIndex, cache, types); rightExtractor = RowExtractors.extractParentNodeKey(rightIndex, cache, types); } else if (joinCondition instanceof EquiJoinCondition) { EquiJoinCondition condition = (EquiJoinCondition)joinCondition; // check if the JOIN was not reversed by an optimization boolean joinReversed = !leftColumns.getSelectorNames().contains(condition.getSelector1Name()); String sel1 = condition.getSelector1Name(); String sel2 = condition.getSelector2Name(); String prop1 = condition.getProperty1Name(); String prop2 = condition.getProperty2Name(); if (joinReversed) { leftExtractor = createExtractFromRow(sel2, prop2, joinQueryContext, leftColumns, sources, null, true); rightExtractor = createExtractFromRow(sel1, prop1, joinQueryContext, rightColumns, sources, null, true); } else { leftExtractor = createExtractFromRow(sel1, prop1, joinQueryContext, leftColumns, sources, null, true); rightExtractor = createExtractFromRow(sel2, prop2, joinQueryContext, rightColumns, sources, null, true); } } else if (joinCondition instanceof DescendantNodeJoinCondition) { DescendantNodeJoinCondition condition = (DescendantNodeJoinCondition)joinCondition; // For this to work, we want the ancestors to be on the left, so that the descendants can quickly // be found given a path of each ancestor ... assert leftColumns.getSelectorNames().contains(condition.getAncestorSelectorName()); String ancestorSelector = condition.getAncestorSelectorName(); String descendantSelector = condition.getDescendantSelectorName(); int ancestorSelectorIndex = leftColumns.getSelectorIndex(ancestorSelector); int descendantSelectorIndex = rightColumns.getSelectorIndex(descendantSelector); leftExtractor = RowExtractors.extractPath(ancestorSelectorIndex, cache, types); rightExtractor = RowExtractors.extractPath(descendantSelectorIndex, cache, types); // This is the only time we need a RangeProducer ... final PathFactory paths = context.getExecutionContext().getValueFactories().getPathFactory(); rangeProducer = new RangeProducer() { @Override public Range getRange( Path leftPath ) { if (leftPath.isRoot()) { // All paths are descendants of the root return new Range<>(leftPath, false, null, true); } // Given the path of the node on the left side of the join, find the range of all paths // that might be considered descendants of the left path.... boolean includeLower = false; // we don't want to include the left node; only descendants // The upper bound path is the same as the left path, just with an incremented SNS ... Path.Segment lastSegment = leftPath.getLastSegment(); Path.Segment upperSegment = paths.createSegment(lastSegment.getName(), lastSegment.getIndex() + 1); Path upperBoundPath = paths.create(leftPath.getParent(), upperSegment); return new Range<>(leftPath, includeLower, upperBoundPath, false); } }; } else { assert false : "Unable to use merge algorithm with join conditions: " + joinCondition; throw new UnsupportedOperationException(); } break; } // Perform conversion if required ... assert leftExtractor != null; assert rightExtractor != null; TypeFactory leftType = leftExtractor.getType(); TypeFactory rightType = rightExtractor.getType(); if (!leftType.equals(rightType)) { // wrap the right extractor with a converting extractor ... final TypeFactory commonType = context.getTypeSystem().getCompatibleType(leftType, rightType); if (!leftType.equals(commonType)) leftExtractor = RowExtractors.convert(leftExtractor, commonType); if (!rightType.equals(commonType)) rightExtractor = RowExtractors.convert(rightExtractor, commonType); } rows = new HashJoinSequence(workspaceName, left, right, leftExtractor, rightExtractor, joinType, context.getBufferManager(), cache, rangeProducer, pack, useHeap); // For each Constraint object applied to the JOIN, simply create a SelectComponent on top ... RowFilter filter = null; List constraints = plan.getPropertyAsList(Property.JOIN_CONSTRAINTS, Constraint.class); if (constraints != null) { for (Constraint constraint : constraints) { RowFilter constraintFilter = createRowFilter(constraint, context, columns, sources); filter = NodeSequence.requireBoth(filter, constraintFilter); } } rows = NodeSequence.filter(rows, filter); // even if filter is null break; case LIMIT: // Create the sequence for the plan node under the LIMIT ... assert plan.getChildCount() == 1; rows = createNodeSequence(originalQuery, context, plan.getFirstChild(), columns, sources); // Calculate the limit ... Integer rowLimit = plan.getProperty(Property.LIMIT_COUNT, Integer.class); Integer offset = plan.getProperty(Property.LIMIT_OFFSET, Integer.class); Limit limit = Limit.NONE; if (rowLimit != null) limit = limit.withRowLimit(rowLimit.intValue()); if (offset != null) limit = limit.withOffset(offset.intValue()); // Then create the limited sequence ... if (!limit.isUnlimited()) { rows = NodeSequence.limit(rows, limit); } break; case NULL: // No results ... rows = NodeSequence.emptySequence(columns.getColumns().size()); break; case PROJECT: // Nothing to do, since the projected columns will be accessed as needed when the results are processed. Instead, // just process the PROJECT node's only child ... PlanNode child = plan.getFirstChild(); columns = context.columnsFor(child); rows = createNodeSequence(originalQuery, context, child, columns, sources); break; case SELECT: // Create the sequence for the plan node under the SELECT ... assert plan.getChildCount() == 1; rows = createNodeSequence(originalQuery, context, plan.getFirstChild(), columns, sources); Constraint constraint = plan.getProperty(Property.SELECT_CRITERIA, Constraint.class); filter = createRowFilter(constraint, context, columns, sources); rows = NodeSequence.filter(rows, filter); break; case SET_OPERATION: Operation operation = plan.getProperty(Property.SET_OPERATION, Operation.class); boolean all = plan.getProperty(Property.SET_USE_ALL, Boolean.class); PlanNode firstPlan = plan.getFirstChild(); PlanNode secondPlan = plan.getLastChild(); Columns firstColumns = context.columnsFor(firstPlan); Columns secondColumns = context.columnsFor(secondPlan); NodeSequence first = createNodeSequence(originalQuery, context, firstPlan, firstColumns, sources); NodeSequence second = createNodeSequence(originalQuery, context, secondPlan, secondColumns, sources); useHeap = 0 >= second.getRowCount() && second.getRowCount() < 100; if (first.width() != second.width()) { // A set operation requires that the 'first' and 'second' sequences have the same width, but this is // not necessarily the case (e.g., when one side involves a JOIN but the other does not). The columns // will dictate which subset of selector indexes in the sequences should be used. first = NodeSequence.slice(first, firstColumns); second = NodeSequence.slice(second, secondColumns); assert first.width() == second.width(); } pack = false; switch (operation) { case UNION: { // This is really just a sequence with the two parts ... rows = NodeSequence.append(first, second); break; } case INTERSECT: { rows = new IntersectSequence(workspaceName, first, second, types, bufferManager, cache, pack, useHeap); break; } case EXCEPT: { rows = new ExceptSequence(workspaceName, first, second, types, bufferManager, cache, pack, useHeap); break; } } if (!all) { useHeap = false; rows = new DistinctSequence(rows, context.getTypeSystem(), context.getBufferManager(), useHeap); } break; case SORT: assert plan.getChildCount() == 1; PlanNode delegate = plan.getFirstChild(); boolean allowDuplicates = true; if (delegate.getType() == Type.DUP_REMOVE) { // This SORT already removes duplicates, so we can skip the first child ... delegate = delegate.getFirstChild(); allowDuplicates = false; } PlanNode parent = plan.getParent(); if (parent != null && parent.getType() == Type.DUP_REMOVE) { // The parent is a DUP_REMOVE (shouldn't really happen in an optimized plan), we should disallow duplicates // ... allowDuplicates = false; } // Create the sequence for the delegate plan node ... rows = createNodeSequence(originalQuery, context, delegate, columns, sources); if (!rows.isEmpty()) { // Prepare to wrap this delegate sequence based upon the SORT_ORDER_BY ... List orderBys = plan.getPropertyAsList(Property.SORT_ORDER_BY, Object.class); if (!orderBys.isEmpty()) { // Create an extractor from the orderings that we'll use for the sorting ... ExtractFromRow sortExtractor = null; pack = false; useHeap = false; NullOrder nullOrder = null; if (orderBys.get(0) instanceof Ordering) { List orderings = new ArrayList(orderBys.size()); for (Object orderBy : orderBys) { orderings.add((Ordering)orderBy); } // Determine the alias-to-name mappings for the selectors in the orderings ... Map sourceNamesByAlias = new HashMap(); for (PlanNode source : plan.findAllAtOrBelow(Type.SOURCE)) { SelectorName name = source.getProperty(Property.SOURCE_NAME, SelectorName.class); SelectorName alias = source.getProperty(Property.SOURCE_ALIAS, SelectorName.class); if (alias != null) sourceNamesByAlias.put(alias, name); } // If there are multiple orderings, then we'll never have nulls. But if there is just one ordering, // we have to handle nulls ... if (orderings.size() == 1) { nullOrder = orderings.get(0).nullOrder(); } // Now create the single sorting extractor ... sortExtractor = createSortingExtractor(orderings, sourceNamesByAlias, context, columns, sources); } else { // Order by the location(s) because it's before a merge-join ... final TypeFactory keyType = context.getTypeSystem().getReferenceFactory(); List extractors = new ArrayList<>(); for (Object ordering : orderBys) { SelectorName selectorName = (SelectorName)ordering; final int index = columns.getSelectorIndex(selectorName.name()); extractors.add(new ExtractFromRow() { @Override public TypeFactory getType() { return keyType; } @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(index); return node != null ? node.getKey() : null; } }); } // This is jsut for a merge join, so use standard null ordering ... nullOrder = NullOrder.NULLS_LAST; // Now create the single sorting extractor ... sortExtractor = RowExtractors.extractorWith(extractors); } // Now create the sorting sequence ... if (sortExtractor != null) { rows = new SortingSequence(workspaceName, rows, sortExtractor, bufferManager, cache, pack, useHeap, allowDuplicates, nullOrder); } } } break; case SOURCE: // Otherwise, just grab all of the nodes ... rows = createNodeSequenceForSource(originalQuery, context, plan, columns, sources); break; default: break; } return rows; } /** * Create a node sequence for the given source. * * @param originalQuery the original query command; may not be null * @param context the context in which the query is to be executed; may not be null * @param sourceNode the {@link Type#SOURCE} plan node for one part of a query; may not be null * @param columns the result column definition; may not be null * @param sources the query sources for the repository; may not be null * @return the sequence of results; null only if the type of plan is not understood */ protected NodeSequence createNodeSequenceForSource( QueryCommand originalQuery, QueryContext context, PlanNode sourceNode, Columns columns, QuerySources sources ) { // The indexes should already be in the correct order, from lowest cost to highest cost ... for (PlanNode indexNode : sourceNode.getChildren()) { if (indexNode.getType() != Type.INDEX) continue; IndexPlan index = indexNode.getProperty(Property.INDEX_SPECIFICATION, IndexPlan.class); NodeSequence sequence = createNodeSequenceForSource(originalQuery, context, sourceNode, index, columns, sources); if (sequence != null) { // Mark the index as being used ... indexNode.setProperty(Property.INDEX_USED, Boolean.TRUE); return sequence; } } // Grab all of the nodes ... return sources.allNodes(1.0f, -1); } /** * Create a node sequence for the given index * * @param originalQuery the original query command; may not be null * @param context the context in which the query is to be executed; may not be null * @param sourceNode the {@link Type#SOURCE} plan node for one part of a query; may not be null * @param index the {@link IndexPlan} specification; may not be null * @param columns the result column definition; may not be null * @param sources the query sources for the repository; may not be null * @return the sequence of results; null only if the type of index is not understood */ protected NodeSequence createNodeSequenceForSource( QueryCommand originalQuery, QueryContext context, PlanNode sourceNode, IndexPlan index, Columns columns, QuerySources sources ) { if (index.getProviderName() == null) { String name = index.getName(); String pathStr = (String)index.getParameters().get(IndexPlanners.PATH_PARAMETER); if (pathStr != null) { if (IndexPlanners.NODE_BY_PATH_INDEX_NAME.equals(name)) { PathFactory paths = context.getExecutionContext().getValueFactories().getPathFactory(); Path path = paths.create(pathStr); return sources.singleNode(path, 1.0f); } if (IndexPlanners.CHILDREN_BY_PATH_INDEX_NAME.equals(name)) { PathFactory paths = context.getExecutionContext().getValueFactories().getPathFactory(); Path path = paths.create(pathStr); return sources.childNodes(path, 1.0f); } if (IndexPlanners.DESCENDANTS_BY_PATH_INDEX_NAME.equals(name)) { PathFactory paths = context.getExecutionContext().getValueFactories().getPathFactory(); Path path = paths.create(pathStr); return sources.descendantNodes(path, 1.0f); } } String idStr = (String)index.getParameters().get(IndexPlanners.ID_PARAMETER); if (idStr != null) { if (IndexPlanners.NODE_BY_ID_INDEX_NAME.equals(name)) { StringFactory string = context.getExecutionContext().getValueFactories().getStringFactory(); String id = string.create(idStr); final String workspaceName = context.getWorkspaceNames().iterator().next(); return sources.singleNode(workspaceName, id, 1.0f); } } } return null; } /** * Create an {@link ExtractFromRow} instance that produces for given row a single object that can be used to sort all rows in * the specified order. * * @param orderings the specification of the sort order; may not be null or empty * @param sourceNamesByAlias the map of selector names keyed by their aliases; may not be null but may be empty * @param context the context in which the query is to be executed; may not be null * @param columns the result column definition; may not be null * @param sources the query sources for the repository; may not be null * @return the extractor; never null */ protected ExtractFromRow createSortingExtractor( List orderings, Map sourceNamesByAlias, QueryContext context, Columns columns, QuerySources sources ) { if (orderings.size() == 1) { return createSortingExtractor(orderings.get(0), sourceNamesByAlias, context, columns, sources); } if (orderings.size() == 2) { ExtractFromRow first = createSortingExtractor(orderings.get(0), sourceNamesByAlias, context, columns, sources); ExtractFromRow second = createSortingExtractor(orderings.get(1), sourceNamesByAlias, context, columns, sources); return RowExtractors.extractorWith(first, second); } if (orderings.size() == 3) { ExtractFromRow first = createSortingExtractor(orderings.get(0), sourceNamesByAlias, context, columns, sources); ExtractFromRow second = createSortingExtractor(orderings.get(1), sourceNamesByAlias, context, columns, sources); ExtractFromRow third = createSortingExtractor(orderings.get(2), sourceNamesByAlias, context, columns, sources); return RowExtractors.extractorWith(first, second, third); } if (orderings.size() == 4) { ExtractFromRow first = createSortingExtractor(orderings.get(0), sourceNamesByAlias, context, columns, sources); ExtractFromRow second = createSortingExtractor(orderings.get(1), sourceNamesByAlias, context, columns, sources); ExtractFromRow third = createSortingExtractor(orderings.get(2), sourceNamesByAlias, context, columns, sources); ExtractFromRow fourth = createSortingExtractor(orderings.get(3), sourceNamesByAlias, context, columns, sources); return RowExtractors.extractorWith(first, second, third, fourth); } List extractors = new ArrayList<>(orderings.size()); for (Ordering ordering : orderings) { extractors.add(createSortingExtractor(ordering, sourceNamesByAlias, context, columns, sources)); } return RowExtractors.extractorWith(extractors); } /** * Create an {@link ExtractFromRow} instance that produces for given row a single object that can be used to sort all rows in * the specified order. * * @param ordering the specification of the sort order; may not be null or empty * @param sourceNamesByAlias the map of selector names keyed by their aliases; may not be null but may be empty * @param context the context in which the query is to be executed; may not be null * @param columns the result column definition; may not be null * @param sources the query sources for the repository; may not be null * @return the extractor; never null */ protected ExtractFromRow createSortingExtractor( Ordering ordering, Map sourceNamesByAlias, QueryContext context, Columns columns, QuerySources sources ) { DynamicOperand operand = ordering.getOperand(); TypeFactory defaultType = context.getTypeSystem().getStringFactory();// only when ordered column is residual or not // defined ExtractFromRow extractor = createExtractFromRow(operand, context, columns, sources, defaultType, false, false); return RowExtractors.extractorWith(extractor, ordering.order(), ordering.nullOrder()); } /** * Create a {@link RowFilter} implementation given the supplied constraints. The resulting filter can be applied to a * NodeSequence by using {@link NodeSequence#filter(NodeSequence, RowFilter)}. * * @param constraint the constraints to be applied by the filter; may not be null; * @param context the context in which the query is to be executed; may not be null * @param columns the result column definition; may not be null * @param sources the query sources for the repository; may not be null * @return the row filter that implements the constraints; never null */ protected RowFilter createRowFilter( final Constraint constraint, final QueryContext context, Columns columns, QuerySources sources ) { assert constraint != null; assert context != null; assert columns != null; assert sources != null; if (constraint instanceof Or) { Or orConstraint = (Or)constraint; final RowFilter left = createRowFilter(orConstraint.left(), context, columns, sources); final RowFilter right = createRowFilter(orConstraint.right(), context, columns, sources); return new RowFilter() { @Override public boolean isCurrentRowValid( Batch batch ) { return left.isCurrentRowValid(batch) || right.isCurrentRowValid(batch); } @Override public String toString() { return "(or " + left + "," + right + " )"; } }; } if (constraint instanceof Not) { Not notConstraint = (Not)constraint; final RowFilter not = createRowFilter(notConstraint.getConstraint(), context, columns, sources); return new RowFilter() { @Override public boolean isCurrentRowValid( Batch batch ) { return !not.isCurrentRowValid(batch); } @Override public String toString() { return "(not " + not + " )"; } }; } if (constraint instanceof And) { And andConstraint = (And)constraint; final RowFilter left = createRowFilter(andConstraint.left(), context, columns, sources); final RowFilter right = createRowFilter(andConstraint.right(), context, columns, sources); return new RowFilter() { @Override public boolean isCurrentRowValid( Batch batch ) { return left.isCurrentRowValid(batch) && right.isCurrentRowValid(batch); } @Override public String toString() { return "(and " + left + "," + right + " )"; } }; } if (constraint instanceof ChildNode) { ChildNode childConstraint = (ChildNode)constraint; PathFactory paths = context.getExecutionContext().getValueFactories().getPathFactory(); final Path parentPath = paths.create(childConstraint.getParentPath()); final NodeCache cache = context.getNodeCache(sources.getWorkspaceName()); final CachedNode parent = sources.getNodeAtPath(parentPath, cache); if (parent == null) { return NodeSequence.NO_PASS_ROW_FILTER; } final NodeKey parentKey = parent.getKey(); final String selectorName = childConstraint.getSelectorName(); final int index = columns.getSelectorIndex(selectorName); return new RowFilter() { @Override public boolean isCurrentRowValid( Batch batch ) { CachedNode node = batch.getNode(index); if (node == null) return false; if (parentKey.equals(node.getParentKey(cache))) return true; // Don't have to check the additional parents since we only find shared nodes in the original location ... return false; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; } if (constraint instanceof DescendantNode) { DescendantNode descendantNode = (DescendantNode)constraint; PathFactory paths = context.getExecutionContext().getValueFactories().getPathFactory(); final Path ancestorPath = paths.create(descendantNode.getAncestorPath()); final NodeCache cache = context.getNodeCache(sources.getWorkspaceName()); final CachedNode ancestor = sources.getNodeAtPath(ancestorPath, cache); if (ancestor == null) { return NodeSequence.NO_PASS_ROW_FILTER; } final NodeKey ancestorKey = ancestor.getKey(); final String selectorName = descendantNode.getSelectorName(); final int index = columns.getSelectorIndex(selectorName); return new RowFilter() { @Override public boolean isCurrentRowValid( Batch batch ) { CachedNode node = batch.getNode(index); while (node != null) { NodeKey parentKey = node.getParentKey(cache); if (parentKey == null) return false; if (ancestorKey.equals(parentKey)) return true; // Don't have to check the additional parents since we only find shared nodes in the original location ... node = cache.getNode(parentKey); } return false; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; } if (constraint instanceof SameNode) { SameNode sameNode = (SameNode)constraint; PathFactory paths = context.getExecutionContext().getValueFactories().getPathFactory(); final Path path = paths.create(sameNode.getPath()); final NodeCache cache = context.getNodeCache(sources.getWorkspaceName()); final CachedNode node = sources.getNodeAtPath(path, cache); if (node == null) { return NodeSequence.NO_PASS_ROW_FILTER; } final NodeKey nodeKey = node.getKey(); final String selectorName = sameNode.getSelectorName(); final int index = columns.getSelectorIndex(selectorName); return new RowFilter() { @Override public boolean isCurrentRowValid( Batch batch ) { CachedNode node = batch.getNode(index); return node != null && nodeKey.equals(node.getKey()); } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; } if (constraint instanceof PropertyExistence) { PropertyExistence propertyExistance = (PropertyExistence)constraint; NameFactory names = context.getExecutionContext().getValueFactories().getNameFactory(); final Name propertyName = names.create(propertyExistance.getPropertyName()); final String selectorName = propertyExistance.selectorName().name(); final int index = columns.getSelectorIndex(selectorName); final NodeCache cache = context.getNodeCache(sources.getWorkspaceName()); assert index >= 0; return new RowFilter() { @Override public boolean isCurrentRowValid( Batch batch ) { CachedNode node = batch.getNode(index); return node != null && node.hasProperty(propertyName, cache); } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; } if (constraint instanceof Between) { Between between = (Between)constraint; final StaticOperand lower = between.getLowerBound(); final StaticOperand upper = between.getUpperBound(); final boolean includeLower = between.isLowerBoundIncluded(); final boolean includeUpper = between.isUpperBoundIncluded(); DynamicOperand dynamicOperand = between.getOperand(); final TypeFactory defaultType = determineType(dynamicOperand, context, columns); final ExtractFromRow operation = createExtractFromRow(dynamicOperand, context, columns, sources, defaultType, true, false); // Determine the literal value in the static operand ... return new RowFilterSupplier() { @Override protected RowFilter createFilter() { // Evaluate the operand, which may have variables ... final Object lowerLiteralValue = literalValue(lower, context, defaultType); final Object upperLiteralValue = literalValue(upper, context, defaultType); // Create the correct operation ... final TypeFactory expectedType = operation.getType(); final Object lowerValue = expectedType.create(lowerLiteralValue); final Object upperValue = expectedType.create(upperLiteralValue); @SuppressWarnings( "unchecked" ) final Comparator comparator = (Comparator)expectedType.getComparator(); if (includeLower) { if (includeUpper) { return new DynamicOperandFilter(operation) { @Override protected boolean evaluate( Object leftHandValue ) { if (leftHandValue == null) return false; // null values never match return comparator.compare(leftHandValue, lowerValue) >= 0 && comparator.compare(leftHandValue, upperValue) <= 0; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; } // Don't include upper ... return new DynamicOperandFilter(operation) { @Override protected boolean evaluate( Object leftHandValue ) { if (leftHandValue == null) return false; // null values never match return comparator.compare(leftHandValue, lowerValue) >= 0 && comparator.compare(leftHandValue, upperValue) < 0; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; } assert !includeLower; // Don't include lower if (includeUpper) { return new DynamicOperandFilter(operation) { @Override protected boolean evaluate( Object leftHandValue ) { if (leftHandValue == null) return false; // null values never match return comparator.compare(leftHandValue, lowerValue) > 0 && comparator.compare(leftHandValue, upperValue) <= 0; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; } // Don't include upper or lower ... return new DynamicOperandFilter(operation) { @Override protected boolean evaluate( Object leftHandValue ) { if (leftHandValue == null) return false; // null values never match return comparator.compare(leftHandValue, lowerValue) > 0 && comparator.compare(leftHandValue, upperValue) < 0; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; } }; } if (constraint instanceof Comparison) { Comparison comparison = (Comparison)constraint; // Create the correct dynamic operation ... final DynamicOperand dynamicOperand = comparison.getOperand1(); final Operator operator = comparison.operator(); final StaticOperand staticOperand = comparison.getOperand2(); TypeFactory expectedType = null; ExtractFromRow op = null; if (operator != Operator.LIKE) { expectedType = determineType(dynamicOperand, context, columns); op = createExtractFromRow(dynamicOperand, context, columns, sources, expectedType, true, false); } else { expectedType = context.getTypeSystem().getStringFactory(); op = createExtractFromRow(dynamicOperand, context, columns, sources, expectedType, true, true); if (op.getType() != expectedType) { // Need to convert the extracted value(s) to strings because this is a LIKE operation ... op = RowExtractors.convert(op, expectedType); } } final TypeFactory defaultType = expectedType; final ExtractFromRow operation = op; // Determine the literal value in the static operand ... return new RowFilterSupplier() { @Override protected RowFilter createFilter() { // Evaluate the operand, which may have variables ... final Object literalValue = literalValue(staticOperand, context, defaultType); // Create the correct operation ... final TypeFactory expectedType = operation.getType(); final Object rhs = expectedType.create(literalValue); @SuppressWarnings( "unchecked" ) final Comparator comparator = (Comparator)expectedType.getComparator(); switch (operator) { case EQUAL_TO: return new DynamicOperandFilter(operation) { @Override protected boolean evaluate( Object leftHandValue ) { if (leftHandValue == null) return false; // null values never match return comparator.compare(leftHandValue, rhs) == 0; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; case NOT_EQUAL_TO: return new DynamicOperandFilter(operation) { @Override protected boolean evaluate( Object leftHandValue ) { if (leftHandValue == null) return false; // null values never match return comparator.compare(leftHandValue, rhs) != 0; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; case GREATER_THAN: return new DynamicOperandFilter(operation) { @Override protected boolean evaluate( Object leftHandValue ) { if (leftHandValue == null) return false; // null values never match return comparator.compare(leftHandValue, rhs) > 0; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; case GREATER_THAN_OR_EQUAL_TO: return new DynamicOperandFilter(operation) { @Override protected boolean evaluate( Object leftHandValue ) { if (leftHandValue == null) return false; // null values never match return comparator.compare(leftHandValue, rhs) >= 0; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; case LESS_THAN: return new DynamicOperandFilter(operation) { @Override protected boolean evaluate( Object leftHandValue ) { if (leftHandValue == null) return false; // null values never match return comparator.compare(leftHandValue, rhs) < 0; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; case LESS_THAN_OR_EQUAL_TO: return new DynamicOperandFilter(operation) { @Override protected boolean evaluate( Object leftHandValue ) { if (leftHandValue == null) return false; // null values never match return comparator.compare(leftHandValue, rhs) <= 0; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; case LIKE: // Convert the LIKE expression to a regular expression final TypeSystem types = context.getTypeSystem(); String expression = types.asString(rhs).trim(); if ("%".equals(expression)) { // We'll accept any non-null value ... return new DynamicOperandFilter(operation) { @Override protected boolean evaluate( Object leftHandValue ) { return leftHandValue != null; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; } String regex = QueryUtil.toRegularExpression(expression); final Pattern pattern = Pattern.compile(regex); return new DynamicOperandFilter(operation) { @Override protected boolean evaluate( Object leftHandValue ) { if (leftHandValue == null) return false; // null values never match String value = types.asString(leftHandValue); return pattern.matcher(value).matches(); } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; } assert false : "Should not get here"; return null; } }; } if (constraint instanceof SetCriteria) { final SetCriteria setCriteria = (SetCriteria)constraint; DynamicOperand operand = setCriteria.getOperand(); final TypeFactory defaultType = determineType(operand, context, columns); // If the set criteria contains a bind variable, then the operand filter should lazily evaluate the bind variable ... final ExtractFromRow operation = createExtractFromRow(operand, context, columns, sources, defaultType, true, false); final boolean trace = LOGGER.isTraceEnabled() && !defaultType.getTypeName().equals("NAME"); return new RowFilterSupplier() { @Override protected RowFilter createFilter() { final Set values = ScanningQueryEngine.literalValues(setCriteria, context, defaultType); return new DynamicOperandFilter(operation) { @Override protected boolean evaluate( Object leftHandValue ) { if (leftHandValue instanceof Object[]) { for (Object leftValue : (Object[])leftHandValue) { if (values.contains(leftValue)) { if (trace) LOGGER.trace("Found '{0}' in values: {1}", leftHandValue, values); return true; } } if (trace) LOGGER.trace("Failed to find '{0}' in values: {1}", leftHandValue, values); return false; } if (values.contains(leftHandValue)) { if (trace) { LOGGER.trace("Found '{0}' in values: {1}", leftHandValue, values); } return true; } if (trace) { LOGGER.trace("Failed to find '{0}' in values: {1}", leftHandValue, values); } return false; } @Override public String toString() { return "(filter " + Visitors.readable(constraint) + ")"; } }; } }; } if (constraint instanceof FullTextSearch) { final TypeFactory strings = context.getTypeSystem().getStringFactory(); final StaticOperand ftsExpression = ((FullTextSearch)constraint).getFullTextSearchExpression(); final FullTextSearch fts; if (ftsExpression instanceof BindVariableName) { Object searchExpression = literalValue(ftsExpression, context, strings); if (searchExpression != null) { fts = ((FullTextSearch)constraint).withFullTextExpression(searchExpression.toString()); } else { fts = (FullTextSearch)constraint; } } else { fts = (FullTextSearch)constraint; } final NodeCache cache = context.getNodeCache(sources.getWorkspaceName()); final BinaryStore binaries = context.getExecutionContext().getBinaryStore(); String selectorName = fts.getSelectorName(); String propertyName = fts.getPropertyName(); final int index = columns.getSelectorIndex(selectorName); ExtractFromRow fullTextExtractor = null; if (propertyName != null) { // This is to search just the designated property of the node (name, all property values) ... final ExtractFromRow propertyValueExtractor = createExtractFromRow(selectorName, propertyName, context, columns, sources, strings, true); fullTextExtractor = new ExtractFromRow() { @Override public TypeFactory getType() { return strings; } @Override public Object getValueInRow( RowAccessor row ) { Object result = propertyValueExtractor.getValueInRow(row); if (result == null) return null; StringBuilder fullTextString = new StringBuilder(); RowExtractors.extractFullTextFrom(result, strings, binaries, fullTextString); return fullTextString.toString(); } }; } else { // This is to search all aspects of the node (name, all property values) ... fullTextExtractor = RowExtractors.extractFullText(index, cache, context.getTypeSystem(), binaries); } // Return a filter that processes all of the text ... final ExtractFromRow extractor = fullTextExtractor; return new DynamicOperandFilter(extractor) { @Override protected boolean evaluate( Object leftHandValue ) { /** * The term will match the extracted value "as-is" via regex, without any stemming or punctuation removal. * This means that the matching is done in a much more strict way than what Lucene did in 3.x If we were to * implement stemming or hyphen removal, we would need to do it *both* in the row extractor * (RowExtractors.extractFullText) and in the term where the regex is built */ return fts.getTerm().matches(leftHandValue.toString()); } }; } if (constraint instanceof Relike) { Relike relike = (Relike)constraint; StaticOperand staticOperand = relike.getOperand1(); Object literalValue = literalValue(staticOperand, context, context.getTypeSystem().getStringFactory()); if (literalValue == null) { return NodeSequence.NO_PASS_ROW_FILTER; } final String literalStr = literalValue.toString(); PropertyValue propertyValue = relike.getOperand2(); NameFactory names = context.getExecutionContext().getValueFactories().getNameFactory(); final Name propertyName = names.create(propertyValue.getPropertyName()); final String selectorName = propertyValue.getSelectorName(); final int index = columns.getSelectorIndex(selectorName); final NodeCache cache = context.getNodeCache(sources.getWorkspaceName()); return new RowFilter() { @Override public boolean isCurrentRowValid( Batch batch ) { CachedNode node = batch.getNode(index); if (node == null) return false; org.modeshape.jcr.value.Property property = node.getProperty(propertyName, cache); if (property == null) return false; for (Object value : property) { if (value == null) continue; // The property value should be a LIKE expression ... String regex = toRegularExpression(value.toString()); final Pattern pattern = Pattern.compile(regex); if (pattern.matcher(literalStr).matches()) return true; } return false; } }; } assert false; return NodeSequence.PASS_ROW_FILTER; } protected TypeFactory determineType( DynamicOperand operand, QueryContext context, Columns columns ) { TypeSystem types = context.getTypeSystem(); if (operand instanceof PropertyValue) { PropertyValue propValue = (PropertyValue)operand; String typeName = columns.getColumnTypeForProperty(propValue.getSelectorName(), propValue.getPropertyName()); return typeName != null ? types.getTypeFactory(typeName) : types.getStringFactory(); } // The types used here must match those in NodeTypeSchemata's constructor if (operand instanceof NodeName) { return types.getNameFactory(); } if (operand instanceof NodePath) { return types.getPathFactory(); } if (operand instanceof Length) { return types.getLongFactory(); } if (operand instanceof NodeDepth) { return types.getLongFactory(); } if (operand instanceof FullTextSearchScore) { return types.getDoubleFactory(); } if (operand instanceof LowerCase) { return types.getStringFactory(); } if (operand instanceof UpperCase) { return types.getStringFactory(); } if (operand instanceof NodeLocalName) { return types.getStringFactory(); } if (operand instanceof ReferenceValue) { return types.getStringFactory(); } if (operand instanceof ArithmeticOperand) { ArithmeticOperand arith = (ArithmeticOperand)operand; TypeFactory leftType = determineType(arith.getLeft(), context, columns); TypeFactory rightType = determineType(arith.getRight(), context, columns); return types.getCompatibleType(leftType, rightType); } return types.getStringFactory(); } /** * Create a {@link ExtractFromRow} implementation that performs the supplied {@link DynamicOperand} against a current row in * the current batch. * * @param operand the dynamic operand * @param context the context in which the query is to be executed; may not be null * @param columns the result column definition; may not be null * @param sources the query sources for the repository; may not be null * @param defaultType the type that should be used by default, or null if an exception should be thrown when the type for the * property name could not be determined * @param allowMultiValued true if the extractor called upon a particular node and multi-valued property return an Object[] * that contains all the resulting values of the property, or false if only the first value should be returned * @param isLike true if the result will be used in a LIKE operation, or false otherwise; this may affect the number of * results that will be returned * @return the dynamic operation implementation; never null */ protected ExtractFromRow createExtractFromRow( DynamicOperand operand, QueryContext context, Columns columns, final QuerySources sources, TypeFactory defaultType, boolean allowMultiValued, boolean isLike ) { assert operand != null; assert context != null; assert columns != null; assert sources != null; if (operand instanceof PropertyValue) { PropertyValue propValue = (PropertyValue)operand; String propertyName = propValue.getPropertyName(); String selectorName = propValue.selectorName().name(); return createExtractFromRow(selectorName, propertyName, context, columns, sources, defaultType, allowMultiValued); } if (operand instanceof ReferenceValue) { ReferenceValue refValue = (ReferenceValue)operand; String propertyName = refValue.getPropertyName(); String selectorName = refValue.selectorName().name(); if (propertyName == null) { return createExtractReferencesFromRow(selectorName, context, columns, sources, defaultType); } return createExtractFromRow(selectorName, propertyName, context, columns, sources, defaultType, allowMultiValued); } if (operand instanceof Length) { Length length = (Length)operand; final PropertyValue value = length.getPropertyValue(); String propertyName = value.getPropertyName(); String selectorName = value.selectorName().name(); final ExtractFromRow getPropValue = createExtractFromRow(selectorName, propertyName, context, columns, sources, defaultType, allowMultiValued); final TypeFactory longType = context.getTypeSystem().getLongFactory(); return new ExtractFromRow() { @Override public Object getValueInRow( RowAccessor row ) { Object typedValue = getPropValue.getValueInRow(row); return getPropValue.getType().length(typedValue); } @Override public TypeFactory getType() { return longType; } @Override public String toString() { return "(length " + getPropValue + ")"; } }; } final TypeFactory stringFactory = context.getTypeSystem().getStringFactory(); if (operand instanceof LowerCase) { LowerCase lowerCase = (LowerCase)operand; final ExtractFromRow delegate = createExtractFromRow(lowerCase.getOperand(), context, columns, sources, defaultType, allowMultiValued, false); return new ExtractFromRow() { @Override public TypeFactory getType() { return stringFactory; } @Override public Object getValueInRow( RowAccessor row ) { String result = stringFactory.create(delegate.getValueInRow(row)); return result != null ? result.toLowerCase() : null; } @Override public String toString() { return "(lowercase " + delegate + ")"; } }; } if (operand instanceof UpperCase) { final UpperCase upperCase = (UpperCase)operand; final ExtractFromRow delegate = createExtractFromRow(upperCase.getOperand(), context, columns, sources, defaultType, allowMultiValued, false); return new ExtractFromRow() { @Override public TypeFactory getType() { return stringFactory; } @Override public Object getValueInRow( RowAccessor row ) { String result = stringFactory.create(delegate.getValueInRow(row)); return result != null ? result.toUpperCase() : null; } @Override public String toString() { return "(uppercase " + delegate + ")"; } }; } if (operand instanceof NodeDepth) { final NodeDepth nodeDepth = (NodeDepth)operand; final int indexInRow = columns.getSelectorIndex(nodeDepth.getSelectorName()); final NodeCache cache = context.getNodeCache(sources.getWorkspaceName()); final TypeFactory longType = context.getTypeSystem().getLongFactory(); return new ExtractFromRow() { @Override public TypeFactory getType() { return longType; // depth is always a long type } @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; return new Long(node.getDepth(cache)); } @Override public String toString() { return "(nodeDepth " + nodeDepth.getSelectorName() + ")"; } }; } if (operand instanceof NodeId) { final NodeId nodeId = (NodeId)operand; final int indexInRow = columns.getSelectorIndex(nodeId.getSelectorName()); final NodeCache cache = context.getNodeCache(sources.getWorkspaceName()); final NodeKey root = cache.getRootKey(); final TypeFactory stringType = context.getTypeSystem().getStringFactory(); return new ExtractFromRow() { @Override public TypeFactory getType() { return stringType; // ID is always a string type } @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; return sources.getIdentifier(node, root); } @Override public String toString() { return "(nodeId " + nodeId.getSelectorName() + ")"; } }; } if (operand instanceof NodePath) { final NodePath nodePath = (NodePath)operand; final int indexInRow = columns.getSelectorIndex(nodePath.getSelectorName()); final NodeCache cache = context.getNodeCache(sources.getWorkspaceName()); if (isLike) { return new ExtractFromRow() { @Override public TypeFactory getType() { return stringFactory; } @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; Path path = node.getPath(cache); if (path.isRoot()) { return stringFactory.create(path); } String[] paths = new String[2]; // Add the normal path ... paths[0] = stringFactory.create(path); // And the path that always has the SNS index ... StringBuilder sb = new StringBuilder(); for (Path.Segment segment : path) { // Add the segment WITH the index ... sb.append("/"); sb.append(stringFactory.create(segment.getName())); sb.append('[').append(segment.getIndex()).append(']'); } paths[1] = sb.toString(); return paths; } @Override public String toString() { return "(nodePath " + nodePath.getSelectorName() + ")"; } }; } // Otherwise, just return the single path ... return new ExtractFromRow() { @Override public TypeFactory getType() { return stringFactory; } @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; Path path = node.getPath(cache); return stringFactory.create(path); } @Override public String toString() { return "(nodePath " + nodePath.getSelectorName() + ")"; } }; } if (operand instanceof NodeName) { final NodeName nodeName = (NodeName)operand; final int indexInRow = columns.getSelectorIndex(nodeName.getSelectorName()); final NodeCache cache = context.getNodeCache(sources.getWorkspaceName()); return new ExtractFromRow() { @Override public TypeFactory getType() { return stringFactory; } @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; Name name = node.getName(cache); return stringFactory.create(name); } @Override public String toString() { return "(nodeName " + nodeName.getSelectorName() + ")"; } }; } if (operand instanceof NodeLocalName) { final NodeLocalName nodeName = (NodeLocalName)operand; final int indexInRow = columns.getSelectorIndex(nodeName.getSelectorName()); final NodeCache cache = context.getNodeCache(sources.getWorkspaceName()); return new ExtractFromRow() { @Override public TypeFactory getType() { return stringFactory; } @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; Name name = node.getName(cache); return name.getLocalName(); // works even for root } @Override public String toString() { return "(localName " + nodeName.getSelectorName() + ")"; } }; } if (operand instanceof FullTextSearchScore) { final FullTextSearchScore fts = (FullTextSearchScore)operand; final int indexInRow = columns.getSelectorIndex(fts.getSelectorName()); final TypeFactory doubleType = context.getTypeSystem().getDoubleFactory(); return new ExtractFromRow() { @Override public TypeFactory getType() { return doubleType; } @Override public Object getValueInRow( RowAccessor row ) { return new Double(row.getScore(indexInRow)); // must convert the float to a double value } @Override public String toString() { return "(fullTextScore " + fts.getSelectorName() + ")"; } }; } if (operand instanceof ArithmeticOperand) { // This works on single-valued properties only ... ArithmeticOperand arith = (ArithmeticOperand)operand; final ExtractFromRow leftOp = createExtractFromRow(arith.getLeft(), context, columns, sources, defaultType, false, false); final ExtractFromRow rightOp = createExtractFromRow(arith.getRight(), context, columns, sources, defaultType, false, false); // compute the expected (common) type ... TypeFactory leftType = leftOp.getType(); TypeFactory rightType = rightOp.getType(); final TypeSystem typeSystem = context.getTypeSystem(); final String commonType = typeSystem.getCompatibleType(leftType.getTypeName(), rightType.getTypeName()); if (typeSystem.getDoubleFactory().getTypeName().equals(commonType)) { final TypeFactory commonTypeFactory = typeSystem.getDoubleFactory(); switch (arith.operator()) { case ADD: return new ExtractFromRow() { @Override public TypeFactory getType() { return commonTypeFactory; } @Override public Object getValueInRow( RowAccessor row ) { Double right = commonTypeFactory.create(rightOp.getValueInRow(row)); Double left = commonTypeFactory.create(leftOp.getValueInRow(row)); if (right == null) return left; if (left == null) return right; return left.doubleValue() / right.doubleValue(); } @Override public String toString() { return "(double + " + leftOp + "," + rightOp + ")"; } }; case SUBTRACT: return new ExtractFromRow() { @Override public TypeFactory getType() { return commonTypeFactory; } @Override public Object getValueInRow( RowAccessor row ) { Double right = commonTypeFactory.create(rightOp.getValueInRow(row)); Double left = commonTypeFactory.create(leftOp.getValueInRow(row)); if (right == null) return left; if (left == null) left = 0.0d; return left.doubleValue() * right.doubleValue(); } @Override public String toString() { return "(double - " + leftOp + "," + rightOp + ")"; } }; case MULTIPLY: return new ExtractFromRow() { @Override public TypeFactory getType() { return commonTypeFactory; } @Override public Object getValueInRow( RowAccessor row ) { Double right = commonTypeFactory.create(rightOp.getValueInRow(row)); Double left = commonTypeFactory.create(leftOp.getValueInRow(row)); if (right == null || left == null) return null; return left.doubleValue() * right.doubleValue(); } @Override public String toString() { return "(double x " + leftOp + "," + rightOp + ")"; } }; case DIVIDE: return new ExtractFromRow() { @Override public TypeFactory getType() { return commonTypeFactory; } @Override public Object getValueInRow( RowAccessor row ) { Double right = commonTypeFactory.create(rightOp.getValueInRow(row)); Double left = commonTypeFactory.create(leftOp.getValueInRow(row)); if (right == null || left == null) return null; return left.doubleValue() / right.doubleValue(); } @Override public String toString() { return "(double / " + leftOp + "," + rightOp + ")"; } }; } } else if (typeSystem.getLongFactory().getTypeName().equals(commonType)) { final TypeFactory commonTypeFactory = typeSystem.getLongFactory(); switch (arith.operator()) { case ADD: return new ExtractFromRow() { @Override public TypeFactory getType() { return commonTypeFactory; } @Override public Object getValueInRow( RowAccessor row ) { Long right = commonTypeFactory.create(rightOp.getValueInRow(row)); Long left = commonTypeFactory.create(leftOp.getValueInRow(row)); if (right == null) return left; if (left == null) return right; return left.longValue() / right.longValue(); } @Override public String toString() { return "(long + " + leftOp + "," + rightOp + ")"; } }; case SUBTRACT: return new ExtractFromRow() { @Override public TypeFactory getType() { return commonTypeFactory; } @Override public Object getValueInRow( RowAccessor row ) { Long right = commonTypeFactory.create(rightOp.getValueInRow(row)); Long left = commonTypeFactory.create(leftOp.getValueInRow(row)); if (right == null) return left; if (left == null) left = 0L; return left.longValue() * right.longValue(); } @Override public String toString() { return "(long - " + leftOp + "," + rightOp + ")"; } }; case MULTIPLY: return new ExtractFromRow() { @Override public TypeFactory getType() { return commonTypeFactory; } @Override public Object getValueInRow( RowAccessor row ) { Long right = commonTypeFactory.create(rightOp.getValueInRow(row)); Long left = commonTypeFactory.create(leftOp.getValueInRow(row)); if (right == null || left == null) return null; return left.longValue() * right.longValue(); } @Override public String toString() { return "(long x " + leftOp + "," + rightOp + ")"; } }; case DIVIDE: return new ExtractFromRow() { @Override public TypeFactory getType() { return commonTypeFactory; } @Override public Object getValueInRow( RowAccessor row ) { Long right = commonTypeFactory.create(rightOp.getValueInRow(row)); Long left = commonTypeFactory.create(leftOp.getValueInRow(row)); if (right == null || left == null) return null; return left.longValue() / right.longValue(); } @Override public String toString() { return "(long / " + leftOp + "," + rightOp + ")"; } }; } } } assert false; return null; } protected static abstract class PropertyValueExtractor implements ExtractFromRow { private final TypeFactory typeFactory; private final String selectorName; private final String propertyName; protected PropertyValueExtractor( String selectorName, String propertyName, TypeFactory typeFactory ) { this.selectorName = selectorName; this.propertyName = propertyName; this.typeFactory = typeFactory; } @Override public TypeFactory getType() { return typeFactory; } @Override public String toString() { return "(" + Visitors.readable(new PropertyValue(new SelectorName(selectorName), propertyName)) + ")"; } } /** * Create a {@link ExtractFromRow} implementation that accesses the value(s) in the property identified by the supplied * selector and property names. * * @param selectorName the name of the selector containing the node(s) to be accessed; may not be null * @param propertyName the name of the property on the node(s) to be accessed; may not be null * @param context the context in which the query is to be executed; may not be null * @param columns the result column definition; may not be null * @param sources the query sources for the repository; may not be null * @param defaultType the type that should be used by default, or null if an exception should be thrown when the type for the * property name could not be determined * @param allowMultiValued true if the extractor called upon a particular node and multi-valued property return an Object[] * that contains all the resulting values of the property, or false if only the first value should be returned * @return the dynamic operation implementation; never null */ protected ExtractFromRow createExtractFromRow( final String selectorName, final String propertyName, QueryContext context, Columns columns, QuerySources sources, TypeFactory defaultType, boolean allowMultiValued ) { final Name propName = context.getExecutionContext().getValueFactories().getNameFactory().create(propertyName); final NodeCache cache = context.getNodeCache(sources.getWorkspaceName()); // Find the expected property type of the value ... assert columns != null; final int indexInRow = columns.getSelectorIndex(selectorName); if (PSEUDO_COLUMN_NAMES.contains(propName)) { if (JcrLexicon.SCORE.equals(propName)) { // This is a special case of obtaining the score from the row ... final TypeFactory typeFactory = context.getTypeSystem().getDoubleFactory(); return new PropertyValueExtractor(selectorName, propertyName, typeFactory) { @Override public Object getValueInRow( RowAccessor row ) { // We have to convert this to a double, since 'float' is not a valid JCR property type ... return new Double(row.getScore(indexInRow)); } }; } if (JcrLexicon.PATH.equals(propName)) { // This is a special case of obtaining the score from the row ... final TypeFactory typeFactory = context.getTypeSystem().getPathFactory(); return new PropertyValueExtractor(selectorName, propertyName, typeFactory) { @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; return node.getPath(cache); } }; } if (ModeShapeLexicon.DEPTH.equals(propName)) { // This is a special case of obtaining the score from the row ... final TypeFactory typeFactory = context.getTypeSystem().getLongFactory(); return new PropertyValueExtractor(selectorName, propertyName, typeFactory) { @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; return node.getDepth(cache); } }; } if (JcrLexicon.NAME.equals(propName)) { // This is a special case of obtaining the score from the row ... final TypeFactory typeFactory = context.getTypeSystem().getNameFactory(); return new PropertyValueExtractor(selectorName, propertyName, typeFactory) { @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; return node.getName(cache); } }; } if (ModeShapeLexicon.LOCALNAME.equals(propName)) { // This is a special case of obtaining the score from the row ... final TypeFactory typeFactory = context.getTypeSystem().getStringFactory(); return new PropertyValueExtractor(selectorName, propertyName, typeFactory) { @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; return node.getName(cache).getLocalName(); } }; } if (ModeShapeLexicon.ID.equals(propName)) { // This is a special case of obtaining the identifier from the row ... final TypeFactory typeFactory = context.getTypeSystem().getStringFactory(); return new PropertyValueExtractor(selectorName, propertyName, typeFactory) { @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; return node.getKey().toString(); } }; } if (JcrLexicon.UUID.equals(propName)) { // This is a special case of obtaining the "jcr:uuid" value from the row, except that the // CachedNode instances don't know about this property ... final TypeFactory typeFactory = context.getTypeSystem().getStringFactory(); final NodeTypes nodeTypes = context.getNodeTypes(); return new PropertyValueExtractor(selectorName, propertyName, typeFactory) { @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; if (nodeTypes.isReferenceable(node.getPrimaryType(cache), node.getMixinTypes(cache))) { // The node is a 'mix:referenceable' node, so return the UUID ... NodeKey key = node.getKey(); return key.getIdentifier(); } return null; } }; } } String expectedType = null; try { expectedType = columns.getColumnTypeForProperty(selectorName, propertyName); } catch (NoSuchElementException e) { if (defaultType == null) throw e; } final TypeFactory typeFactory = expectedType != null ? context.getTypeSystem().getTypeFactory(expectedType) : defaultType; if (allowMultiValued) { return new ExtractFromRow() { @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; org.modeshape.jcr.value.Property prop = node.getProperty(propName, cache); if (prop == null || prop.isEmpty()) return null; if (prop.isSingle()) { return typeFactory.create(prop.getFirstValue()); } assert prop.isMultiple(); Object[] result = new Object[prop.size()]; int i = -1; for (Object value : prop) { result[++i] = typeFactory.create(value); } return result; } @Override public TypeFactory getType() { return typeFactory; } @Override public String toString() { return "(" + selectorName + "." + propertyName + ")"; } }; } return new ExtractFromRow() { @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; org.modeshape.jcr.value.Property prop = node.getProperty(propName, cache); if (prop == null || prop.isEmpty()) return null; return typeFactory.create(prop.getFirstValue()); } @Override public TypeFactory getType() { return typeFactory; } @Override public String toString() { return "(" + selectorName + "." + propertyName + ")"; } }; } /** * Create a {@link ExtractFromRow} implementation that accesses the REFERENCE value(s) in the properties of the node * identified by the supplied selector names. * * @param selectorName the name of the selector containing the node(s) to be accessed; may not be null * @param context the context in which the query is to be executed; may not be null * @param columns the result column definition; may not be null * @param sources the query sources for the repository; may not be null * @param defaultType the type that should be used by default, or null if an exception should be thrown when the type for the * property name could not be determined * @return the dynamic operation implementation; never null */ protected ExtractFromRow createExtractReferencesFromRow( final String selectorName, QueryContext context, Columns columns, QuerySources sources, TypeFactory defaultType ) { final NodeCache cache = context.getNodeCache(sources.getWorkspaceName()); // Find the expected property type of the value ... assert columns != null; final int indexInRow = columns.getSelectorIndex(selectorName); final TypeFactory typeFactory = context.getTypeSystem().getStringFactory(); final boolean trace = LOGGER.isTraceEnabled(); return new ExtractFromRow() { @Override public Object getValueInRow( RowAccessor row ) { CachedNode node = row.getNode(indexInRow); if (node == null) return null; List values = null; for (Iterator iter = node.getProperties(cache); iter.hasNext();) { org.modeshape.jcr.value.Property prop = iter.next(); if (prop == null || prop.isEmpty()) continue; if (prop.isReference() || prop.isSimpleReference()) { if (prop.isSingle()) { Object value = prop.getFirstValue(); if (value != null) { if (values == null) values = new LinkedList<>(); values.add(typeFactory.create(value)); } } else { assert prop.isMultiple(); for (Object value : prop) { if (value == null) continue; if (values == null) values = new LinkedList<>(); values.add(typeFactory.create(value)); } } } } if (values == null || values.isEmpty()) return null; if (trace) { LOGGER.trace("Found references in '{0}': {1}", node.getPath(cache), values); } return values.toArray(); } @Override public TypeFactory getType() { return typeFactory; } @Override public String toString() { return "(references " + selectorName + ")"; } }; } /** * Interface for evaluating a {@link DynamicOperand} against the current row in a {@link Batch} and returning the * corresponding value. */ protected static abstract class DynamicOperandFilter implements RowFilter { private final ExtractFromRow extractor; protected DynamicOperandFilter( ExtractFromRow extractor ) { this.extractor = extractor; } @Override public boolean isCurrentRowValid( Batch batch ) { Object lhs = extractor.getValueInRow(batch); if (lhs == null) return false; // NULL never matches any value, even NULL if (lhs instanceof Object[]) { // The value is an array, meaning the dynamic operand was a multi-valued property ... for (Object lhsValue : (Object[])lhs) { if (evaluate(lhsValue)) return true; } return false; } return evaluate(lhs); } /** * Determine whether the left hand side of the expression (as computed by the dynamic operation) will result in this row * being included in the results. Note that implementations will compare this left hand side for the row with a fixed * right hand side value. * * @param leftHandValue the left hand side of the current row; may be null * @return true if the left hand value satisfies this filter, or false otherwise */ protected abstract boolean evaluate( Object leftHandValue ); } /** * A {@link RowFilter} implementation that lazily initializes the real RowFilter implementation the first time it's needed and * thereafter will simply delegate to the implementation. * * @author Randall Hauch ([email protected]) */ protected static abstract class RowFilterSupplier implements RowFilter { private RowFilter delegate; @Override public final boolean isCurrentRowValid( Batch batch ) { return delegate().isCurrentRowValid(batch); } protected final RowFilter delegate() { if (delegate == null) { delegate = createFilter(); } return delegate; } /** * Instantiate the RowFilter that will be used. This method may be called more than once only if * {@link #isCurrentRowValid(Batch)} is called from multiple threads. * * @return the row filter; may not be null */ protected abstract RowFilter createFilter(); @Override public String toString() { return delegate().toString(); } } /** * Get the literal value that is defined in the supplied {@link StaticOperand}. If the supplied static operand is a * {@link BindVariableValue}, the the variable value is obtained from the {@link QueryContext#getVariables() variables} in the * {@link QueryContext}. Otherwise, this method simply casts the {@link StaticOperand} to a {@link Literal} value. * * @param staticOperand the static operand; may be null * @param context the query context; may not be null * @param type the type factory for the expected type; may be null if the value should be used as-is * @return the literal value; may be null */ protected static Object literalValue( StaticOperand staticOperand, QueryContext context, TypeFactory type ) { // Determine the literal value ... Object literalValue = null; if (staticOperand instanceof BindVariableName) { BindVariableName bindVariable = (BindVariableName)staticOperand; String variableName = bindVariable.getBindVariableName(); literalValue = context.getVariables().get(variableName); // may be null if (LOGGER.isTraceEnabled()) { LOGGER.trace("Using variable '{0}' value: {1}", variableName, literalValue); } if (literalValue instanceof Collection || literalValue instanceof Object[]) return literalValue; return type.create(literalValue); // without converting! } if (staticOperand instanceof LiteralValue) { LiteralValue literal = (LiteralValue)staticOperand; Value value = literal.getLiteralValue(); if (value != null) { // Use the proper type factory to ensure the value is the correct type ... PropertyType propType = PropertyTypeUtil.modePropertyTypeFor(value.getType()); try { literalValue = context.getExecutionContext().getValueFactories().getValueFactory(propType) .create(value.getString()); } catch (RepositoryException e) { // Really shouldn't happen, but just in case ... throw new SystemFailureException(e); } } } else if (staticOperand instanceof Literal) { Literal literal = (Literal)staticOperand; literalValue = literal.value(); } return type != null ? type.create(literalValue) : null; } protected static Set literalValues( SetCriteria setCriteria, QueryContext context, TypeFactory type ) { Set values = new HashSet<>(); for (Object value : setCriteria.getValues()) { if (value instanceof StaticOperand) { Object literal = literalValue((StaticOperand)value, context, type); if (literal instanceof Collection) { for (Object v : (Collection)literal) { if (v != null) values.add(type.create(v)); } values.addAll((Collection)literal); } else if (literal instanceof Object[]) { for (Object v : (Object[])literal) { if (v != null) values.add(type.create(v)); } } else if (literal != null) { values.add(type.create(literal)); } } } return values; } /** * Convert the JCR like expression to a Lucene wildcard expression. The JCR like expression uses '%' to match 0 or more * characters, '_' to match any single character, '\x' to match the 'x' character, and all other characters to match * themselves. * * @param likeExpression the like expression; may not be null * @return the expression that can be used with a WildcardQuery; never null */ protected static String toWildcardExpression( String likeExpression ) { return likeExpression.replace('%', '*').replace('_', '?').replaceAll("\\\\(.)", "$1"); } /** * Convert the JCR like expression to a regular expression. The JCR like expression uses '%' to match 0 or more characters, * '_' to match any single character, '\x' to match the 'x' character, and all other characters to match themselves. Note that * if any regex metacharacters appear in the like expression, they will be escaped within the resulting regular expression. * * @param likeExpression the like expression; may not be null * @return the expression that can be used with a WildcardQuery; never null */ public static String toRegularExpression( String likeExpression ) { // Replace all '\x' with 'x' ... String result = likeExpression.replaceAll("\\\\(.)", "$1"); // Escape characters used as metacharacters in regular expressions, including // '[', '^', '\', '$', '.', '|', '+', '(', and ')' // But leave '?' and '*' result = result.replaceAll("([$.|+()\\[\\\\^\\\\\\\\])", "\\\\$1"); // Replace '%'->'[.]*' and '_'->'[.] // (order of these calls is important!) result = result.replace("*", ".*").replace("?", "."); result = result.replace("%", ".*").replace("_", "."); return result; } // protected RowFilter createRowFilter( DynamicOperand dynamicOperand, Operator op, StaticOperand Constraint constraint, // QueryContext context, Columns columns, QuerySources sources ) { // } public static final class ResultColumns implements org.modeshape.jcr.query.QueryResults.Columns { private static final long serialVersionUID = 1L; protected static final List NO_COLUMNS = Collections.emptyList(); protected static final List NO_TYPES = Collections.emptyList(); protected static final String DEFAULT_SELECTOR_NAME = "Results"; protected static final ResultColumns EMPTY = new ResultColumns(null, null, false, null); private final List columns; private final List columnTypes; private final List columnNames; private final List selectorNames; private final Map selectorIndexBySelectorName; private final Map selectorNameByColumnName; private final Map propertyNameByColumnName; private final Map> columnIndexByPropertyNameBySelectorName; private final boolean includeFullTextSearchScores; protected final static class ColumnInfo { protected final int columnIndex; protected final String type; protected ColumnInfo( int columnIndex, String type ) { this.columnIndex = columnIndex; this.type = type; } @Override public String toString() { return "" + columnIndex + "(" + type + ")"; } } /** * Create a new definition for the query results given the supplied columns. * * @param columns the columns that define the results; should never be modified directly * @param columnTypes the names of the types for each column in columns * @param includeFullTextSearchScores true if the results should include full text search scores, or false otherwise * @param precedingColumns the columns for the preceding plan node and which contain correct selector indexes; may be null */ public ResultColumns( List columns, List columnTypes, boolean includeFullTextSearchScores, Columns precedingColumns ) { this.includeFullTextSearchScores = includeFullTextSearchScores; this.columns = columns != null ? Collections.unmodifiableList(columns) : NO_COLUMNS; this.columnTypes = columnTypes != null ? Collections.unmodifiableList(columnTypes) : NO_TYPES; this.selectorIndexBySelectorName = new HashMap(); this.columnIndexByPropertyNameBySelectorName = new HashMap>(); this.selectorNameByColumnName = new HashMap(); this.propertyNameByColumnName = new HashMap(); Set selectors = new HashSet(); final int columnCount = this.columns.size(); List names = new ArrayList(columnCount); List selectorNames = new ArrayList(columnCount); Set sameNameColumns = findColumnsWithSameNames(this.columns); // Find all the selector names ... int selectorIndex = 0; for (int i = 0, max = this.columns.size(); i != max; ++i) { Column column = this.columns.get(i); assert column != null; String selectorName = column.selectorName().name(); if (selectors.add(selectorName)) { selectorNames.add(selectorName); int index = selectorIndex; if (precedingColumns != null) { index = precedingColumns.getSelectorIndex(selectorName); if (index < 0) index = selectorIndex; } selectorIndexBySelectorName.put(selectorName, index); ++selectorIndex; } } // Now, find all of the column names ... for (int i = 0, max = this.columns.size(); i != max; ++i) { Column column = this.columns.get(i); assert column != null; String selectorName = column.selectorName().name(); String columnName = columnNameFor(column, names, sameNameColumns, selectors); assert columnName != null; propertyNameByColumnName.put(columnName, column.getPropertyName()); selectorNameByColumnName.put(columnName, selectorName); // Insert the entry by selector name and property name ... Map byPropertyName = columnIndexByPropertyNameBySelectorName.get(selectorName); if (byPropertyName == null) { byPropertyName = new HashMap(); columnIndexByPropertyNameBySelectorName.put(selectorName, byPropertyName); } String columnType = this.columnTypes.get(i); byPropertyName.put(column.getPropertyName(), new ColumnInfo(i, columnType)); } if (columns != null && selectorNames.isEmpty()) { String selectorName = DEFAULT_SELECTOR_NAME; selectorNames.add(selectorName); selectorIndexBySelectorName.put(selectorName, 0); } this.selectorNames = Collections.unmodifiableList(selectorNames); this.columnNames = Collections.unmodifiableList(names); } protected static Set findColumnsWithSameNames( List columns ) { Multimap columnNames = ArrayListMultimap.create(); for (Column column : columns) { String columnName = column.getColumnName() != null ? column.getColumnName() : column.getPropertyName(); columnNames.put(columnName, column); } Set results = new HashSet(); for (Map.Entry> entry : columnNames.asMap().entrySet()) { if (entry.getValue().size() > 1) { results.addAll(entry.getValue()); } } return results; } protected static String columnNameFor( Column column, List columnNames, Set columnsWithDuplicateNames, Collection selectorNames ) { String columnName = column.getColumnName() != null ? column.getColumnName() : column.getPropertyName(); boolean qualified = columnName != null ? columnName.startsWith(column.getSelectorName() + ".") : false; boolean aliased = columnName != null ? !columnName.equals(column.getPropertyName()) : false; if (column.getPropertyName() == null || columnNames.contains(columnName) || columnsWithDuplicateNames.contains(column)) { // Per section 6.7.39 of the JSR-283 specification, if the property name for a column is not given // then the name for the column in the result set must be "selectorName.propertyName" ... columnName = column.selectorName() + "." + columnName; } else if (!qualified && !aliased && selectorNames.size() > 1) { // When there is more than one selector, all columns need to be named "selectorName.propertyName" ... columnName = column.selectorName() + "." + columnName; } columnNames.add(columnName); return columnName; } @Override public Iterator iterator() { return columns.iterator(); } @Override public List getColumns() { return columns; } @Override public List getColumnNames() { return columnNames; } @Override public List getColumnTypes() { return columnTypes; } @Override public String getColumnTypeForProperty( String selectorName, String propertyName ) { Map byPropertyName = columnIndexByPropertyNameBySelectorName.get(selectorName); if (byPropertyName == null) { throw new NoSuchElementException(GraphI18n.selectorDoesNotExistInQuery.text(selectorName)); } ColumnInfo result = byPropertyName.get(propertyName); return result != null ? result.type : null; } @Override public int getSelectorIndex( String selectorName ) { if (!selectorNames.contains(selectorName)) return -1; return selectorIndexBySelectorName.get(selectorName); } @Override public List getSelectorNames() { return selectorNames; } @Override public String getPropertyNameForColumnName( String columnName ) { String result = propertyNameByColumnName.get(columnName); return result != null ? result : columnName; } @Override public String getSelectorNameForColumnName( String columnName ) { return selectorNameByColumnName.get(columnName); } @Override public boolean hasFullTextSearchScores() { return includeFullTextSearchScores; } @Override public boolean isUnionCompatible( org.modeshape.jcr.query.QueryResults.Columns other ) { if (this == other) return true; if (other == null) return false; if (this.hasFullTextSearchScores() != other.hasFullTextSearchScores()) return false; if (this.getColumns().size() != other.getColumns().size()) return false; return this.getColumnNames().containsAll(other.getColumnNames()) && other.getColumnNames().containsAll(this.getColumnNames()); } @Override public int hashCode() { return getColumns().hashCode(); } @Override public boolean equals( Object obj ) { if (obj == this) return true; if (obj instanceof ResultColumns) { ResultColumns that = (ResultColumns)obj; return this.getColumns().equals(that.getColumns()); } return false; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("{ ").append("\n"); if (this.columnNames != null) { boolean first = true; for (int i = 0; i != columns.size(); ++i) { Column column = columns.get(i); String type = columnTypes.get(i); if (first) first = false; else sb.append(", ").append("\n"); sb.append(Visitors.readable(column)).append("{").append(type.toUpperCase()).append("}"); } } sb.append("\n").append(" }"); return sb.toString(); } @Override public Columns with( Columns other ) { final int maxNumberOfColumns = this.getColumns().size() + other.getColumns().size(); List allColumns = new ArrayList<>(maxNumberOfColumns); List allTypes = new ArrayList<>(maxNumberOfColumns); Set seen = new HashSet<>(); allColumns.addAll(this.columns); allTypes.addAll(this.columnTypes); Iterator types = other.getColumnTypes().iterator(); for (Column column : other) { String type = types.next(); if (seen.add(column)) { allColumns.add(column); allTypes.add(type); } } boolean fts = this.hasFullTextSearchScores() || other.hasFullTextSearchScores(); return new ResultColumns(allColumns, allTypes, fts, this); } } @ThreadSafe static class ScanQueryContext extends QueryContext { protected final Map columnsByPlanNode; protected ScanQueryContext( ExecutionContext context, RepositoryCache repositoryCache, Set workspaceNames, Map overriddenNodeCachesByWorkspaceName, Schemata schemata, RepositoryIndexes indexDefns, NodeTypes nodeTypes, BufferManager bufferManager, PlanHints hints, Problems problems, Map variables, Map columnsByPlanNode ) { super(context, repositoryCache, workspaceNames, overriddenNodeCachesByWorkspaceName, schemata, indexDefns, nodeTypes, bufferManager, hints, problems, variables); this.columnsByPlanNode = columnsByPlanNode; } /** * Add a {@link Columns} object for the given plan node. * * @param node plan node; may not be null * @param columns the columns to be assocated with this plan node; may not be null */ public void addColumnsFor( PlanNode node, Columns columns ) { columnsByPlanNode.put(node, columns); } /** * Get the {@link Columns} object for the given plan node. * * @param node plan node; may not be null * @return the columns to be assocated with this plan node; may not be null */ public Columns columnsFor( PlanNode node ) { return columnsByPlanNode.get(node); } @Override public ScanQueryContext with( Map variables ) { return new ScanQueryContext(context, repositoryCache, workspaceNames, overriddenNodeCachesByWorkspaceName, schemata, indexDefns, nodeTypes, bufferManager, hints, problems, variables, columnsByPlanNode); } @Override public ScanQueryContext with( PlanHints hints ) { return new ScanQueryContext(context, repositoryCache, workspaceNames, overriddenNodeCachesByWorkspaceName, schemata, indexDefns, nodeTypes, bufferManager, hints, problems, variables, columnsByPlanNode); } @Override public ScanQueryContext with( Problems problems ) { return new ScanQueryContext(context, repositoryCache, workspaceNames, overriddenNodeCachesByWorkspaceName, schemata, indexDefns, nodeTypes, bufferManager, hints, problems, variables, columnsByPlanNode); } @Override public ScanQueryContext with( Schemata schemata ) { return new ScanQueryContext(context, repositoryCache, workspaceNames, overriddenNodeCachesByWorkspaceName, schemata, indexDefns, nodeTypes, bufferManager, hints, problems, variables, columnsByPlanNode); } } }