/* * 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 org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicLong; import java.util.regex.Pattern; import javax.jcr.Node; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.query.Row; import org.junit.Test; import org.modeshape.common.FixFor; import org.modeshape.common.util.FileUtil; import org.modeshape.jcr.api.index.IndexManager; import org.modeshape.jcr.api.query.Query; import org.modeshape.jcr.query.engine.IndexPlanners; /** * This test verifies that the local index provider works when the indexes are updated <em>synchronous</em>. See * {@link LocalIndexProviderAsynchronousTest} for verification of the asynchronous cases. * * @author Randall Hauch (rhauch@redhat.com) * @author Horia Chiorean (hchiorea@redhat.com) * * @see LocalIndexProviderAsynchronousTest */ public class LocalIndexProviderTest extends AbstractIndexProviderTest { @Override protected boolean useSynchronousIndexes() { return true; } @Override protected String providerName() { return "local"; } // --------------------------------------------------------------- // Override these so that we can easily run them via JUnit runner. // --------------------------------------------------------------- @Override @Test public void shouldAllowRegisteringNewIndexDefinitionWithSingleStringColumn() throws Exception { super.shouldAllowRegisteringNewIndexDefinitionWithSingleStringColumn(); } @Override @Test public void shouldUseSingleColumnStringIndexInQueryAgainstSameNodeType() throws Exception { super.shouldUseSingleColumnStringIndexInQueryAgainstSameNodeType(); } @Test public void shouldUseSingleColumnStringIndexForQueryWithNoCriteriaOtherThanPrimaryTypeViaFromClause() throws Exception { registerValueIndex("unstructuredNodes", "nt:unstructured", null, "*", "jcr:primaryType", PropertyType.NAME); // print = true; // Add a node that uses this type ... Node root = session().getRootNode(); Node book1 = root.addNode("myFirstBook"); book1.addMixin("mix:title"); book1.setProperty("jcr:title", "The Title"); Node book2 = root.addNode("mySecondBook"); book2.addMixin("mix:title"); book2.setProperty("jcr:title", "A Different Title"); // Create a node that is not a 'mix:title' and therefore won't be included in the SELECT clauses ... Node other = root.addNode("somethingElse"); other.setProperty("propA", "a value for property A"); other.setProperty("jcr:title", "The Title"); session.save(); // Compute a query plan that should use this index ... Query query = jcrSql2Query("SELECT * FROM [nt:unstructured]"); validateQuery().rowCount(3L).useIndex("unstructuredNodes").validate(query, query.execute()); } @FixFor( "MODE-2307" ) @Test public void shouldUseSingleColumnStringIndexForQueryWithSubselect() throws Exception { registerNodeType("nt:typeWithReference"); registerNodeType("nt:typeWithSysName"); registerValueIndex("refIndex", "nt:typeWithReference", null, "*", "referenceId", PropertyType.STRING); registerValueIndex("sysIndex", "nt:typeWithSysName", null, "*", "sysName", PropertyType.STRING); // print = true; Node root = session().getRootNode(); Node newNode1 = root.addNode("nodeWithSysName", "nt:typeWithSysName"); newNode1.setProperty("sysName", "X"); newNode1.addMixin("mix:referenceable"); Node newNode2 = root.addNode("nodeWithReference", "nt:typeWithReference"); newNode2.setProperty("referenceId", newNode1.getIdentifier()); session().save(); // Compute a query plan that should use this index ... Query query = jcrSql2Query("SELECT A.* FROM [nt:typeWithReference] AS A WHERE A.referenceId = $sysName"); query.bindValue("sysName", valueFactory().createValue(newNode1.getIdentifier())); validateQuery().rowCount(1L).useIndex("refIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT A.* FROM [nt:typeWithReference] AS A WHERE A.referenceId IN ( $sysName )"); query.bindValue("sysName", valueFactory().createValue(newNode1.getIdentifier())); validateQuery().rowCount(1L).useIndex("refIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT B.[jcr:uuid] FROM [nt:typeWithSysName] AS B WHERE B.sysName = $sysName"); query.bindValue("sysName", valueFactory().createValue("X")); validateQuery().rowCount(1L).useIndex("sysIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT A.* FROM [nt:typeWithReference] AS A WHERE A.referenceId IN ( " + "SELECT B.[jcr:uuid] FROM [nt:typeWithSysName] AS B WHERE B.sysName = $sysName )"); query.bindValue("sysName", valueFactory().createValue("X")); validateQuery().rowCount(1L).considerIndexes("refIndex", "sysIndex").validate(query, query.execute()); } @FixFor( "MODE-2307" ) @Test public void shouldUseSingleColumnStringIndexForQueryWithJoin() throws Exception { registerNodeType("nt:typeWithReference"); registerNodeType("nt:typeWithSysName"); registerValueIndex("refIndex", "nt:typeWithReference", null, "*", "referenceId", PropertyType.STRING); registerValueIndex("sysIndex", "nt:typeWithSysName", null, "*", "sysName", PropertyType.STRING); registerNodeTypeIndex("typesIndex", "nt:base", null, "*", "jcr:primaryType", PropertyType.STRING); // print = true; Node root = session().getRootNode(); Node newNode1 = root.addNode("nodeWithSysName", "nt:typeWithSysName"); newNode1.setProperty("sysName", "X"); newNode1.addMixin("mix:referenceable"); Node newNode2 = root.addNode("nodeWithReference", "nt:typeWithReference"); newNode2.setProperty("referenceId", newNode1.getIdentifier()); session.save(); // Compute a query plan that should use this index ... Query query = jcrSql2Query("SELECT A.* FROM [nt:typeWithReference] AS A " + "JOIN [nt:typeWithSysName] AS B ON A.referenceId = B.[jcr:uuid] " // + "WHERE B.sysName = $sysName"); query.bindValue("sysName", valueFactory().createValue("X")); validateQuery().rowCount(1L).considerIndexes("sysIndex", "refIndex", "typesIndex").validate(query, query.execute()); } @FixFor( "MODE-2312" ) @Test public void shouldUseImplicitIdIndex() throws Exception { Node root = session().getRootNode(); Node newNode1 = root.addNode("nodeA"); newNode1.setProperty("foo", "X"); newNode1.addMixin("mix:referenceable"); Node newNode2 = root.addNode("nodeB"); newNode2.setProperty("foo", "Y"); session().save(); // print = true; // Compute a query plan that should use this index ... final String uuid = newNode1.getIdentifier(); Query query = jcrSql2Query("SELECT [jcr:path] FROM [nt:unstructured] WHERE [jcr:uuid] = '" + uuid + "'"); validateQuery().rowCount(1L).useIndex(IndexPlanners.NODE_BY_ID_INDEX_NAME).validate(query, query.execute()); query = jcrSql2Query("SELECT A.* FROM [nt:unstructured] AS A WHERE A.[jcr:uuid] = $uuidValue"); query.bindValue("uuidValue", valueFactory().createValue(uuid)); validateQuery().rowCount(1L).useIndex(IndexPlanners.NODE_BY_ID_INDEX_NAME).validate(query, query.execute()); } @FixFor( "MODE-2312" ) @Test public void shouldUseImplicitPathIndex() throws Exception { Node root = session().getRootNode(); Node newNode1 = root.addNode("nodeA"); newNode1.setProperty("foo", "X"); newNode1.addMixin("mix:referenceable"); Node newNode2 = root.addNode("nodeB"); newNode2.setProperty("foo", "Y"); session().save(); // print = true; // Compute a query plan that should use this index ... final String pathValue = newNode1.getPath(); Query query = jcrSql2Query("SELECT [jcr:path] FROM [nt:unstructured] WHERE [jcr:path] = '" + pathValue + "'"); validateQuery().rowCount(1L).useIndex(IndexPlanners.NODE_BY_PATH_INDEX_NAME).validate(query, query.execute()); query = jcrSql2Query("SELECT A.* FROM [nt:unstructured] AS A WHERE A.[jcr:path] = $pathValue"); query.bindValue("pathValue", valueFactory().createValue(pathValue)); validateQuery().rowCount(1L).useIndex(IndexPlanners.NODE_BY_PATH_INDEX_NAME).validate(query, query.execute()); } @Test public void shouldUseSingleColumnStringIndexForQueryWithNoCriteriaOtherThanMixinViaFromClause() throws Exception { registerValueIndex("titleNodes", "mix:title", null, "*", "jcr:mixinTypes", PropertyType.NAME); // print = true; // Add a node that uses this type ... Node root = session().getRootNode(); Node book1 = root.addNode("myFirstBook"); book1.addMixin("mix:title"); book1.setProperty("jcr:title", "The Title"); Node book2 = root.addNode("mySecondBook"); book2.addMixin("mix:title"); book2.setProperty("jcr:title", "A Different Title"); // Create a node that is not a 'mix:title' and therefore won't be included in the SELECT clauses ... Node other = root.addNode("somethingElse"); other.setProperty("propA", "a value for property A"); other.setProperty("jcr:title", "The Title"); session.save(); // Compute a query plan that should use this index ... Query query = jcrSql2Query("SELECT * FROM [mix:title]"); validateQuery().rowCount(2L).useIndex("titleNodes").validate(query, query.execute()); } @Test public void shouldUseSingleColumnNodeTypeIndexForQueryWithNoCriteriaOtherThanPrimaryTypeViaFromClause() throws Exception { registerNodeTypeIndex("primaryTypes", "nt:base", null, "*", "jcr:primaryType", PropertyType.STRING); registerNodeTypeIndex("mixinTypes", "nt:base", null, "*", "jcr:mixinTypes", PropertyType.STRING); // print = true; // Add a node that uses this type ... Node root = session().getRootNode(); Node book1 = root.addNode("myFirstBook"); book1.addMixin("mix:title"); book1.setProperty("jcr:title", "The Title"); Node book2 = root.addNode("mySecondBook"); book2.addMixin("mix:title"); book2.setProperty("jcr:title", "A Different Title"); // Create a node that is not a 'mix:title' and therefore won't be included in the SELECT clauses ... Node other = root.addNode("somethingElse"); other.setProperty("propA", "a value for property A"); other.setProperty("jcr:title", "The Title"); session.save(); // Compute a query plan that should use this index ... Query query = jcrSql2Query("SELECT * FROM [mix:title]"); validateQuery().rowCount(2L).useIndex("mixinTypes").validate(query, query.execute()); // Compute a query plan that should use this index ... query = jcrSql2Query("SELECT * FROM [nt:unstructured]"); validateQuery().rowCount(3L).useIndex("primaryTypes").validate(query, query.execute()); } @FixFor( "MODE-2297" ) @Test public void shouldExecuteQueryUsingSetOperationOfQueriesWithJoins() throws Exception { registerNodeType("nt:formInstVersion"); registerNodeType("nt:formInst"); Node root = session().getRootNode(); Node baseNode = root.addNode("formInst", "nt:formInst"); baseNode.addMixin("mix:referenceable"); Node v1Node = baseNode.addNode("version", "nt:formInstVersion"); v1Node.addMixin("mix:referenceable"); Node v2Node = baseNode.addNode("version", "nt:formInstVersion"); v2Node.addMixin("mix:referenceable"); v2Node.setProperty("previous_version", v1Node.getIdentifier()); session.save(); // print = true; String sql1 = "SELECT BASE.* from [nt:formInstVersion] as BASE " // + "JOIN [nt:formInst] AS FORMINST ON ISCHILDNODE(BASE,FORMINST)"; Query query = session.getWorkspace().getQueryManager().createQuery(sql1, Query.JCR_SQL2); validateQuery().rowCount(2).validate(query, query.execute()); String sql2 = "SELECT BASE.* from [nt:formInstVersion] as BASE " // + "JOIN [nt:formInst] AS FORMINST ON ISCHILDNODE(BASE,FORMINST) " // + "JOIN [nt:formInstVersion] AS FORMINSTNEXT ON FORMINSTNEXT.previous_version = BASE.[jcr:uuid]"; query = session.getWorkspace().getQueryManager().createQuery(sql2, Query.JCR_SQL2); validateQuery().rowCount(1).validate(query, query.execute()); String sql = sql1 + " UNION " + sql2; query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2); validateQuery().rowCount(2).validate(query, query.execute()); sql = sql2 + " UNION " + sql1; query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2); validateQuery().rowCount(2).validate(query, query.execute()); sql = sql1 + " INTERSECT " + sql2; query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2); validateQuery().rowCount(1).validate(query, query.execute()); sql = sql2 + " INTERSECT " + sql1; query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2); validateQuery().rowCount(1).validate(query, query.execute()); sql = sql1 + " EXCEPT " + sql2; query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2); validateQuery().rowCount(1).validate(query, query.execute()); sql = sql2 + " EXCEPT " + sql1; query = session.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2); validateQuery().rowCount(0).validate(query, query.execute()); } @Test public void shouldNotUseSingleColumnStringIndexInQueryAgainstSuperType() throws Exception { registerValueIndex("descriptionIndex", "mix:title", "Index for the 'jcr:title' property on mix:title", "*", "jcr:title", PropertyType.STRING); // print = true; // Add a node that uses this type ... Node root = session().getRootNode(); Node book1 = root.addNode("myFirstBook"); book1.addMixin("mix:title"); book1.setProperty("jcr:title", "The Title"); Node book2 = root.addNode("mySecondBook"); book2.addMixin("mix:title"); book2.setProperty("jcr:title", "A Different Title"); // Create a node that is not a 'mix:title' and therefore won't be included in the query ... Node other = root.addNode("somethingElse"); other.setProperty("propA", "a value for property A"); other.setProperty("jcr:title", "The Title"); session.save(); // Compute a query plan that will NOT use this index, since the selector doesn't match the index's node type. // If we would use this index, the index doesn't know about non-mix:title nodes like the 'other' node ... Query query = jcrSql2Query("SELECT * FROM [nt:base] WHERE [jcr:title] = 'The Title'"); validateQuery().rowCount(2L).validate(query, query.execute()); // Compute a query plan that will NOT use this index, since the selector doesn't match the index's node type. // If we would use this index, the index doesn't know about non-mix:title nodes like the 'other' node ... query = jcrSql2Query("SELECT * FROM [nt:base] WHERE [jcr:title] LIKE '% Title'"); validateQuery().rowCount(3L).validate(query, query.execute()); } @Test public void shouldUseSingleColumnLongIndexInQueryAgainstSameNodeType() throws Exception { registerNodeTypes("cnd/notionalTypes.cnd"); registerValueIndex("longIndex", "notion:typed", "Long value index", "*", "notion:longProperty", PropertyType.LONG); // print = true; // Add a node that uses this type ... Node root = session().getRootNode(); Node obj1 = root.addNode("notionalObjectA", "notion:typed"); obj1.setProperty("notion:longProperty", 1234L); obj1.setProperty("notion:booleanProperty", true); Node obj2 = root.addNode("notionalObjectB", "notion:typed"); obj2.setProperty("notion:longProperty", 2345L); obj2.setProperty("notion:booleanProperty", true); Node obj3 = root.addNode("notionalObjectB", "notion:typed"); obj3.setProperty("notion:longProperty", -1L); obj3.setProperty("notion:booleanProperty", true); // Create a node that is not a 'notion:typed' and therefore won't be included in the query ... Node other = root.addNode("somethingElse"); other.setProperty("notion:longProperty", 100L); other.setProperty("jcr:title", "The Title"); session.save(); // Issues some queries that should use this index ... Query query = jcrSql2Query("SELECT * FROM [notion:typed] WHERE [notion:longProperty] = 1234"); validateQuery().rowCount(1L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [notion:typed] WHERE [notion:longProperty] <= 1234"); validateQuery().rowCount(2L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [notion:typed] WHERE [notion:longProperty] < 1234"); validateQuery().rowCount(1L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [notion:typed] WHERE [notion:longProperty] > 0"); validateQuery().rowCount(2L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [notion:typed] WHERE [notion:longProperty] BETWEEN 1234 AND 5678"); validateQuery().rowCount(2L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [notion:typed] WHERE [notion:longProperty] BETWEEN 1234 EXCLUSIVE AND 5678"); validateQuery().rowCount(1L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [notion:typed] WHERE [notion:longProperty] NOT BETWEEN 1234 EXCLUSIVE AND 5678"); validateQuery().rowCount(2L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [notion:typed] WHERE [notion:longProperty] <= -1"); validateQuery().rowCount(1L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [notion:typed] WHERE [notion:longProperty] <= CAST('-1' AS LONG)"); validateQuery().rowCount(1L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [notion:typed] WHERE [notion:longProperty] < -1"); validateQuery().rowCount(0L).validate(query, query.execute()); // Issue a query that does not use this index ... query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE [notion:longProperty] > 10"); validateQuery().useNoIndexes().rowCount(3L).validate(query, query.execute()); } @Test public void shouldUseSingleColumnDateIndexInQueryAgainstSameNodeType() throws Exception { registerValueIndex("dateIndex", "mix:lastModified", "Date value index", "*", "jcr:lastModified", PropertyType.DATE); // print = true; // Add a node that uses this type ... Node root = session().getRootNode(); Node obj1 = root.addNode("notionalObjectA"); obj1.addMixin("mix:lastModified"); Node obj2 = root.addNode("notionalObjectB"); obj2.addMixin("mix:lastModified"); // Create a node that is not a 'notion:typed' and therefore won't be included in the query ... Node other = root.addNode("somethingElse"); other.setProperty("jcr:lastModified", Calendar.getInstance()); session.save(); // Issues some queries that should use this index ... Query query = jcrSql2Query("SELECT * FROM [mix:lastModified] WHERE [jcr:lastModified] > CAST('2012-10-21T00:00:00.000' AS DATE)"); validateQuery().rowCount(2L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [mix:lastModified] WHERE [jcr:lastModified] < CAST('2999-10-21T00:00:00.000' AS DATE)"); validateQuery().rowCount(2L).validate(query, query.execute()); // Issue a query that does not use this index ... query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE [jcr:lastModified] > CAST('2012-10-21T00:00:00.000' AS DATE)"); validateQuery().rowCount(3L).validate(query, query.execute()); } @Test public void shouldUseSingleColumnDateAsLongIndexInQueryAgainstSameNodeType() throws Exception { registerValueIndex("dateIndex", "mix:lastModified", "Date value index", "*", "jcr:lastModified", PropertyType.LONG); // print = true; // Add a node that uses this type ... Node root = session().getRootNode(); Node obj1 = root.addNode("notionalObjectA"); obj1.addMixin("mix:lastModified"); Node obj2 = root.addNode("notionalObjectB"); obj2.addMixin("mix:lastModified"); // Create a node that is not a 'notion:typed' and therefore won't be included in the query ... Node other = root.addNode("somethingElse"); other.setProperty("jcr:lastModified", Calendar.getInstance()); session.save(); // Issues some queries that should use this index ... Query query = jcrSql2Query("SELECT * FROM [mix:lastModified] WHERE [jcr:lastModified] > CAST('2012-10-21T00:00:00.000' AS DATE)"); validateQuery().rowCount(2L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [mix:lastModified] WHERE [jcr:lastModified] < CAST('2999-10-21T00:00:00.000' AS DATE)"); validateQuery().rowCount(2L).validate(query, query.execute()); // Issue a query that does not use this index ... query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE [jcr:lastModified] > CAST('2012-10-21T00:00:00.000' AS DATE)"); validateQuery().rowCount(3L).validate(query, query.execute()); } @Test public void shouldUseSingleColumnNodeNameIndexInQueryAgainstSameNodeType() throws Exception { registerValueIndex("nameIndex", "nt:base", "Node name index", "*", "jcr:name", PropertyType.NAME); // print = true; // Add a node that uses this type ... Node book1 = session().getRootNode().addNode("myFirstBook"); book1.addMixin("mix:title"); book1.setProperty("jcr:title", "The Title"); Node book2 = session().getRootNode().addNode("mySecondBook"); book2.addMixin("mix:title"); book2.setProperty("jcr:title", "A Different Title"); Node other = session().getRootNode().addNode("somethingElse"); other.setProperty("propA", "a value for property A"); other.setProperty("jcr:title", "The Title"); session.save(); // Issues some queries that should use this index ... Query query = jcrSql2Query("SELECT * FROM [nt:base] WHERE [jcr:name] = 'myFirstBook'"); validateQuery().rowCount(1L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [nt:base] WHERE NAME() LIKE 'myFirstBook'"); validateQuery().rowCount(1L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE NAME() LIKE '%Book'"); validateQuery().rowCount(2L).validate(query, query.execute()); } @Test public void shouldUseSingleColumnNodeDepthIndexInQueryAgainstSameNodeType() throws Exception { registerValueIndex("depthIndex", "nt:unstructured", "Node depth index", "*", "mode:depth", PropertyType.LONG); // print = true; // Add a node that uses this type ... Node book1 = session().getRootNode().addNode("myFirstBook"); book1.addMixin("mix:title"); book1.setProperty("jcr:title", "The Title"); Node book2 = session().getRootNode().addNode("mySecondBook"); book2.addMixin("mix:title"); book2.setProperty("jcr:title", "A Different Title"); Node other = book2.addNode("chapter"); other.setProperty("propA", "a value for property A"); other.setProperty("jcr:title", "The Title"); session.save(); // Issues some queries that should use this index ... Query query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE [mode:depth] > 0"); validateQuery().rowCount(3L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE DEPTH() > 0"); validateQuery().rowCount(3L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE DEPTH() > 1"); validateQuery().rowCount(1L).validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE DEPTH() >= 2"); validateQuery().rowCount(1L).validate(query, query.execute()); } @Test public void shouldUseSingleColumnNodePathIndexInQueryAgainstSameNodeType() throws Exception { registerValueIndex("pathIndex", "nt:unstructured", "Node path index", "*", "jcr:path", PropertyType.PATH); // print = true; // Add a node that uses this type ... Node book1 = session().getRootNode().addNode("myFirstBook"); book1.addMixin("mix:title"); book1.setProperty("jcr:title", "The Title"); Node book2 = session().getRootNode().addNode("mySecondBook"); book2.addMixin("mix:title"); book2.setProperty("jcr:title", "A Different Title"); Node other = book2.addNode("chapter"); other.setProperty("propA", "a value for property A"); other.setProperty("jcr:title", "The Title"); session.save(); // Issues a query that should NOT use this index because direct lookup by path is lower cost ... Query query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE [jcr:path] = '/myFirstBook'"); validateQuery().rowCount(1L).useIndex("NodeByPath").considerIndex("pathIndex").validate(query, query.execute()); // Issues a query that should NOT use this index ... query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE [jcr:path] LIKE '/my%Book'"); validateQuery().rowCount(2L).useNoIndexes().validate(query, query.execute()); // Issues some queries that should use this index ... query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE [jcr:path] > '/mySecondBook'"); validateQuery().rowCount(1L).useIndex("pathIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE PATH() > '/mySecondBook'"); validateQuery().rowCount(1L).useIndex("pathIndex").validate(query, query.execute()); } @FixFor( "MODE-2290" ) @Test public void shouldUseSingleColumnResidualPropertyIndexInQueryAgainstSameNodeType() throws Exception { registerValueIndex("pathIndex", "nt:unstructured", "Node path index", "*", "someProperty", PropertyType.STRING); registerValueIndex("titleIndex", "mix:title", "Title index", "*", "jcr:title", PropertyType.STRING); // print = true; // Add a node that uses this type ... Node book1 = session().getRootNode().addNode("myFirstBook"); book1.addMixin("mix:title"); book1.setProperty("jcr:title", "The Title"); book1.setProperty("someProperty", "value1"); Node book2 = session().getRootNode().addNode("mySecondBook"); book2.addMixin("mix:title"); book2.setProperty("jcr:title", "A Different Title"); book2.setProperty("someProperty", "value2"); Node other = book2.addNode("chapter"); other.setProperty("propA", "a value for property A"); other.setProperty("jcr:title", "The Title"); other.setProperty("someProperty", "value1"); session.save(); // Issues some queries that should use this index ... Query query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE someProperty = 'value1'"); validateQuery().rowCount(2L).useIndex("pathIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [nt:unstructured] WHERE someProperty = $value"); query.bindValue("value", session().getValueFactory().createValue("value1")); validateQuery().rowCount(2L).useIndex("pathIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT table.* FROM [nt:unstructured] AS table WHERE table.someProperty = 'value1'"); validateQuery().rowCount(2L).useIndex("pathIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT table.* FROM [nt:unstructured] AS table WHERE table.someProperty = $value"); query.bindValue("value", session().getValueFactory().createValue("value1")); validateQuery().rowCount(2L).useIndex("pathIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT * FROM [mix:title] WHERE [jcr:title] = 'The Title'"); validateQuery().rowCount(1L).useIndex("titleIndex").validate(query, query.execute()); query = jcrSql2Query("SELECT title.* FROM [mix:title] as title WHERE title.[jcr:title] = 'The Title'"); validateQuery().rowCount(1L).useIndex("titleIndex").validate(query, query.execute()); } @FixFor( "MODE-2314" ) @Test public void shouldIndexNodeAfterChange() throws Exception { // print = true; registerValueIndex("ref1", "nt:unstructured", "", null, "ref1", PropertyType.STRING); registerValueIndex("ref2", "nt:unstructured", "", null, "ref2", PropertyType.STRING); // Wait until all content has been indexed ... waitForIndexes(500L); Node newNode1 = session.getRootNode().addNode("nodeWithSysName", "nt:unstructured"); session.save(); // THIS IS CAUSING the node not being indexed printMessage("Node Created ..."); final String uuId1 = "cccccccccccccccccccccc-0000-1111-1234-123456789abcd"; newNode1.setProperty("ref1", uuId1); newNode1.setProperty("ref2", uuId1); session.save(); printMessage("Node updated ..."); Query query = jcrSql2Query("SELECT A.ref1 FROM [nt:unstructured] AS A WHERE A.ref2 = $ref2"); query.bindValue("ref2", session().getValueFactory().createValue(uuId1)); validateQuery().rowCount(1L).useIndex("ref2").onEachRow(new ValidateQuery.Predicate() { @Override public void validate( int rowNumber, Row row ) throws RepositoryException { if (rowNumber == 1) { assertThat(row.getValue("ref1").getString(), is(uuId1)); } } }).validate(query, query.execute()); } @FixFor( "MODE-2318" ) @Test public void shouldNotReindexOnStartup() throws Exception { // print = true; registerValueIndex("ref1", "nt:unstructured", "", null, "ref1", PropertyType.STRING); registerValueIndex("ref2", "nt:unstructured", "", null, "ref2", PropertyType.STRING); registerValueIndex("file", "nt:file", "", null, "unused", PropertyType.STRING); Node newNode1 = session.getRootNode().addNode("nodeWithSysName", "nt:unstructured"); final String uuId1 = "cccccccccccccccccccccc-0000-1111-1234-123456789abcd"; newNode1.setProperty("ref1", uuId1); newNode1.setProperty("ref2", uuId1); session.save(); printMessage("Nodes Created ..."); // Shutdown the repository and restart it ... stopRepository(); printMessage("Stopped repository. Restarting ..."); assertStorageLocationUnchangedAfterRestart(); } protected void assertStorageLocationUnchangedAfterRestart() throws Exception { // register the total size and last modified timestamp of the place where indexes are stored for the default provider.. File indexesDir = new File("target/persistent_repository/indexes/local"); assertTrue(indexesDir.exists() && indexesDir.isDirectory() && indexesDir.canRead()); long size = FileUtil.size(indexesDir.getPath()); final AtomicLong lastModifiedDate = lastModifiedFileTime(indexesDir, ".*\\.db"); startRepository(); printMessage("Repository restart complete"); // and now check that the storage folder is unchanged assertTrue(indexesDir.exists() && indexesDir.isDirectory() && indexesDir.canRead()); assertEquals(size, FileUtil.size(indexesDir.getPath())); assertEquals(lastModifiedDate.get(), lastModifiedFileTime(indexesDir, ".*\\.db").get()); } protected AtomicLong lastModifiedFileTime(File indexesDir, String nameRegex) throws IOException { final AtomicLong lastModifiedDate = new AtomicLong(0); final Pattern pattern = Pattern.compile(nameRegex); Files.walkFileTree(indexesDir.toPath(), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { long lastModified = attrs.lastModifiedTime().toMillis(); if (lastModifiedDate.get() < lastModified && pattern.matcher(file.getFileName().toString()).matches()) { lastModifiedDate.set(lastModified); } return super.visitFile(file, attrs); } }); return lastModifiedDate; } @FixFor( "MODE-2296" ) @Test public void shouldUseIndexesEvenWhenLocalIndexDoesNotContainValueUsedInCriteria() throws Exception { registerValueIndex("pathIndex", "nt:unstructured", "Node path index", "*", "someProperty", PropertyType.STRING); // print = true; // Add a node that uses this type ... Node book1 = session().getRootNode().addNode("myFirstBook"); book1.addMixin("mix:title"); book1.setProperty("jcr:title", "The Title"); book1.setProperty("someProperty", "value1"); Node book2 = session().getRootNode().addNode("mySecondBook"); book2.addMixin("mix:title"); book2.setProperty("jcr:title", "A Different Title"); book2.setProperty("someProperty", "value2"); Node other = book2.addNode("chapter"); other.setProperty("propA", "a value for property A"); other.setProperty("jcr:title", "The Title"); other.setProperty("someProperty", "value1"); session.save(); // Issues some queries that should use this index ... String queryStr = "SELECT * FROM [nt:unstructured] WHERE someProperty = 'non-existant'"; Query query = jcrSql2Query(queryStr); validateQuery().rowCount(0L).useIndex("pathIndex").validate(query, query.execute()); // Try with a join ... queryStr = "SELECT a.* FROM [nt:unstructured] AS a JOIN [nt:unstructured] AS b ON a.someProperty = b.someProperty WHERE b.someProperty = 'non-existant-value'"; query = jcrSql2Query(queryStr); validateQuery().rowCount(0L).useIndex("pathIndex").validate(query, query.execute()); } @FixFor( "MODE-2295" ) @Test public void shouldUseIndexesCreatedOnSubtypeUsingColumnsFromResidualProperty() throws RepositoryException, InterruptedException { String typeName = "nt:someType2"; registerNodeType(typeName); registerValueIndex("nt_someType2", typeName, null, "*", "sysName", PropertyType.STRING); Node newNode = session.getRootNode().addNode("SOMENODE", typeName); newNode.setProperty("sysName", "X"); // BEFORE SAVING, issue a query that will NOT use the index. The query should not see the transient node ... String queryStr = "select BASE.* FROM [" + typeName + "] as BASE WHERE NAME(BASE) = 'SOMENODE'"; Query query = jcrSql2Query(queryStr); validateQuery().rowCount(0L).useNoIndexes().validate(query, query.execute()); // Now issue a query that will use the index. The query should not see the transient node... queryStr = "select BASE.* FROM [" + typeName + "] as BASE WHERE BASE.sysName='X'"; query = jcrSql2Query(queryStr); validateQuery().rowCount(0L).useIndex("nt_someType2").validate(query, query.execute()); // Save the transient data ... session.save(); // Issue a query that will NOT use the index ... queryStr = "select BASE.* FROM [" + typeName + "] as BASE WHERE NAME(BASE) = 'SOMENODE'"; query = jcrSql2Query(queryStr); validateQuery().rowCount(1L).useNoIndexes().validate(query, query.execute()); // Now issue a query that will use the index. queryStr = "select BASE.* FROM [" + typeName + "] as BASE WHERE BASE.sysName='X'"; query = jcrSql2Query(queryStr); validateQuery().rowCount(1L).useIndex("nt_someType2").validate(query, query.execute()); registerValueIndex("nt_unstructured", "nt:unstructured", null, "*", "sysName", PropertyType.STRING); Node newNode2 = session.getRootNode().addNode("SOMENODE2", "nt:unstructured"); newNode2.setProperty("sysName", "X"); session.save(); // print = true; queryStr = "select BASE.* FROM [nt:unstructured] as BASE WHERE BASE.sysName='X'"; query = jcrSql2Query(queryStr); validateQuery().useIndex("nt_unstructured").rowCount(2L).validate(query, query.execute()); } @FixFor( "MODE-2313" ) @Test public void shouldAllowAddingIndexWhileSessionsAreQuerying() throws Exception { registerValueIndex("titleNodes", "mix:title", null, "*", "jcr:mixinTypes", PropertyType.NAME); // print = true; // Add a node that uses this type ... Node root = session().getRootNode(); Node book1 = root.addNode("myFirstBook"); book1.addMixin("mix:title"); book1.setProperty("jcr:title", "The Title"); Node book2 = root.addNode("mySecondBook"); book2.addMixin("mix:title"); book2.setProperty("jcr:title", "A Different Title"); // Create a node that is not a 'mix:title' and therefore won't be included in the SELECT clauses ... Node other = root.addNode("somethingElse"); other.setProperty("propA", "a value for property A"); other.setProperty("jcr:title", "The Title"); session.save(); for (int i = 0; i != 5; ++i) { // Compute a query plan that should use this index ... Query query = jcrSql2Query("SELECT * FROM [mix:title]"); for (int j = 0; j != 5; ++j) { validateQuery().rowCount(2L).useIndex("titleNodes").validate(query, query.execute()); } } final int numThreads = 10; final int numQueriesEachThread = 100; final int numIndexes = 4; final CountDownLatch startLatch = new CountDownLatch(1); final CountDownLatch stopLatch = new CountDownLatch(numThreads); final Runnable queryRunner = new Runnable() { @Override public void run() { JcrSession session = null; try { session = repository().login(); Query query = jcrSql2Query(session, "SELECT * FROM [mix:title]"); startLatch.await(); for (int i = 0; i != numQueriesEachThread; ++i) { // Compute a query plan that should use this index, UNLESS the index is currently undergoing rebuilding... validateQuery()./*rowCount(2L).useIndex("titleNodes")*/validate(query, query.execute()); } } catch (Exception e) { e.printStackTrace(); fail(e.getMessage()); } finally { printMessage("Completing thread"); if (session != null) session.logout(); stopLatch.countDown(); } } }; // Start the threads; they'll each wait just before they execute their queries ... for (int i = 0; i != numThreads; ++i) { Thread thread = new Thread(queryRunner); thread.start(); } // Release the latch so everything starts ... startLatch.countDown(); // and then create several indexes ... for (int i = 0; i != numIndexes; ++i) { registerValueIndex("extraIndex" + i, "nt:file", null, "*", "jcr:lastModified", PropertyType.DATE); } // Wait for the threads to complete ... stopLatch.await(); } @FixFor( "MODE-2346" ) @Test public void shouldUseImplicitIndexesWithLowerCardinalityOverExplicitIndexes() throws Exception { String explicitNodesById = "explicitNodesById"; registerValueIndex(explicitNodesById, "nt:unstructured", "Nodes by id explicit index", "*", "jcr:uuid", PropertyType.STRING); String explicitNodesByPath = "explicitNodesByPath"; registerValueIndex(explicitNodesByPath, "nt:unstructured", "Nodes by path explicit index", "*", "jcr:path", PropertyType.PATH); Node root = session().getRootNode(); Node nodeA = root.addNode("nodeA"); nodeA.addMixin("mix:referenceable"); session().save(); // print = true; // Compute a query plan that should use this index ... final String uuid = nodeA.getIdentifier(); Query query = jcrSql2Query("SELECT [jcr:path] FROM [nt:unstructured] WHERE [jcr:uuid] = '" + uuid + "'"); validateQuery() .rowCount(1L) .considerIndexes(IndexPlanners.NODE_BY_ID_INDEX_NAME, explicitNodesById) .useIndex(IndexPlanners.NODE_BY_ID_INDEX_NAME) .validate(query, query.execute()); query = jcrSql2Query("SELECT [jcr:path] FROM [nt:unstructured] WHERE [jcr:path] = '/nodeA'"); validateQuery() .rowCount(1L) .considerIndexes(IndexPlanners.NODE_BY_PATH_INDEX_NAME, explicitNodesByPath) .useIndex(IndexPlanners.NODE_BY_PATH_INDEX_NAME) .validate(query, query.execute()); } @FixFor( "MODE-2346" ) @Test public void shouldUseExplicitIndexesWithLowerCardinalityOverImplicitIndexes() throws Exception { String explicitIndex = "explicitIndex"; registerValueIndex(explicitIndex, "nt:unstructured", "Foo index", "*", "foo", PropertyType.STRING); // wait a bit to make sure the index definitions have been updated Node root = session().getRootNode(); Node nodeA = root.addNode("nodeA"); Node nodeB = nodeA.addNode("nodeB"); nodeB.setProperty("foo", "X"); session().save(); // print = true; // Compute a query plan that should use this index ... Query query = jcrSql2Query( "SELECT [jcr:path] FROM [nt:unstructured] AS node WHERE ISDESCENDANTNODE(node, '/nodeA') AND node.[foo]='X'"); validateQuery() .rowCount(1L) .considerIndexes(IndexPlanners.DESCENDANTS_BY_PATH_INDEX_NAME, explicitIndex) .useIndex(explicitIndex) .validate(query, query.execute()); } @Test @FixFor( "MODE-2355" ) public void shouldUseNoIndexesWhenIndexDoesntApplyToBothSidesOfOR() throws Exception { registerNodeTypes("cnd/authors.cnd"); registerValueIndex("index1", "my:authored", "Authors index", "*", "author", PropertyType.STRING); registerValueIndex("index2", "my:authored", "Coauthors index", "*", "coAuthors", PropertyType.STRING); Node root = session.getRootNode(); Node book1 = root.addNode("book1"); book1.setPrimaryType("my:content"); book1.setProperty("content", "book content 1"); book1.setProperty("author", "author1"); book1.setProperty("coAuthors", new String[] { "author2", "author3" }); Node book2 = root.addNode("book2"); book2.setPrimaryType("my:content"); book2.setProperty("content", "book content 2"); book2.setProperty("author", "author2"); session.save(); Query query = jcrSql2Query("select * from [my:content] where author in ($author) or coAuthors in ($author)"); query.bindValue("author", session.getValueFactory().createValue("author1")); validateQuery() .useNoIndexes() .rowCount(1) .hasNodesAtPaths("/book1") .validate(query, query.execute()); query.bindValue("author", session.getValueFactory().createValue("author2")); final List<String> expectedPaths = new ArrayList<>(Arrays.asList("/book1", "/book2")); validateQuery() .useNoIndexes() .rowCount(2) .onEachRow(new ValidateQuery.Predicate() { @Override public void validate( int rowNumber, Row row ) throws RepositoryException { assertTrue("Path not found", expectedPaths.remove(row.getPath())); } }) .validate(query, query.execute()); assertTrue("Not all paths found: " + expectedPaths, expectedPaths.isEmpty()); } @FixFor( "MODE-2401" ) @Test public void shouldNotConsiderNonQueryableNodeTypes() throws RepositoryException, InterruptedException { String typeName = "nt:nonQueryableFolder"; registerNodeType(typeName, false, false, "nt:folder"); registerNodeTypeIndex("typesIndex", "nt:folder", null, "*", "jcr:primaryType", PropertyType.STRING); session.getRootNode().addNode("nonQueryableFolder", typeName); session.getRootNode().addNode("regularFolder1", "nt:folder"); Node folder2 = session.getRootNode().addNode("regularFolder2", typeName); folder2.addNode("subFolder", "nt:folder"); session.save(); final List<String> expectedResults = new ArrayList<>(Arrays.asList("/regularFolder1", "/regularFolder2/subFolder")); Query query = jcrSql2Query("select folder.[jcr:name] FROM [nt:folder] as folder"); validateQuery() .rowCount(2L) .useIndex("typesIndex") .onEachRow(new ValidateQuery.Predicate() { @Override public void validate( int rowNumber, Row row ) throws RepositoryException { expectedResults.remove(row.getPath()); } }) .validate(query, query.execute()); assertTrue("Not all expected nodes found: " + expectedResults, expectedResults.isEmpty()); } @FixFor( "MODE-2401" ) @Test public void shouldNotConsiderNonQueryableMixins() throws RepositoryException, InterruptedException { String mixinName = "nt:nonQueryableFolderMixin"; registerNodeType(mixinName, false, true); registerNodeTypeIndex("typesIndex", "nt:folder", null, "*", "jcr:primaryType", PropertyType.STRING); Node folder1 = session.getRootNode().addNode("folder1", "nt:folder"); Node folder2 = session.getRootNode().addNode("folder2", "nt:folder"); folder2.addMixin(mixinName); session.save(); // test with the initial node config final List<String> expectedResults = new ArrayList<>(Collections.singletonList("/folder1")); Query query = jcrSql2Query("select folder.[jcr:name] FROM [nt:folder] as folder"); validateQuery() .rowCount(1L) .useIndex("typesIndex") .onEachRow(new ValidateQuery.Predicate() { @Override public void validate( int rowNumber, Row row ) throws RepositoryException { expectedResults.remove(row.getPath()); } }) .validate(query, query.execute()); assertTrue("Not all expected nodes found: " + expectedResults, expectedResults.isEmpty()); // add the mixin on the 1st node and reindex folder1.addMixin(mixinName); session.save(); session.getWorkspace().reindex("/"); query = jcrSql2Query("select folder.[jcr:name] FROM [nt:folder] as folder"); validateQuery() .rowCount(0L) .useIndex("typesIndex") .validate(query, query.execute()); // remove both mixins and reindex folder1.removeMixin(mixinName); folder2.removeMixin(mixinName); session.save(); session.getWorkspace().reindex("/"); final List<String> expectedResults2 = new ArrayList<>(Arrays.asList("/folder1", "/folder2")); query = jcrSql2Query("select folder.[jcr:name] FROM [nt:folder] as folder"); validateQuery() .rowCount(2L) .useIndex("typesIndex") .onEachRow(new ValidateQuery.Predicate() { @Override public void validate( int rowNumber, Row row ) throws RepositoryException { expectedResults2.remove(row.getPath()); } }) .validate(query, query.execute()); assertTrue("Not all expected nodes found: " + expectedResults2, expectedResults2.isEmpty()); } @FixFor( "MODE-2432 ") @Test public void shouldExposeManagedIndexStatuses() throws Exception { String indexName = "explicitIndex"; registerValueIndex(indexName, "nt:unstructured", "Foo index", "*", "foo", PropertyType.STRING); assertEquals(IndexManager.IndexStatus.NON_EXISTENT, indexManager().getIndexStatus("unknown", indexName, "default")); assertEquals(IndexManager.IndexStatus.NON_EXISTENT, indexManager().getIndexStatus(providerName(), "invalid_name", "default")); assertEquals(IndexManager.IndexStatus.NON_EXISTENT, indexManager().getIndexStatus(providerName(), indexName, "invalid_ws")); assertEquals(IndexManager.IndexStatus.ENABLED, indexManager().getIndexStatus(providerName(), indexName, "default")); int nodeCount = 100; for (int i = 0; i < nodeCount; i++) { Node node = session.getRootNode().addNode("node_" + i); node.setProperty("foo", UUID.randomUUID().toString()); } session.save(); assertEquals(IndexManager.IndexStatus.ENABLED, indexManager().getIndexStatus(providerName(), indexName, "default")); Future<Boolean> reindexingResult = session.getWorkspace().reindexAsync(); assertEquals(true, reindexingResult.get()); assertEquals(IndexManager.IndexStatus.ENABLED, indexManager().getIndexStatus(providerName(), indexName, "default")); indexManager().unregisterIndexes(indexName); // removing the actual index is async (event based) Thread.sleep(100); assertEquals(IndexManager.IndexStatus.NON_EXISTENT, indexManager().getIndexStatus(providerName(), indexName, "default")); } @Test @FixFor( "MODE-2432") public void shouldReturnIndexesWithACertainStatus() throws Exception { registerValueIndex("index1", "nt:unstructured", "Foo index", "*", "foo", PropertyType.STRING); registerValueIndex("index2", "nt:unstructured", "Bar index", "*", "bar", PropertyType.STRING); assertEquals(Arrays.asList("index1", "index2"), indexManager().getIndexNames(providerName(), "default", IndexManager.IndexStatus.ENABLED)); assertTrue(indexManager().getIndexNames(providerName(), "default", IndexManager.IndexStatus.REINDEXING).isEmpty()); assertTrue(indexManager().getIndexNames("missing", "default", IndexManager.IndexStatus.ENABLED).isEmpty()); assertTrue(indexManager().getIndexNames(providerName(), "missing", IndexManager.IndexStatus.ENABLED).isEmpty()); } @Test @FixFor( "MODE-2498") public void shouldSelectCorrectIndexWhenMultipleIndexesUseTheSameAncestorProperty() throws Exception { registerNodeType("mix:custom", true, true, "mix:title"); registerNodeType("mix:custom2", true, true, "mix:title"); registerValueIndex("custom_names", "mix:custom", null, "*", "jcr:name", PropertyType.NAME); registerValueIndex("custom2_names", "mix:custom2", null, "*", "jcr:name", PropertyType.NAME); //print = true; // Add a node that uses this type ... Node root = session().getRootNode(); Node book1 = root.addNode("myFirstBook"); book1.addMixin("mix:custom"); book1.setProperty("jcr:title", "The Title"); session.save(); // Compute a query plan that should use this index ... Query query = jcrSql2Query("SELECT * FROM [mix:custom] as custom where custom.[jcr:name] = 'myFirstBook'"); validateQuery().rowCount(1L).useIndex("custom_names").validate(query, query.execute()); } @Test public void shouldSelectIndexWhenMultipleAndedConstraintsApply() throws Exception { registerValueIndex("longValues", "nt:unstructured", "Long values index", "*", "value", PropertyType.LONG); Node root = session().getRootNode(); int valuesCount = 5; for (int i = 0; i < valuesCount; i++) { String name = String.valueOf(i+1); Node node = root.addNode(name); node.setProperty("value", (long) (i+1)); } session.save(); String sql1 = "SELECT number.[jcr:name] FROM [nt:unstructured] as number WHERE (number.value > 1 AND number.value < 3) OR " + "(number.value > 3 AND number.value < 5)"; String sql2 = "SELECT number.[jcr:name] FROM [nt:unstructured] as number WHERE number.value <2"; Query query = jcrSql2Query(sql1 + " UNION " + sql2); validateQuery() .rowCount(2L) .useIndex("longValues") .hasNodesAtPaths("/2", "/4", "/1") .validate(query, query.execute()); } @Test @FixFor( "MODE-2515" ) public void shouldSupportQueryLimitWithMoreThan100Nodes() throws Exception { registerValueIndex("title", "mix:title", null, "*", "jcr:title", PropertyType.STRING); // Add a node that uses this type ... Node root = session().getRootNode(); int nodeCount = 102; for (int i = 0; i < nodeCount; i++) { Node book = root.addNode("book_" + (i+1)); book.addMixin("mix:title"); book.setProperty("jcr:title", "Title"); } session.save(); // Compute a query plan that should use this index ... Query query = jcrSql2Query("SELECT * FROM [mix:title] as book where book.[jcr:title] = 'Title'"); int limit = nodeCount - 1; query.setLimit(limit); validateQuery().rowCount(limit).useIndex("title").validate(query, query.execute()); limit = nodeCount / 2; query.setLimit(limit); validateQuery().rowCount(limit).useIndex("title").validate(query, query.execute()); } @Test @FixFor( "MODE-2515" ) public void shouldSupportQueryOffsetAndLimitWhileSortingMultipleBatches() throws Exception { registerNodeType("nt:testType"); registerNodeTypeIndex("typesIndex", "nt:testType", null, "*", "jcr:primaryType", PropertyType.STRING); Node root = session().getRootNode(); for (int i = 0; i < 102; i++) { Node node1 = root.addNode("node" + i, "nt:testType"); node1.setProperty("stringProp", String.format("%03d", i)); Thread.sleep(10); } session.save(); Query query1 = jcrSql2Query("SELECT * FROM [nt:testType] AS node ORDER by node.[stringProp] DESC LIMIT 1 OFFSET 0"); validateQuery() .rowCount(1L) .useIndex("typesIndex") .hasNodesAtPaths("/node101") .validate(query1, query1.execute()); Query query2 = jcrSql2Query("SELECT * FROM [nt:testType] AS node ORDER by node.[stringProp] DESC LIMIT 1 OFFSET 50"); validateQuery() .rowCount(1L) .useIndex("typesIndex") .hasNodesAtPaths("/node51") .validate(query2, query2.execute()); Query query3 = jcrSql2Query("SELECT * FROM [nt:testType] AS node ORDER by node.[stringProp] DESC LIMIT 1 OFFSET 100"); validateQuery() .rowCount(1L) .useIndex("typesIndex") .hasNodesAtPaths("/node1") .validate(query3, query3.execute()); } @Test @FixFor( "MODE-2576" ) public void shouldNotCorruptBooleanIndexes() throws Exception { registerValueIndex("booleanIndex", "nt:unstructured", "Boolean values index", "*", "isActive", PropertyType.BOOLEAN); Node node1 = session().getRootNode().addNode("node1"); node1.setProperty("isActive", true); Node node2 = session().getRootNode().addNode("node2"); node2.setProperty("isActive", true); session.save(); Query query = jcrSql2Query("SELECT node.[jcr:name] FROM [nt:unstructured] as node WHERE node.isActive = TRUE"); validateQuery() .rowCount(2L) .useIndex("booleanIndex") .hasNodesAtPaths("/node1", "/node2") .validate(query, query.execute()); node2.setProperty("isActive", false); session.save(); validateQuery() .rowCount(1L) .useIndex("booleanIndex") .hasNodesAtPaths("/node1") .validate(query, query.execute()); session.getWorkspace().reindex("/"); validateQuery() .rowCount(1L) .useIndex("booleanIndex") .hasNodesAtPaths("/node1") .validate(query, query.execute()); } @Test @FixFor( "MODE-2510 ") public void snsReorderingsShouldBeReflectedInIndexes() throws Exception { registerNodeType("nt:testType"); registerValueIndex("pathIndex", "nt:testType", "Node path index", "*", "jcr:path", PropertyType.PATH); session().getRootNode().addNode("A", "nt:testType"); //A Node nodeB1 = session().getRootNode().addNode("B", "nt:testType"); //B[1] session().getRootNode().addNode("C", "nt:testType"); //C Node nodeB2 = session().getRootNode().addNode("B", "nt:testType"); //B[2] Node nodeB3 = session().getRootNode().addNode("B", "nt:testType"); //B[3] session().getRootNode().addNode("D", "nt:testType"); //D Node nodeB4 = session().getRootNode().addNode("B", "nt:testType"); //B[4] session.save(); String queryForB1 = "SELECT node.[jcr:path] FROM [nt:testType] as node WHERE node.[jcr:path] >= '/B[1]' AND node.[jcr:path] < '/B[2]'"; Query query = jcrSql2Query(queryForB1); validateQuery() .rowCount(1L) .useIndex("pathIndex") .onEachRow((rowNumber, row) -> assertEquals(row.getNode().getIdentifier(), nodeB1.getIdentifier())) .validate(query, query.execute()); String queryForB2 = "SELECT node.[jcr:path] FROM [nt:testType] as node WHERE node.[jcr:path] >= '/B[2]' AND node.[jcr:path] < '/B[3]'"; query = jcrSql2Query(queryForB2); validateQuery() .rowCount(1L) .useIndex("pathIndex") .onEachRow((rowNumber, row) -> assertEquals(row.getNode().getIdentifier(), nodeB2.getIdentifier())) .validate(query, query.execute()); String queryForB3 = "SELECT node.[jcr:path] FROM [nt:testType] as node WHERE node.[jcr:path] >= '/B[3]' AND node.[jcr:path] < '/B[4]' "; query = jcrSql2Query(queryForB3); validateQuery() .rowCount(1L) .useIndex("pathIndex") .onEachRow((rowNumber, row) -> assertEquals(row.getNode().getIdentifier(), nodeB3.getIdentifier())) .validate(query, query.execute()); String queryForB4 = "SELECT node.[jcr:path] FROM [nt:testType] as node WHERE node.[jcr:path] >= '/B[4]' AND node.[jcr:path] < '/B[5]' "; query = jcrSql2Query(queryForB4); validateQuery() .rowCount(1L) .useIndex("pathIndex") .onEachRow((rowNumber, row) -> assertEquals(row.getNode().getIdentifier(), nodeB4.getIdentifier())) .validate(query, query.execute()); // reorder B[4] before C session().getRootNode().orderBefore("B[4]", "C"); session().save(); query = jcrSql2Query(queryForB1); validateQuery() .rowCount(1L) .useIndex("pathIndex") .onEachRow((rowNumber, row) -> assertEquals(row.getNode().getIdentifier(), nodeB1.getIdentifier())) .validate(query, query.execute()); Node newB2 = assertNode("/B[2]"); assertEquals(nodeB4.getIdentifier(), newB2.getIdentifier()); queryForB2 = "SELECT node.[jcr:path] FROM [nt:testType] as node WHERE node.[jcr:path] >= '/B[2]' AND node.[jcr:path] < '/B[3]'"; query = jcrSql2Query(queryForB2); validateQuery() .rowCount(1L) .useIndex("pathIndex") .onEachRow((rowNumber, row) -> assertEquals(row.getNode().getIdentifier(), newB2.getIdentifier())) .validate(query, query.execute()); Node newB3 = assertNode("/B[3]"); assertEquals(nodeB2.getIdentifier(), newB3.getIdentifier()); queryForB3 = "SELECT node.[jcr:path] FROM [nt:testType] as node WHERE node.[jcr:path] >= '/B[3]' AND node.[jcr:path] < '/B[4]' "; query = jcrSql2Query(queryForB3); validateQuery() .rowCount(1L) .useIndex("pathIndex") .onEachRow((rowNumber, row) -> assertEquals(row.getNode().getIdentifier(), newB3.getIdentifier())) .validate(query, query.execute()); Node newB4 = assertNode("/B[4]"); assertEquals(nodeB3.getIdentifier(), newB4.getIdentifier()); queryForB4 = "SELECT node.[jcr:path] FROM [nt:testType] as node WHERE node.[jcr:path] >= '/B[4]' AND node.[jcr:path] < '/B[5]' "; query = jcrSql2Query(queryForB4); validateQuery() .rowCount(1L) .useIndex("pathIndex") .onEachRow((rowNumber, row) -> assertEquals(row.getNode().getIdentifier(), newB4.getIdentifier())) .validate(query, query.execute()); // reorder B[3] at the end session().getRootNode().orderBefore("B[3]", null); session().save(); query = jcrSql2Query(queryForB1); validateQuery() .rowCount(1L) .useIndex("pathIndex") .onEachRow((rowNumber, row) -> assertEquals(row.getNode().getIdentifier(), nodeB1.getIdentifier())) .validate(query, query.execute()); queryForB2 = "SELECT node.[jcr:path] FROM [nt:testType] as node WHERE node.[jcr:path] >= '/B[2]' AND node.[jcr:path] < '/B[3]'"; query = jcrSql2Query(queryForB2); validateQuery() .rowCount(1L) .useIndex("pathIndex") .onEachRow((rowNumber, row) -> assertEquals(row.getNode().getIdentifier(), newB2.getIdentifier())) .validate(query, query.execute()); queryForB3 = "SELECT node.[jcr:path] FROM [nt:testType] as node WHERE node.[jcr:path] >= '/B[3]' AND node.[jcr:path] < '/B[4]' "; query = jcrSql2Query(queryForB3); validateQuery() .rowCount(1L) .useIndex("pathIndex") .onEachRow((rowNumber, row) -> assertEquals(row.getNode().getIdentifier(), newB4.getIdentifier())) .validate(query, query.execute()); queryForB4 = "SELECT node.[jcr:path] FROM [nt:testType] as node WHERE node.[jcr:path] >= '/B[4]' AND node.[jcr:path] < '/B[5]' "; query = jcrSql2Query(queryForB4); validateQuery() .rowCount(1L) .useIndex("pathIndex") .onEachRow((rowNumber, row) -> assertEquals(row.getNode().getIdentifier(), newB3.getIdentifier())) .validate(query, query.execute()); } }