/*
 * 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;

import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_LIKE;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.hamcrest.core.IsNull.nullValue;
import static org.hamcrest.core.IsSame.sameInstance;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.NamespaceRegistry;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.nodetype.InvalidNodeTypeDefinitionException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.nodetype.NodeTypeTemplate;
import javax.jcr.query.InvalidQueryException;
import javax.jcr.query.Query;
import javax.jcr.query.QueryResult;
import javax.jcr.query.Row;
import javax.jcr.query.RowIterator;
import javax.jcr.query.qom.ChildNode;
import javax.jcr.query.qom.Column;
import javax.jcr.query.qom.Constraint;
import javax.jcr.query.qom.Join;
import javax.jcr.query.qom.Literal;
import javax.jcr.query.qom.Ordering;
import javax.jcr.query.qom.PropertyValue;
import javax.jcr.query.qom.QueryObjectModelConstants;
import javax.jcr.query.qom.Selector;
import javax.jcr.security.AccessControlList;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.Privilege;
import org.modeshape.schematic.document.Document;
import org.modeshape.schematic.document.Json;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.modeshape.common.FixFor;
import org.modeshape.common.util.StringUtil;
import org.modeshape.jcr.ValidateQuery.Predicate;
import org.modeshape.jcr.ValidateQuery.ValidationBuilder;
import org.modeshape.jcr.api.query.QueryManager;
import org.modeshape.jcr.api.query.qom.Limit;
import org.modeshape.jcr.api.query.qom.QueryObjectModelFactory;
import org.modeshape.jcr.api.query.qom.SelectQuery;
import org.modeshape.jcr.cache.CachedNode;
import org.modeshape.jcr.cache.ChildReferences;
import org.modeshape.jcr.cache.NodeCache;
import org.modeshape.jcr.cache.NodeKey;
import org.modeshape.jcr.query.JcrQueryResult;
import org.modeshape.jcr.security.SimplePrincipal;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.Path.Segment;

/**
 * This is a test suite that operates against a complete JcrRepository instance created and managed using the JcrEngine.
 * Essentially this is an integration test, but it does test lower-level functionality of the implementation of the JCR interfaces
 * related to querying. (It is simply more difficult to unit test these implementations because of the difficulty in mocking the
 * many other components to replicate the same functionality.)
 * <p>
 * Also, because queries are read-only, the engine is set up once and used for the entire set of test methods.
 * </p>
 */
public class JcrQueryManagerTest extends MultiUseAbstractTest {

    private static final String[] INDEXED_SYSTEM_NODES_PATHS = new String[] {"/jcr:system/jcr:nodeTypes",
        "/jcr:system/mode:namespaces"};

    /** The total number of nodes excluding '/jcr:system' */
    protected static final int TOTAL_NON_SYSTEM_NODE_COUNT = 25;

    /** The total number of nodes at or below '/jcr:system' */
    protected static int totalSystemNodeCount;

    /** The total number of nodes */
    protected static int totalNodeCount;

    protected static URI resourceUri( String name ) throws URISyntaxException {
        return resourceUrl(name).toURI();
    }

    protected static URL resourceUrl( String name ) {
        return JcrQueryManagerTest.class.getClassLoader().getResource(name);
    }

    protected static InputStream resourceStream( String name ) {
        return JcrQueryManagerTest.class.getClassLoader().getResourceAsStream(name);
    }

    private static String[] prefixEach( String[] original,
                                        String prefix ) {
        String[] result = new String[original.length];
        for (int i = 0; i != original.length; ++i) {
            result[i] = prefix + original[i];
        }
        return result;
    }

    private static String[] allOf( String[]... arrays ) {
        List<String> result = new ArrayList<String>();
        for (String[] array : arrays) {
            for (String value : array) {
                result.add(value);
            }
        }
        return result.toArray(new String[result.size()]);
    }

    protected static String[] carColumnNames() {
        return new String[] {"car:mpgCity", "car:lengthInInches", "car:maker", "car:userRating", "car:mpgHighway", "car:engine",
            "car:valueRating", "jcr:primaryType", "jcr:mixinTypes", "car:wheelbaseInInches", "car:model", "car:year", "car:msrp",
            "jcr:created", "jcr:createdBy", "jcr:name", "jcr:path", "jcr:score", "mode:depth", "mode:id", "mode:localName",
            "car:alternateModels"};
    }

    protected static String[] carColumnNames( String selectorName ) {
        return prefixEach(carColumnNames(), selectorName + ".");
    }

    protected static String[] allColumnNames() {
        return new String[] {"jcr:primaryType", "jcr:mixinTypes", "jcr:name", "jcr:path", "jcr:score", "mode:depth", "mode:id",
            "mode:localName"};
    }

    protected static String[] allColumnNames( String selectorName ) {
        return prefixEach(allColumnNames(), selectorName + ".");
    }

    protected static String[] typedColumnNames() {
        return new String[] {"notion:booleanProperty", "notion:booleanProperty2", "notion:stringProperty",
            "notion:booleanCreatedPropertyWithDefault", "notion:stringPropertyWithDefault",
            "notion:booleanAutoCreatedPropertyWithDefault", "notion:stringAutoCreatedPropertyWithDefault", "notion:longProperty",
            "notion:singleReference", "notion:multipleReferences", "jcr:primaryType", "jcr:mixinTypes", "jcr:name", "jcr:path",
            "jcr:score", "mode:depth", "mode:id", "mode:localName"};
    }

    protected static String[] typedColumnNames( String selectorName ) {
        return prefixEach(typedColumnNames(), selectorName + ".");
    }

    protected static String[] searchColumnNames() {
        return new String[] {"jcr:score"};
    }

    @BeforeClass
    public static void beforeAll() throws Exception {
        String configFileName = JcrQueryManagerTest.class.getSimpleName() + ".json";
        beforeAll(configFileName);
    }

    @SuppressWarnings( "deprecation" )
    protected static void beforeAll( String configFileName ) throws Exception {
        String configFilePath = "config/" + configFileName;
        InputStream configStream = JcrQueryManagerTest.class.getClassLoader().getResourceAsStream(configFilePath);
        assertThat("Unable to find configuration file '" + configFilePath, configStream, is(notNullValue()));

        Document configDoc = Json.read(configStream);
        RepositoryConfiguration config = new RepositoryConfiguration(configDoc, configFileName);
        startRepository(config);

        try {
            // Use a session to load the contents ...
            JcrSession session = repository.login();

            try {
                registerNodeTypes(session, "cnd/fincayra.cnd");
                registerNodeTypes(session, "cnd/magnolia.cnd");
                registerNodeTypes(session, "cnd/notionalTypes.cnd");
                registerNodeTypes(session, "cnd/cars.cnd");
                registerNodeTypes(session, "cnd/validType.cnd");
                registerNodeTypes(session, "cnd/mode-1900.cnd");
                registerNodeTypes(session, "cnd/noquery.cnd");

                InputStream stream = resourceStream("io/cars-system-view.xml");
                try {
                    session.getWorkspace().importXML("/", stream, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
                } catch (Throwable t) {
                    t.printStackTrace();
                } finally {
                    stream.close();
                }

                // Create a branch that contains some same-name-siblings ...
                Node other = session.getRootNode().addNode("Other", "nt:unstructured");
                Node a = other.addNode("NodeA", "nt:unstructured");
                a.addMixin("mix:referenceable");
                a.setProperty("something", "value3 quick brown fox");
                a.setProperty("somethingElse", "value2");
                a.setProperty("propA", "value1");
                Node other2 = other.addNode("NodeA", "nt:unstructured");
                other2.addMixin("mix:referenceable");
                other2.setProperty("something", "value2 quick brown cat wearing hat");
                other2.setProperty("propB", "value1");
                other2.setProperty("propC", "value2");
                Node other3 = other.addNode("NodeA", "nt:unstructured");
                other3.addMixin("mix:referenceable");
                other3.setProperty("something", new String[] {"black dog", "white dog"});
                other3.setProperty("propB", "value1");
                other3.setProperty("propC", "value3");
                Value[] refValues = new Value[2];
                refValues[0] = session.getValueFactory().createValue(other2);
                refValues[1] = session.getValueFactory().createValue(other3);
                other3.setProperty("otherNode", a);
                other3.setProperty("otherNodes", refValues);
                Node c = other.addNode("NodeC", "notion:typed");
                c.setProperty("notion:booleanProperty", true);
                c.setProperty("notion:booleanProperty2", false);
                c.setProperty("propD", "value4");
                c.setProperty("propC", "value1");
                c.setProperty("notion:singleReference", a);
                c.setProperty("notion:multipleReferences", refValues);
                Node b = session.getRootNode().addNode("NodeB", "nt:unstructured");
                b.setProperty("myUrl", "http://www.acme.com/foo/bar");
                b.setProperty("pathProperty", a.getPath());
                session.save();

                // Initialize the nodes count
                initNodesCount();

                // Prime creating a first XPath query and SQL query ...
                session.getWorkspace().getQueryManager().createQuery("//element(*,nt:unstructured)", Query.XPATH);
                session.getWorkspace().getQueryManager().createQuery("SELECT * FROM [nt:base]", Query.JCR_SQL2);
            } finally {
                session.logout();
            }

            // Prime creating the schemata ...
            repository.nodeTypeManager().getRepositorySchemata();
        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        }
    }

    private static void initNodesCount() throws RepositoryException {
        JcrSession session = repository.login();
        try {
            NodeCache systemSession = repository.createSystemSession(session.context(), true);
            totalSystemNodeCount = countAllNodesBelow(systemSession.getRootKey(), systemSession) - 1; // not root
            totalNodeCount = totalSystemNodeCount + TOTAL_NON_SYSTEM_NODE_COUNT;
        } finally {
            session.logout();
        }
    }

    private static int countAllNodesBelow( NodeKey nodeKey,
                                           NodeCache cache ) throws RepositoryException {
        CachedNode node = cache.getNode(nodeKey);
        if (node.isExcludedFromSearch(cache)) return 0;
        int result = 1;
        ChildReferences childReferences = node.getChildReferences(cache);
        for (Iterator<NodeKey> nodeKeyIterator = childReferences.getAllKeys(); nodeKeyIterator.hasNext();) {
            NodeKey childKey = nodeKeyIterator.next();
            result += countAllNodesBelow(childKey, cache);
        }
        return result;
    }

    @AfterClass
    public static void afterAll() throws Exception {
        MultiUseAbstractTest.afterAll();
    }

    @Before
    @Override
    public void beforeEach() throws Exception {
        super.beforeEach();
        print = false;
    }

    protected static void registerNodeTypes( JcrSession session,
                                             String pathToClasspathResource ) throws RepositoryException, IOException {
        URL url = resourceUrl(pathToClasspathResource);
        session.getWorkspace().getNodeTypeManager().registerNodeTypes(url, true);
    }

    @Override
    protected Name name( String name ) {
        return session.nameFactory().create(name);
    }

    protected Segment segment( String segment ) {
        return session.pathFactory().createSegment(segment);
    }

    protected List<Segment> segments( String... segments ) {
        List<Segment> result = new ArrayList<Segment>();
        for (String segment : segments) {
            result.add(segment(segment));
        }
        return result;
    }

    protected ValidationBuilder validateQuery() {
        return ValidateQuery.validateQuery().printDetail(print);
    }

    protected static class ResultPrinter {
        private final NodeIterator iter;
        private int rowNumber = 0;

        protected ResultPrinter( NodeIterator iter ) {
            this.iter = iter;
        }

        protected void printRow() throws RepositoryException {
            Node node = iter.nextNode();
            System.out.println(rowNumberStr() + " " + node.getPath());
        }

        protected void printRows() throws RepositoryException {
            while (iter.hasNext()) {
                printRow();
            }
        }

        protected String rowNumberStr() {
            return StringUtil.justifyRight("" + (++rowNumber), 4, ' ');
        }
    }

    protected static class ResultRowPrinter {
        private final RowIterator iter;
        private final String[] selectorNames;
        private int rowNumber = 0;

        protected ResultRowPrinter( RowIterator iter,
                                    String[] selectorNames ) {
            this.iter = iter;
            this.selectorNames = selectorNames;
        }

        protected void printRow() throws RepositoryException {
            Row row = iter.nextRow();
            System.out.print(rowNumberStr());
            for (String selectorName : selectorNames) {
                System.out.print(" ");
                System.out.print(row.getPath(selectorName));
            }
            System.out.println();
        }

        protected void printRows() throws RepositoryException {
            while (iter.hasNext()) {
                printRow();
            }
        }

        protected String rowNumberStr() {
            return StringUtil.justifyRight("" + (++rowNumber), 4, ' ');
        }
    }

    protected void assertResults( Query query,
                                  QueryResult result,
                                  long numberOfResults ) throws RepositoryException {
        assertThat(query, is(notNullValue()));
        assertThat(result, is(notNullValue()));
        if (print /*|| result.getNodes().getSize() != numberOfResults || result.getRows().getSize() != numberOfResults*/) {
            System.out.println();
            System.out.println(query);
            System.out.println(" plan -> " + ((org.modeshape.jcr.api.query.QueryResult)result).getPlan());
            System.out.println(result);
        }
        if (result.getSelectorNames().length == 1) {
            NodeIterator iter = result.getNodes();
            ResultPrinter resultPrinter = new ResultPrinter(iter);
            if (print || iter.getSize() != numberOfResults) {
                // print anyway since this is an error
                System.out.println();
                System.out.println(" plan -> " + ((JcrQueryResult)result).getPlan());
                System.out.println(result);
                System.out.println(query);
                resultPrinter.printRows();
            }
            assertThat(iter.getSize(), is(numberOfResults));
        } else {
            try {
                result.getNodes();
                if (!print) {
                    // print anyway since this is an error
                    System.out.println();
                    System.out.println(" plan -> " + ((JcrQueryResult)result).getPlan());
                    System.out.println(result);
                    System.out.println(query);
                }
                fail("should not be able to call this method when the query has multiple selectors");
            } catch (RepositoryException e) {
                // expected; can't call this when the query uses multiple selectors ...
            }
            RowIterator iter = result.getRows();
            ResultRowPrinter resultPrinter = new ResultRowPrinter(iter, result.getSelectorNames());
            if (print || iter.getSize() != numberOfResults) {
                // print anyway since this is an error
                System.out.println();
                System.out.println(" plan -> " + ((JcrQueryResult)result).getPlan());
                System.out.println(query);
                System.out.println(result);
                resultPrinter.printRows();
            }
            assertThat(iter.getSize(), is(numberOfResults));
        }
    }

    protected void assertResultsHaveColumns( QueryResult result,
                                             String... columnNames ) throws RepositoryException {
        List<String> expectedNames = new ArrayList<String>();
        for (String name : columnNames) {
            expectedNames.add(name);
        }
        List<String> actualNames = new ArrayList<String>();
        for (String name : result.getColumnNames()) {
            actualNames.add(name);
        }
        Collections.sort(expectedNames);
        Collections.sort(actualNames);
        assertThat(actualNames, is(expectedNames));
    }

    protected void assertResultsHaveRows( QueryResult result,
                                          String columnName,
                                          String... rowValuesAsStrings ) throws RepositoryException {
        RowIterator iter = result.getRows();
        int i = 0;
        while (iter.hasNext()) {
            Row row = iter.nextRow();
            assertThat(row.getValue(columnName).getString(), is(rowValuesAsStrings[i++]));
        }
    }

    protected void assertValueIsNonNullReference( Row row,
                                                  String refColumnName ) throws RepositoryException {
        assertValueIsReference(row, refColumnName, false);
    }

    protected void assertValueIsReference( Row row,
                                           String refColumnName,
                                           boolean mayBeNull ) throws RepositoryException {
        Value value = row.getValue(refColumnName);
        if (!mayBeNull) {
            assertThat(value, is(notNullValue()));
            String ref = value.getString();
            Node referenced = session.getNodeByIdentifier(ref);
            assertThat(referenced, is(notNullValue()));
            assertEquals(ref, referenced.getIdentifier());
        }
    }

    public interface RowResult {
        public RowResult has( String columnName,
                              String value ) throws RepositoryException;

        public RowResult has( String columnName,
                              long value ) throws RepositoryException;

        public RowResult and( String columnName,
                              String value ) throws RepositoryException;

        public RowResult and( String columnName,
                              long value ) throws RepositoryException;
    }

    public class SpecificRowResult implements RowResult {
        private final Row row;

        public SpecificRowResult( Row row ) {
            this.row = row;
        }

        @Override
        public RowResult has( String columnName,
                              String value ) throws RepositoryException {
            assertThat(row.getValue(columnName).getString(), is(value));
            return this;
        }

        @Override
        public RowResult has( String columnName,
                              long value ) throws RepositoryException {
            assertThat(row.getValue(columnName).getLong(), is(value));
            return this;
        }

        @Override
        public RowResult and( String columnName,
                              String value ) throws RepositoryException {
            return has(columnName, value);
        }

        @Override
        public RowResult and( String columnName,
                              long value ) throws RepositoryException {
            return has(columnName, value);
        }
    }

    public class SomeRowResult implements RowResult {
        private final QueryResult result;
        private final List<Row> matchingRows = new LinkedList<Row>();

        public SomeRowResult( QueryResult result ) throws RepositoryException {
            this.result = result;
            for (RowIterator iterator = result.getRows(); iterator.hasNext();) {
                Row row = iterator.nextRow();
                matchingRows.add(row);
            }
        }

        @Override
        public RowResult has( String columnName,
                              long value ) throws RepositoryException {
            // Remove all the rows that don't have a matching value ...
            ListIterator<Row> iter = matchingRows.listIterator();
            while (iter.hasNext()) {
                Row row = iter.next();
                if (row.getValue(columnName).getLong() != value) iter.remove();
            }
            if (matchingRows.isEmpty()) {
                fail("Failed to find row");
            }
            return this;
        }

        @Override
        public RowResult has( String columnName,
                              String value ) throws RepositoryException {
            // Remove all the rows that don't have a matching value ...
            ListIterator<Row> iter = matchingRows.listIterator();
            while (iter.hasNext()) {
                Row row = iter.next();
                Value actualValue = row.getValue(columnName);
                if (actualValue == null && value == null) continue;
                if (actualValue != null && actualValue.getString().equals(value)) continue;
                iter.remove();
            }
            if (matchingRows.isEmpty()) {
                fail("Failed to find row");
            }
            return this;
        }

        @Override
        public RowResult and( String columnName,
                              long value ) throws RepositoryException {
            return has(columnName, value);
        }

        @Override
        public RowResult and( String columnName,
                              String value ) throws RepositoryException {
            return has(columnName, value);
        }

        @Override
        public String toString() {
            return result.toString();
        }
    }

    protected RowResult assertRow( QueryResult result ) throws RepositoryException {
        return new SomeRowResult(result);
    }

    protected RowResult assertRow( QueryResult result,
                                   int rowNumber ) throws RepositoryException {
        RowIterator rowIter = result.getRows();
        Row row = null;
        for (int i = 0; i != rowNumber; ++i) {
            row = rowIter.nextRow();
        }
        assertThat(row, is(notNullValue()));
        return new SpecificRowResult(row);
    }

    @Test
    public void shouldStartUp() {
        assertThat(session, is(notNullValue()));
    }

    @Test
    public void shouldHaveLoadedContent() throws RepositoryException {
        Node node = session.getRootNode().getNode("Cars");
        assertThat(node, is(notNullValue()));
        assertThat(node.hasNode("Sports"), is(true));
        assertThat(node.hasNode("Utility"), is(true));
        assertThat(node.hasNode("Hybrid"), is(true));
        // System.out.println(node.getNode("Hybrid").getNodes().nextNode().getPath());
        assertThat(node.hasNode("Hybrid/Toyota Prius"), is(true));
        assertThat(node.getPrimaryNodeType().getName(), is("nt:unstructured"));
    }

    @Test
    public void shouldReturnQueryManagerFromWorkspace() throws RepositoryException {
        assertThat(session.getWorkspace().getQueryManager(), is(notNullValue()));
    }

    // ----------------------------------------------------------------------------------------------------------------
    // JCR-SQL2 Queries
    // ----------------------------------------------------------------------------------------------------------------

    @Ignore
    @Test
    public void shouldFailIfGetNodesOrGetRowsIsCalledAfterGetRowsCalledOncePerQueryResult() throws RepositoryException {
        String sql = "SELECT * FROM [nt:unstructured]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        org.modeshape.jcr.api.query.QueryResult result = (org.modeshape.jcr.api.query.QueryResult)query.execute();
        assertNotNull(result.getRows());
        try {
            result.getRows();
            fail();
        } catch (RepositoryException e) {
            // expected
        }
        try {
            result.getNodes();
            fail();
        } catch (RepositoryException e) {
            // expected
        }
    }

    @Ignore
    @Test
    public void shouldFailIfGetNodesOrGetRowsIsCalledAfterGetNodesCalledOncePerQueryResult() throws RepositoryException {
        String sql = "SELECT * FROM [nt:unstructured]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        org.modeshape.jcr.api.query.QueryResult result = (org.modeshape.jcr.api.query.QueryResult)query.execute();
        assertNotNull(result.getNodes());
        try {
            result.getRows();
            fail();
        } catch (RepositoryException e) {
            // expected
        }
        try {
            result.getNodes();
            fail();
        } catch (RepositoryException e) {
            // expected
        }
    }

    @Test
    public void shouldGetNodesOrderedByPath() throws RepositoryException {
        String sql = "SELECT * FROM [nt:unstructured] WHERE ISCHILDNODE('/Cars') ORDER BY [jcr:path]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());
    }

    @FixFor("MODE-2499")
    @Test
    public void shouldThrowExceptionWhenXPathSyntaxNotValid() throws RepositoryException {
        String sql = "select node.[jcr:name] from [nt:unstructured] as node";
        try {
            Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.XPATH);
            fail("Should throw InvalidQueryException");
        } catch (InvalidQueryException e) {
        }
    }
    
    @FixFor("MODE-2490")
    @Test
    public void shouldOrderByTwoColumnsEvenIfNullable() throws RepositoryException {
        String sql = "SELECT * FROM [car:Car] ORDER BY [car:maker] DESC NULLS FIRST, [car:msrp] ASC NULLS FIRST";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(13).validate(query, query.execute());
    }

    @FixFor( "MODE-2297" )
    @Test
    public void shouldExecuteQueryUsingSetOperationOfQueriesWithoutJoins() throws RepositoryException {
        // Make sure that /Other/NodeA[3] contains references to /Other/NodeA and /Other/NodeA[2] ...
        Node nodeA1 = session.getNode("/Other/NodeA");
        Node nodeA2 = session.getNode("/Other/NodeA[2]");
        Node nodeA3 = session.getNode("/Other/NodeA[3]");
        assertThat(nodeA3.getProperty("otherNode").getNode(), is(sameInstance(nodeA1)));
        assertThat(nodeA3.getProperty("otherNodes").getValues()[0].getString(), is(nodeA2.getIdentifier()));
        assertThat(nodeA3.getProperty("otherNodes").getValues()[1].getString(), is(nodeA3.getIdentifier()));

        // print = true;
        String sql1 = "SELECT [jcr:path] FROM [nt:unstructured] AS other WHERE ISCHILDNODE(other,'/Other')";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql1, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        String sql2 = "SELECT [jcr:path] FROM [nt:unstructured] AS category WHERE ISCHILDNODE(category,'/Cars')";
        query = session.getWorkspace().getQueryManager().createQuery(sql2, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        String sql = sql1 + " UNION " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(8).validate(query, query.execute());

        sql = sql2 + " UNION " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(8).validate(query, query.execute());

        sql = sql1 + " INTERSECT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql2 + " INTERSECT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql1 + " INTERSECT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        sql = sql2 + " INTERSECT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        sql = sql1 + " EXCEPT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        sql = sql2 + " EXCEPT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());
    }

    @FixFor( "MODE-2297" )
    @Test
    public void shouldExecuteQueryUsingSetOperationOfQueriesAccessSystemContent() throws RepositoryException {

        // print = true;
        String sql1 = "SELECT BASE.[jcr:path] from [mode:nodeTypes] as BASE " //
                      + "JOIN  [mode:system] AS PARENT ON ISCHILDNODE(BASE,PARENT) "//
                      + "JOIN [mode:namespaces] AS REF2 ON REF2.[jcr:uuid] = PARENT.[jcr:uuid]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql1, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        String sql2 = "SELECT BASEx.[jcr:path] from [mode:nodeTypes] as BASEx " //
                      + "JOIN  [mode:nodeTypes] AS REFx ON REFx.[jcr:primaryType] = BASEx.[jcr:uuid]";
        query = session.getWorkspace().getQueryManager().createQuery(sql2, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        String sql = sql1 + " UNION " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql2 + " UNION " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql1 + " INTERSECT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql2 + " INTERSECT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql1 + " INTERSECT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql2 + " INTERSECT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql1 + " EXCEPT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql2 + " EXCEPT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());
    }

    @FixFor( "MODE-2297" )
    @Test
    public void shouldExecuteQueryUsingSetOperationOfQueriesWithJoins() throws RepositoryException {
        // Make sure that /Other/NodeA[3] contains references to /Other/NodeA and /Other/NodeA[2] ...
        Node nodeA1 = session.getNode("/Other/NodeA");
        Node nodeA2 = session.getNode("/Other/NodeA[2]");
        Node nodeA3 = session.getNode("/Other/NodeA[3]");
        assertThat(nodeA3.getProperty("otherNode").getNode(), is(sameInstance(nodeA1)));
        assertThat(nodeA3.getProperty("otherNodes").getValues()[0].getString(), is(nodeA2.getIdentifier()));
        assertThat(nodeA3.getProperty("otherNodes").getValues()[1].getString(), is(nodeA3.getIdentifier()));

        // print = true;
        String sql1 = "SELECT nodeA.[jcr:path] FROM [nt:unstructured] AS other JOIN [nt:unstructured] AS nodeA ON ISCHILDNODE(nodeA,other) WHERE PATH(other) = '/Other'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql1, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        String sql2 = "SELECT category.[jcr:path] FROM [nt:unstructured] AS category JOIN [nt:unstructured] AS cars ON ISCHILDNODE(category,cars) WHERE PATH(cars) = '/Cars'";
        query = session.getWorkspace().getQueryManager().createQuery(sql2, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        String sql = sql1 + " UNION " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(8).validate(query, query.execute());

        sql = sql2 + " UNION " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(8).validate(query, query.execute());

        sql = sql1 + " INTERSECT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql2 + " INTERSECT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql1 + " EXCEPT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        sql = sql2 + " EXCEPT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());
    }

    @FixFor( "MODE-2297" )
    @Test
    public void shouldExecuteQueryUsingSetOperationOfQueriesWithUnnecessaryIdentityJoinViaSameNodeJoinCriteria()
        throws RepositoryException {
        // Make sure that /Other/NodeA[3] contains references to /Other/NodeA and /Other/NodeA[2] ...
        Node nodeA1 = session.getNode("/Other/NodeA");
        Node nodeA2 = session.getNode("/Other/NodeA[2]");
        Node nodeA3 = session.getNode("/Other/NodeA[3]");
        assertThat(nodeA3.getProperty("otherNode").getNode(), is(sameInstance(nodeA1)));
        assertThat(nodeA3.getProperty("otherNodes").getValues()[0].getString(), is(nodeA2.getIdentifier()));
        assertThat(nodeA3.getProperty("otherNodes").getValues()[1].getString(), is(nodeA3.getIdentifier()));

        // print = true;
        String sql1 = "SELECT nodeA.[jcr:path] FROM [nt:unstructured] AS other "
                      + "JOIN [nt:unstructured] AS nodeA ON ISCHILDNODE(nodeA,other) "//
                      + "JOIN [nt:unstructured] AS unused ON ISSAMENODE(other,unused) "//
                      + "WHERE PATH(other) = '/Other'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql1, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        String sql2 = "SELECT category.[jcr:path] FROM [nt:unstructured] AS category "
                      + "JOIN [nt:unstructured] AS cars ON ISCHILDNODE(category,cars) "//
                      + "JOIN [nt:unstructured] AS unused ON ISSAMENODE(cars,unused) "//
                      + "WHERE PATH(cars) = '/Cars'";
        query = session.getWorkspace().getQueryManager().createQuery(sql2, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        String sql = sql1 + " UNION " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(8).validate(query, query.execute());

        sql = sql2 + " UNION " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(8).validate(query, query.execute());

        sql = sql1 + " INTERSECT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql2 + " INTERSECT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql1 + " EXCEPT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        sql = sql2 + " EXCEPT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());
    }

    @FixFor( "MODE-2297" )
    @Test
    public void shouldExecuteQueryUsingSetOperationOfQueriesWithOneSideReturningNoResults() throws RepositoryException {
        // Make sure that /Other/NodeA[3] contains references to /Other/NodeA and /Other/NodeA[2] ...
        Node nodeA1 = session.getNode("/Other/NodeA");
        Node nodeA2 = session.getNode("/Other/NodeA[2]");
        Node nodeA3 = session.getNode("/Other/NodeA[3]");
        assertThat(nodeA3.getProperty("otherNode").getNode(), is(sameInstance(nodeA1)));
        assertThat(nodeA3.getProperty("otherNodes").getValues()[0].getString(), is(nodeA2.getIdentifier()));
        assertThat(nodeA3.getProperty("otherNodes").getValues()[1].getString(), is(nodeA3.getIdentifier()));

        // print = true;
        String sql1 = "SELECT nodeA.[jcr:path] FROM [nt:unstructured] AS other "
                      + "JOIN [nt:unstructured] AS nodeA ON ISCHILDNODE(nodeA,other) "//
                      + "JOIN [nt:unstructured] AS unused ON ISSAMENODE(other,unused) "//
                      + "WHERE PATH(other) = '/Other'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql1, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        String sql2 = "SELECT category.[jcr:path] FROM [nt:unstructured] AS category "
                      + "JOIN [nt:unstructured] AS cars ON ISCHILDNODE(category,cars) "//
                      + "JOIN [nt:unstructured] AS unused ON ISSAMENODE(cars,unused) "//
                      + "WHERE PATH(cars) = '/NonExistantNode'";
        query = session.getWorkspace().getQueryManager().createQuery(sql2, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        String sql = sql1 + " UNION " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        sql = sql2 + " UNION " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        sql = sql1 + " INTERSECT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql2 + " INTERSECT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql1 + " EXCEPT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        sql = sql2 + " EXCEPT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());
    }

    @FixFor( "MODE-2297" )
    @Test
    public void shouldExecuteQueryUsingSetOperationOfQueriesWithUnnecessaryIdentityJoinViaEquiJoinCriteria()
        throws RepositoryException {
        // Make sure that /Other/NodeA[3] contains references to /Other/NodeA and /Other/NodeA[2] ...
        Node nodeA1 = session.getNode("/Other/NodeA");
        Node nodeA2 = session.getNode("/Other/NodeA[2]");
        Node nodeA3 = session.getNode("/Other/NodeA[3]");
        assertThat(nodeA3.getProperty("otherNode").getNode(), is(sameInstance(nodeA1)));
        assertThat(nodeA3.getProperty("otherNodes").getValues()[0].getString(), is(nodeA2.getIdentifier()));
        assertThat(nodeA3.getProperty("otherNodes").getValues()[1].getString(), is(nodeA3.getIdentifier()));

        // print = true;
        String sql1 = "SELECT nodeA.[jcr:path] FROM [nt:unstructured] AS other "
                      + "JOIN [nt:unstructured] AS nodeA ON ISCHILDNODE(nodeA,other) "//
                      + "JOIN [nt:unstructured] AS unused ON other.[jcr:uuid] = unused.[jcr:uuid] "//
                      + "WHERE PATH(other) = '/Other'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql1, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        String sql2 = "SELECT category.[jcr:path] FROM [nt:unstructured] AS category "
                      + "JOIN [nt:unstructured] AS cars ON ISCHILDNODE(category,cars) "//
                      + "JOIN [nt:unstructured] AS unused ON cars.[jcr:uuid] = unused.[jcr:uuid] "//
                      + "WHERE PATH(cars) = '/Cars'";
        query = session.getWorkspace().getQueryManager().createQuery(sql2, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        String sql = sql1 + " UNION " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(8).validate(query, query.execute());

        sql = sql2 + " UNION " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(8).validate(query, query.execute());

        sql = sql1 + " INTERSECT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql2 + " INTERSECT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql1 + " EXCEPT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        sql = sql2 + " EXCEPT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());
    }

    @FixFor( "MODE-2297" )
    @Test
    public void shouldExecuteQueryUsingSetOperationOfQueriesWithNonSimilarQueries() throws RepositoryException {
        // Make sure that /Other/NodeA[3] contains references to /Other/NodeA and /Other/NodeA[2] ...
        Node nodeA1 = session.getNode("/Other/NodeA");
        Node nodeA2 = session.getNode("/Other/NodeA[2]");
        Node nodeA3 = session.getNode("/Other/NodeA[3]");
        assertThat(nodeA3.getProperty("otherNode").getNode(), is(sameInstance(nodeA1)));
        assertThat(nodeA3.getProperty("otherNodes").getValues()[0].getString(), is(nodeA2.getIdentifier()));
        assertThat(nodeA3.getProperty("otherNodes").getValues()[1].getString(), is(nodeA3.getIdentifier()));

        // print = true;
        String sql1 = "SELECT nodeA.[jcr:path] FROM [nt:unstructured] AS other "
                      + "JOIN [nt:unstructured] AS nodeA ON ISCHILDNODE(nodeA,other) "//
                      + "JOIN [nt:unstructured] AS unused ON other.[jcr:uuid] = unused.[jcr:uuid] "//
                      + "WHERE PATH(other) = '/Other'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql1, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        String sql2 = "SELECT base.[jcr:path] FROM [nt:unstructured] AS base "
                      + "JOIN [nt:unstructured] AS unused ON base.[jcr:uuid] = unused.[jcr:uuid] ";
        query = session.getWorkspace().getQueryManager().createQuery(sql2, Query.JCR_SQL2);
        validateQuery().rowCount(24).validate(query, query.execute());

        String sql = sql1 + " UNION " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(24).validate(query, query.execute());

        sql = sql2 + " UNION " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(24).validate(query, query.execute());

        sql = sql1 + " INTERSECT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        sql = sql2 + " INTERSECT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(4).validate(query, query.execute());

        sql = sql1 + " EXCEPT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql2 + " EXCEPT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(20).validate(query, query.execute());
    }

    @FixFor( "MODE-2297" )
    @Test
    public void shouldExecuteQueryUsingSetOperationOfQueriesWithConstraintsOnNonExistantProperties() throws RepositoryException {
        // print = true;
        String sql1 = "SELECT BASE.[jcr:path] from [nt:propertyDefinition] as BASE " //
                      + "JOIN  [nt:nodeType] AS A ON ISCHILDNODE(BASE,A) " //
                      + "JOIN [nt:unstructured] AS B ON B.[jcr:uuid] = A.[jcr:uuid] " //
                      + "WHERE B.[jcr:multiple] = true";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql1, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        String sql2 = "SELECT BASE.[jcr:path] from [nt:unstructured] as BASE " //
                      + "JOIN  [nt:nodeType] AS A ON ISCHILDNODE(BASE,A) " //
                      + "JOIN [nt:unstructured] AS B ON B.[jcr:uuid] = A.[jcr:uuid] " //
                      + "JOIN  [nt:unstructured] AS C ON C.x = BASE.[jcr:uuid] " //
                      + "WHERE A.propertyThatHasntToExistsAtAll = '2'";
        query = session.getWorkspace().getQueryManager().createQuery(sql2, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        String sql = sql1 + " UNION " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql2 + " UNION " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql1 + " INTERSECT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql2 + " INTERSECT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql1 + " EXCEPT " + sql2;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());

        sql = sql2 + " EXCEPT " + sql1;
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(0).validate(query, query.execute());
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldFindNodeByUuid() throws RepositoryException {
        // Find one of the car nodes ...
        Node nodeA = session.getNode("/Other/NodeA[2]");
        assertThat(nodeA, is(notNullValue()));
        String uuid = nodeA.getUUID(); // throws exception if not referenceable ...

        String sql = "SELECT * FROM [nt:unstructured] WHERE [jcr:uuid] = '" + uuid + "'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns(allColumnNames("nt:unstructured")).validate(query, result);

        sql = "SELECT * FROM nt:unstructured WHERE jcr:uuid = '" + uuid + "'";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        result = query.execute();
        validateQuery().rowCount(1).validate(query, result);
    }

    @Test
    public void shouldFailToSkipBeyondSize() throws RepositoryException {
        String sql = "SELECT * FROM [nt:base] WHERE [car:year] < 2009";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        final int count = 13;
        validateQuery().rowCount(count).warnings(1).hasColumns(allColumnNames("nt:base")).validate(query, result);

        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        NodeIterator iter = result.getNodes();
        assertTrue(iter.hasNext());
        assertThat(iter.next(), is(notNullValue()));
        iter.skip(count - 2);
        assertTrue(iter.hasNext());
        assertThat(iter.next(), is(notNullValue()));
        // we're at the next one ...
        assertThat(iter.hasNext(), is(false));
        try {
            iter.skip(3);
            fail("Expected NoSuchElementException if skipping past end");
        } catch (NoSuchElementException e) {
            // expected
        }
        // Do it again and skip past the end ...
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        iter = result.getNodes();
        try {
            iter.skip(count + 1);
            fail("Expected NoSuchElementException if skipping past end");
        } catch (NoSuchElementException e) {
            // expected
        }
    }

    @FixFor( "MODE-1901" )
    @Test
    public void shouldExplainQueryWithoutExecutingQuery() throws RepositoryException {
        String sql = "SELECT * FROM [nt:file]";
        org.modeshape.jcr.api.query.Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        org.modeshape.jcr.api.query.QueryResult result = query.explain();
        validateQuery().rowCount(0).warnings(0).onlyQueryPlan().validate(query, result);
    }

    @FixFor( "MODE-1888" )
    @Test
    public void shouldCaptureWarningsAboutPotentialTypos() throws RepositoryException {
        String sql = "SELECT [jcr.uuid] FROM [nt:file]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(0).warnings(1).hasColumns("jcr.uuid").validate(query, result);
    }

    @FixFor( "MODE-1888" )
    @Test
    public void shouldCaptureWarningsAboutUsingMisspelledColumnOnWrongSelector() throws RepositoryException {
        String sql = "SELECT file.[jcr.uuid] FROM [nt:file] AS file JOIN [mix:referenceable] AS ref ON ISSAMENODE(file,ref)";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(0).warnings(1).hasColumns("file.jcr.uuid").validate(query, result);
    }

    @FixFor( "MODE-1888" )
    @Test
    public void shouldCaptureWarningsAboutUsingColumnOnWrongSelector() throws RepositoryException {
        String sql = "SELECT ref.[jcr:lastModified] FROM [nt:file] AS file JOIN [mix:referenceable] AS ref ON ISSAMENODE(file,ref)";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(0).warnings(1).hasColumns("ref.jcr:lastModified").validate(query, result);
    }

    @FixFor( {"MODE-1888", "MODE-2297"} )
    @Test
    public void shouldNotCaptureWarningsAboutUsingJcrUuidColumnOnWrongSelector() throws RepositoryException {
        String sql = "SELECT file.[jcr:uuid] FROM [nt:file] AS file JOIN [mix:referenceable] AS ref ON ISSAMENODE(file,ref)";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(0).warnings(0).hasColumns("file.jcr:uuid").validate(query, result);
    }

    @FixFor( "MODE-1888" )
    @Test
    public void shouldCaptureWarningAboutUseOfResidualProperties() throws RepositoryException {
        String sql = "SELECT [foo_bar] FROM [nt:unstructured]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(24).warnings(1).hasColumns("foo_bar").validate(query, result);
    }

    @FixFor( "MODE-1888" )
    @Test
    public void shouldNotCaptureWarningAboutUseOfPseudoColumns() throws RepositoryException {
        String sql = "SELECT [jcr:path] FROM [nt:unstructured]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(24).noWarnings().hasColumns("jcr:path").validate(query, result);
    }

    @FixFor( "MODE-1671" )
    @Test
    public void shouldNotCaptureWarningAboutUseOfNodeIdPseudoColumn() throws RepositoryException {
        String sql = "SELECT [mode:id] FROM [nt:unstructured]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(24).noWarnings().hasColumns("mode:id").validate(query, result);
    }

    @FixFor( "MODE-1888" )
    @Test
    public void shouldCaptureWarningAboutUseOfPseudoColumnWithPeriodInsteadOfColonDelimiter() throws RepositoryException {
        String sql = "SELECT [jcr.path] FROM [nt:unstructured]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(24).warnings(1).validate(query, result);
    }

    @FixFor( "MODE-1888" )
    @Test
    public void shouldCaptureWarningAboutUseOfPseudoColumnWithUnderscoreInsteadOfColonDelimiter() throws RepositoryException {
        String sql = "SELECT [jcr_path] FROM [nt:unstructured]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(24).warnings(1).hasColumns("jcr_path").validate(query, result);
    }

    @FixFor( "MODE-1671" )
    @Test
    public void shouldReturnResolvableNodeIdentifierFromQuery() throws RepositoryException {
        String sql = "SELECT [mode:id] AS reallylongvaluethatwillprintcompletely  FROM [nt:unstructured]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        final Map<String, Node> nodesById = new HashMap<>();
        validateQuery().rowCount(24).noWarnings().hasColumns("reallylongvaluethatwillprintcompletely").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                String id = row.getValue("mode:id").getString();
                Node nodeFromQuery = row.getNode();
                Node resolvedNode = session().getNodeByIdentifier(id);
                assertSame(nodeFromQuery, resolvedNode);
                nodesById.put(id, nodeFromQuery);
            }
        }).validate(query, result);

        for (Map.Entry<String, Node> entry : nodesById.entrySet()) {
            final String id = entry.getKey();
            final Node expectedNode = entry.getValue();

            sql = "SELECT [mode:id] AS reallylongvaluethatwillprintcompletely FROM [nt:unstructured] WHERE [mode:id] = '" + id
                  + "'";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            result = query.execute();
            validateQuery().rowCount(1).noWarnings().hasColumns("reallylongvaluethatwillprintcompletely")
                           .onEachRow(new Predicate() {
                               @Override
                               public void validate( int rowNumber,
                                                     Row row ) throws RepositoryException {
                                   String id = row.getValue("mode:id").getString();
                                   Node nodeFromQuery = row.getNode();
                                   String nodeId = nodeFromQuery.getIdentifier();
                                   assertSame(nodeFromQuery, expectedNode);
                                   assertSame(nodeId, id);
                               }
                           }).validate(query, result);

            sql = "SELECT [mode:id] FROM [nt:unstructured] WHERE [mode:id] = $id";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            query.bindValue("id", session().getValueFactory().createValue(id));
            result = query.execute();
            validateQuery().rowCount(1).noWarnings().hasColumns("mode:id").onEachRow(new Predicate() {
                @Override
                public void validate( int rowNumber,
                                      Row row ) throws RepositoryException {
                    String id = row.getValue("mode:id").getString();
                    Node nodeFromQuery = row.getNode();
                    assertSame(nodeFromQuery, expectedNode);
                    assertSame(nodeFromQuery.getIdentifier(), id);
                }
            }).validate(query, result);
        }
    }

    @FixFor( "MODE-1888" )
    @Test
    public void shouldCaptureWarningAboutUseOfNonPluralJcrMixinTypeColumn() throws RepositoryException {
        String sql = "SELECT [jcr:mixinType] FROM [nt:unstructured]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(24).warnings(1).hasColumns("jcr:mixinType").validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllNodes() throws RepositoryException {
        String sql = "SELECT * FROM [nt:base]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allColumnNames("nt:base");
        validateQuery().rowCount(totalNodeCount).noWarnings().hasColumns(columnNames).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllNodesWithOrderByPath() throws RepositoryException {
        String sql = "SELECT * FROM [nt:base] ORDER BY [jcr:path]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allColumnNames("nt:base");
        validateQuery().rowCount(totalNodeCount).noWarnings().hasColumns(columnNames).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllNodesWithOrderByPathUsingAlias() throws RepositoryException {
        String sql = "SELECT * FROM [nt:base] AS all ORDER BY all.[jcr:path]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allColumnNames("all");
        validateQuery().rowCount(totalNodeCount).noWarnings().hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-1671" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllNodesWithOrderByNodeId() throws RepositoryException {
        String sql = "SELECT * FROM [nt:base] ORDER BY [mode:id]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allColumnNames("nt:base");
        validateQuery().rowCount(totalNodeCount).noWarnings().hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-1671" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllNodesWithOrderByNodeIdUsingAlias() throws RepositoryException {
        String sql = "SELECT * FROM [nt:base] AS all ORDER BY all.[mode:id]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allColumnNames("all");
        validateQuery().rowCount(totalNodeCount).noWarnings().hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-1095" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithOrderByUsingPseudoColumnWithSelectStar() throws RepositoryException {
        String sql = "SELECT * FROM [car:Car] WHERE [car:year] < 2009 ORDER BY [jcr:path]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = carColumnNames("car:Car");
        validateQuery().rowCount(13).noWarnings().hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-1095" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithOrderByUsingColumnWithSelectStar() throws RepositoryException {
        String sql = "SELECT * FROM [car:Car] WHERE [car:year] < 2009 ORDER BY [car:year]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = carColumnNames("car:Car");
        validateQuery().rowCount(13).noWarnings().hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-1095" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithOrderByUsingColumnNotInSelect() throws RepositoryException {
        String sql = "SELECT [car:model], [car:maker] FROM [car:Car] WHERE [car:year] <= 2012 ORDER BY [car:year] DESC";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = {"car:model", "car:maker", "car:year"};
        validateQuery().rowCount(13).noWarnings().hasColumns(columnNames).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithOrderByOnResidualColumn() throws RepositoryException {
        String sql = "SELECT x.* FROM [nt:unstructured] AS x ORDER BY x.propC";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(24).warnings(1).hasColumns(allOf(allColumnNames("x"), new String[] {"propC"}))
                       .validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithOrderByOnResidualColumnAndNoAlias() throws RepositoryException {
        String sql = "SELECT * FROM [nt:unstructured] ORDER BY propC";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allOf(allColumnNames("nt:unstructured"), new String[] {"propC"});
        validateQuery().rowCount(24).warnings(1).hasColumns(columnNames).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2Query() throws RepositoryException {
        String sql = "SELECT * FROM [nt:unstructured]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allColumnNames("nt:unstructured");
        validateQuery().rowCount(24).warnings(0).hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-1095" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithJoinCriteriaOnColumnsInSelect() throws RepositoryException {
        String sql = "SELECT x.*, x.somethingElse, y.*, y.propC FROM [nt:unstructured] AS x INNER JOIN [nt:unstructured] AS y ON x.somethingElse = y.propC";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allOf(allColumnNames("x"), allColumnNames("y"), new String[] {"x.somethingElse", "y.propC"});
        validateQuery().rowCount(1).warnings(2).withRows().withRow("/Other/NodeA", "/Other/NodeA[2]").endRows()
                       .hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-1095" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithJoinCriteriaOnColumnsInSelectAndOrderBy()
        throws RepositoryException {
        String sql = "SELECT x.*, y.* FROM [nt:unstructured] AS x INNER JOIN [nt:unstructured] AS y ON x.somethingElse = y.propC ORDER BY x.propC";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allOf(allColumnNames("x"), allColumnNames("y"), new String[] {"x.propC"});
        validateQuery().rowCount(1).warnings(3).withRows().withRow("/Other/NodeA", "/Other/NodeA[2]").endRows()
                       .hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-1095" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithJoinCriteriaOnSomeColumnsInSelect() throws RepositoryException {
        String sql = "SELECT y.*, y.propC FROM [nt:unstructured] AS x INNER JOIN [nt:unstructured] AS y ON x.somethingElse = y.propC";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allOf(allColumnNames("y"), new String[] {"y.propC"});
        validateQuery().rowCount(1).warnings(2).withRows().withRow("/Other/NodeA[2]").endRows().hasColumns(columnNames)
                       .validate(query, result);
    }

    @FixFor( {"MODE-1095", "MODE-1680"} )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithOrderByAndJoinCriteriaOnColumnsNotInSelect()
        throws RepositoryException {
        String sql = "SELECT y.*, y.propC FROM [nt:unstructured] AS x INNER JOIN [nt:unstructured] AS y ON x.somethingElse = y.propC ORDER BY x.propC";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allOf(allColumnNames("y"), new String[] {"y.propC"});
        validateQuery().rowCount(1).warnings(3).hasColumns(columnNames).hasNodesAtPaths("/Other/NodeA[2]")
                       .validate(query, result);
    }

    @FixFor( "MODE-1055" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllNodesWithCriteria() throws RepositoryException {
        String sql = "SELECT * FROM [nt:base] WHERE [car:year] < 2009";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).warnings(1).hasColumns(allColumnNames("nt:base")).validate(query, result);
    }

    @FixFor( "MODE-1055" )
    @Test
    public void shouldReturnNullValuesForNonExistantPropertiesInSelectClauseOfJcrSql2Query() throws RepositoryException {
        String sql = "SELECT bogus, laughable, [car:year] FROM [nt:base] WHERE [car:year] < 2009";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = {"bogus", "laughable", "car:year"};
        validateQuery().rowCount(13).warnings(3).hasColumns(columnNames).onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertThat(row.getValue("bogus"), is(nullValue()));
                assertThat(row.getValue("laughable"), is(nullValue()));
                assertThat(row.getValue("car:year"), is(not(nullValue())));
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1055" )
    @Test
    public void shouldNotMatchNodesWhenQueryUsesNonExistantPropertyInCriteriaInSelectClauseOfJcrSql2Query()
        throws RepositoryException {
        String sql = "SELECT bogus, laughable, [car:year] FROM [nt:base] WHERE argle < 2009";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = {"bogus", "laughable", "car:year"};
        validateQuery().rowCount(0).warnings(4).hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-1055" )
    @Test
    public void shouldNotOrderByNonExistantPropertyInCriteriaInSelectClauseOfJcrSql2Query() throws RepositoryException {
        String sql = "SELECT bogus, laughable, [car:year] FROM [nt:base] ORDER BY argle";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = {"bogus", "laughable", "car:year", "argle"};
        validateQuery().rowCount(totalNodeCount).warnings(4).hasColumns(columnNames).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllCarNodes() throws RepositoryException {
        String sql = "SELECT * FROM [car:Car]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns(carColumnNames("car:Car")).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllCarNodesOrderedByYear() throws RepositoryException {
        String sql = "SELECT * FROM [car:Car] ORDER BY [car:year]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns(carColumnNames("car:Car")).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllCarNodesOrderedByDescendingLexicographicalPriceAndNullsFirst()
        throws RepositoryException {
        String sql = "SELECT * FROM [car:Car] ORDER BY [car:msrp] DESC NULLS FIRST";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns(carColumnNames("car:Car")).onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                // Results are sorted by lexicographic MSRP (as a string, not as a number)!!!
                if (rowNumber == 1) {
                    assertThat(row.getValue("car:model").getString(), is("DTS"));
                    assertThat(row.getValue("car:msrp"), is(nullValue()));
                    assertThat(row.getValue("car:mpgCity"), is(nullValue()));
                } else if (rowNumber == 2) {
                    assertThat(row.getValue("car:model").getString(), is("LR3"));
                    assertThat(row.getValue("car:msrp").getString(), is("$48,525"));
                    assertThat(row.getValue("car:mpgCity").getLong(), is(12L));
                } else if (rowNumber == 3) {
                    assertThat(row.getValue("car:model").getString(), is("IS350"));
                    assertThat(row.getValue("car:msrp").getString(), is("$36,305"));
                    assertThat(row.getValue("car:mpgCity").getLong(), is(18L));
                } else if (rowNumber == 11) {
                    assertThat(row.getValue("car:model").getString(), is("DB9"));
                    assertThat(row.getValue("car:msrp").getString(), is("$171,600"));
                    assertThat(row.getValue("car:mpgCity").getLong(), is(12L));
                }
            }
        }).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllCarNodesOrderedByDescendingLexicographicalPriceAndNullsLast()
        throws RepositoryException {
        String sql = "SELECT * FROM [car:Car] ORDER BY [car:msrp] DESC NULLS LAST";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns(carColumnNames("car:Car")).onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                // Results are sorted by lexicographic MSRP (as a string, not as a number)!!!
                if (rowNumber == 1) {
                    assertThat(row.getValue("car:model").getString(), is("LR3"));
                    assertThat(row.getValue("car:msrp").getString(), is("$48,525"));
                    assertThat(row.getValue("car:mpgCity").getLong(), is(12L));
                } else if (rowNumber == 2) {
                    assertThat(row.getValue("car:model").getString(), is("IS350"));
                    assertThat(row.getValue("car:msrp").getString(), is("$36,305"));
                    assertThat(row.getValue("car:mpgCity").getLong(), is(18L));
                } else if (rowNumber == 10) {
                    assertThat(row.getValue("car:model").getString(), is("DB9"));
                    assertThat(row.getValue("car:msrp").getString(), is("$171,600"));
                    assertThat(row.getValue("car:mpgCity").getLong(), is(12L));
                } else if (rowNumber == 13) {
                    assertThat(row.getValue("car:model").getString(), is("DTS"));
                    assertThat(row.getValue("car:msrp"), is(nullValue()));
                    assertThat(row.getValue("car:mpgCity"), is(nullValue()));
                }
            }
        }).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllCarNodesOrderedByAscendingLexicographicalPriceAndNullsFirst()
        throws RepositoryException {
        String sql = "SELECT * FROM [car:Car] ORDER BY [car:msrp] ASC NULLS FIRST";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns(carColumnNames("car:Car")).onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                // Results are sorted by lexicographic MSRP (as a string, not as a number)!!!
                if (rowNumber == 1) {
                    assertThat(row.getValue("car:model").getString(), is("DTS"));
                    assertThat(row.getValue("car:msrp"), is(nullValue()));
                    assertThat(row.getValue("car:mpgCity"), is(nullValue()));
                } else if (rowNumber == 13) {
                    assertThat(row.getValue("car:model").getString(), is("LR3"));
                    assertThat(row.getValue("car:msrp").getString(), is("$48,525"));
                    assertThat(row.getValue("car:mpgCity").getLong(), is(12L));
                } else if (rowNumber == 12) {
                    assertThat(row.getValue("car:model").getString(), is("IS350"));
                    assertThat(row.getValue("car:msrp").getString(), is("$36,305"));
                    assertThat(row.getValue("car:mpgCity").getLong(), is(18L));
                } else if (rowNumber == 12) {
                    assertThat(row.getValue("car:model").getString(), is("DB9"));
                    assertThat(row.getValue("car:msrp").getString(), is("$171,600"));
                    assertThat(row.getValue("car:mpgCity").getLong(), is(12L));
                }
            }
        }).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllCarNodesOrderedByAscendingLexicographicalPriceAndNullsLast()
        throws RepositoryException {
        String sql = "SELECT * FROM [car:Car] ORDER BY [car:msrp] ASC NULLS LAST";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        // print = true;
        validateQuery().rowCount(13).hasColumns(carColumnNames("car:Car")).onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                // Results are sorted by lexicographic MSRP (as a string, not as a number)!!!
                if (rowNumber == 3) {
                    assertThat(row.getValue("car:model").getString(), is("DB9"));
                    assertThat(row.getValue("car:msrp").getString(), is("$171,600"));
                    assertThat(row.getValue("car:mpgCity").getLong(), is(12L));
                } else if (rowNumber == 11) {
                    assertThat(row.getValue("car:model").getString(), is("IS350"));
                    assertThat(row.getValue("car:msrp").getString(), is("$36,305"));
                    assertThat(row.getValue("car:mpgCity").getLong(), is(18L));
                } else if (rowNumber == 12) {
                    assertThat(row.getValue("car:model").getString(), is("LR3"));
                    assertThat(row.getValue("car:msrp").getString(), is("$48,525"));
                    assertThat(row.getValue("car:mpgCity").getLong(), is(12L));
                } else if (rowNumber == 13) {
                    assertThat(row.getValue("car:model").getString(), is("DTS"));
                    assertThat(row.getValue("car:msrp"), is(nullValue()));
                    assertThat(row.getValue("car:mpgCity"), is(nullValue()));
                }
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1234" )
    @Test
    public void shouldAllowEqualityCriteriaOnPropertyDefinedWithBooleanPropertyDefinition() throws RepositoryException {
        assertQueryWithBooleanValue(1, "");
        assertQueryWithBooleanValue(1, "WHERE [notion:booleanProperty] = true");
        assertQueryWithBooleanValue(0, "WHERE [notion:booleanProperty] = false");

        assertQueryWithBooleanValue(1, "WHERE [notion:booleanProperty] > false");
        assertQueryWithBooleanValue(1, "WHERE [notion:booleanProperty] >= false");
        assertQueryWithBooleanValue(0, "WHERE [notion:booleanProperty] > true");
        assertQueryWithBooleanValue(1, "WHERE [notion:booleanProperty] >= true");

        assertQueryWithBooleanValue(0, "WHERE [notion:booleanProperty] < false");
        assertQueryWithBooleanValue(0, "WHERE [notion:booleanProperty] <= false");
        assertQueryWithBooleanValue(0, "WHERE [notion:booleanProperty] < true");
        assertQueryWithBooleanValue(1, "WHERE [notion:booleanProperty] <= true");

        assertQueryWithBooleanValue(0, "WHERE [notion:booleanProperty2] = true");
        assertQueryWithBooleanValue(1, "WHERE [notion:booleanProperty2] = false");

        assertQueryWithBooleanValue(0, "WHERE [notion:booleanProperty2] > false");
        assertQueryWithBooleanValue(1, "WHERE [notion:booleanProperty2] >= false");
        assertQueryWithBooleanValue(0, "WHERE [notion:booleanProperty2] > true");
        assertQueryWithBooleanValue(0, "WHERE [notion:booleanProperty2] >= true");

        assertQueryWithBooleanValue(0, "WHERE [notion:booleanProperty2] < false");
        assertQueryWithBooleanValue(1, "WHERE [notion:booleanProperty2] <= false");
        assertQueryWithBooleanValue(1, "WHERE [notion:booleanProperty2] < true");
        assertQueryWithBooleanValue(1, "WHERE [notion:booleanProperty2] <= true");

    }

    protected void assertQueryWithBooleanValue( int numResults,
                                                String criteria ) throws RepositoryException {
        String[] columnNames = {"notion:booleanProperty", "notion:booleanProperty2"};
        String queryStr = "SELECT [notion:booleanProperty], [notion:booleanProperty2] FROM [notion:typed] AS node " + criteria;
        Query query = session.getWorkspace().getQueryManager().createQuery(queryStr, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(numResults).hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-1611" )
    @Test
    public void shouldAllowQomEqualityCriteriaOnPropertyDefinedWithBooleanPropertyDefinition() throws RepositoryException {
        assertQomQueryWithBooleanValue(1, null, null, false);
        assertQomQueryWithBooleanValue(1, "notion:booleanProperty", JCR_OPERATOR_EQUAL_TO, true);
        assertQomQueryWithBooleanValue(0, "notion:booleanProperty", JCR_OPERATOR_EQUAL_TO, false);

        assertQomQueryWithBooleanValue(1, "notion:booleanProperty", JCR_OPERATOR_GREATER_THAN, false);
        assertQomQueryWithBooleanValue(1, "notion:booleanProperty", JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO, false);
        assertQomQueryWithBooleanValue(0, "notion:booleanProperty", JCR_OPERATOR_GREATER_THAN, true);
        assertQomQueryWithBooleanValue(1, "notion:booleanProperty", JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO, true);

        assertQomQueryWithBooleanValue(0, "notion:booleanProperty", JCR_OPERATOR_LESS_THAN, false);
        assertQomQueryWithBooleanValue(0, "notion:booleanProperty", JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO, false);
        assertQomQueryWithBooleanValue(0, "notion:booleanProperty", JCR_OPERATOR_LESS_THAN, true);
        assertQomQueryWithBooleanValue(1, "notion:booleanProperty", JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO, true);

        assertQomQueryWithBooleanValue(0, "notion:booleanProperty2", JCR_OPERATOR_EQUAL_TO, true);
        assertQomQueryWithBooleanValue(1, "notion:booleanProperty2", JCR_OPERATOR_EQUAL_TO, false);

        assertQomQueryWithBooleanValue(0, "notion:booleanProperty2", JCR_OPERATOR_GREATER_THAN, false);
        assertQomQueryWithBooleanValue(1, "notion:booleanProperty2", JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO, false);
        assertQomQueryWithBooleanValue(0, "notion:booleanProperty2", JCR_OPERATOR_GREATER_THAN, true);
        assertQomQueryWithBooleanValue(0, "notion:booleanProperty2", JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO, true);

        assertQomQueryWithBooleanValue(0, "notion:booleanProperty2", JCR_OPERATOR_LESS_THAN, false);
        assertQomQueryWithBooleanValue(1, "notion:booleanProperty2", JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO, false);
        assertQomQueryWithBooleanValue(1, "notion:booleanProperty2", JCR_OPERATOR_LESS_THAN, true);
        assertQomQueryWithBooleanValue(1, "notion:booleanProperty2", JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO, true);

    }

    protected void assertQomQueryWithBooleanValue( int numResults,
                                                   String propertyName,
                                                   String operator,
                                                   boolean propertyValue ) throws RepositoryException {
        QueryObjectModelFactory qomFactory = session.getWorkspace().getQueryManager().getQOMFactory();
        ValueFactory valueFactory = session.getValueFactory();
        Value propertyValueObj = valueFactory.createValue(propertyValue);

        Selector selector = qomFactory.selector("notion:typed", "node");
        Constraint constraint = null;
        if (propertyName != null && operator != null) {
            PropertyValue propValue = qomFactory.propertyValue("node", propertyName);
            Literal literal = qomFactory.literal(propertyValueObj);
            constraint = qomFactory.comparison(propValue, operator, literal);
        }
        Column[] columns = new Column[2];
        columns[0] = qomFactory.column("node", "notion:booleanProperty", "notion:booleanProperty");
        columns[1] = qomFactory.column("node", "notion:booleanProperty2", "notion:booleanProperty2");
        Ordering[] orderings = null;

        // Build and execute the query ...
        Query query = qomFactory.createQuery(selector, constraint, orderings, columns);
        assertThat(query, is(notNullValue()));
        QueryResult result = query.execute();
        String[] columnNames = {"notion:booleanProperty", "notion:booleanProperty2"};
        validateQuery().rowCount(numResults).hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-1611" )
    @Test
    public void shouldAllowQomUseOfIsChildNodeInWhereClause() throws RepositoryException {
        QueryObjectModelFactory qomFactory = session.getWorkspace().getQueryManager().getQOMFactory();

        Selector selector = qomFactory.selector("nt:base", "category");
        ChildNode childNodeConstraint = qomFactory.childNode("category", "/Cars");
        Constraint constraint = childNodeConstraint;
        Column[] columns = new Column[0];
        Ordering[] orderings = null;

        // Build and execute the query ...
        Query query = qomFactory.createQuery(selector, constraint, orderings, columns);
        assertThat(query.getStatement(), is("SELECT * FROM [nt:base] AS category WHERE ISCHILDNODE(category,'/Cars')"));
        QueryResult result = query.execute();
        validateQuery().rowCount(4).hasColumns(allColumnNames("category")).validate(query, result);
    }

    @FixFor( "MODE-1057" )
    @Test
    public void shouldAllowEqualityNumericCriteriaOnPropertyDefinedWithNumericPropertyDefinition() throws RepositoryException {
        assertQueryWithLongValue(13, "");
        assertQueryWithLongValue(2, "WHERE [car:userRating] = 3");
        assertQueryWithLongValue(1, "WHERE [car:userRating] < 3");
        assertQueryWithLongValue(8, "WHERE [car:userRating] > 3");
        assertQueryWithLongValue(3, "WHERE [car:userRating] <= 3");
        assertQueryWithLongValue(10, "WHERE [car:userRating] >= 3");
        assertQueryWithLongValue(9, "WHERE [car:userRating] <> 3");
        assertQueryWithLongValue(9, "WHERE [car:userRating] != 3");
    }

    @FixFor( "MODE-1057" )
    @Test
    public void shouldAllowEqualityStringCriteriaOnPropertyDefinedWithNumericPropertyDefinition() throws RepositoryException {
        assertQueryWithLongValue(13, "");
        assertQueryWithLongValue(2, "WHERE [car:userRating] = '3'");
        assertQueryWithLongValue(1, "WHERE [car:userRating] < '3'");
        assertQueryWithLongValue(8, "WHERE [car:userRating] > '3'");
        assertQueryWithLongValue(3, "WHERE [car:userRating] <= '3'");
        assertQueryWithLongValue(10, "WHERE [car:userRating] >= '3'");
        assertQueryWithLongValue(9, "WHERE [car:userRating] <> '3'");
        assertQueryWithLongValue(9, "WHERE [car:userRating] != '3'");
    }

    protected void assertQueryWithLongValue( int numResults,
                                             String criteria ) throws RepositoryException {
        String[] columnNames = {"car:maker", "car:model", "car:year", "car:userRating"};
        String queryStr = "SELECT [car:maker], [car:model], [car:year], [car:userRating] FROM [car:Car] AS car " + criteria;
        Query query = session.getWorkspace().getQueryManager().createQuery(queryStr, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(numResults).hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-2178" )
    @Test
    public void shouldAllowQueryForPropertyWithEmptyStringAsCriteria() throws RepositoryException {
        // print = true;
        String[] columnNames = {"car:engine", "car:maker", "car:model", "car:year", "car:userRating"};
        String queryStr = "SELECT [car:engine], [car:maker], [car:model], [car:year], [car:userRating] FROM [car:Car]";
        Query query = session.getWorkspace().getQueryManager().createQuery(queryStr, Query.JCR_SQL2);
        validateQuery().rowCount(13L).hasColumns(columnNames).validate(query, query.execute());

        queryStr = "SELECT [car:engine], [car:maker], [car:model], [car:year], [car:userRating] FROM [car:Car] WHERE LENGTH([car:engine]) = 0";
        query = session.getWorkspace().getQueryManager().createQuery(queryStr, Query.JCR_SQL2);
        validateQuery().rowCount(8L).hasColumns(columnNames).validate(query, query.execute());

        queryStr = "SELECT [car:engine], [car:maker], [car:model], [car:year], [car:userRating] FROM [car:Car] WHERE [car:engine] IS NULL OR [car:engine] = ''";
        query = session.getWorkspace().getQueryManager().createQuery(queryStr, Query.JCR_SQL2);
        validateQuery().rowCount(8L).hasColumns(columnNames).validate(query, query.execute());

        queryStr = "SELECT * FROM [car:Car] WHERE LENGTH([car:engine]) = 0";
        query = session.getWorkspace().getQueryManager().createQuery(queryStr, Query.JCR_SQL2);
        validateQuery().rowCount(8L).validate(query, query.execute());

        // There are some nodes that don't have a 'car:engine' property, but none of the existing property values is
        // an empty string, so this query should return no rows ...
        queryStr = "SELECT [car:engine], [car:maker], [car:model], [car:year], [car:userRating] FROM [car:Car] WHERE [car:engine] = ''";
        query = session.getWorkspace().getQueryManager().createQuery(queryStr, Query.JCR_SQL2);
        validateQuery().rowCount(0L).hasColumns(columnNames).validate(query, query.execute());

        Node car = session.getNode("/Cars/Hybrid/Toyota Prius");
        assertThat(car.hasProperty("car:engine"), is(false));
        try {
            // Change a node to have an empty 'car:engine' property value ...
            car.setProperty("car:engine", "");
            session.save();

            // Now issue the same query again, and this time we should have 1 row that has an empty 'car:engine' prop value ...
            query = session.getWorkspace().getQueryManager().createQuery(queryStr, Query.JCR_SQL2);
            validateQuery().rowCount(1L).hasColumns(columnNames).validate(query, query.execute());
        } finally {
            // Remove the property again ...
            car.getProperty("car:engine").remove();
            session.save();
        }
    }

    @FixFor( "MODE-1824" )
    @Test
    public void shouldBeAbleToExecuteQueryWithTwoColumns() throws RepositoryException {
        QueryManager queryManager = session.getWorkspace().getQueryManager();
        QueryObjectModelFactory factory = queryManager.getQOMFactory();
        Selector car1Selector = factory.selector("car:Car", "car1");
        Selector car2Selector = factory.selector("car:Car", "car2");
        Join join = factory.join(car1Selector, car2Selector, QueryObjectModelConstants.JCR_JOIN_TYPE_INNER,
                                 factory.equiJoinCondition("car1", "car:maker", "car2", "car:maker"));
        Column[] columns = new Column[] {factory.column("car1", "car:maker", "maker"),
            factory.column("car2", "car:model", "model")};
        Query query = factory.createQuery(join, null, null, columns);
        QueryResult result = query.execute();
        String[] columnNames = {columns[0].getColumnName(), columns[1].getColumnName()}; // the aliases
        validateQuery().rowCount(21).hasColumns(columnNames).validate(query, result);
    }

    @Test
    public void shouldBeAbleToQueryWithLimitOfZeroOnNonJoin() throws RepositoryException {
        // Try with the LIMIT expression ...
        String sql = "SELECT [jcr:path] FROM [car:Car] LIMIT 0";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(0).hasColumns("jcr:path").validate(query, result);

        // Try with the method ...
        sql = "SELECT [jcr:path] FROM [car:Car]";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        query.setLimit(0);
        result = query.execute();
        validateQuery().rowCount(0).hasColumns("jcr:path").validate(query, result);
    }

    @Test
    public void shouldBeAbleToQueryWithTooLargeLimitOnNonJoin() throws RepositoryException {
        // Try with the LIMIT expression ...
        String sql = "SELECT [jcr:path] FROM [car:Car] LIMIT 100";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns("jcr:path").validate(query, result);

        // Try with the method ...
        sql = "SELECT [jcr:path] FROM [car:Car]";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        query.setLimit(100);
        result = query.execute();
        validateQuery().rowCount(13).hasColumns("jcr:path").validate(query, result);
    }

    @Test
    public void shouldBeAbleToQueryWithLimitOnNonJoin() throws RepositoryException {
        // Try with the LIMIT expression ...
        String sql = "SELECT [jcr:path] FROM [car:Car] LIMIT 2";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(2).hasColumns("jcr:path").validate(query, result);

        // Try with the method ...
        sql = "SELECT [jcr:path] FROM [car:Car]";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        query.setLimit(2);
        result = query.execute();
        validateQuery().rowCount(2).hasColumns("jcr:path").validate(query, result);
    }

    @Test
    public void shouldBeAbleToQueryWithOffsetOnNonJoin() throws RepositoryException {
        // Try with the OFFSET expression ...
        String sql = "SELECT [jcr:path] FROM [car:Car] OFFSET 2";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(11).hasColumns("jcr:path").validate(query, result);

        // Try with the method ...
        sql = "SELECT [jcr:path] FROM [car:Car]";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        query.setOffset(2);
        result = query.execute();
        validateQuery().rowCount(11).hasColumns("jcr:path").validate(query, result);
    }

    @Test
    public void shouldBeAbleToQueryWithZeroOffsetOnNonJoin() throws RepositoryException {
        // Try with the OFFSET expression ...
        String sql = "SELECT [jcr:path] FROM [car:Car] OFFSET 0";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns("jcr:path").validate(query, result);

        // Try with the method ...
        sql = "SELECT [jcr:path] FROM [car:Car]";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        query.setOffset(0);
        result = query.execute();
        validateQuery().rowCount(13).hasColumns("jcr:path").validate(query, result);
    }

    @Test
    public void shouldBeAbleToQueryWithTooLargeOffsetOnNonJoin() throws RepositoryException {
        // Try with the OFFSET expression ...
        String sql = "SELECT [jcr:path] FROM [car:Car] OFFSET 100";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(0).hasColumns("jcr:path").validate(query, result);

        // Try with the method ...
        sql = "SELECT [jcr:path] FROM [car:Car]";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        query.setOffset(100);
        result = query.execute();
        validateQuery().rowCount(0).hasColumns("jcr:path").validate(query, result);
    }

    @Test
    public void shouldBeAbleToQueryWithLimitAndOffsetOnNonJoin() throws RepositoryException {
        // Try with the OFFSET expression ...
        String sql = "SELECT [jcr:path] FROM [car:Car] ORDER BY [jcr:path]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        final String[] allCarPaths = {"/Cars/Hybrid/Nissan Altima", "/Cars/Hybrid/Toyota Highlander",
            "/Cars/Hybrid/Toyota Prius", "/Cars/Luxury/Bentley Continental", "/Cars/Luxury/Cadillac DTS",
            "/Cars/Luxury/Lexus IS350", "/Cars/Sports/Aston Martin DB9", "/Cars/Sports/Infiniti G37", "/Cars/Utility/Ford F-150",
            "/Cars/Utility/Hummer H3", "/Cars/Utility/Land Rover LR2", "/Cars/Utility/Land Rover LR3",
            "/Cars/Utility/Toyota Land Cruiser"};
        validateQuery().rowCount(13).hasColumns("jcr:path").hasNodesAtPaths(allCarPaths).validate(query, result);

        // Try with the OFFSET expression ...
        sql = "SELECT [jcr:path] FROM [car:Car] ORDER BY [jcr:path] LIMIT 2 OFFSET 2";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().rowCount(2).hasColumns("jcr:path").hasNodesAtPaths(allCarPaths[2], allCarPaths[3])
                       .validate(query, result);

        // Try with the method ...
        sql = "SELECT [jcr:path] FROM [car:Car]";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        query.setOffset(2);
        result = query.execute();
        validateQuery().rowCount(11).hasColumns("jcr:path").validate(query, result);
    }

    @FixFor( "MODE-1865" )
    @Test
    public void shouldBeAbleToQueryWithLimit1() throws RepositoryException, Exception {
        Node top = session.getRootNode().addNode("top");
        for (int i = 0; i != 10; ++i) {
            Node parent = top.addNode("qwer" + i, "modetest:parent");
            parent.setProperty("modetest:parentField", 5L);
            Node intermediate = parent.getNode("modetest:folder");
            Node child = intermediate.addNode("asdf");
            child.setProperty("modetest:childField", i % 2 == 0 ? "bar" : "foo");
        }

        try {
            session.save();
            Thread.sleep(100L);
            String sql = "SELECT p.* AS parent FROM [modetest:parent] AS p INNER JOIN [modetest:child] AS c ON ISDESCENDANTNODE(c,p) WHERE p.[modetest:parentField] = CAST('5' AS LONG) AND c.[modetest:childField] = 'bar'";
            Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            QueryResult result = query.execute();
            validateQuery().rowCount(5).onEachRow(new Predicate() {
                @Override
                public void validate( int rowNumber,
                                      Row row ) throws RepositoryException {
                    // Path of all rows should have an even digit at the end ...
                    String path = row.getPath();
                    int lastIntValue = Integer.parseInt(path.substring(path.length() - 1));
                    assertTrue(lastIntValue % 2 == 0);
                }
            }).validate(query, result);

            // Try again but with LIMIT 1 (via method)...
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            query.setLimit(1L);
            result = query.execute();
            validateQuery().rowCount(1).validate(query, result);

            // Try again but with LIMIT 1 (via statement)...
            query = session.getWorkspace().getQueryManager().createQuery(sql + " LIMIT 1", Query.JCR_SQL2);
            result = query.execute();
            validateQuery().rowCount(1).validate(query, result);
        } finally {
            top.remove();
            session.save();
        }
    }

    @Test
    @FixFor( "MODE-2435" )
    public void shouldCorrectlyExecuteOrderByWithOffsetAndLimit() throws RepositoryException {
        // no order by
        String sql = "SELECT [jcr:path] FROM [car:Car] LIMIT 10 OFFSET 15";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        assertThat("result should contain zero rows", result.getRows().getSize(), is(0L));

        // with order by
        sql = "SELECT [jcr:path] FROM [car:Car] ORDER BY [jcr:path] LIMIT 10 OFFSET 15";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        assertThat("result should contain zero rows", result.getRows().getSize(), is(0L));

        sql = "SELECT [jcr:path] FROM [car:Car] ORDER BY [jcr:path] LIMIT 1 OFFSET 0";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().hasNodesAtPaths("/Cars/Hybrid/Nissan Altima").validate(query, result);

        sql = "SELECT [jcr:path] FROM [car:Car] ORDER BY [jcr:path] LIMIT 1 OFFSET 1";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().hasNodesAtPaths("/Cars/Hybrid/Toyota Highlander").validate(query, result);

        sql = "SELECT [jcr:path] FROM [car:Car] ORDER BY [jcr:path] LIMIT 3 OFFSET 0";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().hasNodesAtPaths("/Cars/Hybrid/Nissan Altima", 
                                        "/Cars/Hybrid/Toyota Highlander",
                                        "/Cars/Hybrid/Toyota Prius").validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllCarsUnderHybrid() throws RepositoryException {
        String sql = "SELECT car.[car:maker], car.[car:model], car.[car:year], car.[car:msrp], car.[jcr:path] FROM [car:Car] AS car WHERE PATH(car) LIKE '%/Hybrid/%' ORDER BY [car:model]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = {"car:maker", "car:model", "car:year", "car:msrp", "jcr:path"};
        validateQuery().rowCount(3).hasColumns(columnNames).onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                if (rowNumber == 1) {
                    assertThat(row.getValue("car:model").getString(), is("Altima"));
                    assertThat(row.getValue("car:msrp").getString(), is("$18,260"));
                    assertThat(row.getValue("car:year").getLong(), is(2008L));
                } else if (rowNumber == 2) {
                    assertThat(row.getValue("car:model").getString(), is("Highlander"));
                    assertThat(row.getValue("car:msrp").getString(), is("$34,200"));
                    assertThat(row.getValue("car:year").getLong(), is(2008L));
                } else if (rowNumber == 3) {
                    assertThat(row.getValue("car:model").getString(), is("Prius"));
                    assertThat(row.getValue("car:msrp").getString(), is("$21,500"));
                    assertThat(row.getValue("car:year").getLong(), is(2008L));
                }
            }
        }).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithPathCriteria() throws RepositoryException {
        String sql = "SELECT [jcr:path] FROM [nt:unstructured] WHERE PATH() = '/Cars/Hybrid/Toyota Highlander'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertThat(row.getNode().getPath(), is("/Cars/Hybrid/Toyota Highlander"));
            }
        }).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryForNodeNamedHybrid() throws RepositoryException {
        String sql = "SELECT [jcr:path] FROM [nt:unstructured] AS hybrid WHERE NAME(hybrid) = 'Hybrid'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertThat(row.getNode().getName(), is("Hybrid"));
            }
        }).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryForNodeLocalNamedHybrid() throws RepositoryException {
        String sql = "SELECT [jcr:path] FROM [nt:unstructured] AS hybrid WHERE LOCALNAME(hybrid) = 'Hybrid'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertThat(row.getNode().getName(), is("Hybrid"));
            }
        }).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryUsingJoinToFindAllCarsUnderHybrid() throws RepositoryException {
        String sql = "SELECT car.[car:maker], car.[car:model], car.[car:year], car.[car:msrp] FROM [car:Car] AS car JOIN [nt:unstructured] AS hybrid ON ISCHILDNODE(car,hybrid) WHERE NAME(hybrid) = 'Hybrid' ORDER BY NAME(car)";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = {"car.car:maker", "car.car:model", "car.car:year", "car.car:msrp"};
        validateQuery().rowCount(3).hasColumns(columnNames).onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                if (rowNumber == 1) {
                    assertThat(row.getValue("car:model").getString(), is("Altima"));
                    assertThat(row.getValue("car:msrp").getString(), is("$18,260"));
                    assertThat(row.getValue("car:year").getLong(), is(2008L));
                } else if (rowNumber == 2) {
                    assertThat(row.getValue("car:model").getString(), is("Highlander"));
                    assertThat(row.getValue("car:msrp").getString(), is("$34,200"));
                    assertThat(row.getValue("car:year").getLong(), is(2008L));
                } else if (rowNumber == 3) {
                    assertThat(row.getValue("car:model").getString(), is("Prius"));
                    assertThat(row.getValue("car:msrp").getString(), is("$21,500"));
                    assertThat(row.getValue("car:year").getLong(), is(2008L));
                }
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1825" )
    @Test
    public void shouldBeAbleToExecuteQueryForAllColumns() throws RepositoryException {
        QueryManager queryManager = session.getWorkspace().getQueryManager();
        QueryObjectModelFactory factory = queryManager.getQOMFactory();
        Selector car1Selector = factory.selector("car:Car", "car1");
        Selector car2Selector = factory.selector("car:Car", "car2");
        Join join = factory.join(car1Selector, car2Selector, QueryObjectModelConstants.JCR_JOIN_TYPE_INNER,
                                 factory.equiJoinCondition("car1", "car:maker", "car2", "car:maker"));
        Column[] columns = new Column[] {factory.column("car1", null, null)};
        Constraint constraint = factory.comparison(factory.propertyValue("car1", "car:maker"),
                                                   QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO,
                                                   factory.literal(session.getValueFactory().createValue("Toyota")));
        Ordering[] orderings = new Ordering[] {factory.descending(factory.propertyValue("car1", "car:year"))};
        Query query = factory.createQuery(join, constraint, orderings, columns);
        QueryResult result = query.execute();
        validateQuery().rowCount(9).hasColumns(carColumnNames("car1")).validate(query, result);
    }

    @FixFor( "MODE-1833" )
    @Test
    public void shouldBeAbleToQueryAllColumnsOnSimpleType() throws RepositoryException {
        QueryManager queryManager = session.getWorkspace().getQueryManager();
        QueryObjectModelFactory factory = queryManager.getQOMFactory();
        Query query = factory.createQuery(factory.selector("modetest:simpleType", "type1"), null, null,
                                          new Column[] {factory.column("type1", null, null)});
        QueryResult result = query.execute();
        validateQuery().rowCount(0).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllUnstructuredNodes() throws RepositoryException {
        String sql = "SELECT * FROM [nt:unstructured]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(24).hasColumns(allColumnNames("nt:unstructured")).validate(query, result);
    }

    @FixFor( "MODE-1309" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryUsingResidualPropertiesForJoinCriteria() throws RepositoryException {
        String sql = "SELECT x.propA AS pa, y.propB as pb FROM [nt:unstructured] AS x INNER JOIN [nt:unstructured] AS y ON x.propA = y.propB WHERE x.propA = 'value1'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        final Set<String> expectedPaths = new HashSet<String>();
        expectedPaths.add("/Other/NodeA[2]");
        expectedPaths.add("/Other/NodeA[3]");
        validateQuery().rowCount(2).hasColumns("pa", "pb").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                // All the rows are identical ...
                assertThat(row.getValue("pa").getString(), is("value1"));
                assertThat(row.getValue("pb").getString(), is("value1"));
                // The path of the first column is the same ...
                assertThat(row.getNode("x").getPath(), is("/Other/NodeA"));
                // The path of the second selector will vary in each row ...
                assertThat(expectedPaths.remove(row.getNode("y").getPath()), is(true));
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1309" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QuerySelectingResidualProperty() throws RepositoryException {
        String sql = "SELECT a.propB FROM [nt:unstructured] AS a WHERE a.propB = 'value1'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(2).hasColumns("propB").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                // All the rows are identical ...
                assertThat(row.getValue("propB").getString(), is("value1"));
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1309" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QuerySelectingResidualPropertyWithAlias() throws RepositoryException {
        String sql = "SELECT a.propB AS foo FROM [nt:unstructured] AS a WHERE a.propB = 'value1'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(2).hasColumns("foo").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                // All the rows are identical ...
                assertThat(row.getValue("foo").getString(), is("value1"));
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1309" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QuerySelectingResidualPropertyWithAliasUsingAliasInConstraint()
        throws RepositoryException {
        String sql = "SELECT a.propB AS foo FROM [nt:unstructured] AS a WHERE a.foo = 'value1'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(2).hasColumns("foo").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                // All the rows are identical ...
                assertThat(row.getValue("foo").getString(), is("value1"));
            }
        }).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllUnstructuredNodesWithCriteriaOnMultiValuedProperty()
        throws RepositoryException {
        String sql = "SELECT * FROM [nt:unstructured] WHERE something = 'white dog' and something = 'black dog'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        // print = true;
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns(allColumnNames("nt:unstructured")).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindAllUnstructuredNodesWithLikeCriteriaOnMultiValuedProperty()
        throws RepositoryException {
        String sql = "SELECT * FROM [nt:unstructured] WHERE something LIKE 'white%' and something LIKE 'black%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        // print = true;
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns(allColumnNames("nt:unstructured")).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithChildNodeJoin() throws RepositoryException {
        String sql = "SELECT car.* from [car:Car] as car JOIN [nt:unstructured] as category ON ISCHILDNODE(car,category) WHERE NAME(category) LIKE 'Utility'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        assertThat(query, is(notNullValue()));
        QueryResult result = query.execute();
        validateQuery().rowCount(5).hasColumns(carColumnNames("car")).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithChildNodeJoinAndColumnsFromBothSidesOfJoin()
        throws RepositoryException {
        String sql = "SELECT car.*, category.[jcr:primaryType] from [car:Car] as car JOIN [nt:unstructured] as category ON ISCHILDNODE(car,category) WHERE NAME(category) LIKE 'Utility'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allOf(carColumnNames("car"), new String[] {"category.jcr:primaryType"});
        validateQuery().rowCount(5).hasColumns(columnNames).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithDescendantNodeJoinWithoutCriteria() throws RepositoryException {
        // The 'all' selector will find all nodes, including '/Car' and '/Car/Sports'. Thus,
        // '/Car/Sports/Infiniti G37' will be a descendant of both '/Car' and '/Car/Sports', and thus will appear
        // once joined with '/Car' and once joined with '/Car/Sports'. These two tuples will be similar, but they are
        // actually not repeats (since the columns from 'all' will be different).
        String sql = "SELECT * FROM [car:Car] as car JOIN [nt:unstructured] as all ON ISDESCENDANTNODE(car,all)";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allOf(carColumnNames("car"), allColumnNames("all"));
        validateQuery().rowCount(26).hasColumns(columnNames).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithDescendantNodeJoinWithDepthCriteria() throws RepositoryException {
        String sql = "SELECT * FROM [car:Car] as car JOIN [nt:unstructured] as category ON ISDESCENDANTNODE(car,category) WHERE DEPTH(category) = 2";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allOf(carColumnNames("car"), allColumnNames("category"));
        validateQuery().rowCount(13).hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-2151" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithChildCountCriteria() throws RepositoryException {
        String sql = "SELECT * FROM [car:Car] as car";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        String[] columnNames = carColumnNames("car");
        // No cars have children
        validateQuery().rowCount(13).hasColumns(columnNames).validate(query, query.execute());

        sql = "SELECT * FROM [car:Car] as car WHERE CHILDCOUNT(car) = 0";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(13).hasColumns(columnNames).validate(query, query.execute());

        sql = "SELECT [jcr:path], [mode:childCount] FROM [nt:unstructured] WHERE CHILDCOUNT() = 4";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(2).validate(query, query.execute());
    }

    @FixFor( "MODE-2151" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithChildNodeJoinWithChildCountCriteria() throws RepositoryException {
        String sql = "SELECT * FROM [car:Car] as car JOIN [nt:unstructured] as category ON ISCHILDNODE(car,category) WHERE CHILDCOUNT(category) > 4";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        String[] columnNames = allOf(carColumnNames("car"), allColumnNames("category"));
        // Just the "Utility" nodes ...
        validateQuery().rowCount(5).hasColumns(columnNames).validate(query, query.execute());

        sql = "SELECT * FROM [car:Car] as car JOIN [nt:unstructured] as category ON ISCHILDNODE(car,category) WHERE CHILDCOUNT(category) > 2";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        // Just the "Utility", "Hybrid", and "Luxury" nodes ...
        validateQuery().rowCount(11).hasColumns(columnNames).validate(query, query.execute());

        sql = "SELECT * FROM [car:Car] as car JOIN [nt:unstructured] as category ON ISCHILDNODE(car,category)";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        // All categories ...
        validateQuery().rowCount(13).hasColumns(columnNames).validate(query, query.execute());
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithDescendantNodeJoin() throws RepositoryException {
        String sql = "SELECT car.* from [car:Car] as car JOIN [nt:unstructured] as category ON ISDESCENDANTNODE(car,category) WHERE NAME(category) LIKE 'Utility'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(5).hasColumns(carColumnNames("car")).validate(query, result);
    }

    @FixFor( "MODE-1809" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2UnionOfQueriesWithJoins() throws RepositoryException {
        String sql1 = "SELECT car.* from [car:Car] as car JOIN [nt:unstructured] as category ON ISDESCENDANTNODE(car,category) WHERE NAME(category) LIKE 'Utility'";
        String sql2 = "SELECT car.* from [car:Car] as car JOIN [nt:unstructured] as category ON ISDESCENDANTNODE(car,category) WHERE NAME(category) LIKE 'Sports'";
        String sql = sql1 + " UNION " + sql2;

        String[] columnNames = carColumnNames("car");
        // print = true;
        Query query = session.getWorkspace().getQueryManager().createQuery(sql1, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(5).hasColumns(columnNames).validate(query, result);

        query = session.getWorkspace().getQueryManager().createQuery(sql2, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().rowCount(2).hasColumns(columnNames).validate(query, result);

        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().rowCount(7).hasColumns(columnNames).validate(query, result);
    }

    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithDescendantNodeJoinAndColumnsFromBothSidesOfJoin()
        throws RepositoryException {
        String sql = "SELECT car.*, category.[jcr:primaryType] from [car:Car] as car JOIN [nt:unstructured] as category ON ISDESCENDANTNODE(car,category) WHERE NAME(category) LIKE 'Utility'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allOf(carColumnNames("car"), new String[] {"category.jcr:primaryType"});
        validateQuery().rowCount(5).hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-829" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithDescendantNodeJoinUsingNtBase() throws RepositoryException {
        String sql = "SELECT * FROM [nt:base] AS category JOIN [nt:base] AS cars ON ISDESCENDANTNODE(cars,category) WHERE ISCHILDNODE(category,'/Cars')";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = {"category.jcr:primaryType", "cars.jcr:primaryType", "cars.jcr:mixinTypes",
            "category.jcr:mixinTypes", "cars.jcr:name", "cars.jcr:path", "cars.jcr:score", "cars.mode:depth", "cars.mode:id",
            "cars.mode:localName", "category.jcr:name", "category.jcr:path", "category.jcr:score", "category.mode:depth",
            "category.mode:id", "category.mode:localName"};
        validateQuery().rowCount(13).hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-829" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithDescendantNodeJoinUsingNtBaseAndNameConstraint()
        throws RepositoryException {
        String sql = "SELECT * FROM [nt:base] AS category JOIN [nt:base] AS cars ON ISDESCENDANTNODE(cars,category) WHERE ISCHILDNODE(category,'/Cars') AND NAME(cars) LIKE 'Toyota%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = {"category.jcr:primaryType", "cars.jcr:primaryType", "cars.jcr:mixinTypes",
            "category.jcr:mixinTypes", "cars.jcr:name", "cars.jcr:path", "cars.jcr:score", "cars.mode:depth", "cars.mode:id",
            "cars.mode:localName", "category.jcr:name", "category.jcr:path", "category.jcr:score", "category.mode:depth",
            "category.mode:id", "category.mode:localName"};
        validateQuery().rowCount(3).hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-829" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithDescendantNodeJoinUsingNonExistantNameColumnOnTypeWithResidualProperties()
        throws RepositoryException {
        String sql = "SELECT * FROM [nt:unstructured] AS category JOIN [nt:unstructured] AS cars ON ISDESCENDANTNODE(cars,category) WHERE ISCHILDNODE(category,'/Cars') AND cars.name = 'd2'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = {"category.jcr:primaryType", "cars.jcr:primaryType", "cars.jcr:mixinTypes",
            "category.jcr:mixinTypes", "cars.jcr:name", "cars.jcr:path", "cars.jcr:score", "cars.mode:depth", "cars.mode:id",
            "cars.mode:localName", "category.jcr:name", "category.jcr:path", "category.jcr:score", "category.mode:depth",
            "category.mode:id", "category.mode:localName"};
        // no nodes have a 'name' property (strictly speaking)
        validateQuery().rowCount(0).hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-829" )
    @Test
    public void shouldReturnNoResultsForJcrSql2QueryWithDescendantNodeJoinUsingNonExistantNameColumnOnTypeWithNoResidualProperties()
        throws RepositoryException {
        String sql = "SELECT * FROM [nt:base] AS category JOIN [nt:base] AS cars ON ISDESCENDANTNODE(cars,category) WHERE ISCHILDNODE(category,'/Cars') AND cars.name = 'd2'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = {"category.jcr:primaryType", "cars.jcr:primaryType", "cars.jcr:mixinTypes",
            "category.jcr:mixinTypes", "cars.jcr:name", "cars.jcr:path", "cars.jcr:score", "cars.mode:depth", "cars.mode:id",
            "cars.mode:localName", "category.jcr:name", "category.jcr:path", "category.jcr:score", "category.mode:depth",
            "category.mode:id", "category.mode:localName"};
        // no results, because one side of the join has criteria on a non-existant column
        validateQuery().rowCount(0).hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-869" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithSubqueryInCriteria() throws RepositoryException {
        String sql = "SELECT * FROM [car:Car] WHERE [car:maker] IN (SELECT [car:maker] FROM [car:Car] WHERE [car:year] >= 2008)";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        // there are 13 cars made by makers that made cars in or after 2008
        // The only car made before 2008 is Toyota Land Rover, but Toyota also makes the Highlander and Prius in 2008.
        validateQuery().rowCount(13).hasColumns(carColumnNames("car:Car")).validate(query, result);
    }

    @FixFor( "MODE-869" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithSubqueryInCriteria2() throws RepositoryException {
        String sql = "SELECT [car:maker] FROM [car:Car] WHERE PATH() LIKE '%/Hybrid/%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(3).hasColumns("car:maker").validate(query, result);

        sql = "SELECT * FROM [car:Car] WHERE [car:maker] IN ('Toyota','Nissan')";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        // the 4 kinds of cars made by makers that make hybrids
        validateQuery().rowCount(4).hasColumns(carColumnNames("car:Car")).validate(query, result);

        sql = "SELECT * FROM [car:Car] WHERE [car:maker] IN (SELECT [car:maker] FROM [car:Car] WHERE PATH() LIKE '%/Hybrid/%')";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        // the 4 kinds of cars made by makers that make hybrids
        validateQuery().rowCount(4).hasColumns(carColumnNames("car:Car")).validate(query, result);
    }

    @FixFor( "MODE-2425" )
    @Test
    public void shouldBeAbleToUsePathOperandWithInQuery() throws RepositoryException {
        String sql = "SELECT [car:maker] FROM [car:Car] WHERE PATH()" +
                     " IN ('/Cars/Hybrid/Toyota Prius', '/Cars/Hybrid/Toyota Highlander', '/Cars/Hybrid/Nissan Altima')";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        final List<String> expectedMakers = new ArrayList<>(Arrays.asList("Toyota", "Toyota", "Nissan"));
        validateQuery().rowCount(3).onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber, Row row ) throws RepositoryException {
                String actualValue = row.getValue("car:maker").getString();
                expectedMakers.remove(actualValue);                
            }
        }).validate(query, result);
        assertTrue("Not all expected car makers found", expectedMakers.isEmpty());
    }

    @FixFor( "MODE-1873" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithSubqueryInCriteriaWhenSubquerySelectsPseudoColumn()
        throws RepositoryException {
        String sql = "SELECT [jcr:path] FROM [nt:unstructured] WHERE PATH() LIKE '/Other/%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(4).hasColumns("jcr:path").validate(query, result);

        sql = "SELECT [jcr:path],[pathProperty] FROM [nt:unstructured] WHERE [pathProperty] = '/Other/NodeA'";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path", "pathProperty").validate(query, result);

        sql = "SELECT [jcr:path],[pathProperty] FROM [nt:unstructured] WHERE [pathProperty] IN ('/Other/NodeA[2]', '/Other/NodeA', '/Other/NodeC', '/Other/NodeA[3]')";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path", "pathProperty").validate(query, result);

        sql = "SELECT [jcr:path] FROM [nt:unstructured] WHERE [pathProperty] IN (SELECT [jcr:path] FROM [nt:unstructured] WHERE PATH() LIKE '/Other/%')";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path").validate(query, result);
    }

    @FixFor( "MODE-909" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithOrderBy() throws RepositoryException {
        String sql = "SELECT [jcr:primaryType] from [nt:base] ORDER BY [jcr:primaryType]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(totalNodeCount).hasColumns("jcr:primaryType").onEachRow(new Predicate() {
            private String lastPrimaryType;

            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                String primaryType = row.getValue("jcr:primaryType").getString();
                if (lastPrimaryType != null) {
                    assertThat(primaryType.compareTo(lastPrimaryType) >= 0, is(true));
                }
                lastPrimaryType = primaryType;
            }
        }).validate(query, result);
    }

    protected Predicate pathOrder() {
        return new Predicate() {
            private Path lastPath;

            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                String pathStr = row.getValue("jcr:path").getString();
                Path path = path(pathStr);
                if (lastPath != null) {
                    assertThat(path.compareTo(lastPath) >= 0, is(true));
                }
                lastPath = path;
            }
        };
    }

    @FixFor( "MODE-2138" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithOrderByPathPseudoColumn() throws RepositoryException {
        String sql = "SELECT [jcr:path] from [nt:base] ORDER BY [jcr:path]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(totalNodeCount).hasColumns("jcr:path").onEachRow(pathOrder()).validate(query, result);
    }

    @FixFor( "MODE-2138" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithOrderByPath() throws RepositoryException {
        String sql = "SELECT [jcr:path] from [nt:base] ORDER BY [jcr:path]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(totalNodeCount).hasColumns("jcr:path").onEachRow(pathOrder()).validate(query, result);
    }

    @FixFor( {"MODE-1277", "MODE-1485"} )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithFullOuterJoin() throws RepositoryException {
        String sql = "SELECT car.[jcr:name], category.[jcr:primaryType], car.[jcr:path], category.[jcr:path] from [car:Car] as car FULL OUTER JOIN [nt:unstructured] as category ON ISCHILDNODE(car,category) WHERE NAME(car) LIKE 'Toyota*'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = {"car.jcr:name", "category.jcr:primaryType", "car.jcr:path", "category.jcr:path"};
        // Because every "Toyota" car has a parent, there will be no 'car' nodes that don't have a category. However,
        // we'll see every category node, and there are 25 of them. Therefore, we'll see 25 nodes.
        validateQuery().rowCount(25).hasColumns(columnNames).onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                // Every row should have a category ...
                assertNotNull(row.getNode("category"));
                // Every non-null car should be a Toyota ...
                Node car = row.getNode("car");
                if (car != null) assertTrue(car.getName().contains("Toyota"));
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1750" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithLeftOuterJoinOnNullCondition() throws RepositoryException {
        String sql = "SELECT car1.[jcr:name] from [car:Car] as car1 LEFT OUTER JOIN [car:Car] as car2 ON car1.[car:alternateModesl] = car2.[UUID]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns("car1.jcr:name").validate(query, result);
    }

    @FixFor( "MODE-2187" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithLeftOuterJoinOnIsChildNode() throws RepositoryException {
        String sql = "SELECT left.[jcr:path] FROM [nt:unstructured] AS left LEFT OUTER JOIN [nt:unstructured] AS right ON ISCHILDNODE(left,right) WHERE ISDESCENDANTNODE(left,'/Cars')";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(17).hasColumns("left.jcr:path").validate(query, result);
    }

    @FixFor( "MODE-2494" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithTwoLeftOuterJoinsOnIsChildNodeWithSubsequentIsChildNode() throws RepositoryException {
        String sql = "SELECT parent.[jcr:path], child1.[jcr:name], desc.[jcr:name] FROM [nt:unstructured] AS parent " +
                "LEFT OUTER JOIN [nt:unstructured] AS child1 ON ISCHILDNODE(child1,parent) " +
                "INNER JOIN [nt:unstructured] AS desc on ISCHILDNODE(desc, child1) " +
                "LEFT OUTER JOIN [nt:unstructured] AS child2 ON ISCHILDNODE(child2,parent) " +
                "WHERE ISCHILDNODE(parent,'/') " +
                "AND NAME(child2) = 'Hybrid' " +
                "AND NAME(desc) LIKE 'Nissan%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).validate(query, result);
    }

    @FixFor( "MODE-2494" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithTwoLeftOuterJoinsOnIsChildNodeWithSubsequentIsDescendantNode() throws RepositoryException {
        String sql = "SELECT parent.[jcr:path], child1.[jcr:name], desc.[jcr:name] FROM [nt:unstructured] AS parent " +
                "LEFT OUTER JOIN [nt:unstructured] AS child1 ON ISCHILDNODE(child1,parent) " +
                "INNER JOIN [nt:unstructured] AS desc on ISDESCENDANTNODE(desc, child1) " +
                "LEFT OUTER JOIN [nt:unstructured] AS child2 ON ISCHILDNODE(child2,parent) " +
                "WHERE ISCHILDNODE(parent,'/') " +
                "AND NAME(child2) = 'Hybrid' " +
                "AND NAME(desc) LIKE 'Nissan%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).validate(query, result);
    }

    @FixFor( "MODE-2494" )
    @Test
    @Ignore("This is not fixed by the fix for MODE-2494, and points to a potentially deeper problem, " +
            "possibly in ReplaceViews." +
            "Note: this query has the same semantics as that in " +
            "'shouldBeAbleToCreateAndExecuteJcrSql2QueryWithTwoLeftOuterJoinsOnIsChildNodeWithSubsequentIsDescendantNode' " +
            "and should work exactly the same way.")
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithTwoLeftOuterJoinsOnIsChildNodeWithSubsequentIsDescendantNodeOutOfOrder() throws RepositoryException {
        String sql = "SELECT parent.[jcr:path], child1.[jcr:name], child2.[jcr:name], desc.[jcr:name] FROM [nt:unstructured] AS parent " +
                "LEFT OUTER JOIN [nt:unstructured] AS child1 ON ISCHILDNODE(child1,parent) " +
                "LEFT OUTER JOIN [nt:unstructured] AS child2 ON ISCHILDNODE(child2,parent) " +
                "INNER JOIN [nt:unstructured] AS desc on ISDESCENDANTNODE(desc, child1) " +
                "WHERE ISCHILDNODE(parent,'/') " +
                "AND NAME(child2) = 'Hybrid' " +
                "AND NAME(desc) LIKE 'Nissan%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).validate(query, result);
    }

    @FixFor( "MODE-1750" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithRightOuterJoinOnNullCondition() throws RepositoryException {
        String sql = "SELECT car2.[jcr:name] from [car:Car] as car1 RIGHT OUTER JOIN [car:Car] as car2 ON car1.[car:alternateModesl] = car2.[UUID]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns("car2.jcr:name").validate(query, result);
    }

    @FixFor( "MODE-2057" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithJoinAndNoCriteria() throws RepositoryException {
        String sql = "SELECT category.[jcr:path], cars.[jcr:path] FROM [nt:unstructured] AS category JOIN [car:Car] AS cars ON ISCHILDNODE(cars,category)";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns("category.jcr:path", "cars.jcr:path").validate(query, result);
    }

    @FixFor( "MODE-2057" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithJoinAndDepthCriteria() throws RepositoryException {
        String sql = "SELECT category.[jcr:path], cars.[jcr:path] FROM [nt:unstructured] AS category JOIN [car:Car] AS cars ON ISCHILDNODE(cars,category) WHERE DEPTH(category) = 2";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns("category.jcr:path", "cars.jcr:path").validate(query, result);
    }

    @FixFor( "MODE-2057" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithLeftOuterJoinAndDepthCriteria() throws RepositoryException {
        String sql = "SELECT category.[jcr:path], cars.[jcr:path] FROM [nt:unstructured] AS category LEFT OUTER JOIN [car:Car] AS cars ON ISCHILDNODE(cars,category) WHERE DEPTH(category) = 2";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(17).hasColumns("category.jcr:path", "cars.jcr:path").validate(query, result);
    }

    @FixFor( "MODE-2057" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithLeftOuterJoinWithFullTextSearch() throws RepositoryException {
        String sql = "SELECT category.[jcr:path], cars.[jcr:path] FROM [nt:unstructured] AS category LEFT OUTER JOIN [car:Car] AS cars ON ISCHILDNODE(cars,category) WHERE contains(category.*, 'Utility') AND contains(cars.*, 'Toyota') ";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("category.jcr:path", "cars.jcr:path").validate(query, result);
    }

    @FixFor( "MODE-2057" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithJoinWithFullTextSearch() throws RepositoryException {
        String sql = "SELECT category.[jcr:path], cars.[jcr:path] FROM [nt:unstructured] AS category JOIN [car:Car] AS cars ON ISCHILDNODE(cars,category) WHERE contains(category.*, 'Utility') AND contains(cars.*, 'Toyota') ";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("category.jcr:path", "cars.jcr:path").validate(query, result);
    }

    @FixFor( "MODE-2057" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithUnionAndFullTextSearch() throws RepositoryException {
        String sql = "SELECT category.[jcr:path] AS p FROM [nt:unstructured] AS category WHERE contains(category.*, 'Utility')"
                     + "UNION "
                     + "SELECT category.[jcr:path] AS p FROM [nt:unstructured] AS category JOIN [car:Car] AS cars ON ISCHILDNODE(cars,category) WHERE contains(cars.*, 'Toyota') ";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(2).hasColumns("p").validate(query, result);
    }

    @FixFor( "MODE-1679" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindReferenceableNodes() throws RepositoryException {
        String sql = "SELECT [jcr:uuid] FROM [mix:referenceable]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(4).hasColumns("jcr:uuid").validate(query, result);
    }

    @FixFor( "MODE-1679" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindJcrUuidOfNodeWithPathCriteria() throws RepositoryException {
        String sql = "SELECT [jcr:uuid] FROM [mix:referenceable] AS node WHERE PATH(node) = '/Other/NodeA[2]'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:uuid").validate(query, result);
    }

    @FixFor( "MODE-1679" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryToFindNodesOfParticularPrimaryType() throws RepositoryException {
        String sql = "SELECT [notion:singleReference], [notion:multipleReferences] FROM [notion:typed]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("notion:singleReference", "notion:multipleReferences").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertValueIsNonNullReference(row, "notion:singleReference");
                assertValueIsNonNullReference(row, "notion:multipleReferences");
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1679" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithSingleReferenceConstraintUsingSubquery() throws RepositoryException {
        String sql = "SELECT [notion:singleReference] FROM [notion:typed] WHERE [notion:singleReference] IN ( SELECT [jcr:uuid] FROM [mix:referenceable] AS node WHERE PATH(node) = '/Other/NodeA')";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("notion:singleReference").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertValueIsNonNullReference(row, "notion:singleReference");
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1679" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithSingleReferenceConstraintUsingStringIdentifier()
        throws RepositoryException {
        String id = session.getNode("/Other/NodeA").getIdentifier();
        assertThat(id, is(notNullValue()));
        String sql = "SELECT [notion:singleReference] FROM [notion:typed] AS typed WHERE [notion:singleReference] = '" + id + "'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("notion:singleReference").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertValueIsNonNullReference(row, "notion:singleReference");
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1679" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithSingleReferenceConstraintUsingJoin() throws RepositoryException {
        String sql = "SELECT typed.* FROM [notion:typed] AS typed JOIN [mix:referenceable] AS target ON typed.[notion:singleReference] = target.[jcr:uuid] WHERE PATH(target) = '/Other/NodeA'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns(typedColumnNames("typed")).onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertValueIsNonNullReference(row, "typed.notion:singleReference");
                assertValueIsNonNullReference(row, "typed.notion:multipleReferences");
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1679" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithMultipleReferenceConstraintUsingSubquery()
        throws RepositoryException {
        String sql = "SELECT [notion:multipleReferences] FROM [notion:typed] WHERE [notion:multipleReferences] IN ( SELECT [jcr:uuid] FROM [mix:referenceable] AS node WHERE PATH(node) = '/Other/NodeA[2]')";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("notion:multipleReferences").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertValueIsNonNullReference(row, "notion:multipleReferences");
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1679" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithMultipleReferenceConstraintUsingStringIdentifier()
        throws RepositoryException {
        String id = session.getNode("/Other/NodeA[2]").getIdentifier();
        assertThat(id, is(notNullValue()));
        String sql = "SELECT [notion:multipleReferences] FROM [notion:typed] AS typed WHERE [notion:multipleReferences] = '" + id
                     + "'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("notion:multipleReferences").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertValueIsNonNullReference(row, "notion:multipleReferences");
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1679" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithMultipleReferenceConstraintUsingJoin() throws RepositoryException {
        String sql = "SELECT typed.* FROM [notion:typed] AS typed JOIN [mix:referenceable] AS target ON typed.[notion:multipleReferences] = target.[jcr:uuid] WHERE PATH(target) = '/Other/NodeA[2]'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        assertThat(query, is(notNullValue()));
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns(typedColumnNames("typed")).onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertValueIsNonNullReference(row, "typed.notion:singleReference");
                assertValueIsNonNullReference(row, "typed.notion:multipleReferences");
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-934" )
    @Test
    public void shouldParseQueryWithUnqualifiedPathInSelectOfJcrSql2Query() throws RepositoryException {
        String sql = "select [jcr:primaryType], [jcr:path] FROM [nt:base]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(totalNodeCount).hasColumns("jcr:primaryType", "jcr:path").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertNotNull(row.getValue("jcr:primaryType"));
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-934" )
    @Test
    public void shouldParseQueryWithUnqualifiedPathInSelectAndCriteriaOfJcrSql2Query() throws RepositoryException {
        String sql = "select [jcr:primaryType], [jcr:path] FROM [nt:base] WHERE [jcr:path] LIKE '/Cars/%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(17).hasColumns("jcr:primaryType", "jcr:path").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertNotNull(row.getValue("jcr:primaryType"));
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-934" )
    @Test
    public void shouldParseQueryWithUnqualifiedPathInSelectAndUnqualifiedNameInCriteriaOfJcrSql2Query()
        throws RepositoryException {
        String sql = "select [jcr:primaryType], [jcr:path] FROM [nt:base] WHERE [jcr:name] LIKE '%3%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(4).hasColumns("jcr:primaryType", "jcr:path").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertNotNull(row.getValue("jcr:primaryType"));
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-934" )
    @Test
    public void shouldParseQueryWithUnqualifiedPathInSelectAndUnqualifiedLocalNameInCriteriaOfJcrSql2Query()
        throws RepositoryException {
        String sql = "select [jcr:primaryType], [jcr:path] FROM [nt:base] WHERE [mode:localName] LIKE '%3%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(4).hasColumns("jcr:primaryType", "jcr:path").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertNotNull(row.getValue("jcr:primaryType"));
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-934" )
    @Test
    public void shouldParseQueryWithJcrPathInJoinCriteriaOfJcrSql2Query() throws RepositoryException {
        String sql = "select base.[jcr:primaryType], base.[jcr:path], car.[car:year] FROM [nt:base] AS base JOIN [car:Car] AS car ON car.[jcr:path] = base.[jcr:path]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns("base.jcr:primaryType", "base.jcr:path", "car.car:year").validate(query, result);
    }

    @FixFor( "MODE-934" )
    @Test
    public void shouldNotIncludePseudoColumnsInSelectStarOfJcrSql2Query() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("select * FROM [nt:base]", Query.JCR_SQL2);
        assertThat(query, is(notNullValue()));
        QueryResult result = query.execute();
        validateQuery().rowCount(totalNodeCount).hasColumns(allColumnNames("nt:base")).validate(query, result);
    }

    @FixFor( "MODE-1738" )
    @Test
    public void shouldSupportJoinWithOrderByOnPseudoColumn() throws RepositoryException {
        String sql = "SELECT category.[jcr:path], cars.[car:maker], cars.[car:lengthInInches] FROM [nt:unstructured] AS category JOIN [car:Car] AS cars ON ISDESCENDANTNODE(cars,category) WHERE ISCHILDNODE(category,'/Cars') ORDER BY cars.[mode:localName]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] expectedColumns = {"category.jcr:path", "cars.car:maker", "cars.mode:localName", "cars.car:lengthInInches"};
        validateQuery().rowCount(13).hasColumns(expectedColumns).validate(query, result);
    }

    @FixFor( "MODE-1738" )
    @Test
    public void shouldSupportJoinWithOrderByOnActualColumn() throws RepositoryException {
        String sql = "SELECT category.[jcr:path], cars.[car:maker], cars.[car:lengthInInches] FROM [nt:unstructured] AS category JOIN [car:Car] AS cars ON ISDESCENDANTNODE(cars,category) WHERE ISCHILDNODE(category,'/Cars') ORDER BY cars.[car:maker]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] expectedColumns = {"category.jcr:path", "cars.car:maker", "cars.car:lengthInInches"};
        validateQuery().rowCount(13).hasColumns(expectedColumns).validate(query, result);
    }

    @FixFor( "MODE-1737" )
    @Test
    public void shouldSupportSelectDistinct() throws RepositoryException {
        String sql = "SELECT DISTINCT cars.[car:maker], cars.[car:lengthInInches] FROM [car:Car] AS cars";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] expectedColumns = {"car:maker", "car:lengthInInches"};
        validateQuery().rowCount(13).hasColumns(expectedColumns).validate(query, result);
    }

    @FixFor( "MODE-1737" )
    @Test
    public void shouldSupportJoinWithSelectDistinct() throws RepositoryException {
        String sql = "SELECT DISTINCT category.[jcr:path], cars.[car:maker], cars.[car:lengthInInches] FROM [nt:unstructured] AS category JOIN [car:Car] AS cars ON ISDESCENDANTNODE(cars,category) WHERE ISCHILDNODE(category,'/Cars')";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] expectedColumns = {"category.jcr:path", "cars.car:maker", "cars.car:lengthInInches"};
        validateQuery().rowCount(13).hasColumns(expectedColumns).validate(query, result);
    }

    @FixFor( "MODE-1020" )
    @Test
    public void shouldFindAllPublishAreas() throws Exception {
        String sql = "SELECT [jcr:path], [jcr:title], [jcr:description] FROM [mode:publishArea]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(0).hasColumns("jcr:path", "jcr:title", "jcr:description").validate(query, result);
    }

    @FixFor( "MODE-1052" )
    @Test
    public void shouldProperlyUseNotWithPathConstraints() throws Exception {
        // Find all nodes that are children of '/Cars' ... there should be 4 ...
        String sql = "SELECT [jcr:path] FROM [nt:base] WHERE ISCHILDNODE([nt:base],'/Cars') ORDER BY [jcr:path]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] paths = {"/Cars/Hybrid", "/Cars/Luxury", "/Cars/Sports", "/Cars/Utility"};
        validateQuery().rowCount(4).hasColumns("jcr:path").hasNodesAtPaths(paths).validate(query, result);

        // Find all nodes ... there should be 24 ...
        sql = "SELECT [jcr:path] FROM [nt:base] ORDER BY [jcr:path]";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().rowCount(totalNodeCount).hasColumns("jcr:path").validate(query, result);

        // Find all nodes that are NOT children of '/Cars' (and not under '/jcr:system') ...
        sql = "SELECT [jcr:path] FROM [nt:base] WHERE NOT(ISCHILDNODE([nt:base],'/Cars')) AND NOT(ISDESCENDANTNODE([nt:base],'/jcr:system')) ORDER BY [jcr:path]";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        paths = new String[] {"/", "/Cars", "/Cars/Hybrid/Nissan Altima", "/Cars/Hybrid/Toyota Highlander",
            "/Cars/Hybrid/Toyota Prius", "/Cars/Luxury/Bentley Continental", "/Cars/Luxury/Cadillac DTS",
            "/Cars/Luxury/Lexus IS350", "/Cars/Sports/Aston Martin DB9", "/Cars/Sports/Infiniti G37", "/Cars/Utility/Ford F-150",
            "/Cars/Utility/Hummer H3", "/Cars/Utility/Land Rover LR2", "/Cars/Utility/Land Rover LR3",
            "/Cars/Utility/Toyota Land Cruiser", "/NodeB", "/Other", "/Other/NodeA", "/Other/NodeA[2]", "/Other/NodeA[3]",
            "/Other/NodeC", "/jcr:system"};
        validateQuery().rowCount(22).hasColumns("jcr:path").validate(query, result);
    }

    @FixFor( "MODE-1110" )
    @Test
    public void shouldExecuteQueryWithThreeInnerJoinsAndCriteriaOnDifferentSelectors() throws Exception {
        String sql = "SELECT * from [nt:base] as car INNER JOIN [nt:base] as categories ON ISDESCENDANTNODE(car, categories) "
                     + " INNER JOIN [nt:base] as carsNode ON ISDESCENDANTNODE (categories, carsNode) "
                     + " WHERE PATH(carsNode) = '/Cars' AND ISDESCENDANTNODE( categories, '/Cars') OR car.[jcr:primaryType] IS NOT NULL";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        String[] columnNames = allOf(allColumnNames("car"), allColumnNames("categories"), allColumnNames("carsNode"));
        validateQuery().rowCount(13).hasColumns(columnNames).validate(query, result);
    }

    @FixFor( "MODE-2054" )
    @Test
    public void shouldExecuteJoinWithOrConstraintsOnEachSide() throws Exception {
        // Find all of the cars under 'Utility' and 'Luxury' categories ...
        String sql = "SELECT car.[jcr:path], category.[jcr:path] from [car:Car] as car INNER JOIN [nt:unstructured] as category ON ISCHILDNODE(car, category) "
                     + " WHERE NAME(category) LIKE '%y'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        // print = true;
        validateQuery().rowCount(8).validate(query, query.execute());

        sql = "SELECT car.[jcr:path], category.[jcr:path] from [car:Car] as car INNER JOIN [nt:unstructured] as category ON ISCHILDNODE(car, category) ";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(13).validate(query, query.execute());

        sql = "SELECT car.[jcr:path], category.[jcr:path] from [car:Car] as car INNER JOIN [nt:unstructured] as category ON ISCHILDNODE(car, category) "
              + " WHERE NAME(category) LIKE '%y' OR NAME(car) LIKE 'Toyota %'";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(10).validate(query, query.execute());
    }

    @FixFor( "MODE-1418" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithFullTextSearchWithSelectorAndOneProperty()
        throws RepositoryException {
        String sql = "select [jcr:path] from [nt:unstructured] as n where contains(n.something, 'cat wearing')";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path").validate(query, result);
    }

    @FixFor( "MODE-1418" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithFullTextSearchWithSelectorAndAllProperties()
        throws RepositoryException {
        String sql = "select [jcr:path] from [nt:unstructured] as n where contains(n.*, 'cat wearing')";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path").validate(query, result);
    }

    @FixFor( "MODE-1418" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithFullTextSearchWithNoSelectorAndOneProperty()
        throws RepositoryException {
        String sql = "select [jcr:path] from [nt:unstructured] as n where contains(something,'cat wearing')";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path").hasNodesAtPaths("/Other/NodeA[2]").validate(query, result);
    }

    @FixFor( "MODE-1829" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithFullTextSearchUsingLeadingWildcard() throws RepositoryException {
        String sql = "select [jcr:path] from [nt:unstructured] as n where contains(n.something, '*earing')";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path").validate(query, result);

        sql = "select [jcr:path] from [nt:unstructured] as n where contains(n.something, '*earing*')";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path").validate(query, result);
    }

    @FixFor( "MODE-1829" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithFullTextSearchUsingTrailingWildcard() throws RepositoryException {
        String sql = "select [jcr:path] from [nt:unstructured] as n where contains(n.something, 'wea*')";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path").validate(query, result);
    }

    @Test
    @FixFor( "MODE-1547" )
    public void shouldBeAbleToExecuteFullTextSearchQueriesOnPropertiesWhichIncludeStopWords() throws Exception {
        String propertyText = "the quick Brown fox jumps over to the dog in at the gate";
        Node ftsNode = session.getRootNode().addNode("FTSNode").setProperty("FTSProp", propertyText).getParent();
        ftsNode.addMixin("mix:title");
        try {
            session.save();

            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains([mix:title].*,'"
                                         + propertyText + "')");

            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(FTSProp,'" + propertyText
                                         + "')");
            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(n.*,'" + propertyText
                                         + "')");

            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(FTSProp,'"
                                         + propertyText.toUpperCase() + "')");
            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(n.*,'"
                                         + propertyText.toUpperCase() + "')");

            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(FTSProp,'the quick Dog')");
            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(n.*,'the quick Dog')");

            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(FTSProp,'the quick jumps over gate')");
            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(n.*,'the quick jumps over gate')");

            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(FTSProp,'the gate')");
            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(n.*,'the gate')");
        } finally {
            // Try to remove the node (which messes up the expected results from subsequent tests) ...
            ftsNode.remove();
            session.save();
        }
    }

    @Test
    @FixFor( "MODE-2448" )
    public void shouldBeAbleToExecuteFullTextSearchQueriesOnPropertiesWhichIncludeUmlauts() throws Exception {
        String propertyText = "Änderung: der schnelle braune Fuchs springt über den Hund";
        Node ftsNode = session.getRootNode().addNode("FTSNode").setProperty("FTSProp", propertyText).getParent();
        ftsNode.addMixin("mix:title");
        try {
            session.save();

            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains([mix:title].*,'"
                                         + propertyText + "')");

            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(FTSProp,'" + propertyText
                                         + "')");
            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(n.*,'" + propertyText
                                         + "')");

            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(FTSProp,'"
                                         + propertyText.toUpperCase() + "')");
            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(n.*,'"
                                         + propertyText.toUpperCase() + "')");

            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(FTSProp,'Änderung')");
            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(FTSProp,'änderung')");
            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(FTSProp,'ÄNDERUNG')");
            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(FTSProp,'über den Hund')");
            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(FTSProp,'Über den Hund')");
            executeQueryWithSingleResult("select [jcr:path] from [mix:title] as n where contains(FTSProp,'ÜbEr dEn Hund')");
        } finally {
            // Try to remove the node (which messes up the expected results from subsequent tests) ...
            ftsNode.remove();
            session.save();
        }
    }

    private void executeQueryWithSingleResult( String sql ) throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, JcrRepository.QueryLanguage.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path").validate(query, result);
    }

    @FixFor( "MODE-1840" )
    @Test
    public void shouldBeAbleToCreateAndExecuteJcrSql2QueryWithBindVariableInsideContains() throws RepositoryException {
        String sql = "select [jcr:path] from [nt:unstructured] as n where contains(n.something, $expression)";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        query.bindValue("expression", session.getValueFactory().createValue("cat wearing"));
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path").hasNodesAtPaths("/Other/NodeA[2]").validate(query, result);
    }

    @FixFor( "MODE-1840" )
    @Test( expected = InvalidQueryException.class )
    public void shouldNotBeAbleToCreateAndExecuteJcrSql2QueryWithBindVariableInsideContainsIfVariableIsNotBound()
        throws RepositoryException {
        String sql = "select [jcr:path] from [nt:unstructured] as n where contains(n.something, $expression)";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        assertThat(query, is(notNullValue()));
        // do not bind a value
        // query.bindValue("expression", session.getValueFactory().createValue("cat wearing"));
        query.execute();
    }

    @FixFor( "MODE-1145" )
    @Test
    public void shouldParseFincayraQuery() throws Exception {
        String sql = "SELECT * FROM [fincayra.Post] AS post";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(0).validate(query, result);

        sql = "SELECT * FROM [fincayra.User] AS u WHERE u.email='test1@innobuilt.com'";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().rowCount(0).validate(query, result);

        sql = "SELECT post.\"jcr:uuid\", post.\"text\", post.\"user\" FROM [fincayra.Post] AS post JOIN [fincayra.User] AS u ON post.\"user\"=u.\"jcr:uuid\" WHERE u.email='test1@innobuilt.com'";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().rowCount(0).hasColumns("post.jcr:uuid", "post.text", "post.user").validate(query, result);
    }

    @FixFor( "MODE-1145" )
    @Test
    public void shouldParseFincayraQuery2() throws Exception {
        String sql = "SELECT post.\"jcr:uuid\", post.\"text\", post.\"user\" FROM [fincayra.UnstrPost] AS post JOIN [fincayra.UnstrUser] AS u ON post.\"user\"=u.\"jcr:uuid\" WHERE u.email='test1@innobuilt.com'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(0).hasColumns("post.jcr:uuid", "post.text", "post.user").validate(query, result);
    }

    @FixFor( "MODE-1145" )
    @Test
    public void shouldParseQueryWithResidualPropertyInSelectAndCriteria() throws Exception {
        String sql = "SELECT [jcr:path], something FROM [nt:unstructured] AS u WHERE something LIKE 'value%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        assertThat(query, is(notNullValue()));
        QueryResult result = query.execute();
        validateQuery().rowCount(2).hasColumns("jcr:path", "something").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertThat(row.getNode().hasProperty("something"), is(true));
            }
        }).validate(query, result);
    }

    @Test
    public void shouldFindSystemNodesUsingPathCriteria() throws Exception {
        String queryString = "select [jcr:path] from [nt:base] where [jcr:path] like '/jcr:system/%' and [jcr:path] not like '/jcr:system/%/%'";
        assertNodesAreFound(queryString, Query.JCR_SQL2, INDEXED_SYSTEM_NODES_PATHS);
    }

    @FixFor( "MODE-2286" )
    @Test
    public void shouldFindSystemNodesUsingPathLikeCriteriaWithNoSnsIndexSpecified() throws Exception {
        String sql = "select [jcr:path] from [nt:base] where [jcr:path] like '/Other/NodeA'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(1).validate(query, query.execute());
    }

    @FixFor( "MODE-2286" )
    @Test
    public void shouldFindSystemNodesUsingPathLikeCriteria() throws Exception {
        String sql = "select [jcr:path] from [nt:base] where [jcr:path] like '/Other/NodeA[%]'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(3).validate(query, query.execute());
    }

    @FixFor( "MODE-2286" )
    @Test
    public void shouldFindSystemNodesUsingPathLikeCriteriaWithAllSnsIndexSpecified() throws Exception {
        String sql = "select [jcr:path] from [nt:base] where [jcr:path] like '/Other[1]/NodeA[%]'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        validateQuery().rowCount(3).validate(query, query.execute());
    }

    @Test
    public void shouldFindSystemNodesUsingIsChildNodeCriteria() throws Exception {
        String queryString = "select [jcr:path] from [nt:base] where ischildnode('/jcr:system')";
        assertNodesAreFound(queryString, Query.JCR_SQL2, INDEXED_SYSTEM_NODES_PATHS);
    }

    @Test
    public void shouldFindBuiltInNodeTypes() throws Exception {
        String queryString = "select [jcr:path] from [nt:base] where ischildnode('/jcr:system/jcr:nodeTypes')";
        QueryManager queryManager = session.getWorkspace().getQueryManager();
        Query query = queryManager.createQuery(queryString, Query.JCR_SQL2);
        QueryResult result = query.execute();
        long numNodetypes = session.getWorkspace().getNodeTypeManager().getAllNodeTypes().getSize();
        validateQuery().rowCount(numNodetypes).validate(query, result);
    }

    @Test
    @FixFor( "MODE-1550" )
    public void shouldFindChildrenOfRootUsingIsChildNodeCriteria() throws Exception {
        Node node1 = session.getRootNode().addNode("node1");
        Node node2 = session.getRootNode().addNode("node2");

        try {
            // We didn't save our changes, so we shouldn't find the newly-added nodes
            String queryString = "select [jcr:path] from [nt:base] where ischildnode('/')";
            assertNodesAreFound(queryString, Query.JCR_SQL2, "/jcr:system", "/Cars", "/Other", "/NodeB");

            // Now save the session, and re-query to find the newly-added nodes ...
            session.save();

            // We should now find the newly-added nodes ...
            assertNodesAreFound(queryString, Query.JCR_SQL2, "/jcr:system", "/Cars", "/Other", "/NodeB", "/node1", "/node2");
        } finally {
            node1.remove();
            node2.remove();
            session.save();
        }
    }

    @SuppressWarnings( "deprecation" )
    @FixFor( "MODE-1680" )
    @Test
    public void testOrderByWithAliases() throws Exception {
        // fill the repository with test data
        Node src = session.getRootNode().addNode("src", "nt:folder");

        // add node f1 with child jcr:content
        try {
            Node f1 = src.addNode("f1", "nt:file");
            f1.addMixin("mix:simpleVersionable");
            Node content1 = f1.addNode("jcr:content", "nt:resource");
            content1.setProperty("jcr:data", session.getValueFactory().createBinary("Node f1".getBytes()));

            // save and slip a bit to have difference in time of node creation.
            session.save();
            Thread.sleep(100);

            // add node f2 with child jcr:content
            Node f2 = src.addNode("f2", "nt:file");
            f2.addMixin("mix:simpleVersionable");
            Node content2 = f2.addNode("jcr:content", "nt:resource");
            content2.setProperty("jcr:data", session.getValueFactory().createBinary("Node f2".getBytes()));

            session.save();

            // print = true;
            printMessage("-------------------- MyQueryTest---------------------");

            String descOrder = "SELECT [nt:file].[jcr:created] FROM [nt:file] INNER JOIN [nt:base] AS content ON ISCHILDNODE(content,[nt:file]) WHERE ([nt:file].[jcr:mixinTypes] = 'mix:simpleVersionable' AND NAME([nt:file]) LIKE 'f%') ORDER BY content.[jcr:lastModified] DESC";
            String ascOrder = "SELECT [nt:file].[jcr:created] FROM [nt:file] INNER JOIN [nt:base] AS content ON ISCHILDNODE(content,[nt:file]) WHERE ([nt:file].[jcr:mixinTypes] = 'mix:simpleVersionable' AND NAME([nt:file]) LIKE 'f%') ORDER BY content.[jcr:lastModified] ASC";

            QueryManager queryManager = session.getWorkspace().getQueryManager();
            Query query = queryManager.createQuery(descOrder, Query.JCR_SQL2);
            QueryResult result = query.execute();

            // checking first query
            RowIterator it = result.getRows();
            assertEquals(2, it.getSize());

            Node n1 = it.nextRow().getNode();
            Node n2 = it.nextRow().getNode();

            assertEquals("f2", n1.getName());
            assertEquals("f1", n2.getName());

            // the same request with other order
            query = queryManager.createQuery(ascOrder, Query.JCR_SQL2);
            result = query.execute();

            // checking second query
            it = result.getRows();
            assertEquals(2, it.getSize());

            n1 = it.nextRow().getNode();
            n2 = it.nextRow().getNode();

            assertEquals("f1", n1.getName());
            assertEquals("f2", n2.getName());

            // Try the XPath query ...
            String descOrderX = "/jcr:root//element(*,nt:file)[(@jcr:mixinTypes = 'mix:simpleVersionable')] order by jcr:content/@jcr:lastModified descending";
            String ascOrderX = "/jcr:root//element(*,nt:file)[(@jcr:mixinTypes = 'mix:simpleVersionable')] order by jcr:content/@jcr:lastModified ascending";
            query = queryManager.createQuery(descOrderX, Query.XPATH);
            result = query.execute();
            // checking first query
            it = result.getRows();
            assertEquals(2, it.getSize());

            n1 = it.nextRow().getNode();
            n2 = it.nextRow().getNode();

            assertEquals("f2", n1.getName());
            assertEquals("f1", n2.getName());

            // the same request with other order
            query = queryManager.createQuery(ascOrderX, Query.XPATH);
            result = query.execute();

            // checking second query
            it = result.getRows();
            assertEquals(2, it.getSize());

            n1 = it.nextRow().getNode();
            n2 = it.nextRow().getNode();

            assertEquals("f1", n1.getName());
            assertEquals("f2", n2.getName());
        } finally {
            src.remove();
            session.save();
        }
    }

    @Test
    @FixFor( "MODE-1900" )
    public void shouldSelectDistinctNodesWhenJoiningMultiValueReferenceProperties() throws Exception {
        Node nodeA = session.getRootNode().addNode("A", "test:node");
        nodeA.setProperty("test:name", "A");

        Node nodeB = session.getRootNode().addNode("B", "test:node");
        nodeB.setProperty("test:name", "B");
        JcrValue nodeBRef = session.getValueFactory().createValue(nodeB);

        Node nodeC = session.getRootNode().addNode("C", "test:node");
        nodeC.setProperty("test:name", "C");
        JcrValue nodeCRef = session.getValueFactory().createValue(nodeC);

        try {
            Node relationship = nodeA.addNode("relationship", "test:relationship");
            relationship.setProperty("test:target", new JcrValue[] {nodeBRef, nodeCRef});

            session.save();
            QueryManager queryManager = session.getWorkspace().getQueryManager();

            // Find the reference in the node ...
            String queryString = "SELECT relationship.[test:target] FROM [test:relationship] AS relationship";
            Query query = queryManager.createQuery(queryString, Query.JCR_SQL2);
            validateQuery().rowCount(1).validate(query, query.execute());

            // Find the UUIDs of the 'test:node' (which are all 'mix:referenceable') ...
            queryString = "SELECT [jcr:uuid], [jcr:path] FROM [test:node]";
            query = queryManager.createQuery(queryString, Query.JCR_SQL2);
            validateQuery().rowCount(3).onEachRow(new Predicate() {
                @Override
                public void validate( int rowNumber,
                                      Row row ) throws RepositoryException {
                    assertNotNull(row.getValue("jcr:uuid"));
                }
            }).validate(query, query.execute());

            queryString = "SELECT relationship.[test:target], target.[jcr:path] "
                          + "   FROM [test:relationship] AS relationship "
                          + "   JOIN [test:node] AS target ON relationship.[test:target] = target.[jcr:uuid] ";
            query = queryManager.createQuery(queryString, Query.JCR_SQL2);
            validateQuery().rowCount(2).validate(query, query.execute());

            queryString = "SELECT node.[jcr:path], relationship.[test:target], target.[jcr:path] FROM [test:node] AS node "
                          + "   JOIN [test:relationship] AS relationship ON ISCHILDNODE(relationship, node) "
                          + "   JOIN [test:node] AS target ON relationship.[test:target] = target.[jcr:uuid] "
                          + "   WHERE node.[test:name] = 'A'";
            query = queryManager.createQuery(queryString, Query.JCR_SQL2);
            validateQuery().rowCount(2).validate(query, query.execute());

            queryString = "SELECT DISTINCT target.* FROM [test:node] AS node "
                          + "   JOIN [test:relationship] AS relationship ON ISCHILDNODE(relationship, node) "
                          + "   JOIN [test:node] AS target ON relationship.[test:target] = target.[jcr:uuid] "
                          + "   WHERE node.[test:name] = 'A'";
            query = queryManager.createQuery(queryString, Query.JCR_SQL2);
            validateQuery().rowCount(2).validate(query, query.execute());
        } finally {
            nodeA.remove();
            nodeB.remove();
            nodeC.remove();
            session.save();
        }
    }

    @Test
    @FixFor( "MODE-1969" )
    public void shouldRetrieveStrongReferrers() throws Exception {
        Node nodeA = session.getRootNode().addNode("A");
        nodeA.addMixin("mix:referenceable");
        Node nodeB = session.getRootNode().addNode("B");
        nodeB.addMixin("mix:referenceable");

        Node referrerA = session.getRootNode().addNode("referrerA");
        referrerA.setProperty("nodeARef", nodeA);
        Node referrerB = session.getRootNode().addNode("referrerB");
        referrerB.setProperty("nodeBRef", nodeB);
        List<String> referrerIds = Arrays.asList(referrerA.getIdentifier(), referrerB.getIdentifier());
        Collections.sort(referrerIds);

        session.save();

        try {
            String queryString = "SELECT * from [nt:unstructured] where REFERENCE() IN " + idList(nodeA, nodeB);
            QueryManager queryManager = session.getWorkspace().getQueryManager();
            Query query = queryManager.createQuery(queryString, Query.JCR_SQL2);
            QueryResult result = query.execute();
            final List<String> resultIds = new ArrayList<String>();
            validateQuery().rowCount(2).onEachRow(new Predicate() {
                @Override
                public void validate( int rowNumber,
                                      Row row ) throws RepositoryException {
                    resultIds.add(row.getNode().getIdentifier());
                }
            }).validate(query, result);

            Collections.sort(resultIds);
            assertEquals(referrerIds, resultIds);
        } finally {
            nodeA.remove();
            nodeB.remove();
            referrerA.remove();
            referrerB.remove();
            session.save();
        }
    }

    @Test
    @FixFor( "MODE-1969" )
    public void shouldRetrieveWeakReferrers() throws Exception {
        Node nodeA = session.getRootNode().addNode("A");
        nodeA.addMixin("mix:referenceable");
        Node nodeB = session.getRootNode().addNode("B");
        nodeB.addMixin("mix:referenceable");

        Node referrerA = session.getRootNode().addNode("referrerA");
        referrerA.setProperty("nodeAWRef", session.getValueFactory().createValue(nodeA, true));
        Node referrerB = session.getRootNode().addNode("referrerB");
        referrerB.setProperty("nodeBWRef", session.getValueFactory().createValue(nodeB, true));
        List<String> referrerIds = Arrays.asList(referrerA.getIdentifier(), referrerB.getIdentifier());
        Collections.sort(referrerIds);

        session.save();

        try {
            String queryString = "SELECT * from [nt:unstructured] where REFERENCE() IN " + idList(nodeA, nodeB);
            QueryManager queryManager = session.getWorkspace().getQueryManager();
            Query query = queryManager.createQuery(queryString, Query.JCR_SQL2);
            QueryResult result = query.execute();
            final List<String> resultIds = new ArrayList<String>();
            validateQuery().rowCount(2).onEachRow(new Predicate() {
                @Override
                public void validate( int rowNumber,
                                      Row row ) throws RepositoryException {
                    resultIds.add(row.getNode().getIdentifier());
                }
            }).validate(query, result);

            Collections.sort(resultIds);
            assertEquals(referrerIds, resultIds);
        } finally {
            nodeA.remove();
            nodeB.remove();
            referrerA.remove();
            referrerB.remove();

            session.save();
        }
    }

    @Test
    @FixFor( "MODE-1969" )
    public void shouldRetrieveSimpleReferrers() throws Exception {
        Node nodeA = session.getRootNode().addNode("A");
        nodeA.addMixin("mix:referenceable");
        Node nodeB = session.getRootNode().addNode("B");
        nodeB.addMixin("mix:referenceable");

        Node referrerA = session.getRootNode().addNode("referrerA");
        referrerA.setProperty("nodeASRef", session.getValueFactory().createSimpleReference(nodeA));
        Node referrerB = session.getRootNode().addNode("referrerB");
        referrerB.setProperty("nodeBSRef", session.getValueFactory().createSimpleReference(nodeB));
        List<String> referrerIds = Arrays.asList(referrerA.getIdentifier(), referrerB.getIdentifier());
        Collections.sort(referrerIds);

        session.save();
        try {
            String queryString = "SELECT * from [nt:unstructured] where REFERENCE() IN " + idList(nodeA, nodeB);
            QueryManager queryManager = session.getWorkspace().getQueryManager();
            Query query = queryManager.createQuery(queryString, Query.JCR_SQL2);
            QueryResult result = query.execute();

            final List<String> resultIds = new ArrayList<String>();
            validateQuery().rowCount(2).onEachRow(new Predicate() {
                @Override
                public void validate( int rowNumber,
                                      Row row ) throws RepositoryException {
                    resultIds.add(row.getNode().getIdentifier());
                }
            }).validate(query, result);

            Collections.sort(resultIds);
            assertEquals(referrerIds, resultIds);

        } finally {
            nodeA.remove();
            nodeB.remove();
            referrerA.remove();
            referrerB.remove();
            session.save();
        }
    }

    @Test
    @FixFor( "MODE-1969" )
    public void shouldRetrieveStrongWeakSimpleReferrers() throws Exception {
        Node nodeA = session.getRootNode().addNode("A");
        nodeA.addMixin("mix:referenceable");
        Node nodeB = session.getRootNode().addNode("B");
        nodeB.addMixin("mix:referenceable");

        Node referrerA = session.getRootNode().addNode("referrerA");
        referrerA.setProperty("nodeARef", nodeA);
        Node referrerB = session.getRootNode().addNode("referrerB");
        referrerB.setProperty("nodeBWRef", session.getValueFactory().createValue(nodeB, true));
        Node referrerC = session.getRootNode().addNode("referrerC");
        referrerC.setProperty("nodeCSRef", session.getValueFactory().createSimpleReference(nodeB));
        List<String> referrerIds = Arrays.asList(referrerA.getIdentifier(), referrerB.getIdentifier(), referrerC.getIdentifier());
        Collections.sort(referrerIds);

        session.save();

        try {
            String queryString = "SELECT * from [nt:unstructured] where REFERENCE() IN " + idList(nodeA, nodeB);
            QueryManager queryManager = session.getWorkspace().getQueryManager();
            Query query = queryManager.createQuery(queryString, Query.JCR_SQL2);
            QueryResult result = query.execute();

            final List<String> resultIds = new ArrayList<String>();
            validateQuery().rowCount(3).onEachRow(new Predicate() {
                @Override
                public void validate( int rowNumber,
                                      Row row ) throws RepositoryException {
                    resultIds.add(row.getNode().getIdentifier());
                }
            }).validate(query, result);

            Collections.sort(resultIds);
            assertEquals(referrerIds, resultIds);
        } finally {
            nodeA.remove();
            nodeB.remove();
            referrerA.remove();
            referrerB.remove();
            referrerC.remove();
            session.save();
        }
    }

    @Test
    @FixFor( "MODE-2053" )
    public void shouldRunLeftOuterJoin() throws Exception {
        Node parent1 = session.getRootNode().addNode("name_p1", "modetest:intermediate");
        Node parent2 = session.getRootNode().addNode("name_p2", "modetest:intermediate");
        parent2.addNode("name_c1", "modetest:child");
        session.save();

        try {
            String queryString = "SELECT parent.* FROM [modetest:intermediate] as parent LEFT OUTER JOIN [modetest:child] as child ON ISCHILDNODE(child, parent)"
                                 + " WHERE parent.[jcr:name] LIKE 'name%' OR child.[jcr:name] LIKE 'name%'";
            assertNodesAreFound(queryString, Query.JCR_SQL2, "/name_p1", "/name_p2");
        } finally {
            parent1.remove();
            parent2.remove();
            session.save();
        }
    }

    @FixFor( "MODE-2027" )
    @Test
    public void shouldSearchAllPropertiesUsingDotSelectorJCRSql2FullTextSearch() throws RepositoryException {
        String sql = "SELECT cars.[jcr:path] FROM [car:Car] AS cars WHERE contains(., 'Toyota') ";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        final List<String> actual = new ArrayList<String>();
        validateQuery().onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                actual.add(row.getNode().getIdentifier());
            }
        }).validate(query, query.execute());

        sql = "SELECT cars.[jcr:path] FROM [car:Car] AS cars WHERE contains(cars.*, 'Toyota')";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        final List<String> expected = new ArrayList<String>();
        validateQuery().rowCount(actual.size()).onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                expected.add(row.getNode().getIdentifier());
            }
        }).validate(query, query.execute());

        assertEquals(expected, actual);
    }

    @FixFor( "MODE-2062" )
    @Test
    public void fullTextShouldWorkWithBindVar() throws Exception {
        Node n1 = session.getRootNode().addNode("n1");
        n1.setProperty("n1-prop-1", "wow");
        n1.setProperty("n1-prop-2", "any");

        Node n2 = session.getRootNode().addNode("n2");
        n2.setProperty("n2-prop-1", "test");

        try {
            session.save();

            // test with literal
            String queryString = "select * from [nt:unstructured] as a where contains(a.*, 'wow')";
            assertNodesAreFound(queryString, Query.JCR_SQL2, "/n1");

            // test with bind
            String queryStringWithBind = "select * from [nt:unstructured] as a where contains(a.*, $text)";
            QueryManager queryManager = session.getWorkspace().getQueryManager();
            Query query = queryManager.createQuery(queryStringWithBind, Query.JCR_SQL2);
            query.bindValue("text", session.getValueFactory().createValue("wow"));
            QueryResult result = query.execute();
            validateQuery().rowCount(1).hasNodesAtPaths("/n1").validate(query, result);
        } finally {
            n1.remove();
            n2.remove();
            session.save();
        }
    }

    @FixFor( "MODE-2095" )
    @Test
    public void shouldSearchUsingDateRangeQuery() throws Exception {
        Node testParent = session.getRootNode().addNode("date_parent");
        Node node1 = testParent.addNode("date_node1");
        node1.setProperty("now", Calendar.getInstance());
        Node node2 = testParent.addNode("date_node2");
        node2.setProperty("now", Calendar.getInstance());
        session.save();

        try {
            String queryString = "select * from [nt:unstructured] as node where "
                                 + "node.now <= CAST('2999-10-21T00:00:00.000' AS DATE) and node.now >= CAST('1999-10-21T00:00:00.000' AS DATE)";
            assertNodesAreFound(queryString, Query.JCR_SQL2, "/date_parent/date_node1", "/date_parent/date_node2");
        } finally {
            testParent.remove();
            session.save();
        }
    }

    // // ----------------------------------------------------------------------------------------------------------------
    // // Full-text Search Queries
    // // ----------------------------------------------------------------------------------------------------------------
    //
    @FixFor( "MODE-1418" )
    @Test
    public void shouldBeAbleToCreateAndExecuteFullTextSearchQueryOfPhrase() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("cat wearing", JcrRepository.QueryLanguage.SEARCH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns(searchColumnNames()).validate(query, result);
    }

    @FixFor( "MODE-905" )
    @Test
    public void shouldBeAbleToCreateAndExecuteFullTextSearchQuery() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("land", JcrRepository.QueryLanguage.SEARCH);
        QueryResult result = query.execute();
        validateQuery().rowCount(4).hasColumns(searchColumnNames()).validate(query, result);
    }

    @FixFor( "MODE-905" )
    @Test
    public void shouldBeAbleToCreateAndExecuteFullTextSearchQueryWithName() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("highlander", JcrRepository.QueryLanguage.SEARCH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns(searchColumnNames()).validate(query, result);
    }

    // ----------------------------------------------------------------------------------------------------------------
    // JCR-SQL Queries
    // ----------------------------------------------------------------------------------------------------------------

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToCreateAndExecuteSqlQueryWithOrderByClause() throws RepositoryException {
        String sql = "SELECT car:model FROM car:Car WHERE car:model IS NOT NULL ORDER BY car:model ASC";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns("jcr:path", "jcr:score", "car:model").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToCreateAndExecuteSqlQueryWithOrderByPathClause() throws RepositoryException {
        String sql = "SELECT car:model FROM car:Car WHERE car:model IS NOT NULL ORDER BY PATH() ASC";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns("jcr:path", "jcr:score", "car:model").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToCreateAndExecuteSqlQueryWithPathCriteriaAndOrderByClause() throws RepositoryException {
        String sql = "SELECT car:model FROM car:Car WHERE jcr:path LIKE '/Cars/%' ORDER BY car:model ASC";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns("jcr:path", "jcr:score", "car:model").validate(query, result);
    }

    /**
     * Tests that the child nodes (but no grandchild nodes) are returned.
     *
     * @throws RepositoryException
     */
    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToCreateAndExecuteSqlQueryWithChildAxisCriteria() throws RepositoryException {
        String sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '/Cars/%' AND NOT jcr:path LIKE '/Cars/%/%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = query.execute();
        validateQuery().rowCount(4).hasColumns(allColumnNames()).validate(query, result);
    }

    /**
     * Tests that the child nodes (but no grandchild nodes) are returned.
     *
     * @throws RepositoryException
     */
    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToCreateAndExecuteSqlQueryWithContainsCriteria() throws RepositoryException {
        String sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '/Cars/%' AND NOT jcr:path LIKE '/Cars/%/%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = query.execute();
        validateQuery().rowCount(4).hasColumns(allColumnNames()).validate(query, result);
    }

    @Test
    @FixFor( "MODE-791" )
    @SuppressWarnings( "deprecation" )
    public void shouldReturnNodesWithPropertyConstrainedByTimestamp() throws Exception {
        String sql = "SELECT car:model, car:year, car:maker FROM car:Car " + "WHERE jcr:path LIKE '/Cars/%' "
                     + "AND (car:msrp LIKE '$3%' OR car:msrp LIKE '$2') " + "AND (car:year LIKE '2008' OR car:year LIKE '2009') "
                     + "AND car:valueRating > '1' " + "AND jcr:created > TIMESTAMP '1974-07-10T00:00:00.000-05:00' "
                     + "AND jcr:created < TIMESTAMP '3074-07-10T00:00:00.000-05:00'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = query.execute();
        String[] columnNames = {"car:model", "car:year", "car:maker", "jcr:path", "jcr:score"};
        validateQuery().rowCount(5).hasColumns(columnNames).onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertTrue(row.getNode().hasProperty("car:model"));
            }
        }).validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @FixFor( "MODE-934" )
    @Test
    public void shouldParseQueryWithUnqualifiedPathInSelectOfJcrSqlQuery() throws RepositoryException {
        String sql = "select jcr:primaryType, jcr:path FROM nt:base";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = query.execute();
        String[] columnNames = {"jcr:primaryType", "jcr:path", "jcr:score"};
        validateQuery().rowCount(totalNodeCount).hasColumns(columnNames).validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @FixFor( "MODE-934" )
    @Test
    public void shouldParseQueryWithUnqualifiedPathInSelectAndCriteriaOfJcrSqlQuery() throws RepositoryException {
        String sql = "select jcr:primaryType, jcr:path FROM nt:base WHERE jcr:path LIKE '/Cars/%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = query.execute();
        String[] columnNames = {"jcr:primaryType", "jcr:path", "jcr:score"};
        validateQuery().rowCount(17).hasColumns(columnNames).validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @FixFor( "MODE-934" )
    @Test
    public void shouldParseQueryWithUnqualifiedPathInSelectAndUnqualifiedNameInCriteriaOfJcrSqlQuery() throws RepositoryException {
        String sql = "select jcr:primaryType, jcr:path FROM nt:base WHERE jcr:name LIKE '%3%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = query.execute();
        String[] columnNames = {"jcr:primaryType", "jcr:path", "jcr:score"};
        validateQuery().rowCount(4).hasColumns(columnNames).validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @FixFor( "MODE-934" )
    @Test
    public void shouldParseQueryWithUnqualifiedPathInSelectAndUnqualifiedLocalNameInCriteriaOfJcrSqlQuery()
        throws RepositoryException {
        String sql = "select jcr:primaryType, jcr:path FROM nt:base WHERE mode:localName LIKE '%3%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = query.execute();
        String[] columnNames = {"jcr:primaryType", "jcr:path", "jcr:score"};
        validateQuery().rowCount(4).hasColumns(columnNames).validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @FixFor( "MODE-934" )
    @Test
    public void shouldParseQueryWithJcrPathInJoinCriteriaOfJcrSqlQuery() throws RepositoryException {
        String sql = "select nt:base.jcr:primaryType, nt:base.jcr:path, car:Car.car:year "
                     + "FROM nt:base, car:Car WHERE car:Car.jcr:path = nt:base.jcr:path";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = query.execute();
        String[] columnNames = {"nt:base.jcr:primaryType", "nt:base.jcr:path", "car:Car.car:year", "jcr:path", "jcr:score"};
        validateQuery().rowCount(13).hasColumns(columnNames).validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @FixFor( "MODE-934" )
    @Test
    public void shouldNotIncludePseudoColumnsInSelectStarOfJcrSqlQuery() throws RepositoryException {
        String sql = "select * FROM nt:base";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.SQL);
        QueryResult result = query.execute();
        validateQuery().rowCount(totalNodeCount).hasColumns(allColumnNames()).validate(query, result);
    }

    // ----------------------------------------------------------------------------------------------------------------
    // XPath Queries
    // ----------------------------------------------------------------------------------------------------------------

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToCreateXPathQuery() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("//element(*,car:Car)", Query.XPATH);
        validateQuery().rowCount(13).validate(query, query.execute());

        query = session.getWorkspace().getQueryManager().createQuery("//element(*,nt:unstructured)", Query.XPATH);
        validateQuery().rowCount(24).validate(query, query.execute());
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindAllNodes() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("//element(*,nt:base)", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(totalNodeCount).hasColumns(allColumnNames()).validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindAllNodesOrderingByPath() throws RepositoryException {
        String xpath = "//element(*,nt:base) order by @jcr:path";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(totalNodeCount).hasColumns(allColumnNames()).validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindAllNodesOrderingByAttribute() throws RepositoryException {
        String xpath = "//element(*,car:Car) order by @car:maker";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns("car:maker", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindAllUnstructuredNodes() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("//element(*,nt:unstructured)", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(24).hasColumns(allColumnNames()).validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindAllUnstructuredNodesOrderedByPropertyValue() throws RepositoryException {
        QueryManager manager = session.getWorkspace().getQueryManager();
        Query query = manager.createQuery("//element(*,nt:unstructured) order by @jcr:primaryType", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(24).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);

        query = manager.createQuery("//element(*,car:Car) order by @car:year", Query.XPATH);
        result = query.execute();
        validateQuery().rowCount(13).hasColumns("car:year", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindNodesUnderNode() throws RepositoryException {
        String xpath = " /jcr:root/Cars/Hybrid/*";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(3).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindNodesUnderNodeAndWithProperty() throws RepositoryException {
        String xpath = " /jcr:root/Cars/Hybrid/*[@car:year]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(3).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindNodesUnderNodeAndWithPropertyOrderedByProperty() throws RepositoryException {
        String xpath = " /jcr:root/Cars/Hybrid/*[@car:year] order by @car:year ascending";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(3).hasColumns("car:year", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindNodesUnderPath() throws RepositoryException {
        String xpath = " /jcr:root/Cars//*";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(17).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindNodesWithAllSnsIndexesUnderPath() throws RepositoryException {
        String xpath = " /jcr:root//NodeA";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(3).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindNodesUnderPathAndWithProperty() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery(" /jcr:root/Cars//*[@car:year]", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindNodesUnderPathAndWithPropertyOrderedByProperty() throws RepositoryException {
        String xpath = " /jcr:root/Cars//*[@car:year] order by @car:year ascending";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(13).hasColumns("car:year", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindAllUnstructuredNodesOrderedByScore() throws RepositoryException {
        String xpath = "//element(*,nt:unstructured) order by jcr:score()";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(24).hasColumns(allColumnNames()).validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindSameNameSiblingsByIndex() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("/jcr:root/Other/NodeA", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(3).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);

        query = session.getWorkspace().getQueryManager().createQuery("/jcr:root/Other/NodeA[2]", Query.XPATH);
        result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").hasNodesAtPaths("/Other/NodeA[2]")
                       .validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindAllCarNodes() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("//element(*,car:Car)", Query.XPATH);
        QueryResult result = query.execute();
        String[] columnNames = {"jcr:primaryType", "jcr:mixinTypes", "jcr:path", "jcr:score", "jcr:created", "jcr:createdBy",
            "jcr:name", "mode:localName", "mode:depth", "mode:id", "car:mpgCity", "car:userRating", "car:mpgHighway",
            "car:engine", "car:model", "car:year", "car:maker", "car:lengthInInches", "car:valueRating", "car:wheelbaseInInches",
            "car:msrp", "car:alternateModels"};
        validateQuery().rowCount(13).hasColumns(columnNames).validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindRootNode() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("/jcr:root", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindChildOfRootNode() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("/jcr:root/Cars", Query.XPATH);
        assertThat(query, is(notNullValue()));
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindChildOfRootNodeWithTypeCriteria() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("/jcr:root/Cars[@jcr:primaryType]", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindNodeWithPathAndAttrbuteCriteria() throws RepositoryException {
        String xpath = "/jcr:root/Cars/Sports/Infiniti_x0020_G37[@car:year='2008']";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindNodeWithAttrbuteCriteria() throws RepositoryException {
        String xpath = "//Infiniti_x0020_G37[@car:year='2008']";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindNodeWithPathUnderRootAndAttrbuteCriteria() throws RepositoryException {
        String xpath = "/jcr:root/NodeB[@myUrl='http://www.acme.com/foo/bar']";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @FixFor( "MODE-686" )
    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindAnywhereNodeWithNameAndAttrbuteCriteriaMatchingUrl()
        throws RepositoryException {
        String xpath = "//NodeB[@myUrl='http://www.acme.com/foo/bar']";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryToFindNodeWithNameMatch() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("//NodeB", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    /*
     * Adding this test case since its primarily a test of integration between the RNTM and the query engine
     */
    @Test( expected = InvalidNodeTypeDefinitionException.class )
    public void shouldNotAllowUnregisteringUsedPrimaryType() throws Exception {
        Session adminSession = null;
        try {
            adminSession = repository.login();
            assertThat(adminSession.getNamespaceURI("car"), is("http://www.modeshape.org/examples/cars/1.0"));
            // Re-register one of the used namespaces in a session ...
            adminSession.setNamespacePrefix("cars", "http://www.modeshape.org/examples/cars/1.0");

            JcrNodeTypeManager nodeTypeManager = (JcrNodeTypeManager)adminSession.getWorkspace().getNodeTypeManager();
            nodeTypeManager.unregisterNodeTypes(Collections.singletonList("cars:Car"));
        } finally {
            if (adminSession != null) adminSession.logout();
        }
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithContainsCriteria() throws RepositoryException {
        String xpath = "/jcr:root//*[jcr:contains(., 'liter')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        assertThat(query, is(notNullValue()));
        QueryResult result = query.execute();
        validateQuery().rowCount(2).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithContainsCriteriaAndPluralWord() throws RepositoryException {
        String xpath = "/jcr:root//*[jcr:contains(., 'liters')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        assertThat(query, is(notNullValue()));
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithComplexContainsCriteria() throws RepositoryException {
        String xpath = "/jcr:root//*[jcr:contains(., '\"liters V 12\"')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        assertThat(query, is(notNullValue()));
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithComplexContainsCriteriaWithHyphen() throws RepositoryException {
        String xpath = "/jcr:root//*[jcr:contains(., '\"5-speed\"')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithComplexContainsCriteriaWithHyphenAndNumberAndWildcard()
        throws RepositoryException {
        String xpath = "/jcr:root//*[jcr:contains(., '\"spee*\"')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(2).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithComplexContainsCriteriaWithNoHyphenAndNoWildcard() throws RepositoryException {
        String xpath = "/jcr:root//*[jcr:contains(., '\"heavy duty\"')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();

        // by default there is no stemming or punctuation replacement
        assertFalse(result.getRows().hasNext());
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithComplexContainsCriteriaWithHyphenAndNoWildcard() throws RepositoryException {
        String xpath = "/jcr:root//*[jcr:contains(., '\"heavy-duty\"')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithComplexContainsCriteriaWithNoHyphenAndWildcard() throws RepositoryException {
        String xpath = "/jcr:root//*[jcr:contains(., '\"heavy-du*\"')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithComplexContainsCriteriaWithNoHyphenAndLeadingWildcard()
        throws RepositoryException {
        String xpath = "/jcr:root//*[jcr:contains(., '\"*avy-duty\"')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithComplexContainsCriteriaWithHyphenAndWildcard() throws RepositoryException {
        String xpath = "/jcr:root//*[jcr:contains(., '\"heavy-du*\"')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @Ignore
    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithComplexContainsCriteriaWithHyphenAndLeadingWildcard()
        throws RepositoryException {
        String xpath = "/jcr:root//*[jcr:contains(., '\"*-speed\"')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(2).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @FixFor( "MODE-790" )
    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithCompoundCriteria() throws Exception {
        String xpath = "/jcr:root/Cars//element(*,car:Car)[@car:year='2008' and jcr:contains(., '\"liters V 12\"')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        String[] columnNames = {"jcr:primaryType", "jcr:mixinTypes", "jcr:path", "jcr:score", "jcr:created", "jcr:createdBy",
            "jcr:name", "mode:localName", "mode:depth", "mode:id", "car:mpgCity", "car:userRating", "car:mpgHighway",
            "car:engine", "car:model", "car:year", "car:maker", "car:lengthInInches", "car:valueRating", "car:wheelbaseInInches",
            "car:msrp", "car:alternateModels"};
        validateQuery().rowCount(1).hasColumns(columnNames).validate(query, result);

        // Query again with a different criteria that should return no nodes ...
        xpath = "/jcr:root/Cars//element(*,car:Car)[@car:year='2007' and jcr:contains(., '\"liter V 12\"')]";
        query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        result = query.execute();
        validateQuery().rowCount(0).hasColumns(columnNames).validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithElementTestForChildrenOfRoot() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("/jcr:root/element()", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(4).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithElementTestForAllNodesBelowRoot() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("/jcr:root//element()", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(totalNodeCount).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithElementTestForChildOfRootWithName() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("/jcr:root/element(Cars)", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithElementTestForSingleNodeBelowRootWithName() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("/jcr:root//element(Utility)", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithElementTestForChildrenOfRootWithName() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("/jcr:root/Other/element(NodeA)", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(3).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithElementTestForMultipleNodesBelowRootWithName() throws RepositoryException {
        Query query = session.getWorkspace().getQueryManager().createQuery("/jcr:root//element(NodeA)", Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(3).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @Ignore
    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithRangeCriteria() throws RepositoryException {
        String xpath = "/jcr:root/Other/*[@somethingElse <= 'value2' and @somethingElse > 'value1']";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldBeAbleToExecuteXPathQueryWithNewlyRegisteredNamespace() throws RepositoryException {
        NamespaceRegistry namespaceRegistry = session.getWorkspace().getNamespaceRegistry();
        namespaceRegistry.registerNamespace("newPrefix", "newUri");

        try {
            // We don't have any elements that use this yet, but let's at least verify that it can execute.
            Query query = session.getWorkspace().getQueryManager()
                                 .createQuery("//*[@newPrefix:someColumn = 'someValue']", Query.XPATH);
            query.execute();
        } finally {
            namespaceRegistry.unregisterNamespace("newPrefix");
        }
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldNotReturnNodesWithNoPropertyForPropertyCriterion() throws Exception {
        String xpath = "/jcr:root/Cars//*[@car:wheelbaseInInches]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertThat(row.getNode().hasProperty("car:wheelbaseInInches"), is(true));
            }
        }).validate(query, result);
    }

    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldNotReturnNodesWithNoPropertyForLikeCriterion() throws Exception {
        String xpath = "/jcr:root/Cars//*[jcr:like(@car:wheelbaseInInches, '%')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:primaryType", "jcr:path", "jcr:score").onEachRow(new Predicate() {
            @Override
            public void validate( int rowNumber,
                                  Row row ) throws RepositoryException {
                assertThat(row.getNode().hasProperty("car:wheelbaseInInches"), is(true));
            }
        }).validate(query, result);
    }

    @FixFor( "MODE-1144" )
    @SuppressWarnings( "deprecation" )
    @Test
    public void shouldParseMagnoliaXPathQuery() throws Exception {
        String xpath = "//*[@jcr:primaryType='mgnl:content']//*[jcr:contains(., 'paragraph')]";
        Query query = session.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH);
        QueryResult result = query.execute();
        assertNotNull(result);
        assertFalse(result.getRows().hasNext());
    }

    // ----------------------------------------------------------------------------------------------------------------
    // QOM Queries
    // ----------------------------------------------------------------------------------------------------------------

    @FixFor( "MODE-1468" )
    @Test
    public void shouldAllowCreationAndExecutionOfQueryObjectModel() throws Exception {
        QueryManager queryManager = session.getWorkspace().getQueryManager();
        QueryObjectModelFactory qomFactory = queryManager.getQOMFactory();
        Selector selector = qomFactory.selector("car:Car", "car");
        PropertyValue propValue = qomFactory.propertyValue("car", "car:userRating");
        Literal literal = qomFactory.literal(session.getValueFactory().createValue("4")); // use a String since it's LIKE
        Constraint constraint = qomFactory.comparison(propValue, JCR_OPERATOR_LIKE, literal);
        Column[] columns = new Column[4];
        columns[0] = qomFactory.column("car", "car:maker", "maker");
        columns[1] = qomFactory.column("car", "car:model", "car:model");
        columns[2] = qomFactory.column("car", "car:year", "car:year");
        columns[3] = qomFactory.column("car", "car:userRating", "car:userRating");
        Ordering[] orderings = null;

        // Build and execute the query ...
        Query query1 = qomFactory.createQuery(selector, constraint, orderings, columns);
        QueryResult result1 = query1.execute();
        String[] columnNames = {"maker", "car:model", "car:year", "car:userRating"};
        validateQuery().rowCount(4).hasColumns(columnNames).validate(query1, result1);

        // Now get the JCR-SQL2 statement from the QOM ...
        String expectedExpr = "SELECT car.[car:maker] AS maker, car.[car:model], car.[car:year], car.[car:userRating] FROM [car:Car] AS car WHERE car.[car:userRating] LIKE '4'";
        String expr = query1.getStatement();
        assertThat(expr, is(expectedExpr));

        // Now execute it ...
        Query query2 = queryManager.createQuery(expr, Query.JCR_SQL2);
        QueryResult result2 = query2.execute();
        validateQuery().rowCount(4).hasColumns(columnNames).validate(query2, result2);
    }

    @FixFor( "MODE-1468" )
    @Test
    public void shouldAllowCreationAndExecutionOfQueryObjectModelWithLimit() throws Exception {
        QueryManager queryManager = session.getWorkspace().getQueryManager();
        QueryObjectModelFactory qomFactory = queryManager.getQOMFactory();
        Selector selector = qomFactory.selector("car:Car", "car");
        PropertyValue propValue = qomFactory.propertyValue("car", "car:userRating");
        Literal literal = qomFactory.literal(session.getValueFactory().createValue("4")); // use a String since it's LIKE
        Constraint constraint = qomFactory.comparison(propValue, JCR_OPERATOR_LIKE, literal);
        Column[] columns = new Column[4];
        columns[0] = qomFactory.column("car", "car:maker", "maker");
        columns[1] = qomFactory.column("car", "car:model", "car:model");
        columns[2] = qomFactory.column("car", "car:year", "car:year");
        columns[3] = qomFactory.column("car", "car:userRating", "car:userRating");
        Ordering[] orderings = null;
        Limit limit = qomFactory.limit(2, 0);
        boolean isDistinct = false;

        // Build and execute the query ...
        SelectQuery selectQuery = qomFactory.select(selector, constraint, orderings, columns, limit, isDistinct);
        Query query1 = qomFactory.createQuery(selectQuery);
        QueryResult result1 = query1.execute();
        String[] columnNames = {"maker", "car:model", "car:year", "car:userRating"};
        validateQuery().rowCount(2).hasColumns(columnNames).validate(query1, result1);

        // Now get the JCR-SQL2 statement from the QOM ...
        String expectedExpr = "SELECT car.[car:maker] AS maker, car.[car:model], car.[car:year], car.[car:userRating] FROM [car:Car] AS car WHERE car.[car:userRating] LIKE '4' LIMIT 2";
        String expr = query1.getStatement();
        assertThat(expr, is(expectedExpr));

        // Now execute it ...
        Query query2 = queryManager.createQuery(expr, Query.JCR_SQL2);
        QueryResult result2 = query2.execute();
        validateQuery().rowCount(2).hasColumns(columnNames).validate(query2, result2);
    }

    @Test
    @FixFor( "MODE-2209" )
    public void queriesShouldTakePermissionsIntoAccount() throws Exception {
        AccessControlManager acm = session.getAccessControlManager();

        Node parent = session.getRootNode().addNode("parent");
        parent.addNode("child1");
        AccessControlList acl = acl("/parent/child1");
        parent.addNode("child2");
        session.save();

        try {
            String queryString = "select [jcr:path] from [nt:unstructured] as node where ISCHILDNODE(node, '/parent')";
            assertNodesAreFound(queryString, Query.JCR_SQL2, "/parent/child1", "/parent/child2");

            // remove the READ permission for child1
            acl.addAccessControlEntry(SimplePrincipal.EVERYONE,
                                      new Privilege[] {acm.privilegeFromName(Privilege.JCR_WRITE),
                                          acm.privilegeFromName(Privilege.JCR_REMOVE_NODE),
                                          acm.privilegeFromName(Privilege.JCR_MODIFY_ACCESS_CONTROL)});
            acm.setPolicy("/parent/child1", acl);
            session.save();

            QueryManager queryManager = session.getWorkspace().getQueryManager();
            Query query = queryManager.createQuery(queryString, Query.JCR_SQL2);
            QueryResult result = query.execute();

            // assert that only child2 is still visible in the query results
            NodeIterator nodes = result.getNodes();
            assertEquals(1, nodes.getSize());
            assertEquals("/parent/child2", nodes.nextNode().getPath());
            assertFalse(nodes.hasNext());

            RowIterator rows = result.getRows();
            assertEquals(1, rows.getSize());
            assertEquals("/parent/child2", rows.nextRow().getNode().getPath());
            assertFalse(rows.hasNext());
        } finally {
            acl.addAccessControlEntry(SimplePrincipal.EVERYONE, new Privilege[] {acm.privilegeFromName(Privilege.JCR_ALL)});
            acm.setPolicy("/parent/child1", acl);
            session.save();

            acm.removePolicy("/parent/child1", null);
            parent.remove();
            session.save();
        }
    }

    @Test
    @FixFor( "MODE-2220" )
    public void shouldSupportLowerCaseOperand() throws Exception {
        Node nodeA = session.getRootNode().addNode("A");
        nodeA.setProperty("something", "SOME UPPERCASE TEXT");
        session.save();

        try {
            String sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE LOWER(node.something) LIKE '%uppercase%'";
            Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            NodeIterator nodes = query.execute().getNodes();
            assertEquals(1, nodes.getSize());
            assertEquals("/A", nodes.nextNode().getPath());
        } finally {
            nodeA.remove();
            session.save();
        }
    }

    @Test
    @FixFor( "MODE-2220" )
    public void shouldSupportUpperCaseOperand() throws Exception {
        String sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE UPPER(node.something) LIKE '%FOX%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        NodeIterator nodes = query.execute().getNodes();
        assertEquals(1, nodes.getSize());
        assertEquals("/Other/NodeA", nodes.nextNode().getPath());
    }
    
    @Test
    @FixFor( "MODE-2403" )
    public void likeOperandShouldBeCaseSensitive() throws Exception {
        Node n = session.getRootNode().addNode("N");
        n.setProperty("prop", "Capital0");
        session.save();

        try {
            String sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE node.prop LIKE 'capital%'";
            Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            NodeIterator nodes = query.execute().getNodes();
            assertEquals(0, nodes.getSize());

            sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE node.prop LIKE '%capital%'";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            nodes = query.execute().getNodes();
            assertEquals(0, nodes.getSize());

            sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE node.prop LIKE 'Capital%'";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            nodes = query.execute().getNodes();
            assertEquals(1, nodes.getSize());
            assertEquals("/N", nodes.nextNode().getPath());

            sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE node.prop LIKE '%Capital%'";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            nodes = query.execute().getNodes();
            assertEquals(1, nodes.getSize());
            assertEquals("/N", nodes.nextNode().getPath());
        } finally {
            n.remove();
            session.save();
        }
    }
    
    @Test
    @FixFor( "MODE-2247" )
    public void shouldBeAbleToExecuteIntersectOperationWithSimpleCriteria() throws Exception {
        String sql1 = "SELECT car1.[jcr:path] FROM [car:Car] AS car1";
        String sql2 = "SELECT car2.[jcr:path] FROM [car:Car] AS car2 WHERE car2.[car:mpgCity] = 12";
        String queryString = sql1 + " INTERSECT " + sql2;

        List<String> expectedPaths = new ArrayList<>(Arrays.asList("/Cars/Sports/Aston Martin DB9",
                                                                   "/Cars/Utility/Land Rover LR3"));

        Query query = session.getWorkspace().getQueryManager().createQuery(queryString, Query.JCR_SQL2);
        NodeIterator nodes = query.execute().getNodes();
        assertEquals(2, nodes.getSize());
        while (nodes.hasNext()) {
            String path = nodes.nextNode().getPath();
            assertTrue(path + " not found", expectedPaths.remove(path));
        }
    }

    @Test
    @FixFor( "MODE-2247" )
    public void shouldBeAbleToExecuteIntersectOperationWithJoinCriteria() throws RepositoryException {
        String sql = "SELECT category.[jcr:path] AS p FROM [nt:unstructured] AS category "
                     + " INTERSECT  "
                     + "SELECT category.[jcr:path] AS p FROM [nt:unstructured] AS category JOIN [car:Car] AS cars ON ISCHILDNODE(cars,category) WHERE cars.[jcr:name]='Land Rover LR3'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("p").hasNodesAtPaths("/Cars/Utility").validate(query, result);
    }

    @Test
    @FixFor( "MODE-2247" )
    public void shouldBeAbleToExecuteIntersectAllOperation() throws RepositoryException {
        String sql = "SELECT category.[jcr:path] AS p FROM [nt:unstructured] AS category "
                     + " INTERSECT ALL "
                     + "SELECT category.[jcr:path] AS p FROM [nt:unstructured] AS category JOIN [car:Car] AS cars ON ISCHILDNODE(cars,category) WHERE cars.[jcr:name] LIKE '%Rover%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(2).hasColumns("p").hasNodesAtPaths("/Cars/Utility", "/Cars/Utility").validate(query, result);
    }

    @Test
    @FixFor( "MODE-2247" )
    public void shouldBeAbleToExecuteExceptOperationWithSimpleCriteria() throws Exception {
        String sql1 = "SELECT car1.[jcr:path] FROM [car:Car] AS car1 WHERE car1.[jcr:name] LIKE '%Land Rover%'";
        String sql2 = "SELECT car2.[jcr:path] FROM [car:Car] AS car2 WHERE car2.[jcr:name] LIKE '%LR3%'";
        String queryString = sql1 + " EXCEPT " + sql2;
        Query query = session.getWorkspace().getQueryManager().createQuery(queryString, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasColumns("jcr:path").hasNodesAtPaths("/Cars/Utility/Land Rover LR2")
                       .validate(query, result);
    }

    @Test
    @FixFor( "MODE-2247" )
    public void shouldBeAbleToExecuteExceptOperationWithJoinCriteria() throws RepositoryException {
        String sql = "SELECT category.[jcr:path] AS p FROM [nt:unstructured] AS category WHERE ISCHILDNODE(category,'/Cars')"
                     + "EXCEPT "
                     + "SELECT category.[jcr:path] AS p FROM [nt:unstructured] AS category JOIN [car:Car] AS cars ON ISCHILDNODE(cars,category) "
                     + " WHERE cars.[jcr:name] LIKE '%Rover%' OR cars.[jcr:name] LIKE '%Toyota%'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        List<String> expectedPaths = new ArrayList<>(Arrays.asList("/Cars/Sports", "/Cars/Luxury"));

        NodeIterator nodes = query.execute().getNodes();
        assertEquals(2, nodes.getSize());
        while (nodes.hasNext()) {
            String path = nodes.nextNode().getPath();
            assertTrue(path + " not found", expectedPaths.remove(path));
        }
    }

    @Test
    @FixFor( "MODE-2247" )
    public void shouldBeAbleToExecuteExceptAllOperation() throws RepositoryException {
        String sql = "SELECT category.[jcr:path] AS p FROM [nt:unstructured] AS category JOIN [car:Car] AS cars ON ISCHILDNODE(cars,category) "
                     + " WHERE cars.[jcr:name] LIKE '%Rover%' "
                     + "EXCEPT ALL "
                     + "SELECT node.[jcr:path] AS p FROM [nt:unstructured] AS node WHERE NOT ISCHILDNODE(node,'/Cars')";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(2).hasColumns("p").hasNodesAtPaths("/Cars/Utility", "/Cars/Utility").validate(query, result);
    }

    @Test
    @FixFor( "MODE-2267" )
    public void shouldAllowNameAndLocalNameConstraints() throws Exception {
        Node node1 = session.getRootNode().addNode("test:testNode1");
        Node node2 = session.getRootNode().addNode("test:testNode2");
        session.save();
        try {
            String sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE node.[mode:localName] LIKE '%testNode%'";
            Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            NodeIterator nodes = query.execute().getNodes();
            assertEquals(2, nodes.getSize());

            sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE node.[jcr:name] LIKE '%test%'";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            nodes = query.execute().getNodes();
            assertEquals(2, nodes.getSize());
        } finally {
            node1.remove();
            node2.remove();
            session.save();
        }
    }

    @Test
    @FixFor( "MODE-2275" )
    public void shouldAllowQueryingForRuntimeRegisteredNodeTypes() throws Exception {
        NamespaceRegistry namespaceRegistry = session.getWorkspace().getNamespaceRegistry();
        namespaceRegistry.registerNamespace("foo", "http://www.modeshape.org/foo/1.0");
        Node node1 = null;
        Node node2 = null;

        try {
            registerNodeType("foo:nodeType1");
            node1 = session.getRootNode().addNode("foo1", "foo:nodeType1");
            session.save();
            String sql = "SELECT node.[jcr:name] FROM [foo:nodeType1] AS node";
            Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            NodeIterator nodes = query.execute().getNodes();
            assertEquals(1, nodes.getSize());
            assertEquals("foo1", nodes.nextNode().getName());

            registerNodeType("foo:nodeType2");
            node2 = session.getRootNode().addNode("foo2", "foo:nodeType2");
            session.save();
            sql = "SELECT node.[jcr:name] FROM [foo:nodeType2] AS node";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            nodes = query.execute().getNodes();
            assertEquals(1, nodes.getSize());
            assertEquals("foo2", nodes.nextNode().getName());
        } finally {
            // remove the nodes to avoid influencing the other tests
            if (node1 != null) {
                node1.remove();
            }
            if (node2 != null) {
                node2.remove();
            }
            session.save();

            // remove the custom types and namespaces to avoid influencing the other tests
            JcrNodeTypeManager nodeTypeManager = session.getWorkspace().getNodeTypeManager();
            nodeTypeManager.unregisterNodeType("foo:nodeType1");
            nodeTypeManager.unregisterNodeType("foo:nodeType2");
            namespaceRegistry.unregisterNamespace("foo");
        }
    }

    @Test
    @FixFor( "MODE-2329" )
    public void shouldAllowUsingExpandedSelectorNameInQOM() throws Exception {
        QueryObjectModelFactory qomFactory = session.getWorkspace().getQueryManager().getQOMFactory();

        Selector selector = qomFactory.selector(NodeType.NT_BASE, "category");

        // Build and execute the query ...
        Query query = qomFactory.createQuery(selector, qomFactory.childNode("category", "/Cars"), null, new Column[0]);
        assertThat(query.getStatement(), is("SELECT * FROM [{http://www.jcp.org/jcr/nt/1.0}base] AS category WHERE ISCHILDNODE(category,'/Cars')"));
        QueryResult result = query.execute();
        validateQuery().rowCount(4).hasColumns(allColumnNames("category")).validate(query, result);
    }

    @Test
    @FixFor( { "MODE-1055", "MODE-2347" } )
    public void shouldAllowMissingSelectorColumnsInQOM() throws Exception {
        Node node = session.getRootNode().addNode("test", "modetest:simpleType");
        node.setProperty("fieldA", "A_value");
        session.save();
        // query for a property present in a subtype which doesn't have any residuals, using a super-type selector
        String sql = "SELECT * FROM [nt:base] AS node WHERE node.fieldA = 'A_value'";
        try {
            QueryObjectModelFactory qomFactory = session.getWorkspace().getQueryManager().getQOMFactory();
            Selector selector = qomFactory.selector("nt:base", "node");
            PropertyValue propValue = qomFactory.propertyValue("node", "fieldA");
            Literal literal = qomFactory.literal(session.getValueFactory().createValue("A_value"));
            Constraint constraint = qomFactory.comparison(propValue, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal);
            Query query = qomFactory.createQuery(selector, constraint, null, new Column[0]);

            assertThat(query.getStatement(), is(sql));
            QueryResult result = query.execute();
            validateQuery().rowCount(1).validate(query, result);
        } finally {
            node.remove();
            session.save();
        }
    }

    @Test
    @FixFor( "MODE-2401" )
    public void shouldNotReturnNonQueryableNodeTypes() throws Exception {
        Node folder1 = session.getRootNode().addNode("folder1", "test:noQueryFolder");
        Node folder2 = session.getRootNode().addNode("folder2", "nt:folder");
        Node folder3 = session.getRootNode().addNode("folder3", "test:noQueryFolder");
        Node folder31 = folder3.addNode("folder3_1", "nt:folder");
        Node folder311 = folder31.addNode("folder3_1", "test:noQueryFolder");
        folder311.addNode("folder3_1_1", "nt:folder");
        session.save();

        try {
            String sql = "SELECT folder.[jcr:name] FROM [nt:folder] AS folder";
            Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            NodeIterator nodes = query.execute().getNodes();
            assertEquals(3, nodes.getSize());
            Set<String> names = new TreeSet<>();
            while (nodes.hasNext()) {
                names.add(nodes.nextNode().getName());
            }
            assertArrayEquals(new String[] { "folder2", "folder3_1", "folder3_1_1" }, names.toArray(new String[0]));
        } finally {
            folder1.remove();
            folder2.remove();
            folder3.remove();
            session.save();
        }
    } 
    
    @Test
    @FixFor( "MODE-2401" )
    public void shouldNotReturnNodeWithNoQueryMixin() throws Exception {
        Node folder1 = session.getRootNode().addNode("folder1", "nt:folder");
        folder1.addMixin("test:noQueryMixin");
        Node folder2 = session.getRootNode().addNode("folder2", "nt:folder");
        session.save();

        String sql = "SELECT folder.[jcr:name] FROM [nt:folder] AS folder";

        try {
            Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            NodeIterator nodes = query.execute().getNodes();
            assertEquals(1, nodes.getSize());
            Set<String> names = new TreeSet<>();
            while (nodes.hasNext()) {
                names.add(nodes.nextNode().getName());
            }
            assertArrayEquals(new String[] { "folder2" }, names.toArray(new String[0]));
            
            //add a mixin on the 2nd node and reindex
            folder2.addMixin("test:noQueryMixin");
            session.save();
            
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            nodes = query.execute().getNodes();
            assertEquals(0, nodes.getSize());
            
            //remove the mixins from both nodesx
            folder1.removeMixin("test:noQueryMixin");
            folder2.removeMixin("test:noQueryMixin");
            session.save();
            
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            nodes = query.execute().getNodes();
            assertEquals(2, nodes.getSize());
            names = new TreeSet<>();
            while (nodes.hasNext()) {
                names.add(nodes.nextNode().getName());
            }
            assertArrayEquals(new String[] { "folder1", "folder2" }, names.toArray(new String[0]));
        } finally {
            folder1.remove();
            folder2.remove();
            session.save();
        }
    }

    @Test
    @FixFor( "MODE-2491" )
    public void shouldSupportCaseOperandsForMultiValuedProperties() throws Exception {
        Node metaData = session.getRootNode().addNode("metaData", "nt:unstructured");
        metaData.setProperty("lowerCase", new String[]{"a", "b", "c"});        
        metaData.setProperty("upperCase", new String[] { "A", "B", "C" });
        session.save();

        try {
            String sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE LOWER(node.upperCase) = 'a'";
            Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            NodeIterator nodes = query.execute().getNodes();
            assertEquals(1, nodes.getSize());
            assertEquals("/metaData", nodes.nextNode().getPath());

            sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE LOWER(node.upperCase) = 'b'";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            nodes = query.execute().getNodes();
            assertEquals(1, nodes.getSize());
            assertEquals("/metaData", nodes.nextNode().getPath());
            
            sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE LOWER(node.upperCase) = 'c'";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            nodes = query.execute().getNodes();
            assertEquals(1, nodes.getSize());
            assertEquals("/metaData", nodes.nextNode().getPath());   
            
            sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE UPPER(node.lowerCase) = 'A'";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            nodes = query.execute().getNodes();
            assertEquals(1, nodes.getSize());
            assertEquals("/metaData", nodes.nextNode().getPath());

            sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE UPPER(node.lowerCase) = 'B'";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            nodes = query.execute().getNodes();
            assertEquals(1, nodes.getSize());
            assertEquals("/metaData", nodes.nextNode().getPath());
            
            sql = "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE UPPER(node.lowerCase) = 'C'";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            nodes = query.execute().getNodes();
            assertEquals(1, nodes.getSize());
            assertEquals("/metaData", nodes.nextNode().getPath());
        } finally {
            metaData.remove();
            session.save();
        }
    }

    @Test
    @FixFor( "MODE-2166" )
    public void shouldSupportCastDynamicOperand() throws Exception {
        String sql = "SELECT car.[jcr:path] FROM [car:Car] as car WHERE CAST(car.[car:year] AS long) = 1967";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(1).hasNodesAtPaths("/Cars/Utility/Toyota Land Cruiser").validate(query, result);                            
    } 
    
    @Test(expected = org.modeshape.jcr.value.ValueFormatException.class)
    @FixFor( "MODE-2166" )
    public void shouldFailWhenAttemptingToCastToInvalidType() throws Exception {
        String sql = "SELECT car.[jcr:path] FROM [car:Car] as car WHERE CAST(car.[car:maker] AS DATE) = 'Toyota'";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().validate(query, result);                            
    }
    
    @Test
    @FixFor( "MODE-1727" )
    public void shouldPerformOrderByUsingCustomLocale() throws Exception {
        Node testRoot = session.getRootNode().addNode("words", "nt:unstructured");
        Node word1 = testRoot.addNode("word1", "modetest:simpleType");
        word1.setProperty("fieldA", "peach");
        Node word2 = testRoot.addNode("word2", "modetest:simpleType");
        word2.setProperty("fieldA", "péché");
        Node word3 = testRoot.addNode("word3", "modetest:simpleType");
        word3.setProperty("fieldA", "pêche");
        Node word4 = testRoot.addNode("word4", "modetest:simpleType");
        word4.setProperty("fieldA", "sin");
        session.save();

        String sql = "SELECT word.[jcr:path] FROM [modetest:simpleType] as word ORDER BY word.fieldA ASC";

        try {
            Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            QueryResult result = query.execute();
            validateQuery()
                    .rowCount(4)
                    .hasNodesAtPaths("/words/word1", "/words/word2", "/words/word3", "/words/word4").validate(query, result);

            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2, Locale.FRENCH);
            result = query.execute();
            validateQuery()
                    .rowCount(4)
                    .hasNodesAtPaths("/words/word1", "/words/word3", "/words/word2", "/words/word4").validate(query, result);
        } finally {
            session.getNode("/words").remove();
            session.save();
        }
    }
    
    @Test
    @FixFor( "MODE-2579" )
    public void shouldRunFTSQueryWithMultivaluedProperty() throws Exception {
        Node testRoot = session.getRootNode().addNode("ftsMVP", "nt:unstructured");
        Node node1 = testRoot.addNode("node1");   
        node1.addMixin("mix:title");
        node1.setProperty("FTSProp", new String[] {"the quick", "brown fox", "jumps over"});

        Node node2 = testRoot.addNode("node2");   
        node2.addMixin("mix:title");
        node2.setProperty("FTSProp", "the quick brown fox jumps over");
        session.save();

        try {
            String sql = "SELECT node.[jcr:path] FROM [mix:title] as node WHERE contains(node.*, 'fox')";
            Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            QueryResult result = query.execute();
            validateQuery().rowCount(2).hasNodesAtPaths(node1.getPath(), node2.getPath()).validate(query, result);
        
            sql = "SELECT node.[jcr:path] FROM [mix:title] as node WHERE contains(FTSProp, 'the')";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            result = query.execute();
            validateQuery().rowCount(2).hasNodesAtPaths(node1.getPath(), node2.getPath()).validate(query, result);

            sql = "SELECT node.[jcr:path] FROM [mix:title] as node WHERE contains(node.FTSProp, 'jumps')";
            query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
            result = query.execute();
            validateQuery().rowCount(2).hasNodesAtPaths(node1.getPath(), node2.getPath()).validate(query, result);
        } finally {
            testRoot.remove();
            session.save();
        }
    }
    
    @FixFor( "MODE-2666" )
    @Test
    public void shouldBeAbleToOrderByTheSameColumnRepeatedly() throws RepositoryException {
        String sql = "SELECT [jcr:name] FROM [nt:base] ORDER BY [jcr:name], [jcr:name]";
        Query query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        QueryResult result = query.execute();
        validateQuery().rowCount(totalNodeCount).noWarnings().validate(query, result);

        sql = "SELECT [jcr:name] FROM [nt:base] ORDER BY [jcr:name], [jcr:name], [jcr:name]";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().rowCount(totalNodeCount).noWarnings().validate(query, result);
    
        sql = "SELECT [jcr:name] FROM [nt:base] ORDER BY [jcr:name], [jcr:name], [jcr:name], [jcr:name]";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().rowCount(totalNodeCount).noWarnings().validate(query, result);    
        
        sql = "SELECT [jcr:name] FROM [nt:base] ORDER BY [jcr:name], [jcr:name], [jcr:name], [jcr:name], [jcr:name]";
        query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2);
        result = query.execute();
        validateQuery().rowCount(totalNodeCount).noWarnings().validate(query, result);
    }

    private void registerNodeType( String typeName ) throws RepositoryException {
        NodeTypeManager nodeTypeManager = session.getWorkspace().getNodeTypeManager();

        // Create a template for the node type ...
        NodeTypeTemplate type = nodeTypeManager.createNodeTypeTemplate();
        type.setName(typeName);
        type.setDeclaredSuperTypeNames(new String[] {"nt:unstructured"});
        type.setAbstract(false);
        type.setOrderableChildNodes(true);
        type.setMixin(false);
        type.setQueryable(true);
        nodeTypeManager.registerNodeType(type, true);
    }

    private String idList( Node... nodes ) throws RepositoryException {
        StringBuilder builder = new StringBuilder("(");
        for (int i = 0; i < nodes.length - 1; i++) {
            builder.append("'").append(nodes[i].getIdentifier()).append("'").append(",");
        }
        if (nodes.length > 0) {
            builder.append("'").append(nodes[nodes.length - 1].getIdentifier()).append("'");
        }
        builder.append(")");
        return builder.toString();
    }
}
