Index: docs/reference/src/main/docbook/en-US/content/jcr/query_and_search.xml =================================================================== --- docs/reference/src/main/docbook/en-US/content/jcr/query_and_search.xml (revision 2216) +++ docs/reference/src/main/docbook/en-US/content/jcr/query_and_search.xml (working copy) @@ -621,13 +621,24 @@ ORDER BY nodeSet1.title, Support for the IN and NOT IN clauses to more easily and concisely supply multiple of discrete static operands. - For example, "WHERE ... [my:type].[prop1] IN (3,5,7,10,11,50) ...". + For example, "WHERE ... prop1 IN (3,5,7,10,11,50) ...". Support for the BETWEEN clause to more easily and concisely supply a range of discrete operands. - For example, "WHERE ... [my:type].[prop1] BETWEEN 3 EXCLUSIVE AND 10 ...". + For example, "WHERE ... prop1 BETWEEN 3 EXCLUSIVE AND 10 ...". + + + + + Support for (non-correlated) subqueries in the WHERE clause, wherever a static operand can be used. + Subqueries can even be used within another subquery. All subqueries must return a single column, and each row's single + value will be treated as a literal value. If the subquery is used in a clause that expects a single value + (e.g., in a comparison), only the subquery's first row will be used. If the subquery is used in a clause that + allows multiple values (e.g., IN (...)), then all of the subquery's rows will be used. + For example, this query "WHERE ... prop1 IN ( SELECT my:prop2 FROM my:type2 WHERE my:prop3 < '1000' ) AND ..." + will use the results of the subquery as the literal values in the IN clause. @@ -694,9 +705,13 @@ ntname ::= quotedntname | unquotedntname quotedntname ::= ''' unquotedntname ''' unquotedntname ::= /* A node type name */ -value ::= ''' literalvalue ''' | literalvalue +value ::= literal | subquery + +literal ::= ''' literalvalue ''' | literalvalue literalvalue ::= /* A property value (in standard string form) */ +subquery ::= '(' QueryCommand ')' | QueryCommand + like ::= propname 'LIKE' likepattern [ escape ] likepattern ::= ''' likechar { likepattern } ''' likechar ::= char | '%' | '_' @@ -812,6 +827,17 @@ offset ::= /* Non-negative integer value */ For detail, see the grammar for order-by clauses. + + + Support for (non-correlated) subqueries in the WHERE clause, wherever a static operand can be used. + Subqueries can even be used within another subquery. All subqueries must return a single column, and each row's single + value will be treated as a literal value. If the subquery is used in a clause that expects a single value + (e.g., in a comparison), only the subquery's first row will be used. If the subquery is used in a clause that + allows multiple values (e.g., IN (...)), then all of the subquery's rows will be used. + For example, this query "WHERE ... [my:type].[prop1] IN ( SELECT [my:prop2] FROM [my:type2] WHERE [my:prop3] < '1000' ) AND ..." + will use the results of the subquery as the literal values in the IN clause. + + @@ -1095,7 +1121,7 @@ simplePath ::= /* A JCR Path (rather Name) that contains only SQL-legal Static Operands + + Subqueries + + Dynamic Operands 0; if (numRightOperands == 1) { - return createQuery(left, Operator.EQUAL_TO, setCriteria.rightOperands().iterator().next()); + StaticOperand rightOperand = setCriteria.rightOperands().iterator().next(); + if (rightOperand instanceof Literal) { + return createQuery(left, Operator.EQUAL_TO, setCriteria.rightOperands().iterator().next()); + } } BooleanQuery setQuery = new BooleanQuery(); for (StaticOperand right : setCriteria.rightOperands()) { - Query rightQuery = createQuery(left, Operator.EQUAL_TO, right); - if (rightQuery == null) return null; - setQuery.add(rightQuery, Occur.SHOULD); + if (right instanceof BindVariableName) { + // This single value is a variable name, which may evaluate to a single value or multiple values ... + BindVariableName var = (BindVariableName)right; + Object value = variables.get(var.variableName()); + if (value instanceof Iterable) { + Iterator iter = ((Iterable)value).iterator(); + while (iter.hasNext()) { + Object resolvedValue = iter.next(); + if (resolvedValue == null) continue; + StaticOperand elementInRight = null; + if (resolvedValue instanceof Literal) { + elementInRight = (Literal)resolvedValue; + } else { + elementInRight = new Literal(resolvedValue); + } + Query rightQuery = createQuery(left, Operator.EQUAL_TO, elementInRight); + if (rightQuery == null) continue; + setQuery.add(rightQuery, Occur.SHOULD); + } + } + if (value == null) { + throw new LuceneException(LuceneI18n.missingVariableValue.text(var.variableName())); + } + } else { + Query rightQuery = createQuery(left, Operator.EQUAL_TO, right); + if (rightQuery == null) return null; + setQuery.add(rightQuery, Occur.SHOULD); + } } return setQuery; } @@ -683,6 +711,15 @@ public abstract class AbstractLuceneSearchEngine) { + // We can only return one value ... + Iterator iter = ((Iterable)value).iterator(); + if (iter.hasNext()) return iter.next(); + value = null; + } + if (value == null) { + throw new LuceneException(LuceneI18n.missingVariableValue.text(variableName)); + } if (!caseSensitive) value = lowerCase(value); } else { assert false; Index: extensions/modeshape-search-lucene/src/main/java/org/modeshape/search/lucene/LuceneI18n.java =================================================================== --- extensions/modeshape-search-lucene/src/main/java/org/modeshape/search/lucene/LuceneI18n.java (revision 2216) +++ extensions/modeshape-search-lucene/src/main/java/org/modeshape/search/lucene/LuceneI18n.java (working copy) @@ -35,6 +35,7 @@ public class LuceneI18n { public static I18n locationForIndexesCannotBeWritten; public static I18n errorWhileCommittingIndexChanges; public static I18n errorWhileRollingBackIndexChanges; + public static I18n missingVariableValue; static { try { Index: extensions/modeshape-search-lucene/src/main/resources/org/modeshape/search/lucene/LuceneI18n.properties =================================================================== --- extensions/modeshape-search-lucene/src/main/resources/org/modeshape/search/lucene/LuceneI18n.properties (revision 2216) +++ extensions/modeshape-search-lucene/src/main/resources/org/modeshape/search/lucene/LuceneI18n.properties (working copy) @@ -28,3 +28,5 @@ locationForIndexesCannotBeWritten = Location "{0}" cannot be used for search ind errorWhileCommittingIndexChanges = Error while committing changes to the indexes for the "{0}" workspace of the "{1}" source: {2} errorWhileRollingBackIndexChanges = Error while rolling back changes to the indexes for the "{0}" workspace of the "{1}" source: {2} + +missingVariableValue = Variable "{0}" has no value \ No newline at end of file Index: modeshape-graph/src/main/java/org/modeshape/graph/query/QueryBuilder.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/QueryBuilder.java (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/QueryBuilder.java (working copy) @@ -86,6 +86,7 @@ import org.modeshape.graph.query.model.SetCriteria; import org.modeshape.graph.query.model.SetQuery; import org.modeshape.graph.query.model.Source; import org.modeshape.graph.query.model.StaticOperand; +import org.modeshape.graph.query.model.Subquery; import org.modeshape.graph.query.model.TypeSystem; import org.modeshape.graph.query.model.UpperCase; import org.modeshape.graph.query.model.Visitors; @@ -1908,6 +1909,16 @@ public class QueryBuilder { /** * Define the right-hand side of a comparison. * + * @param subquery the subquery + * @return the constraint builder; never null + */ + public ConstraintBuilder literal( QueryCommand subquery ) { + return comparisonBuilder.is(operator, subquery); + } + + /** + * Define the right-hand side of a comparison. + * * @param variableName the name of the variable * @return the constraint builder; never null */ @@ -2180,6 +2191,26 @@ public class QueryBuilder { /** * Define the upper boundary value of a range. * + * @param subquery the subquery + * @return the constraint builder; never null + */ + public ConstraintBuilder subquery( Subquery subquery ) { + return comparisonBuilder.constraintBuilder.setConstraint(new Between(comparisonBuilder.left, lowerBound, subquery)); + } + + /** + * Define the upper boundary value of a range. + * + * @param subquery the subquery + * @return the constraint builder; never null + */ + public ConstraintBuilder subquery( QueryCommand subquery ) { + return subquery(new Subquery(subquery)); + } + + /** + * Define the upper boundary value of a range. + * * @param literal the literal value that is to be cast * @return the constraint builder; never null */ @@ -2438,6 +2469,26 @@ public class QueryBuilder { /** * Define the lower boundary value of a range. * + * @param subquery the subquery + * @return the constraint builder; never null + */ + public AndBuilder subquery( Subquery subquery ) { + return new AndBuilder(new UpperBoundary(comparisonBuilder, subquery)); + } + + /** + * Define the lower boundary value of a range. + * + * @param subquery the subquery + * @return the constraint builder; never null + */ + public AndBuilder subquery( QueryCommand subquery ) { + return subquery(new Subquery(subquery)); + } + + /** + * Define the lower boundary value of a range. + * * @param literal the literal value that is to be cast * @return the constraint builder; never null */ @@ -2703,6 +2754,16 @@ public class QueryBuilder { this.constraintBuilder = constraintBuilder; } + public ConstraintBuilder isInSubquery( QueryCommand subquery ) { + CheckArg.isNotNull(subquery, "subquery"); + return this.constraintBuilder.setConstraint(new SetCriteria(left, new Subquery(subquery))); + } + + public ConstraintBuilder isInSubquery( Subquery subquery ) { + CheckArg.isNotNull(subquery, "subquery"); + return this.constraintBuilder.setConstraint(new SetCriteria(left, subquery)); + } + public ConstraintBuilder isIn( Object... literals ) { CheckArg.isNotNull(literals, "literals"); Collection right = new ArrayList(); @@ -2841,22 +2902,63 @@ public class QueryBuilder { * Define the right-hand-side of the constraint using the supplied operator. * * @param operator the operator; may not be null - * @param literal the literal value + * @param subquery the subquery + * @return the builder used to create the constraint clause, ready to be used to create other constraints clauses or + * complete already-started clauses; never null + */ + public ConstraintBuilder is( Operator operator, + QueryCommand subquery ) { + assert operator != null; + return is(operator, subquery); + } + + /** + * Define the right-hand-side of the constraint using the supplied operator. + * + * @param operator the operator; may not be null + * @param subquery the subquery * @return the builder used to create the constraint clause, ready to be used to create other constraints clauses or * complete already-started clauses; never null */ public ConstraintBuilder is( Operator operator, - Object literal ) { + Subquery subquery ) { assert operator != null; - Literal value = literal instanceof Literal ? (Literal)literal : new Literal(literal); - return this.constraintBuilder.setConstraint(new Comparison(left, operator, value)); + return is(operator, subquery); + } + + /** + * Define the right-hand-side of the constraint using the supplied operator. + * + * @param operator the operator; may not be null + * @param literalOrSubquery the literal value or subquery + * @return the builder used to create the constraint clause, ready to be used to create other constraints clauses or + * complete already-started clauses; never null + */ + public ConstraintBuilder is( Operator operator, + Object literalOrSubquery ) { + assert operator != null; + return this.constraintBuilder.setConstraint(new Comparison(left, operator, adapt(literalOrSubquery))); + } + + protected StaticOperand adapt( Object literalOrSubquery ) { + if (literalOrSubquery instanceof QueryCommand) { + // Wrap the query in a subquery ... + return new Subquery((QueryCommand)literalOrSubquery); + } + if (literalOrSubquery instanceof Subquery) { + return (Subquery)literalOrSubquery; + } + if (literalOrSubquery instanceof Literal) { + return (Literal)literalOrSubquery; + } + return new Literal(literalOrSubquery); } /** * Define the right-hand-side of the constraint using the supplied operator. * - * @param lowerBoundLiteral the literal value that represents the lower bound of the range (inclusive) - * @param upperBoundLiteral the literal value that represents the upper bound of the range (inclusive) + * @param lowerBoundLiteral the literal value that represents the lower bound of the range (inclusive); may be a subquery + * @param upperBoundLiteral the literal value that represents the upper bound of the range (inclusive); may be a subquery * @return the builder used to create the constraint clause, ready to be used to create other constraints clauses or * complete already-started clauses; never null */ @@ -2864,9 +2966,7 @@ public class QueryBuilder { Object upperBoundLiteral ) { assert lowerBoundLiteral != null; assert upperBoundLiteral != null; - Literal lower = lowerBoundLiteral instanceof Literal ? (Literal)lowerBoundLiteral : new Literal(lowerBoundLiteral); - Literal upper = upperBoundLiteral instanceof Literal ? (Literal)upperBoundLiteral : new Literal(upperBoundLiteral); - return this.constraintBuilder.setConstraint(new Between(left, lower, upper)); + return this.constraintBuilder.setConstraint(new Between(left, adapt(lowerBoundLiteral), adapt(upperBoundLiteral))); } /** @@ -2949,78 +3049,78 @@ public class QueryBuilder { /** * Define the right-hand-side of the constraint to be equivalent to the supplied literal value. * - * @param literal the literal value + * @param literalOrSubquery the literal value or a subquery * @return the builder used to create the constraint clause, ready to be used to create other constraints clauses or * complete already-started clauses; never null */ - public ConstraintBuilder isEqualTo( Object literal ) { - return is(Operator.EQUAL_TO, literal); + public ConstraintBuilder isEqualTo( Object literalOrSubquery ) { + return is(Operator.EQUAL_TO, literalOrSubquery); } /** * Define the right-hand-side of the constraint to be greater than the supplied literal value. * - * @param literal the literal value + * @param literalOrSubquery the literal value or a subquery * @return the builder used to create the constraint clause, ready to be used to create other constraints clauses or * complete already-started clauses; never null */ - public ConstraintBuilder isGreaterThan( Object literal ) { - return is(Operator.GREATER_THAN, literal); + public ConstraintBuilder isGreaterThan( Object literalOrSubquery ) { + return is(Operator.GREATER_THAN, literalOrSubquery); } /** * Define the right-hand-side of the constraint to be greater than or equal to the supplied literal value. * - * @param literal the literal value + * @param literalOrSubquery the literal value or a subquery * @return the builder used to create the constraint clause, ready to be used to create other constraints clauses or * complete already-started clauses; never null */ - public ConstraintBuilder isGreaterThanOrEqualTo( Object literal ) { - return is(Operator.GREATER_THAN_OR_EQUAL_TO, literal); + public ConstraintBuilder isGreaterThanOrEqualTo( Object literalOrSubquery ) { + return is(Operator.GREATER_THAN_OR_EQUAL_TO, literalOrSubquery); } /** * Define the right-hand-side of the constraint to be less than the supplied literal value. * - * @param literal the literal value + * @param literalOrSubquery the literal value or a subquery * @return the builder used to create the constraint clause, ready to be used to create other constraints clauses or * complete already-started clauses; never null */ - public ConstraintBuilder isLessThan( Object literal ) { - return is(Operator.LESS_THAN, literal); + public ConstraintBuilder isLessThan( Object literalOrSubquery ) { + return is(Operator.LESS_THAN, literalOrSubquery); } /** * Define the right-hand-side of the constraint to be less than or equal to the supplied literal value. * - * @param literal the literal value + * @param literalOrSubquery the literal value or a subquery * @return the builder used to create the constraint clause, ready to be used to create other constraints clauses or * complete already-started clauses; never null */ - public ConstraintBuilder isLessThanOrEqualTo( Object literal ) { - return is(Operator.LESS_THAN_OR_EQUAL_TO, literal); + public ConstraintBuilder isLessThanOrEqualTo( Object literalOrSubquery ) { + return is(Operator.LESS_THAN_OR_EQUAL_TO, literalOrSubquery); } /** * Define the right-hand-side of the constraint to be LIKE the supplied literal value. * - * @param literal the literal value + * @param literalOrSubquery the literal value or a subquery * @return the builder used to create the constraint clause, ready to be used to create other constraints clauses or * complete already-started clauses; never null */ - public ConstraintBuilder isLike( Object literal ) { - return is(Operator.LIKE, literal); + public ConstraintBuilder isLike( Object literalOrSubquery ) { + return is(Operator.LIKE, literalOrSubquery); } /** * Define the right-hand-side of the constraint to be not equal to the supplied literal value. * - * @param literal the literal value + * @param literalOrSubquery the literal value or a subquery * @return the builder used to create the constraint clause, ready to be used to create other constraints clauses or * complete already-started clauses; never null */ - public ConstraintBuilder isNotEqualTo( Object literal ) { - return is(Operator.NOT_EQUAL_TO, literal); + public ConstraintBuilder isNotEqualTo( Object literalOrSubquery ) { + return is(Operator.NOT_EQUAL_TO, literalOrSubquery); } /** Index: modeshape-graph/src/main/java/org/modeshape/graph/query/QueryContext.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/QueryContext.java (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/QueryContext.java (working copy) @@ -23,7 +23,6 @@ */ package org.modeshape.graph.query; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import net.jcip.annotations.Immutable; @@ -68,7 +67,7 @@ public class QueryContext { this.hints = hints != null ? hints : new PlanHints(); this.schemata = schemata; this.problems = problems != null ? problems : new SimpleProblems(); - this.variables = variables != null ? Collections.unmodifiableMap(new HashMap(variables)) : Collections.emptyMap(); + this.variables = variables != null ? new HashMap(variables) : new HashMap(); assert this.typeSystem != null; assert this.hints != null; assert this.schemata != null; Index: modeshape-graph/src/main/java/org/modeshape/graph/query/model/Subquery.java new file mode 100644 =================================================================== --- /dev/null (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/model/Subquery.java (working copy) @@ -0,0 +1,97 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.modeshape.graph.query.model; + +/** + * A representation of a non-correlated subquery. This component uses composition to hold the various types of QueryCommand + * objects, rather than inheriting from StaticOperand and QueryCommand. + */ +public class Subquery implements StaticOperand { + + private static final long serialVersionUID = 1L; + + private final QueryCommand query; + + /** + * Create a new subquery component that uses the supplied query as the subquery expression. + * + * @param query the Command representing the subquery. + */ + public Subquery( QueryCommand query ) { + this.query = query; + } + + /** + * Get the query representing the subquery. + * + * @return the subquery + */ + public QueryCommand query() { + return query; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.query.model.Visitable#accept(org.modeshape.graph.query.model.Visitor) + */ + public void accept( Visitor visitor ) { + visitor.visit(this); + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return Visitors.readable(this); + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return query.hashCode(); + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals( Object obj ) { + if (obj == this) return true; + if (obj instanceof Subquery) { + Subquery that = (Subquery)obj; + return this.query.equals(that.query) || this.query.toString().equals(that.query.toString()); + } + return false; + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/query/model/Visitor.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/model/Visitor.java (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/model/Visitor.java (working copy) @@ -86,6 +86,8 @@ public interface Visitor { void visit( Query obj ); + void visit( Subquery obj ); + void visit( ReferenceValue obj ); void visit( SameNode obj ); Index: modeshape-graph/src/main/java/org/modeshape/graph/query/model/Visitors.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/model/Visitors.java (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/model/Visitors.java (working copy) @@ -23,6 +23,7 @@ */ package org.modeshape.graph.query.model; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -83,6 +84,31 @@ public class Visitors { } /** + * Using a visitor, obtain the {@link Subquery} objects that are contained within the supplied {@link Visitable object}. This + * method does find Subquery objets nested in other Subquery objects. + * + * @param visitable the visitable + * @param includeNestedSubqueries true if any Subquery objects within other Subquery objects should be included, or false if + * only the top-level Subquery objects should be included + * @return the collection of subqueries; never null but possibly empty if no subqueries were found + */ + public static Collection subqueries( Visitable visitable, + final boolean includeNestedSubqueries ) { + final Collection subqueries = new LinkedList(); + Visitors.visitAll(visitable, new Visitors.AbstractVisitor() { + @Override + public void visit( Subquery subquery ) { + subqueries.add(subquery); + if (includeNestedSubqueries) { + // Now look for any subqueries in the subquery ... + subquery.query().accept(this); + } + } + }); + return subqueries; + } + + /** * Get a map of the selector names keyed by their aliases. * * @param visitable the object to be visited @@ -243,6 +269,11 @@ public class Visitors { } @Override + public void visit( Subquery obj ) { + // do nothing ... + } + + @Override public void visit( ReferenceValue ref ) { symbols.add(ref.selectorName()); } @@ -517,6 +548,14 @@ public class Visitors { /** * {@inheritDoc} * + * @see org.modeshape.graph.query.model.Visitor#visit(org.modeshape.graph.query.model.Subquery) + */ + public void visit( Subquery obj ) { + } + + /** + * {@inheritDoc} + * * @see org.modeshape.graph.query.model.Visitor#visit(org.modeshape.graph.query.model.ReferenceValue) */ public void visit( ReferenceValue obj ) { @@ -588,17 +627,15 @@ public class Visitors { this.strategy = strategy; } - protected final void enqueue( Visitable objectToBeVisited ) { + protected void enqueue( Visitable objectToBeVisited ) { if (objectToBeVisited != null) { itemQueue.add(objectToBeVisited); } } - protected final void enqueue( Iterable objectsToBeVisited ) { + protected void enqueue( Iterable objectsToBeVisited ) { for (Visitable objectToBeVisited : objectsToBeVisited) { - if (objectToBeVisited != null) { - itemQueue.add(objectToBeVisited); - } + enqueue(objectToBeVisited); } } @@ -622,7 +659,7 @@ public class Visitors { * * @param strategy the visitor that should be called at every node. */ - protected WalkAllVisitor( Visitor strategy ) { + public WalkAllVisitor( Visitor strategy ) { super(strategy); } @@ -950,6 +987,17 @@ public class Visitors { /** * {@inheritDoc} * + * @see org.modeshape.graph.query.model.Visitor#visit(org.modeshape.graph.query.model.Subquery) + */ + public void visit( Subquery subquery ) { + strategy.visit(subquery); + enqueue(subquery.query()); + visitNext(); + } + + /** + * {@inheritDoc} + * * @see org.modeshape.graph.query.model.Visitor#visit(org.modeshape.graph.query.model.ReferenceValue) */ public void visit( ReferenceValue referenceValue ) { @@ -1491,6 +1539,17 @@ public class Visitors { /** * {@inheritDoc} * + * @see org.modeshape.graph.query.model.Visitor#visit(org.modeshape.graph.query.model.Subquery) + */ + public void visit( Subquery subquery ) { + append('('); + subquery.query().accept(this); + append(')'); + } + + /** + * {@inheritDoc} + * * @see org.modeshape.graph.query.model.Visitor#visit(org.modeshape.graph.query.model.SameNode) */ public void visit( SameNode sameNode ) { Index: modeshape-graph/src/main/java/org/modeshape/graph/query/optimize/RaiseVariableName.java new file mode 100644 =================================================================== --- /dev/null (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/optimize/RaiseVariableName.java (working copy) @@ -0,0 +1,83 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.modeshape.graph.query.optimize; + +import java.util.LinkedList; +import net.jcip.annotations.Immutable; +import org.modeshape.graph.query.QueryContext; +import org.modeshape.graph.query.plan.PlanNode; +import org.modeshape.graph.query.plan.PlanNode.Property; +import org.modeshape.graph.query.plan.PlanNode.Traversal; +import org.modeshape.graph.query.plan.PlanNode.Type; + +/** + * An {@link OptimizerRule optimizer rule} that moves up higher in the plan any {@link Property#VARIABLE_NAME variable name} + * property to the node immediately under a {@link Type#DEPENDENT_QUERY dependent query} node. + */ +@Immutable +public class RaiseVariableName implements OptimizerRule { + + public static final RaiseVariableName INSTANCE = new RaiseVariableName(); + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.query.optimize.OptimizerRule#execute(org.modeshape.graph.query.QueryContext, + * org.modeshape.graph.query.plan.PlanNode, java.util.LinkedList) + */ + public PlanNode execute( QueryContext context, + PlanNode plan, + LinkedList ruleStack ) { + for (PlanNode depQuery : plan.findAllAtOrBelow(Traversal.PRE_ORDER, Type.DEPENDENT_QUERY)) { + // Check the left ... + PlanNode left = depQuery.getFirstChild(); + raiseVariableName(left); + + // Check the right ... + PlanNode right = depQuery.getLastChild(); + raiseVariableName(right); + } + return plan; + } + + protected void raiseVariableName( PlanNode node ) { + if (node.getType() != Type.DEPENDENT_QUERY) { + String variableName = removeVariableName(node); + if (variableName != null) { + node.setProperty(Property.VARIABLE_NAME, variableName); + } + } + } + + protected String removeVariableName( PlanNode node ) { + if (node == null) return null; + String variableName = node.getProperty(Property.VARIABLE_NAME, String.class); + if (variableName != null) { + node.removeProperty(Property.VARIABLE_NAME); + return variableName; + } + // Look for it in the left side ... + return removeVariableName(node.getFirstChild()); + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/query/optimize/RuleBasedOptimizer.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/optimize/RuleBasedOptimizer.java (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/optimize/RuleBasedOptimizer.java (working copy) @@ -69,6 +69,9 @@ public class RuleBasedOptimizer implements Optimizer { */ protected void populateRuleStack( LinkedList ruleStack, PlanHints hints ) { + if (hints.hasSubqueries) { + ruleStack.addFirst(RaiseVariableName.INSTANCE); + } ruleStack.addFirst(RewriteAsRangeCriteria.INSTANCE); if (hints.hasJoin) { ruleStack.addFirst(ChooseJoinAlgorithm.USE_ONLY_NESTED_JOIN_ALGORITHM); Index: modeshape-graph/src/main/java/org/modeshape/graph/query/parse/SqlQueryParser.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/parse/SqlQueryParser.java (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/parse/SqlQueryParser.java (working copy) @@ -85,6 +85,7 @@ import org.modeshape.graph.query.model.SetCriteria; import org.modeshape.graph.query.model.SetQuery; import org.modeshape.graph.query.model.Source; import org.modeshape.graph.query.model.StaticOperand; +import org.modeshape.graph.query.model.Subquery; import org.modeshape.graph.query.model.TypeSystem; import org.modeshape.graph.query.model.UpperCase; import org.modeshape.graph.query.model.FullTextSearch.Term; @@ -511,6 +512,9 @@ public class SqlQueryParser implements QueryParser { while (tokens.hasNext()) { if (tokens.matchesAnyOf("UNION", "INTERSECT", "EXCEPT")) { command = parseSetQuery(tokens, command, typeSystem); + } else if (tokens.matches(')')) { + // There's more in this token stream, but we'll stop reading ... + break; } else { Position pos = tokens.previousPosition(); String msg = GraphI18n.unexpectedToken.text(tokens.consume(), pos.getLine(), pos.getColumn()); @@ -921,6 +925,17 @@ public class SqlQueryParser implements QueryParser { } return bindVariableName(value); } + if (tokens.canConsume('(')) { + // Sometimes the subqueries are wrapped with parentheses ... + StaticOperand result = parseStaticOperand(tokens, typeSystem); + tokens.consume(')'); + return result; + } + if (tokens.matches("SELECT")) { + // This is a subquery. This object is stateless, so we can reuse this object ... + QueryCommand subqueryExpression = parseQueryCommand(tokens, typeSystem); + return new Subquery(subqueryExpression); + } return parseLiteral(tokens, typeSystem); } Index: modeshape-graph/src/main/java/org/modeshape/graph/query/plan/CanonicalPlanner.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/plan/CanonicalPlanner.java (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/plan/CanonicalPlanner.java (working copy) @@ -24,6 +24,7 @@ package org.modeshape.graph.query.plan; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -46,7 +47,10 @@ import org.modeshape.graph.query.model.Selector; import org.modeshape.graph.query.model.SelectorName; import org.modeshape.graph.query.model.SetQuery; import org.modeshape.graph.query.model.Source; +import org.modeshape.graph.query.model.Subquery; +import org.modeshape.graph.query.model.Visitable; import org.modeshape.graph.query.model.Visitors; +import org.modeshape.graph.query.model.Visitors.WalkAllVisitor; import org.modeshape.graph.query.plan.PlanNode.Property; import org.modeshape.graph.query.plan.PlanNode.Type; import org.modeshape.graph.query.validate.Schemata; @@ -125,7 +129,8 @@ public class CanonicalPlanner implements Planner { plan = createPlanNode(context, query.source(), usedSources); // Attach criteria (on top) ... - plan = attachCriteria(context, plan, query.constraint()); + Map subqueriesByVariableName = new HashMap(); + plan = attachCriteria(context, plan, query.constraint(), subqueriesByVariableName); // Attach groupbys (on top) ... // plan = attachGrouping(context,plan,query.getGroupBy()); @@ -142,9 +147,18 @@ public class CanonicalPlanner implements Planner { plan = attachSorting(context, plan, query.orderings()); plan = attachLimits(context, plan, query.limits()); + // Now add in the subqueries as dependent joins, in reverse order ... + plan = attachSubqueries(context, plan, subqueriesByVariableName); + // Validate that all the parts of the query are resolvable ... validate(context, query, usedSources); + // Now we need to validate all of the subqueries ... + for (Subquery subquery : Visitors.subqueries(query, false)) { + // Just do it by creating a plan, even though we aren't doing anything with these plans ... + createPlan(context, subquery.query()); + } + return plan; } @@ -158,8 +172,17 @@ public class CanonicalPlanner implements Planner { protected void validate( QueryContext context, QueryCommand query, Map usedSelectors ) { - // Resolve everything ... - Visitors.visitAll(query, new Validator(context, usedSelectors)); + // // Resolve everything ... + // Visitors.visitAll(query, new Validator(context, usedSelectors)); + // Resolve everything (except subqueries) ... + Validator validator = new Validator(context, usedSelectors); + query.accept(new WalkAllVisitor(validator) { + @Override + protected void enqueue( Visitable objectToBeVisited ) { + if (objectToBeVisited instanceof Subquery) return; + super.enqueue(objectToBeVisited); + } + }); } /** @@ -261,11 +284,13 @@ public class CanonicalPlanner implements Planner { * @param context the context in which the query is being planned * @param plan the existing plan, which joins all source groups * @param constraint the criteria or constraint from the query + * @param subqueriesByVariableName the subqueries by variable name * @return the updated plan, or the existing plan if there were no constraints; never null */ protected PlanNode attachCriteria( final QueryContext context, PlanNode plan, - Constraint constraint ) { + Constraint constraint, + Map subqueriesByVariableName ) { if (constraint == null) return plan; context.getHints().hasCriteria = true; @@ -278,6 +303,10 @@ public class CanonicalPlanner implements Planner { // Do this in reverse order so that the top-most SELECT node corresponds to the first constraint. while (!andableConstraints.isEmpty()) { Constraint criteria = andableConstraints.removeLast(); + + // Replace any subqueries with bind variables ... + criteria = PlanUtil.replaceSubqueriesWithBindVariables(context, criteria, subqueriesByVariableName); + // Create the select node ... PlanNode criteriaNode = new PlanNode(Type.SELECT); criteriaNode.setProperty(Property.SELECT_CRITERIA, criteria); @@ -285,7 +314,7 @@ public class CanonicalPlanner implements Planner { // Add selectors to the criteria node ... criteriaNode.addSelectors(Visitors.getSelectorsReferencedBy(criteria)); - // Is a full-text search of some kind included ... + // Is there at least one full-text search or subquery ... Visitors.visitAll(criteria, new Visitors.AbstractVisitor() { @Override public void visit( FullTextSearch obj ) { @@ -296,6 +325,11 @@ public class CanonicalPlanner implements Planner { criteriaNode.addFirstChild(plan); plan = criteriaNode; } + + if (!subqueriesByVariableName.isEmpty()) { + context.getHints().hasSubqueries = true; + + } return plan; } @@ -471,4 +505,45 @@ public class CanonicalPlanner implements Planner { return dupNode; } + /** + * Attach plan nodes for each subquery, resulting with the first subquery at the top of the plan tree. + * + * @param context the context in which the query is being planned + * @param plan the existing plan + * @param subqueriesByVariableName the queries by the variable name used in substitution + * @return the updated plan, or the existing plan if there were no limits + */ + protected PlanNode attachSubqueries( QueryContext context, + PlanNode plan, + Map subqueriesByVariableName ) { + // Order the variable names in reverse order ... + List varNames = new ArrayList(subqueriesByVariableName.keySet()); + Collections.sort(varNames); + Collections.reverse(varNames); + + for (String varName : varNames) { + Subquery subquery = subqueriesByVariableName.get(varName); + // Plan out the subquery ... + PlanNode subqueryNode = createPlan(context, subquery.query()); + setSubqueryVariableName(subqueryNode, varName); + + // Create a DEPENDENT_QUERY node, with the subquery on the LHS (so it is executed first) ... + PlanNode depQuery = new PlanNode(Type.DEPENDENT_QUERY); + depQuery.addChildren(subqueryNode, plan); + depQuery.addSelectors(subqueryNode.getSelectors()); + depQuery.addSelectors(plan.getSelectors()); + plan = depQuery; + } + return plan; + } + + protected void setSubqueryVariableName( PlanNode subqueryPlan, + String varName ) { + if (subqueryPlan.getType() != Type.DEPENDENT_QUERY) { + subqueryPlan.setProperty(Property.VARIABLE_NAME, varName); + return; + } + // Otherwise, this is a dependent query, and our subquery should be on the right (last child) ... + setSubqueryVariableName(subqueryPlan.getLastChild(), varName); + } } Index: modeshape-graph/src/main/java/org/modeshape/graph/query/plan/PlanHints.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/plan/PlanHints.java (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/plan/PlanHints.java (working copy) @@ -59,6 +59,8 @@ public final class PlanHints implements Serializable, Cloneable { public boolean hasFullTextSearch = false; + public boolean hasSubqueries = false; + /** Flag indicates that the plan has at least one view somewhere */ public boolean hasView = false; @@ -82,6 +84,7 @@ public final class PlanHints implements Serializable, Cloneable { sb.append(", hasLimit=").append(hasLimit); sb.append(", hasOptionalJoin=").append(hasOptionalJoin); sb.append(", hasFullTextSearch=").append(hasFullTextSearch); + sb.append(", hasSubqueries=").append(hasSubqueries); sb.append(", showPlan=").append(showPlan); sb.append(", validateColumnExistance=").append(validateColumnExistance); sb.append('}'); Index: modeshape-graph/src/main/java/org/modeshape/graph/query/plan/PlanNode.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/plan/PlanNode.java (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/plan/PlanNode.java (working copy) @@ -83,7 +83,9 @@ public final class PlanNode implements Iterable, Readable, Cloneable, /** A node that limits the number of tuples returned */ LIMIT("Limit"), /** A node the performs set operations on two sets of tuples, including UNION */ - SET_OPERATION("SetOperation"); + SET_OPERATION("SetOperation"), + /** A node that contains two nodes, where the left side must be done before the right */ + DEPENDENT_QUERY("DependentQuery"); private static final Map TYPE_BY_SYMBOL; static { @@ -198,7 +200,10 @@ public final class PlanNode implements Iterable, Readable, Cloneable, * For ACESS nodes, this signifies that the node will never return results. Value is a {@link Boolean} object, though the * mere presence of this property signifies that it is no longer needed. */ - ACCESS_NO_RESULTS + ACCESS_NO_RESULTS, + + /** For dependenty queries, defines the variable where the results will be placed. */ + VARIABLE_NAME } private Type type; @@ -1152,7 +1157,7 @@ public final class PlanNode implements Iterable, Readable, Cloneable, } /** - * Find all of the nodes of the specified type that are at or below this node. + * Find all of the nodes of the specified type that are at or below this node, using pre-order traversal. * * @param typeToFind the type of node to find; may not be null * @return the collection of nodes that are at or below this node that all have the supplied type; never null but possibly Index: modeshape-graph/src/main/java/org/modeshape/graph/query/plan/PlanUtil.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/plan/PlanUtil.java (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/plan/PlanUtil.java (working copy) @@ -39,6 +39,7 @@ import org.modeshape.graph.query.model.And; import org.modeshape.graph.query.model.ArithmeticOperand; import org.modeshape.graph.query.model.ArithmeticOperator; import org.modeshape.graph.query.model.Between; +import org.modeshape.graph.query.model.BindVariableName; import org.modeshape.graph.query.model.ChildNode; import org.modeshape.graph.query.model.ChildNodeJoinCondition; import org.modeshape.graph.query.model.Column; @@ -68,6 +69,7 @@ import org.modeshape.graph.query.model.SameNodeJoinCondition; import org.modeshape.graph.query.model.SelectorName; import org.modeshape.graph.query.model.SetCriteria; import org.modeshape.graph.query.model.StaticOperand; +import org.modeshape.graph.query.model.Subquery; import org.modeshape.graph.query.model.UpperCase; import org.modeshape.graph.query.model.Visitors; import org.modeshape.graph.query.model.Visitors.AbstractVisitor; @@ -416,6 +418,7 @@ public class PlanUtil { case DUP_REMOVE: case LIMIT: case NULL: + case DEPENDENT_QUERY: case ACCESS: // None of these have to be changed ... break; @@ -758,6 +761,7 @@ public class PlanUtil { case GROUP: // Don't yet use GROUP BY case SET_OPERATION: + case DEPENDENT_QUERY: case DUP_REMOVE: case LIMIT: case NULL: @@ -1155,4 +1159,97 @@ public class PlanUtil { } } + public static Constraint replaceSubqueriesWithBindVariables( QueryContext context, + Constraint constraint, + Map subqueriesByVariableName ) { + if (constraint instanceof And) { + And and = (And)constraint; + Constraint left = replaceSubqueriesWithBindVariables(context, and.left(), subqueriesByVariableName); + Constraint right = replaceSubqueriesWithBindVariables(context, and.right(), subqueriesByVariableName); + if (left == and.left() && right == and.right()) return and; + return new And(left, right); + } + if (constraint instanceof Or) { + Or or = (Or)constraint; + Constraint left = replaceSubqueriesWithBindVariables(context, or.left(), subqueriesByVariableName); + Constraint right = replaceSubqueriesWithBindVariables(context, or.right(), subqueriesByVariableName); + if (left == or.left() && right == or.right()) return or; + return new Or(left, right); + } + if (constraint instanceof Not) { + Not not = (Not)constraint; + Constraint wrapped = replaceSubqueriesWithBindVariables(context, not.constraint(), subqueriesByVariableName); + if (wrapped == not.constraint()) return not; + return new Not(wrapped); + } + if (constraint instanceof SameNode) { + return constraint; + } + if (constraint instanceof ChildNode) { + return constraint; + } + if (constraint instanceof DescendantNode) { + return constraint; + } + if (constraint instanceof PropertyExistence) { + return constraint; + } + if (constraint instanceof FullTextSearch) { + return constraint; + } + if (constraint instanceof Between) { + Between between = (Between)constraint; + DynamicOperand lhs = between.operand(); + StaticOperand lower = between.lowerBound(); // Current only a literal; therefore, no reference to selector + StaticOperand upper = between.upperBound(); // Current only a literal; therefore, no reference to selector + StaticOperand newLower = replaceSubqueriesWithBindVariables(context, lower, subqueriesByVariableName); + StaticOperand newUpper = replaceSubqueriesWithBindVariables(context, upper, subqueriesByVariableName); + if (lower == newLower && upper == newUpper) return between; + return new Between(lhs, newLower, newUpper, between.isLowerBoundIncluded(), between.isUpperBoundIncluded()); + } + if (constraint instanceof Comparison) { + Comparison comparison = (Comparison)constraint; + DynamicOperand lhs = comparison.operand1(); + StaticOperand rhs = comparison.operand2(); // Current only a literal; therefore, no reference to selector + StaticOperand newRhs = replaceSubqueriesWithBindVariables(context, rhs, subqueriesByVariableName); + if (rhs == newRhs) return comparison; + return new Comparison(lhs, comparison.operator(), newRhs); + } + if (constraint instanceof SetCriteria) { + SetCriteria criteria = (SetCriteria)constraint; + DynamicOperand lhs = criteria.leftOperand(); + boolean foundSubquery = false; + List newStaticOperands = new LinkedList(); + for (StaticOperand rhs : criteria.rightOperands()) { + StaticOperand newRhs = replaceSubqueriesWithBindVariables(context, rhs, subqueriesByVariableName); + newStaticOperands.add(newRhs); + if (rhs != newRhs) { + foundSubquery = true; + } + } + if (!foundSubquery) return criteria; + return new SetCriteria(lhs, newStaticOperands); + } + return constraint; + } + + public static StaticOperand replaceSubqueriesWithBindVariables( QueryContext context, + StaticOperand staticOperand, + Map subqueriesByVariableName ) { + if (staticOperand instanceof Subquery) { + Subquery subquery = (Subquery)staticOperand; + // Create a variable name ... + int i = 1; + String variableName = "__subquery"; + while (context.getVariables().containsKey(variableName + i)) { + ++i; + } + variableName = variableName + i; + subqueriesByVariableName.put(variableName, subquery); + context.getVariables().put(variableName, null); + // Replace with a variable substitution ... + return new BindVariableName(variableName); + } + return staticOperand; + } } Index: modeshape-graph/src/main/java/org/modeshape/graph/query/process/DependentQueryComponent.java new file mode 100644 =================================================================== --- /dev/null (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/process/DependentQueryComponent.java (working copy) @@ -0,0 +1,130 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.modeshape.graph.query.process; + +import java.util.ArrayList; +import java.util.List; +import org.modeshape.graph.query.QueryContext; +import org.modeshape.graph.query.QueryResults.Columns; +import org.modeshape.graph.query.plan.PlanNode.Type; + +/** + * A {@link ProcessingComponent} that executes a {@link Type#DEPENDENT_QUERY dependent query} node by first executing the left + * component and then executing the right component. If a variable name is specified, this component will save the query results + * from the the corresponding component into the {@link QueryContext#getVariables() variables} in the {@link QueryContext}. + */ +public class DependentQueryComponent extends ProcessingComponent { + + private final ProcessingComponent left; + private final ProcessingComponent right; + private final String leftVariableName; + private final String rightVariableName; + + public DependentQueryComponent( QueryContext context, + ProcessingComponent left, + ProcessingComponent right, + String leftVariableName, + String rightVariableName ) { + super(context, right.getColumns()); + this.left = left; + this.right = right; + this.leftVariableName = leftVariableName; + this.rightVariableName = rightVariableName; + } + + /** + * Get the processing component that serves as the left side of the join. + * + * @return the left-side processing component; never null + */ + protected final ProcessingComponent left() { + return left; + } + + /** + * Get the processing component that serves as the right side of the join. + * + * @return the right-side processing component; never null + */ + protected final ProcessingComponent right() { + return right; + } + + /** + * Get the columns definition for the results from the left, independent query that is processed first. + * + * @return the left-side columns; never null + */ + protected final Columns colunnsOfIndependentQuery() { + return left.getColumns(); + } + + /** + * Get the columns definition for the results from the right component that is dependent upon the left. + * + * @return the right-side columns; never null + */ + protected final Columns colunnsOfDependentQuery() { + return right.getColumns(); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.query.process.ProcessingComponent#execute() + */ + @Override + public List execute() { + // First execute the left side ... + List leftResults = left.execute(); + if (left.getColumns().getColumnCount() > 0) { + saveResultsToVariable(leftResults, leftVariableName); + } + + // Then execute the right side ... + List rightResults = right.execute(); + if (right.getColumns().getColumnCount() > 0) { + saveResultsToVariable(rightResults, rightVariableName); + } + return rightResults; + } + + protected void saveResultsToVariable( List results, + String variableName ) { + if (results == null || results.isEmpty()) return; + if (variableName == null) return; + + // Grab the first value in each of the tuples, and set on the query context ... + List singleColumnResults = new ArrayList(results.size()); + // Make sure there is at least one column (in the first record; remaining tuples should be the same) ... + Object[] firstTuple = results.get(0); + if (firstTuple.length != 0) { + for (Object[] tuple : results) { + singleColumnResults.add(tuple[0]); + } + } + // Place the single column results into the variable ... + getContext().getVariables().put(variableName, singleColumnResults); + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/query/process/QueryProcessor.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/process/QueryProcessor.java (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/process/QueryProcessor.java (working copy) @@ -359,6 +359,24 @@ public abstract class QueryProcessor implements Processor { } } break; + case DEPENDENT_QUERY: + // Create the components under the JOIN ... + assert node.getChildCount() == 2; + leftPlan = node.getFirstChild(); + rightPlan = node.getLastChild(); + + // Define the columns for each side, taken from the supplied columns ... + leftColumns = createColumnsFor(leftPlan, columns); + rightColumns = createColumnsFor(rightPlan, columns); + + left = createComponent(originalQuery, context, leftPlan, leftColumns, analyzer); + right = createComponent(originalQuery, context, rightPlan, rightColumns, analyzer); + + // Look for a variable name on the left and right plans ... + String leftVariableName = leftPlan.getProperty(Property.VARIABLE_NAME, String.class); + String rightVariableName = rightPlan.getProperty(Property.VARIABLE_NAME, String.class); + component = new DependentQueryComponent(context, left, right, leftVariableName, rightVariableName); + break; case SOURCE: assert false : "Source nodes should always be below ACCESS nodes by the time a plan is executed"; throw new UnsupportedOperationException(); Index: modeshape-graph/src/main/java/org/modeshape/graph/query/validate/Validator.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/query/validate/Validator.java (revision 2216) +++ modeshape-graph/src/main/java/org/modeshape/graph/query/validate/Validator.java (working copy) @@ -54,6 +54,7 @@ import org.modeshape.graph.query.model.ReferenceValue; import org.modeshape.graph.query.model.SameNode; import org.modeshape.graph.query.model.SameNodeJoinCondition; import org.modeshape.graph.query.model.SelectorName; +import org.modeshape.graph.query.model.Subquery; import org.modeshape.graph.query.model.TypeSystem; import org.modeshape.graph.query.model.Visitor; import org.modeshape.graph.query.model.Visitors.AbstractVisitor; @@ -384,6 +385,16 @@ public class Validator extends AbstractVisitor { /** * {@inheritDoc} * + * @see org.modeshape.graph.query.model.Visitors.AbstractVisitor#visit(org.modeshape.graph.query.model.Subquery) + */ + @Override + public void visit( Subquery subquery ) { + // Don't validate subqueries; this is done as a separate step ... + } + + /** + * {@inheritDoc} + * * @see org.modeshape.graph.query.model.Visitors.AbstractVisitor#visit(org.modeshape.graph.query.model.SameNode) */ @Override Index: modeshape-graph/src/test/java/org/modeshape/graph/query/optimize/RuleBasedOptimizerTest.java =================================================================== --- modeshape-graph/src/test/java/org/modeshape/graph/query/optimize/RuleBasedOptimizerTest.java (revision 2216) +++ modeshape-graph/src/test/java/org/modeshape/graph/query/optimize/RuleBasedOptimizerTest.java (working copy) @@ -31,6 +31,7 @@ import java.util.LinkedList; import java.util.List; import org.junit.Before; import org.junit.Test; +import org.modeshape.common.FixFor; import org.modeshape.common.collection.Problems; import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.GraphI18n; @@ -38,6 +39,7 @@ import org.modeshape.graph.query.AbstractQueryTest; import org.modeshape.graph.query.QueryContext; import org.modeshape.graph.query.model.ArithmeticOperand; import org.modeshape.graph.query.model.ArithmeticOperator; +import org.modeshape.graph.query.model.BindVariableName; import org.modeshape.graph.query.model.Column; import org.modeshape.graph.query.model.Comparison; import org.modeshape.graph.query.model.DynamicOperand; @@ -279,6 +281,133 @@ public class RuleBasedOptimizerTest extends AbstractQueryTest { assertPlanMatches(expected); } + @FixFor( "MODE-869" ) + @Test + public void shouldOptimizePlanForSimpleQueryWithSubqueryInCriteria() { + node = optimize("SELECT c11, c12 FROM t1 WHERE c13 IN (SELECT c21 FROM t2 WHERE c22 < CAST('3' AS LONG))"); + // Create the expected plan ... + PlanNode expected = new PlanNode(Type.DEPENDENT_QUERY, selector("t1"), selector("t2")); + + PlanNode subquery = new PlanNode(Type.ACCESS, expected, selector("t2")); + subquery.setProperty(Property.VARIABLE_NAME, "__subquery1"); + PlanNode project2 = new PlanNode(Type.PROJECT, subquery, selector("t2")); + project2.setProperty(Property.PROJECT_COLUMNS, columns(column("t2", "c21"))); + PlanNode select2 = new PlanNode(Type.SELECT, project2, selector("t2")); + select2.setProperty(Property.SELECT_CRITERIA, new Comparison(new PropertyValue(selector("t2"), "c22"), + Operator.LESS_THAN, new Literal(3L))); + PlanNode source2 = new PlanNode(Type.SOURCE, select2, selector("t2")); + source2.setProperty(Property.SOURCE_NAME, selector("t2")); + source2.setProperty(Property.SOURCE_COLUMNS, context.getSchemata().getTable(selector("t2")).getColumns()); + + PlanNode mainQuery = new PlanNode(Type.ACCESS, expected, selector("t1")); + PlanNode project = new PlanNode(Type.PROJECT, mainQuery, selector("t1")); + project.setProperty(Property.PROJECT_COLUMNS, columns(column("t1", "c11"), column("t1", "c12"))); + PlanNode select = new PlanNode(Type.SELECT, project, selector("t1")); + select.setProperty(Property.SELECT_CRITERIA, new SetCriteria(new PropertyValue(selector("t1"), "c13"), + new BindVariableName("__subquery1"))); + PlanNode source = new PlanNode(Type.SOURCE, select, selector("t1")); + source.setProperty(Property.SOURCE_NAME, selector("t1")); + source.setProperty(Property.SOURCE_COLUMNS, context.getSchemata().getTable(selector("t1")).getColumns()); + + // Compare the expected and actual plan ... + assertPlanMatches(expected); + } + + @FixFor( "MODE-869" ) + @Test + public void shouldOptimizePlanForSimpleQueryWithMultipleSubqueriesInCriteria() { + node = optimize("SELECT c11, c12 FROM t1 WHERE c13 IN (SELECT c21 FROM t2 WHERE c22 < CAST('3' AS LONG)) AND c12 = (SELECT c22 FROM t2 WHERE c23 = 'extra')"); + // Create the expected plan ... + print = true; + PlanNode expected = new PlanNode(Type.DEPENDENT_QUERY, selector("t1"), selector("t2")); + + PlanNode subquery1 = new PlanNode(Type.ACCESS, expected, selector("t2")); + subquery1.setProperty(Property.VARIABLE_NAME, "__subquery1"); + PlanNode project1 = new PlanNode(Type.PROJECT, subquery1, selector("t2")); + project1.setProperty(Property.PROJECT_COLUMNS, columns(column("t2", "c22"))); + PlanNode select1 = new PlanNode(Type.SELECT, project1, selector("t2")); + select1.setProperty(Property.SELECT_CRITERIA, new Comparison(new PropertyValue(selector("t2"), "c23"), Operator.EQUAL_TO, + new Literal("extra"))); + PlanNode source1 = new PlanNode(Type.SOURCE, select1, selector("t2")); + source1.setProperty(Property.SOURCE_NAME, selector("t2")); + source1.setProperty(Property.SOURCE_COLUMNS, context.getSchemata().getTable(selector("t2")).getColumns()); + + PlanNode depQuery2 = new PlanNode(Type.DEPENDENT_QUERY, expected, selector("t1"), selector("t2")); + + PlanNode subquery2 = new PlanNode(Type.ACCESS, depQuery2, selector("t2")); + subquery2.setProperty(Property.VARIABLE_NAME, "__subquery2"); + PlanNode project2 = new PlanNode(Type.PROJECT, subquery2, selector("t2")); + project2.setProperty(Property.PROJECT_COLUMNS, columns(column("t2", "c21"))); + PlanNode select2 = new PlanNode(Type.SELECT, project2, selector("t2")); + select2.setProperty(Property.SELECT_CRITERIA, new Comparison(new PropertyValue(selector("t2"), "c22"), + Operator.LESS_THAN, new Literal(3L))); + PlanNode source2 = new PlanNode(Type.SOURCE, select2, selector("t2")); + source2.setProperty(Property.SOURCE_NAME, selector("t2")); + source2.setProperty(Property.SOURCE_COLUMNS, context.getSchemata().getTable(selector("t2")).getColumns()); + + PlanNode mainQuery = new PlanNode(Type.ACCESS, depQuery2, selector("t1")); + PlanNode project = new PlanNode(Type.PROJECT, mainQuery, selector("t1")); + project.setProperty(Property.PROJECT_COLUMNS, columns(column("t1", "c11"), column("t1", "c12"))); + PlanNode firstSelect = new PlanNode(Type.SELECT, project, selector("t1")); + firstSelect.setProperty(Property.SELECT_CRITERIA, new SetCriteria(new PropertyValue(selector("t1"), "c13"), + new BindVariableName("__subquery2"))); + PlanNode secondSelect = new PlanNode(Type.SELECT, firstSelect, selector("t1")); + secondSelect.setProperty(Property.SELECT_CRITERIA, new Comparison(new PropertyValue(selector("t1"), "c12"), + Operator.EQUAL_TO, new BindVariableName("__subquery1"))); + PlanNode source = new PlanNode(Type.SOURCE, secondSelect, selector("t1")); + source.setProperty(Property.SOURCE_NAME, selector("t1")); + source.setProperty(Property.SOURCE_COLUMNS, context.getSchemata().getTable(selector("t1")).getColumns()); + + // Compare the expected and actual plan ... + assertPlanMatches(expected); + } + + @FixFor( "MODE-869" ) + @Test + public void shouldOptimizePlanForSimpleQueryWithNestedSubqueriesInCriteria() { + node = optimize("SELECT c11, c12 FROM t1 WHERE c13 IN (SELECT c21 FROM t2 WHERE c22 < (SELECT c22 FROM t2 WHERE c23 = 'extra'))"); + // Create the expected plan ... + print = true; + PlanNode expected = new PlanNode(Type.DEPENDENT_QUERY, selector("t1"), selector("t2")); + + PlanNode depQuery2 = new PlanNode(Type.DEPENDENT_QUERY, expected, selector("t2")); + + PlanNode subquery2 = new PlanNode(Type.ACCESS, depQuery2, selector("t2")); + subquery2.setProperty(Property.VARIABLE_NAME, "__subquery2"); + PlanNode project2 = new PlanNode(Type.PROJECT, subquery2, selector("t2")); + project2.setProperty(Property.PROJECT_COLUMNS, columns(column("t2", "c22"))); + PlanNode select2 = new PlanNode(Type.SELECT, project2, selector("t2")); + select2.setProperty(Property.SELECT_CRITERIA, new Comparison(new PropertyValue(selector("t2"), "c23"), Operator.EQUAL_TO, + new Literal("extra"))); + PlanNode source2 = new PlanNode(Type.SOURCE, select2, selector("t2")); + source2.setProperty(Property.SOURCE_NAME, selector("t2")); + source2.setProperty(Property.SOURCE_COLUMNS, context.getSchemata().getTable(selector("t2")).getColumns()); + + PlanNode subquery1 = new PlanNode(Type.ACCESS, depQuery2, selector("t2")); + subquery1.setProperty(Property.VARIABLE_NAME, "__subquery1"); + PlanNode project1 = new PlanNode(Type.PROJECT, subquery1, selector("t2")); + project1.setProperty(Property.PROJECT_COLUMNS, columns(column("t2", "c21"))); + PlanNode select1 = new PlanNode(Type.SELECT, project1, selector("t2")); + select1.setProperty(Property.SELECT_CRITERIA, new Comparison(new PropertyValue(selector("t2"), "c22"), + Operator.LESS_THAN, new BindVariableName("__subquery2"))); + PlanNode source1 = new PlanNode(Type.SOURCE, select1, selector("t2")); + source1.setProperty(Property.SOURCE_NAME, selector("t2")); + source1.setProperty(Property.SOURCE_COLUMNS, context.getSchemata().getTable(selector("t2")).getColumns()); + + PlanNode mainQuery = new PlanNode(Type.ACCESS, expected, selector("t1")); + PlanNode project = new PlanNode(Type.PROJECT, mainQuery, selector("t1")); + project.setProperty(Property.PROJECT_COLUMNS, columns(column("t1", "c11"), column("t1", "c12"))); + PlanNode select = new PlanNode(Type.SELECT, project, selector("t1")); + select.setProperty(Property.SELECT_CRITERIA, new SetCriteria(new PropertyValue(selector("t1"), "c13"), + new BindVariableName("__subquery1"))); + PlanNode source = new PlanNode(Type.SOURCE, select, selector("t1")); + source.setProperty(Property.SOURCE_NAME, selector("t1")); + source.setProperty(Property.SOURCE_COLUMNS, context.getSchemata().getTable(selector("t1")).getColumns()); + + // Compare the expected and actual plan ... + assertPlanMatches(expected); + } + @Test public void shouldOptimizePlanForEquiJoinQuery() { node = optimize("SELECT t1.c11, t1.c12, t2.c23 FROM t1 JOIN t2 ON t1.c11 = t2.c21"); @@ -521,7 +650,6 @@ public class RuleBasedOptimizerTest extends AbstractQueryTest { @Test public void shouldOptimizePlanForQueryUsingTableAndOrderByClause() { - print = true; node = optimize("SELECT t1.c11 AS c1 FROM t1 WHERE t1.c11 = 'x' AND t1.c12 = 'y' ORDER BY t1.c11, t1.c12 DESC"); // Create the expected plan ... @@ -546,7 +674,6 @@ public class RuleBasedOptimizerTest extends AbstractQueryTest { @Test public void shouldOptimizePlanForQueryUsingTableWithAliasAndOrderByClause() { - print = true; node = optimize("SELECT X.c11 AS c1 FROM t1 AS X WHERE X.c11 = 'x' AND X.c12 = 'y' ORDER BY X.c11, X.c12 DESC"); // Create the expected plan ... @@ -572,7 +699,6 @@ public class RuleBasedOptimizerTest extends AbstractQueryTest { @Test public void shouldOptimizePlanForQueryUsingTableWithAliasAndOrderByClauseUsingAliasedColumn() { - print = true; node = optimize("SELECT X.c11 AS c1 FROM t1 AS X WHERE X.c11 = 'x' AND X.c12 = 'y' ORDER BY X.c1, X.c12 DESC"); // Create the expected plan ... @@ -598,7 +724,6 @@ public class RuleBasedOptimizerTest extends AbstractQueryTest { @Test public void shouldOptimizePlanForQueryUsingViewAndOrderByClause() { - print = true; node = optimize("SELECT v2.c11 AS c1 FROM v2 WHERE v2.c11 = 'x' AND v2.c12 = 'y' ORDER BY v2.c11, v2.c12 DESC"); // Create the expected plan ... @@ -640,7 +765,6 @@ public class RuleBasedOptimizerTest extends AbstractQueryTest { @Test public void shouldOptimizePlanForQueryUsingViewWithAliasAndOrderByClause() { - print = true; node = optimize("SELECT Q.c11 AS c1 FROM v2 AS Q WHERE Q.c11 = 'x' AND Q.c12 = 'y' ORDER BY Q.c11, Q.c12 DESC"); // Create the expected plan ... Index: modeshape-graph/src/test/java/org/modeshape/graph/query/parse/SqlQueryParserTest.java =================================================================== --- modeshape-graph/src/test/java/org/modeshape/graph/query/parse/SqlQueryParserTest.java (revision 2216) +++ modeshape-graph/src/test/java/org/modeshape/graph/query/parse/SqlQueryParserTest.java (working copy) @@ -33,6 +33,7 @@ import static org.mockito.Mockito.mock; import java.util.List; import org.junit.Before; import org.junit.Test; +import org.modeshape.common.FixFor; import org.modeshape.common.text.ParsingException; import org.modeshape.common.text.Position; import org.modeshape.common.text.TokenStream; @@ -68,11 +69,13 @@ import org.modeshape.graph.query.model.Order; import org.modeshape.graph.query.model.Ordering; import org.modeshape.graph.query.model.PropertyExistence; import org.modeshape.graph.query.model.PropertyValue; +import org.modeshape.graph.query.model.QueryCommand; import org.modeshape.graph.query.model.ReferenceValue; import org.modeshape.graph.query.model.SameNode; import org.modeshape.graph.query.model.SelectorName; import org.modeshape.graph.query.model.Source; import org.modeshape.graph.query.model.StaticOperand; +import org.modeshape.graph.query.model.Subquery; import org.modeshape.graph.query.model.TypeSystem; import org.modeshape.graph.query.model.UpperCase; import org.modeshape.graph.query.model.FullTextSearch.Conjunction; @@ -117,6 +120,17 @@ public class SqlQueryParserTest { parse("SELECT \"jcr:column1\" FROM \"dna:tableA\""); } + @FixFor( "MODE-869" ) + @Test + public void shouldParseQueriesWithSubqueries() { + parse("SELECT * FROM tableA WHERE PATH() LIKE (SELECT path FROM tableB)"); + parse("SELECT * FROM tableA WHERE PATH() LIKE (SELECT path FROM tableB) AND tableA.propX = 'foo'"); + parse("SELECT * FROM tableA WHERE PATH() LIKE (((SELECT path FROM tableB)))"); + parse("SELECT * FROM tableA WHERE PATH() LIKE (SELECT path FROM tableB WHERE prop < 2)"); + parse("SELECT * FROM tableA WHERE PATH() IN (SELECT path FROM tableB) AND tableA.propX = 'foo'"); + parse("SELECT * FROM tableA WHERE PATH() NOT IN (SELECT path FROM tableB) AND tableA.propX = 'foo'"); + } + @Test public void shouldParseQueriesSelectingFromAllTables() { parse("SELECT * FROM __AllTables__"); @@ -781,10 +795,35 @@ public class SqlQueryParserTest { assertThat(result.get(2), is((StaticOperand)literal("4"))); } + @FixFor( "MODE-869" ) + @Test + public void shouldParseInClauseContainingSubqueryWithNoCriteria() { + List result = parser.parseInClause(tokens("IN (SELECT * FROM tableA)"), typeSystem); + assertThat(result.size(), is(1)); + assertThat(result.get(0), is((StaticOperand)subquery("SELECT * FROM tableA"))); + } + + @FixFor( "MODE-869" ) + @Test + public void shouldParseInClauseContainingSubqueryWithNestedCriteriaAndParentheses() { + String expression = "SELECT * FROM tableA WHERE (foo < 3 AND (bar = 22))"; + List result = parser.parseInClause(tokens("IN (" + expression + ")"), typeSystem); + assertThat(result.size(), is(1)); + assertThat(result.get(0), is((StaticOperand)subquery(expression))); + } + protected Literal literal( Object literalValue ) { return new Literal(literalValue); } + protected QueryCommand query( String subquery ) { + return parser.parseQuery(subquery, typeSystem); + } + + protected Subquery subquery( String subquery ) { + return new Subquery(query(subquery)); + } + // ---------------------------------------------------------------------------------------------------------------- // parseFullTextSearchExpression // ---------------------------------------------------------------------------------------------------------------- @@ -1002,6 +1041,28 @@ public class SqlQueryParserTest { assertThat((Double)literal.value(), is(typeSystem.getDoubleFactory().create("123"))); } + @FixFor( "MODE-869" ) + @Test + public void shouldParseStaticOperandWithSubquery() { + QueryCommand expected = parser.parseQuery(tokens("SELECT * FROM tableA"), typeSystem); + StaticOperand operand = parser.parseStaticOperand(tokens("SELECT * FROM tableA"), typeSystem); + assertThat(operand, is(instanceOf(Subquery.class))); + Subquery subquery = (Subquery)operand; + assertThat(subquery.query(), is(expected)); + } + + @FixFor( "MODE-869" ) + @Test + public void shouldParseStaticOperandWithSubqueryWithoutConsumingExtraTokens() { + QueryCommand expected = parser.parseQuery(tokens("SELECT * FROM tableA"), typeSystem); + TokenStream tokens = tokens("SELECT * FROM tableA)"); + StaticOperand operand = parser.parseStaticOperand(tokens, typeSystem); + assertThat(operand, is(instanceOf(Subquery.class))); + Subquery subquery = (Subquery)operand; + assertThat(subquery.query(), is(expected)); + assertThat(tokens.canConsume(')'), is(true)); + } + // ---------------------------------------------------------------------------------------------------------------- // parseLiteral // ---------------------------------------------------------------------------------------------------------------- Index: modeshape-graph/src/test/java/org/modeshape/graph/query/plan/CanonicalPlannerTest.java =================================================================== --- modeshape-graph/src/test/java/org/modeshape/graph/query/plan/CanonicalPlannerTest.java (revision 2216) +++ modeshape-graph/src/test/java/org/modeshape/graph/query/plan/CanonicalPlannerTest.java (working copy) @@ -29,21 +29,33 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; +import org.junit.Before; +import org.junit.Test; +import org.modeshape.common.FixFor; import org.modeshape.common.collection.Problems; import org.modeshape.common.collection.SimpleProblems; import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.query.QueryBuilder; import org.modeshape.graph.query.QueryContext; +import org.modeshape.graph.query.model.And; +import org.modeshape.graph.query.model.BindVariableName; import org.modeshape.graph.query.model.Column; +import org.modeshape.graph.query.model.Comparison; +import org.modeshape.graph.query.model.Constraint; +import org.modeshape.graph.query.model.DynamicOperand; +import org.modeshape.graph.query.model.Literal; +import org.modeshape.graph.query.model.NodePath; +import org.modeshape.graph.query.model.Operator; +import org.modeshape.graph.query.model.PropertyValue; import org.modeshape.graph.query.model.QueryCommand; import org.modeshape.graph.query.model.SelectorName; +import org.modeshape.graph.query.model.SetCriteria; +import org.modeshape.graph.query.model.StaticOperand; import org.modeshape.graph.query.model.TypeSystem; import org.modeshape.graph.query.plan.PlanNode.Property; import org.modeshape.graph.query.plan.PlanNode.Type; import org.modeshape.graph.query.validate.ImmutableSchemata; import org.modeshape.graph.query.validate.Schemata; -import org.junit.Before; -import org.junit.Test; /** * @@ -317,4 +329,314 @@ public class CanonicalPlannerTest { assertThat(plan.getSelectors(), is(selectors("t1"))); } + @FixFor( "MODE-869" ) + @Test + public void shouldProducePlanWhenUsingSubquery() { + // Define the schemata ... + schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3").addTable("otherTable", + "columnA", + "columnB").build(); + // Define the subquery command ... + QueryCommand subquery = builder.select("columnA").from("otherTable").query(); + builder = new QueryBuilder(typeSystem); + + // Define the query command (which uses the subquery) ... + query = builder.selectStar().from("someTable").where().path("someTable").isLike(subquery).end().query(); + queryContext = new QueryContext(schemata, typeSystem, hints, problems); + plan = planner.createPlan(queryContext, query); + // print = true; + print(plan); + assertThat(problems.hasErrors(), is(false)); + assertThat(problems.isEmpty(), is(true)); + + // The top node should be the dependent query ... + assertThat(plan.getType(), is(Type.DEPENDENT_QUERY)); + assertThat(plan.getChildCount(), is(2)); + + // The first child should be the plan for the subquery ... + PlanNode subqueryPlan = plan.getFirstChild(); + assertProjectNode(subqueryPlan, "columnA"); + assertThat(subqueryPlan.getProperty(Property.VARIABLE_NAME, String.class), is("__subquery1")); + assertThat(subqueryPlan.getChildCount(), is(1)); + assertThat(subqueryPlan.getSelectors(), is(selectors("otherTable"))); + PlanNode subquerySource = subqueryPlan.getFirstChild(); + assertSourceNode(subquerySource, "otherTable", null, "columnA", "columnB"); + assertThat(subquerySource.getChildCount(), is(0)); + + // The second child should be the plan for the regular query ... + PlanNode queryPlan = plan.getLastChild(); + assertProjectNode(queryPlan, "column1", "column2", "column3"); + assertThat(queryPlan.getType(), is(PlanNode.Type.PROJECT)); + assertThat(queryPlan.getChildCount(), is(1)); + assertThat(queryPlan.getSelectors(), is(selectors("someTable"))); + PlanNode criteriaNode = queryPlan.getFirstChild(); + assertThat(criteriaNode.getType(), is(PlanNode.Type.SELECT)); + assertThat(criteriaNode.getChildCount(), is(1)); + assertThat(criteriaNode.getSelectors(), is(selectors("someTable"))); + assertThat(criteriaNode.getProperty(Property.SELECT_CRITERIA), + is((Object)like(nodePath("someTable"), var("__subquery1")))); + + PlanNode source = criteriaNode.getFirstChild(); + assertSourceNode(source, "someTable", null, "column1", "column2", "column3"); + assertThat(source.getChildCount(), is(0)); + } + + @FixFor( "MODE-869" ) + @Test + public void shouldProducePlanWhenUsingSubqueryInSubquery() { + // Define the schemata ... + schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3") + .addTable("otherTable", "columnA", "columnB") + .addTable("stillOther", "columnX", "columnY") + .build(); + // Define the innermost subquery command ... + QueryCommand subquery2 = builder.select("columnY") + .from("stillOther") + .where() + .propertyValue("stillOther", "columnX") + .isLessThan() + .cast(3) + .asLong() + .end() + .query(); + builder = new QueryBuilder(typeSystem); + + // Define the outer subquery command ... + QueryCommand subquery1 = builder.select("columnA") + .from("otherTable") + .where() + .propertyValue("otherTable", "columnB") + .isEqualTo(subquery2) + .end() + .query(); + builder = new QueryBuilder(typeSystem); + + // Define the query command (which uses the subquery) ... + query = builder.selectStar().from("someTable").where().path("someTable").isLike(subquery1).end().query(); + queryContext = new QueryContext(schemata, typeSystem, hints, problems); + plan = planner.createPlan(queryContext, query); + // print = true; + print(plan); + assertThat(problems.hasErrors(), is(false)); + assertThat(problems.isEmpty(), is(true)); + + // The top node should be the dependent query ... + assertThat(plan.getType(), is(Type.DEPENDENT_QUERY)); + assertThat(plan.getChildCount(), is(2)); + + // The first child of the top node should be a dependent query ... + PlanNode depQuery1 = plan.getFirstChild(); + assertThat(depQuery1.getType(), is(PlanNode.Type.DEPENDENT_QUERY)); + assertThat(depQuery1.getChildCount(), is(2)); + + // The first child should be the plan for the 2nd subquery (since it has to be executed first) ... + PlanNode subqueryPlan2 = depQuery1.getFirstChild(); + assertProjectNode(subqueryPlan2, "columnY"); + assertThat(subqueryPlan2.getProperty(Property.VARIABLE_NAME, String.class), is("__subquery2")); + assertThat(subqueryPlan2.getChildCount(), is(1)); + assertThat(subqueryPlan2.getSelectors(), is(selectors("stillOther"))); + PlanNode criteriaNode2 = subqueryPlan2.getFirstChild(); + assertThat(criteriaNode2.getType(), is(PlanNode.Type.SELECT)); + assertThat(criteriaNode2.getChildCount(), is(1)); + assertThat(criteriaNode2.getSelectors(), is(selectors("stillOther"))); + assertThat(criteriaNode2.getProperty(Property.SELECT_CRITERIA), is((Object)lessThan(property("stillOther", "columnX"), + literal(3L)))); + PlanNode subquerySource2 = criteriaNode2.getFirstChild(); + assertSourceNode(subquerySource2, "stillOther", null, "columnX", "columnY"); + assertThat(subquerySource2.getChildCount(), is(0)); + + // The second child of the dependent query should be the plan for the subquery ... + PlanNode subqueryPlan1 = depQuery1.getLastChild(); + assertProjectNode(subqueryPlan1, "columnA"); + assertThat(subqueryPlan1.getProperty(Property.VARIABLE_NAME, String.class), is("__subquery1")); + assertThat(subqueryPlan1.getChildCount(), is(1)); + assertThat(subqueryPlan1.getSelectors(), is(selectors("otherTable"))); + PlanNode criteriaNode1 = subqueryPlan1.getFirstChild(); + assertThat(criteriaNode1.getType(), is(PlanNode.Type.SELECT)); + assertThat(criteriaNode1.getChildCount(), is(1)); + assertThat(criteriaNode1.getSelectors(), is(selectors("otherTable"))); + assertThat(criteriaNode1.getProperty(Property.SELECT_CRITERIA), is((Object)equals(property("otherTable", "columnB"), + var("__subquery2")))); + PlanNode subquerySource1 = criteriaNode1.getFirstChild(); + assertSourceNode(subquerySource1, "otherTable", null, "columnA", "columnB"); + assertThat(subquerySource1.getChildCount(), is(0)); + + // The second child of the top node should be the plan for the regular query ... + PlanNode queryPlan = plan.getLastChild(); + assertProjectNode(queryPlan, "column1", "column2", "column3"); + assertThat(queryPlan.getType(), is(PlanNode.Type.PROJECT)); + assertThat(queryPlan.getChildCount(), is(1)); + assertThat(queryPlan.getSelectors(), is(selectors("someTable"))); + PlanNode criteriaNode = queryPlan.getFirstChild(); + assertThat(criteriaNode.getType(), is(PlanNode.Type.SELECT)); + assertThat(criteriaNode.getChildCount(), is(1)); + assertThat(criteriaNode.getSelectors(), is(selectors("someTable"))); + assertThat(criteriaNode.getProperty(Property.SELECT_CRITERIA), + is((Object)like(nodePath("someTable"), var("__subquery1")))); + + PlanNode source = criteriaNode.getFirstChild(); + assertSourceNode(source, "someTable", null, "column1", "column2", "column3"); + assertThat(source.getChildCount(), is(0)); + } + + @FixFor( "MODE-869" ) + @Test + public void shouldProducePlanWhenUsingTwoSubqueries() { + // Define the schemata ... + schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3") + .addTable("otherTable", "columnA", "columnB") + .addTable("stillOther", "columnX", "columnY") + .build(); + // Define the first subquery command ... + QueryCommand subquery1 = builder.select("columnA") + .from("otherTable") + .where() + .propertyValue("otherTable", "columnB") + .isEqualTo("winner") + .end() + .query(); + builder = new QueryBuilder(typeSystem); + + // Define the second subquery command ... + QueryCommand subquery2 = builder.select("columnY") + .from("stillOther") + .where() + .propertyValue("stillOther", "columnX") + .isLessThan() + .cast(3) + .asLong() + .end() + .query(); + builder = new QueryBuilder(typeSystem); + + // Define the query command (which uses the subquery) ... + query = builder.selectStar() + .from("someTable") + .where() + .path("someTable") + .isLike(subquery2) + .and() + .propertyValue("someTable", "column3") + .isInSubquery(subquery1) + .end() + .query(); + queryContext = new QueryContext(schemata, typeSystem, hints, problems); + plan = planner.createPlan(queryContext, query); + // print = true; + print(plan); + assertThat(problems.hasErrors(), is(false)); + assertThat(problems.isEmpty(), is(true)); + + // The top node should be the dependent query ... + assertThat(plan.getType(), is(Type.DEPENDENT_QUERY)); + assertThat(plan.getChildCount(), is(2)); + + // The first child of the top node should be the plan for subquery1 ... + PlanNode subqueryPlan1 = plan.getFirstChild(); + assertProjectNode(subqueryPlan1, "columnA"); + assertThat(subqueryPlan1.getProperty(Property.VARIABLE_NAME, String.class), is("__subquery1")); + assertThat(subqueryPlan1.getChildCount(), is(1)); + assertThat(subqueryPlan1.getSelectors(), is(selectors("otherTable"))); + PlanNode criteriaNode1 = subqueryPlan1.getFirstChild(); + assertThat(criteriaNode1.getType(), is(PlanNode.Type.SELECT)); + assertThat(criteriaNode1.getChildCount(), is(1)); + assertThat(criteriaNode1.getSelectors(), is(selectors("otherTable"))); + assertThat(criteriaNode1.getProperty(Property.SELECT_CRITERIA), is((Object)equals(property("otherTable", "columnB"), + literal("winner")))); + PlanNode subquerySource1 = criteriaNode1.getFirstChild(); + assertSourceNode(subquerySource1, "otherTable", null, "columnA", "columnB"); + assertThat(subquerySource1.getChildCount(), is(0)); + + // The second child of the top node should be a dependent query ... + PlanNode depQuery2 = plan.getLastChild(); + assertThat(depQuery2.getType(), is(PlanNode.Type.DEPENDENT_QUERY)); + assertThat(depQuery2.getChildCount(), is(2)); + + // The first child of the second dependent should be the plan for the 2nd subquery (since it has to be executed first) ... + PlanNode subqueryPlan2 = depQuery2.getFirstChild(); + assertProjectNode(subqueryPlan2, "columnY"); + assertThat(subqueryPlan2.getProperty(Property.VARIABLE_NAME, String.class), is("__subquery2")); + assertThat(subqueryPlan2.getChildCount(), is(1)); + assertThat(subqueryPlan2.getSelectors(), is(selectors("stillOther"))); + PlanNode criteriaNode2 = subqueryPlan2.getFirstChild(); + assertThat(criteriaNode2.getType(), is(PlanNode.Type.SELECT)); + assertThat(criteriaNode2.getChildCount(), is(1)); + assertThat(criteriaNode2.getSelectors(), is(selectors("stillOther"))); + assertThat(criteriaNode2.getProperty(Property.SELECT_CRITERIA), is((Object)lessThan(property("stillOther", "columnX"), + literal(3L)))); + PlanNode subquerySource2 = criteriaNode2.getFirstChild(); + assertSourceNode(subquerySource2, "stillOther", null, "columnX", "columnY"); + assertThat(subquerySource2.getChildCount(), is(0)); + + // The second child of the second dependent node should be the plan for the regular query ... + PlanNode queryPlan = depQuery2.getLastChild(); + assertProjectNode(queryPlan, "column1", "column2", "column3"); + assertThat(queryPlan.getType(), is(PlanNode.Type.PROJECT)); + assertThat(queryPlan.getChildCount(), is(1)); + assertThat(queryPlan.getSelectors(), is(selectors("someTable"))); + PlanNode criteriaNode3 = queryPlan.getFirstChild(); + assertThat(criteriaNode3.getType(), is(PlanNode.Type.SELECT)); + assertThat(criteriaNode3.getChildCount(), is(1)); + assertThat(criteriaNode3.getSelectors(), is(selectors("someTable"))); + assertThat(criteriaNode3.getProperty(Property.SELECT_CRITERIA), + is((Object)like(nodePath("someTable"), var("__subquery2")))); + PlanNode criteriaNode4 = criteriaNode3.getFirstChild(); + assertThat(criteriaNode4.getProperty(Property.SELECT_CRITERIA), is((Object)in(property("someTable", "column3"), + var("__subquery1")))); + + PlanNode source = criteriaNode4.getFirstChild(); + assertSourceNode(source, "someTable", null, "column1", "column2", "column3"); + assertThat(source.getChildCount(), is(0)); + } + + protected NodePath nodePath( String selectorName ) { + return nodePath(selector(selectorName)); + } + + protected NodePath nodePath( SelectorName selectorName ) { + return new NodePath(selectorName); + } + + protected PropertyValue property( String selectorName, + String columnName ) { + return property(selector(selectorName), columnName); + } + + protected PropertyValue property( SelectorName selectorName, + String columnName ) { + return new PropertyValue(selectorName, columnName); + } + + protected BindVariableName var( String variableName ) { + return new BindVariableName(variableName); + } + + protected Literal literal( Object value ) { + return new Literal(value); + } + + protected And and( Constraint left, + Constraint right ) { + return new And(left, right); + } + + protected Comparison like( DynamicOperand left, + StaticOperand right ) { + return new Comparison(left, Operator.LIKE, right); + } + + protected Comparison lessThan( DynamicOperand left, + StaticOperand right ) { + return new Comparison(left, Operator.LESS_THAN, right); + } + + protected Comparison equals( DynamicOperand left, + StaticOperand right ) { + return new Comparison(left, Operator.EQUAL_TO, right); + } + + protected SetCriteria in( DynamicOperand left, + StaticOperand... right ) { + return new SetCriteria(left, right); + } } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/RepositoryQueryManager.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/RepositoryQueryManager.java (revision 2216) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/RepositoryQueryManager.java (working copy) @@ -23,6 +23,14 @@ */ package org.modeshape.jcr; +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import javax.jcr.RepositoryException; +import javax.jcr.query.InvalidQueryException; import org.apache.lucene.analysis.snowball.SnowballAnalyzer; import org.apache.lucene.util.Version; import org.modeshape.common.collection.Problems; @@ -68,15 +76,6 @@ import org.modeshape.search.lucene.LuceneConfiguration; import org.modeshape.search.lucene.LuceneConfigurations; import org.modeshape.search.lucene.LuceneSearchEngine; -import javax.jcr.RepositoryException; -import javax.jcr.query.InvalidQueryException; -import java.io.File; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - /** * */ @@ -473,7 +472,6 @@ abstract class RepositoryQueryManager { super(context, columns, accessNode); accessRequest = new AccessQueryRequest(context.getWorkspaceName(), sourceName, getColumns(), andedConstraints, limit, context.getSchemata(), context.getVariables()); - context.getProcessor().process(accessRequest); } /** @@ -492,6 +490,8 @@ abstract class RepositoryQueryManager { */ @Override public List execute() { + GraphQueryContext context = (GraphQueryContext)getContext(); + context.getProcessor().process(accessRequest); if (accessRequest.getError() != null) { I18n msg = GraphI18n.errorWhilePerformingQuery; getContext().getProblems().addError(accessRequest.getError(), Index: modeshape-jcr/src/test/java/org/modeshape/jcr/JcrQueryManagerTest.java =================================================================== --- modeshape-jcr/src/test/java/org/modeshape/jcr/JcrQueryManagerTest.java (revision 2216) +++ modeshape-jcr/src/test/java/org/modeshape/jcr/JcrQueryManagerTest.java (working copy) @@ -303,7 +303,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 23); + assertResults(query, result, 24); assertResultsHaveColumns(result, "jcr:primaryType"); } @@ -313,7 +313,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 12); + assertResults(query, result, 13); assertResultsHaveColumns(result, carColumnNames()); } @@ -324,7 +324,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 12); + assertResults(query, result, 13); assertResultsHaveColumns(result, carColumnNames()); } @@ -335,7 +335,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 12); + assertResults(query, result, 13); assertResultsHaveColumns(result, carColumnNames()); // Results are sorted by lexicographic MSRP (as a string, not as a number)!!! assertRow(result, 1).has("car:model", "LR3").and("car:msrp", "$48,525").and("car:mpgCity", 12); @@ -379,7 +379,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 22); + assertResults(query, result, 23); assertResultsHaveColumns(result, "jcr:primaryType"); } @@ -390,7 +390,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 4); + assertResults(query, result, 5); assertResultsHaveColumns(result, carColumnNames()); } @@ -401,7 +401,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 4L); + assertResults(query, result, 5L); String[] expectedColumnNames = {"car:mpgCity", "car:lengthInInches", "car:maker", "car:userRating", "car:engine", "car:mpgHighway", "car:valueRating", "car.jcr:primaryType", "car:wheelbaseInInches", "car:year", "car:model", "car:msrp", "jcr:created", "jcr:createdBy", "category.jcr:primaryType"}; @@ -415,7 +415,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 12L); + assertResults(query, result, 13L); String[] expectedColumnNames = {"car:mpgCity", "car:lengthInInches", "car:maker", "car:userRating", "car:engine", "car:mpgHighway", "car:valueRating", "car.jcr:primaryType", "car:wheelbaseInInches", "car:year", "car:model", "car:msrp", "jcr:created", "jcr:createdBy", "category.jcr:primaryType"}; @@ -429,7 +429,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 4); + assertResults(query, result, 5); assertResultsHaveColumns(result, carColumnNames()); } @@ -441,7 +441,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 4L); + assertResults(query, result, 5L); String[] expectedColumnNames = {"car:mpgCity", "car:lengthInInches", "car:maker", "car:userRating", "car:engine", "car:mpgHighway", "car:valueRating", "car.jcr:primaryType", "car:wheelbaseInInches", "car:year", "car:model", "car:msrp", "jcr:created", "jcr:createdBy", "category.jcr:primaryType"}; @@ -456,7 +456,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 12L); + assertResults(query, result, 13L); assertResultsHaveColumns(result, "category.jcr:primaryType", "cars.jcr:primaryType"); } @@ -469,7 +469,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 2L); + assertResults(query, result, 3L); assertResultsHaveColumns(result, "category.jcr:primaryType", "cars.jcr:primaryType"); } @@ -496,6 +496,34 @@ public class JcrQueryManagerTest { query.execute(); } + @FixFor( "MODE-869" ) + @Test + public void shouldBeAbleToCreateAndExecuteSqlQueryWithSubqueryInCriteria() throws RepositoryException { + Query query = session.getWorkspace() + .getQueryManager() + .createQuery("SELECT * FROM [car:Car] WHERE [car:maker] IN (SELECT [car:maker] FROM [car:Car] WHERE [car:year] >= 2008)", + Query.JCR_SQL2); + assertThat(query, is(notNullValue())); + QueryResult result = query.execute(); + assertThat(result, is(notNullValue())); + assertResults(query, result, 13); // the 13 types of cars made by makers that made cars in 2008 + assertResultsHaveColumns(result, carColumnNames()); + } + + @FixFor( "MODE-869" ) + @Test + public void shouldBeAbleToCreateAndExecuteSqlQueryWithSubqueryInCriteria2() throws RepositoryException { + Query query = session.getWorkspace() + .getQueryManager() + .createQuery("SELECT * FROM [car:Car] WHERE [car:maker] IN (SELECT [car:maker] FROM [car:Car] WHERE PATH() LIKE '%/Hybrid/%')", + Query.JCR_SQL2); + assertThat(query, is(notNullValue())); + QueryResult result = query.execute(); + assertThat(result, is(notNullValue())); + assertResults(query, result, 4); // the 4 types of cars made by makers that make hybrids + assertResultsHaveColumns(result, carColumnNames()); + } + // ---------------------------------------------------------------------------------------------------------------- // JCR-SQL Queries // ---------------------------------------------------------------------------------------------------------------- @@ -510,7 +538,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 12); + assertResults(query, result, 13); assertResultsHaveColumns(result, "jcr:path", "jcr:score", "car:model"); } @@ -524,7 +552,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 12); + assertResults(query, result, 13); assertResultsHaveColumns(result, "jcr:path", "jcr:score", "car:model"); } @@ -538,7 +566,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 12); + assertResults(query, result, 13); assertResultsHaveColumns(result, "jcr:path", "jcr:score", "car:model"); } @@ -611,11 +639,11 @@ public class JcrQueryManagerTest { public void shouldBeAbleToCreateXPathQuery() throws RepositoryException { Query query = session.getWorkspace().getQueryManager().createQuery("//element(*,car:Car)", Query.XPATH); assertThat(query, is(notNullValue())); - assertResults(query, query.execute(), 12); + assertResults(query, query.execute(), 13); query = session.getWorkspace().getQueryManager().createQuery("//element(*,nt:unstructured)", Query.XPATH); assertThat(query, is(notNullValue())); - assertResults(query, query.execute(), 22); + assertResults(query, query.execute(), 23); } @SuppressWarnings( "deprecation" ) @@ -624,7 +652,7 @@ public class JcrQueryManagerTest { Query query = session.getWorkspace().getQueryManager().createQuery("//element(*,nt:base)", Query.XPATH); assertThat(query, is(notNullValue())); QueryResult result = query.execute(); - assertResults(query, result, 23); + assertResults(query, result, 24); assertResultsHaveColumns(result, "jcr:primaryType", "jcr:path", "jcr:score"); } @@ -636,7 +664,7 @@ public class JcrQueryManagerTest { .createQuery("//element(*,nt:base) order by @jcr:path", Query.XPATH); assertThat(query, is(notNullValue())); QueryResult result = query.execute(); - assertResults(query, result, 23); + assertResults(query, result, 24); assertResultsHaveColumns(result, "jcr:primaryType", "jcr:path", "jcr:score"); } @@ -648,7 +676,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); // print = true; - assertResults(query, result, 12); + assertResults(query, result, 13); assertResultsHaveColumns(result, "car:maker", "jcr:path", "jcr:score"); } @@ -658,7 +686,7 @@ public class JcrQueryManagerTest { Query query = session.getWorkspace().getQueryManager().createQuery("//element(*,nt:unstructured)", Query.XPATH); assertThat(query, is(notNullValue())); QueryResult result = query.execute(); - assertResults(query, result, 22); + assertResults(query, result, 23); assertThat(result, is(notNullValue())); assertResultsHaveColumns(result, "jcr:primaryType", "jcr:path", "jcr:score"); } @@ -670,14 +698,14 @@ public class JcrQueryManagerTest { Query query = manager.createQuery("//element(*,nt:unstructured) order by @jcr:primaryType", Query.XPATH); assertThat(query, is(notNullValue())); QueryResult result = query.execute(); - assertResults(query, result, 22); + assertResults(query, result, 23); assertThat(result, is(notNullValue())); assertResultsHaveColumns(result, "jcr:primaryType", "jcr:path", "jcr:score"); query = manager.createQuery("//element(*,car:Car) order by @car:year", Query.XPATH); assertThat(query, is(notNullValue())); result = query.execute(); - assertResults(query, result, 12); + assertResults(query, result, 13); assertThat(result, is(notNullValue())); assertResultsHaveColumns(result, "car:year", "jcr:path", "jcr:score"); } @@ -724,7 +752,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); // print = true; - assertResults(query, result, 16); + assertResults(query, result, 17); assertThat(result, is(notNullValue())); assertResultsHaveColumns(result, "jcr:primaryType", "jcr:path", "jcr:score"); } @@ -747,7 +775,7 @@ public class JcrQueryManagerTest { Query query = session.getWorkspace().getQueryManager().createQuery(" /jcr:root/Cars//*[@car:year]", Query.XPATH); assertThat(query, is(notNullValue())); QueryResult result = query.execute(); - assertResults(query, result, 12); + assertResults(query, result, 13); assertThat(result, is(notNullValue())); assertResultsHaveColumns(result, "jcr:primaryType", "jcr:path", "jcr:score"); } @@ -760,7 +788,7 @@ public class JcrQueryManagerTest { .createQuery(" /jcr:root/Cars//*[@car:year] order by @car:year ascending", Query.XPATH); assertThat(query, is(notNullValue())); QueryResult result = query.execute(); - assertResults(query, result, 12); + assertResults(query, result, 13); assertThat(result, is(notNullValue())); assertResultsHaveColumns(result, "car:year", "jcr:path", "jcr:score"); } @@ -772,7 +800,7 @@ public class JcrQueryManagerTest { Query.XPATH); assertThat(query, is(notNullValue())); QueryResult result = query.execute(); - assertResults(query, result, 22); + assertResults(query, result, 23); assertThat(result, is(notNullValue())); assertResultsHaveColumns(result, "jcr:primaryType", "jcr:path", "jcr:score"); } @@ -804,7 +832,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 12); + assertResults(query, result, 13); assertResultsHaveColumns(result, "jcr:primaryType", "jcr:path", @@ -1106,7 +1134,7 @@ public class JcrQueryManagerTest { assertThat(query, is(notNullValue())); QueryResult result = query.execute(); assertThat(result, is(notNullValue())); - assertResults(query, result, 23); + assertResults(query, result, 24); assertResultsHaveColumns(result, "jcr:primaryType", "jcr:path", "jcr:score"); } Index: modeshape-jcr/src/test/resources/io/cars-system-view.xml =================================================================== --- modeshape-jcr/src/test/resources/io/cars-system-view.xml (revision 2216) +++ modeshape-jcr/src/test/resources/io/cars-system-view.xml (working copy) @@ -145,5 +145,17 @@ 20 4.6L 260 hp V8 5-speed (heavy-duty) + + car:Car + Toyota + Land Cruiser FJ55 + 1967 + $13,255 + 5 + 3 + 7 + 9 + 3.9 L I6 F +