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

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import java.util.Collection;
import java.util.Collections;
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.jcr.ExecutionContext;
import org.modeshape.jcr.NodeTypes;
import org.modeshape.jcr.RepositoryIndexes;
import org.modeshape.jcr.api.query.qom.Operator;
import org.modeshape.jcr.cache.RepositoryCache;
import org.modeshape.jcr.query.BufferManager;
import org.modeshape.jcr.query.QueryBuilder;
import org.modeshape.jcr.query.QueryContext;
import org.modeshape.jcr.query.model.And;
import org.modeshape.jcr.query.model.BindVariableName;
import org.modeshape.jcr.query.model.Column;
import org.modeshape.jcr.query.model.Comparison;
import org.modeshape.jcr.query.model.Constraint;
import org.modeshape.jcr.query.model.DynamicOperand;
import org.modeshape.jcr.query.model.Literal;
import org.modeshape.jcr.query.model.NodePath;
import org.modeshape.jcr.query.model.PropertyValue;
import org.modeshape.jcr.query.model.QueryCommand;
import org.modeshape.jcr.query.model.SelectorName;
import org.modeshape.jcr.query.model.SetCriteria;
import org.modeshape.jcr.query.model.StaticOperand;
import org.modeshape.jcr.query.model.Subquery;
import org.modeshape.jcr.query.model.TypeSystem;
import org.modeshape.jcr.query.plan.PlanNode.Property;
import org.modeshape.jcr.query.plan.PlanNode.Type;
import org.modeshape.jcr.query.validate.ImmutableSchemata;
import org.modeshape.jcr.query.validate.Schemata;

/**
 * 
 */
public class CanonicalPlannerTest {

    private ExecutionContext executionContext;
    private CanonicalPlanner planner;
    private TypeSystem typeSystem;
    private QueryBuilder builder;
    private PlanHints hints;
    private QueryCommand query;
    private PlanNode plan;
    private Problems problems;
    private Schemata schemata;
    private RepositoryIndexes indexDefns;
    private NodeTypes nodeTypes;
    private ImmutableSchemata.Builder schemataBuilder;
    private QueryContext queryContext;
    private boolean print;
    private RepositoryCache repoCache;
    private Set<String> workspaces;
    private BufferManager bufferMgr;

    @Before
    public void beforeEach() {
        planner = new CanonicalPlanner();
        executionContext = new ExecutionContext();
        typeSystem = executionContext.getValueFactories().getTypeSystem();
        repoCache = mock(RepositoryCache.class);
        workspaces = Collections.singleton("workspace");
        hints = new PlanHints();
        builder = new QueryBuilder(typeSystem);
        problems = new SimpleProblems();
        nodeTypes = mock(NodeTypes.class);
        indexDefns = mock(RepositoryIndexes.class);
        schemataBuilder = ImmutableSchemata.createBuilder(executionContext, nodeTypes);
        bufferMgr = new BufferManager(executionContext);
        print = false;
    }

    protected void print( PlanNode plan ) {
        if (print) System.out.println(plan);
    }

    protected SelectorName selector( String name ) {
        return new SelectorName(name);
    }

    protected Set<SelectorName> selectors( String... names ) {
        Set<SelectorName> selectors = new HashSet<SelectorName>();
        for (String name : names) {
            selectors.add(selector(name));
        }
        return selectors;
    }

    @SuppressWarnings( "unchecked" )
    protected void assertProjectNode( PlanNode node,
                                      String... columnNames ) {
        assertThat(node.getType(), is(Type.PROJECT));
        if (columnNames.length != 0) {
            assertThat(node.hasCollectionProperty(Property.PROJECT_COLUMNS), is(true));
        }
        List<Column> columns = node.getProperty(Property.PROJECT_COLUMNS, List.class);
        assertThat(columns.size(), is(columnNames.length));
        for (int i = 0; i != columns.size(); ++i) {
            Column column = columns.get(i);
            assertThat(column.getColumnName(), is(columnNames[i]));
        }
    }

    @SuppressWarnings( "unchecked" )
    protected void assertSourceNode( PlanNode node,
                                     String sourceName,
                                     String sourceAlias,
                                     String... availableColumns ) {
        assertThat(node.getType(), is(Type.SOURCE));
        assertThat(node.getProperty(Property.SOURCE_NAME, SelectorName.class), is(selector(sourceName)));
        if (sourceAlias != null) {
            assertThat(node.getProperty(Property.SOURCE_ALIAS, SelectorName.class), is(selector(sourceAlias)));
        } else {
            assertThat(node.hasProperty(Property.SOURCE_ALIAS), is(false));
        }
        Collection<Schemata.Column> columns = (Collection<Schemata.Column>)node.getProperty(Property.SOURCE_COLUMNS);
        assertThat(columns.size(), is(availableColumns.length));
        int i = 0;
        for (Schemata.Column column : columns) {
            String expectedName = availableColumns[i++];
            assertThat(column.getName(), is(expectedName));
        }
    }

    protected QueryContext initQueryContext() {
        queryContext = new QueryContext(executionContext, repoCache, workspaces, schemata, indexDefns, nodeTypes, bufferMgr,
                                        hints, problems);
        return queryContext;
    }

    @Test
    public void shouldProducePlanForSelectStarFromTable() {
        schemata = schemataBuilder.addTable("__ALLNODES__", "column1", "column2", "column3").build();
        query = builder.selectStar().fromAllNodes().query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.isEmpty(), is(true));
        assertProjectNode(plan, "column1", "column2", "column3");
        assertThat(plan.getType(), is(PlanNode.Type.PROJECT));
        assertThat(plan.getChildCount(), is(1));
        PlanNode source = plan.getFirstChild();
        assertSourceNode(source, "__ALLNODES__", null, "column1", "column2", "column3");
        assertThat(source.getChildCount(), is(0));
        print(plan);
    }

    @Test
    public void shouldProduceErrorWhenSelectingNonExistantTable() {
        schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3").build();
        query = builder.selectStar().fromAllNodes().query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.hasErrors(), is(true));
    }

    @Test
    public void shouldProduceErrorWhenSelectingNonExistantColumnOnExistingTable() {
        schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3").build();
        query = builder.select("column1", "column4").from("someTable").query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.hasErrors(), is(true));
    }

    @Test
    public void shouldProducePlanWhenSelectingAllColumnsOnExistingTable() {
        schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3").build();
        query = builder.selectStar().from("someTable").query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        print(plan);
        assertThat(problems.hasErrors(), is(false));
        assertThat(problems.isEmpty(), is(true));
        assertProjectNode(plan, "column1", "column2", "column3");
        assertThat(plan.getType(), is(PlanNode.Type.PROJECT));
        assertThat(plan.getChildCount(), is(1));
        PlanNode source = plan.getFirstChild();
        assertSourceNode(source, "someTable", null, "column1", "column2", "column3");
        assertThat(source.getChildCount(), is(0));
    }

    @Test
    public void shouldProducePlanWhenSelectingColumnsFromTableWithoutAlias() {
        schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3").build();
        query = builder.select("column1", "column2").from("someTable").where().path("someTable").isEqualTo(1L).end().query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.hasErrors(), is(false));
        assertThat(plan.getType(), is(PlanNode.Type.PROJECT));
        assertThat(plan.getSelectors(), is(selectors("someTable")));
    }

    @Test
    public void shouldProducePlanWhenSelectingColumnsFromTableWithAlias() {
        schemata = schemataBuilder.addTable("dna:someTable", "column1", "column2", "column3").build();
        query = builder.select("column1", "column2").from("dna:someTable AS t1").where().path("t1").isEqualTo(1L).end().query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.hasErrors(), is(false));
        print(plan);
        assertThat(plan.getType(), is(PlanNode.Type.PROJECT));
        assertThat(plan.getSelectors(), is(selectors("t1")));
    }

    @Test
    public void shouldProducePlanWhenSelectingAllColumnsFromTableWithAlias() {
        schemata = schemataBuilder.addTable("dna:someTable", "column1", "column2", "column3").build();
        query = builder.selectStar().from("dna:someTable AS t1").where().path("t1").isEqualTo(1L).end().query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.hasErrors(), is(false));
        print(plan);
        assertThat(plan.getType(), is(PlanNode.Type.PROJECT));
        assertThat(plan.getSelectors(), is(selectors("t1")));
    }

    @Test
    public void shouldProduceErrorWhenFullTextSearchingTableWithNoSearchableColumns() {
        schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3").build();
        // Make sure the query without the search criteria does not have an error
        query = builder.select("column1", "column2").from("someTable").query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.hasErrors(), is(false));

        query = builder.select("column1", "column2").from("someTable").where().search("someTable", "term1").end().query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.hasErrors(), is(true));
    }

    @Test
    public void shouldProducePlanWhenFullTextSearchingTableWithAtLeastOneSearchableColumn() {
        schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3").makeSearchable("someTable", "column1")
                                  .build();
        query = builder.select("column1", "column4").from("someTable").where().search("someTable", "term1").end().query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.hasErrors(), is(true));
    }

    @Test
    public void shouldProduceErrorWhenFullTextSearchingColumnThatIsNotSearchable() {
        schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3").build();
        // Make sure the query without the search criteria does not have an error
        query = builder.select("column1", "column2").from("someTable").query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.hasErrors(), is(false));

        query = builder.select("column1", "column2").from("someTable").where().search("someTable", "column2", "term1").end()
                       .query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.hasErrors(), is(true));
    }

    @Test
    public void shouldProducePlanWhenFullTextSearchingColumnThatIsSearchable() {
        schemata = schemataBuilder.addTable("someTable", "column1", "column2", "column3").makeSearchable("someTable", "column1")
                                  .build();
        query = builder.select("column1", "column4").from("someTable").where().search("someTable", "column1", "term1").end()
                       .query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.hasErrors(), is(true));
    }

    @Test
    public void shouldProducePlanWhenOrderByClauseIsUsed() {
        schemata = schemataBuilder.addTable("dna:someTable", "column1", "column2", "column3").build();
        query = builder.selectStar().from("dna:someTable AS t1").where().path("t1").isEqualTo(1L).end().orderBy().ascending()
                       .propertyValue("t1", "column1").end().query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.hasErrors(), is(false));
        print(plan);
        assertThat(plan.getType(), is(PlanNode.Type.SORT));
        assertThat(plan.getSelectors(), is(selectors("t1")));
    }

    @Test
    public void shouldProducePlanWhenOrderByClauseWithScoreIsUsed() {
        schemata = schemataBuilder.addTable("dna:someTable", "column1", "column2", "column3").build();
        query = builder.selectStar().from("dna:someTable AS t1").where().path("t1").isEqualTo(1L).end().orderBy().ascending()
                       .fullTextSearchScore("t1").end().query();
        initQueryContext();
        plan = planner.createPlan(queryContext, query);
        assertThat(problems.hasErrors(), is(false));
        print(plan);
        assertThat(plan.getType(), is(PlanNode.Type.SORT));
        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();
        initQueryContext();
        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(Subquery.VARIABLE_PREFIX + "1"));
        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(Subquery.VARIABLE_PREFIX + "1"))));

        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();
        initQueryContext();
        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(Subquery.VARIABLE_PREFIX + "2"));
        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(Subquery.VARIABLE_PREFIX + "1"));
        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(Subquery.VARIABLE_PREFIX + "2"))));
        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(Subquery.VARIABLE_PREFIX + "1"))));

        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();
        initQueryContext();
        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(Subquery.VARIABLE_PREFIX + "1"));
        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(Subquery.VARIABLE_PREFIX + "2"));
        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(Subquery.VARIABLE_PREFIX + "2"))));
        PlanNode criteriaNode4 = criteriaNode3.getFirstChild();
        assertThat(criteriaNode4.getProperty(Property.SELECT_CRITERIA),
                   is((Object)in(property("someTable", "column3"), var(Subquery.VARIABLE_PREFIX + "1"))));

        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);
    }
}
