blob: d6656e352550ca1c2cb48dd4f6eee933bdb115a0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.jackrabbit.oak.plugins.index.lucene;
import static java.util.Arrays.asList;
import static org.apache.jackrabbit.oak.api.QueryEngine.NO_BINDINGS;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NODE_TYPE;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.REINDEX_PROPERTY_NAME;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_PROPERTY_NAME;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import java.text.ParseException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import javax.jcr.PropertyType;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.oak.Oak;
import org.apache.jackrabbit.oak.api.ContentRepository;
import org.apache.jackrabbit.oak.api.Result;
import org.apache.jackrabbit.oak.api.ResultRow;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.plugins.index.lucene.util.IndexDefinitionBuilder;
import org.apache.jackrabbit.oak.plugins.index.nodetype.NodeTypeIndexProvider;
import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants;
import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.InitialContentHelper;
import org.apache.jackrabbit.oak.query.AbstractQueryTest;
import org.apache.jackrabbit.oak.spi.commit.Observer;
import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider;
import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
import org.junit.Test;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
public class FunctionIndexTest extends AbstractQueryTest {
private LuceneIndexEditorProvider editorProvider;
private NodeStore nodeStore;
@Override
protected ContentRepository createRepository() {
editorProvider = new LuceneIndexEditorProvider();
LuceneIndexProvider provider = new LuceneIndexProvider();
nodeStore = new MemoryNodeStore(InitialContentHelper.INITIAL_CONTENT);
return new Oak(nodeStore)
.with(new OpenSecurityProvider())
.with((QueryIndexProvider) provider)
.with((Observer) provider)
.with(editorProvider)
.with(new PropertyIndexEditorProvider())
.with(new NodeTypeIndexProvider())
.createContentRepository();
}
@Test
public void noIndexTest() throws Exception {
Tree test = root.getTree("/").addChild("test");
for (int idx = 0; idx < 3; idx++) {
Tree low = test.addChild("" + (char) ('a' + idx));
low.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
Tree up = test.addChild("" + (char) ('A' + idx));
up.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
}
root.commit();
String query = "select [jcr:path] from [nt:base] where lower(localname()) = 'b'";
assertThat(explain(query), containsString("traverse"));
assertQuery(query, Lists.newArrayList("/test/b", "/test/B"));
String queryXPath = "/jcr:root/test//*[fn:lower-case(fn:local-name()) = 'b']";
assertThat(explainXpath(queryXPath), containsString("traverse"));
assertQuery(queryXPath, "xpath", Lists.newArrayList("/test/b", "/test/B"));
queryXPath = "/jcr:root/test//*[fn:lower-case(fn:local-name()) > 'b']";
assertThat(explainXpath(queryXPath), containsString("traverse"));
assertQuery(queryXPath, "xpath", Lists.newArrayList("/test/c", "/test/C"));
query = "select [jcr:path] from [nt:base] where lower(localname()) = 'B'";
assertThat(explain(query), containsString("traverse"));
assertQuery(query, Lists.<String>newArrayList());
}
@Test
public void lowerCaseLocalName() throws Exception {
Tree luceneIndex = createIndex("lowerLocalName", Collections.<String>emptySet());
luceneIndex.setProperty("excludedPaths",
Lists.newArrayList("/jcr:system", "/oak:index"), Type.STRINGS);
Tree func = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
.addChild("nt:base")
.addChild(FulltextIndexConstants.PROP_NODE)
.addChild("lowerLocalName");
func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "lower(localname())");
Tree test = root.getTree("/").addChild("test");
for (int idx = 0; idx < 3; idx++) {
Tree low = test.addChild("" + (char) ('a' + idx));
low.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
Tree up = test.addChild("" + (char) ('A' + idx));
up.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
}
root.commit();
String query = "select [jcr:path] from [nt:base] where lower(localname()) = 'b'";
assertThat(explain(query), containsString("lucene:lowerLocalName"));
assertQuery(query, Lists.newArrayList("/test/b", "/test/B"));
String queryXPath = "/jcr:root//*[fn:lower-case(fn:local-name()) = 'b']";
assertThat(explainXpath(queryXPath), containsString("lucene:lowerLocalName"));
assertQuery(queryXPath, "xpath", Lists.newArrayList("/test/b", "/test/B"));
queryXPath = "/jcr:root//*[fn:lower-case(fn:local-name()) > 'b']";
assertThat(explainXpath(queryXPath), containsString("lucene:lowerLocalName"));
assertQuery(queryXPath, "xpath", Lists.newArrayList("/test/c", "/test/C", "/test"));
query = "select [jcr:path] from [nt:base] where lower(localname()) = 'B'";
assertThat(explain(query), containsString("lucene:lowerLocalName"));
assertQuery(query, Lists.<String>newArrayList());
}
@Test
public void lengthName() throws Exception {
Tree luceneIndex = createIndex("lengthName", Collections.<String>emptySet());
luceneIndex.setProperty("excludedPaths",
Lists.newArrayList("/jcr:system", "/oak:index"), Type.STRINGS);
Tree func = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
.addChild("nt:base")
.addChild(FulltextIndexConstants.PROP_NODE)
.addChild("lengthName");
func.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
func.setProperty(FulltextIndexConstants.PROP_TYPE, PropertyType.TYPENAME_LONG);
func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "fn:string-length(fn:name())");
Tree test = root.getTree("/").addChild("test");
for (int idx = 1; idx < 1000; idx *= 10) {
Tree testNode = test.addChild("test" + idx);
testNode.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
}
root.commit();
String query = "select [jcr:path] from [nt:base] where length(name()) = 6";
assertThat(explain(query), containsString("lucene:lengthName"));
assertQuery(query, Lists.newArrayList("/test/test10"));
String queryXPath = "/jcr:root//*[fn:string-length(fn:name()) = 7]";
assertThat(explainXpath(queryXPath), containsString("lucene:lengthName"));
assertQuery(queryXPath, "xpath", Lists.newArrayList("/test/test100"));
queryXPath = "/jcr:root//* order by fn:string-length(fn:name())";
assertThat(explainXpath(queryXPath), containsString("lucene:lengthName"));
assertQuery(queryXPath, "xpath", Lists.newArrayList(
"/test", "/test/test1", "/test/test10", "/test/test100"));
}
@Test
public void length() throws Exception {
Tree luceneIndex = createIndex("length", Collections.<String>emptySet());
luceneIndex.setProperty("excludedPaths",
Lists.newArrayList("/jcr:system", "/oak:index"), Type.STRINGS);
Tree func = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
.addChild("nt:base")
.addChild(FulltextIndexConstants.PROP_NODE)
.addChild("lengthName");
func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "fn:string-length(@value)");
Tree test = root.getTree("/").addChild("test");
for (int idx = 1; idx <= 1000; idx *= 10) {
Tree testNode = test.addChild("test" + idx);
testNode.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
testNode.setProperty("value", new byte[idx]);
}
root.commit();
String query = "select [jcr:path] from [nt:base] where length([value]) = 100";
assertThat(explain(query), containsString("lucene:length"));
assertQuery(query, Lists.newArrayList("/test/test100"));
String queryXPath = "/jcr:root//*[fn:string-length(@value) = 10]";
assertThat(explainXpath(queryXPath), containsString("lucene:length"));
assertQuery(queryXPath, "xpath", Lists.newArrayList("/test/test10"));
}
@Test
public void upperCase() throws Exception {
Tree luceneIndex = createIndex("upper", Collections.<String>emptySet());
Tree func = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
.addChild("nt:base")
.addChild(FulltextIndexConstants.PROP_NODE)
.addChild("upperName");
func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "fn:upper-case(@name)");
Tree test = root.getTree("/").addChild("test");
test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
List<String> paths = Lists.newArrayList();
for (int idx = 0; idx < 15; idx++) {
Tree a = test.addChild("n"+idx);
a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
a.setProperty("name", "10% foo");
paths.add("/test/n" + idx);
}
root.commit();
String query = "select [jcr:path] from [nt:unstructured] where upper([name]) = '10% FOO'";
assertThat(explain(query), containsString("lucene:upper"));
assertQuery(query, paths);
query = "select [jcr:path] from [nt:unstructured] where upper([name]) like '10\\% FOO'";
assertThat(explain(query), containsString("lucene:upper"));
assertQuery(query, paths);
}
@Test
public void upperCaseRelative() throws Exception {
Tree luceneIndex = createIndex("upper", Collections.<String>emptySet());
Tree func = luceneIndex.addChild(FulltextIndexConstants.INDEX_RULES)
.addChild("nt:base")
.addChild(FulltextIndexConstants.PROP_NODE)
.addChild("upperName");
func.setProperty(FulltextIndexConstants.PROP_FUNCTION, "upper([data/name])");
Tree test = root.getTree("/").addChild("test");
test.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
List<String> paths = Lists.newArrayList();
for (int idx = 0; idx < 15; idx++) {
Tree a = test.addChild("n"+idx);
a.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
Tree b = a.addChild("data");
b.setProperty("jcr:primaryType", "nt:unstructured", Type.NAME);
b.setProperty("name", "foo");
paths.add("/test/n" + idx);
}
root.commit();
String query = "select [jcr:path] from [nt:unstructured] where upper([data/name]) = 'FOO'";
assertThat(explain(query), containsString("lucene:upper"));
assertQuery(query, paths);
String queryXPath = "/jcr:root//element(*, nt:unstructured)[fn:upper-case(data/@name) = 'FOO']";
assertThat(explainXpath(queryXPath), containsString("lucene:upper"));
assertQuery(queryXPath, "xpath", paths);
for (int idx = 0; idx < 15; idx++) {
Tree a = test.getChild("n"+idx);
Tree b = a.getChild("data");
b.setProperty("name", "bar");
}
root.commit();
query = "select [jcr:path] from [nt:unstructured] where upper([data/name]) = 'BAR'";
assertThat(explain(query), containsString("lucene:upper"));
assertQuery(query, paths);
queryXPath = "/jcr:root//element(*, nt:unstructured)[fn:upper-case(data/@name) = 'BAR']";
assertThat(explainXpath(queryXPath), containsString("lucene:upper"));
assertQuery(queryXPath, "xpath", paths);
}
@Test
public void coalesce() throws Exception{
IndexDefinitionBuilder idxb = new IndexDefinitionBuilder().noAsync();
idxb.indexRule("nt:base").property("foo", null).function(
"lower(coalesce([jcr:content/foo2], coalesce([jcr:content/foo], localname())))"
);
Tree idx = root.getTree("/").getChild("oak:index").addChild("test1");
idxb.build(idx);
root.commit();
Tree rootTree = root.getTree("/");
rootTree.addChild("a").addChild("jcr:content").setProperty("foo2", "BAR");
rootTree.addChild("b").addChild("jcr:content").setProperty("foo", "bAr");
Tree child = rootTree.addChild("c").addChild("jcr:content");
child.setProperty("foo", "bar");
child.setProperty("foo2", "bar1");
rootTree.addChild("bar");
root.commit();
assertPlanAndQuery(
"select * from [nt:base] where lower(coalesce([jcr:content/foo2], coalesce([jcr:content/foo], localname()))) = 'bar'",
"lucene:test1(/oak:index/test1)", asList("/a", "/b", "/bar"));
}
@Test
public void coalesceOrdering() throws Exception{
IndexDefinitionBuilder idxb = new IndexDefinitionBuilder().noAsync();
idxb.indexRule("nt:base").property("foo", null).function(
"coalesce([jcr:content/foo2], [jcr:content/foo])"
).ordered();
Tree idx = root.getTree("/").getChild("oak:index").addChild("test1");
idxb.build(idx);
root.commit();
Tree rootTree = root.getTree("/");
rootTree.addChild("a").addChild("jcr:content").setProperty("foo2", "a");
rootTree.addChild("b").addChild("jcr:content").setProperty("foo", "b");
Tree child = rootTree.addChild("c").addChild("jcr:content");
child.setProperty("foo", "c");
child.setProperty("foo2", "a1");
root.commit();
assertOrderedPlanAndQuery(
"select * from [nt:base] order by coalesce([jcr:content/foo2], [jcr:content/foo])",
"lucene:test1(/oak:index/test1)", asList("/a", "/c", "/b"));
assertOrderedPlanAndQuery(
"select * from [nt:base] order by coalesce([jcr:content/foo2], [jcr:content/foo]) DESC",
"lucene:test1(/oak:index/test1)", asList("/b", "/c", "/a"));
}
protected String explain(String query){
String explain = "explain " + query;
return executeQuery(explain, "JCR-SQL2").get(0);
}
protected String explainXpath(String query) throws ParseException {
String explain = "explain " + query;
Result result = executeQuery(explain, "xpath", NO_BINDINGS);
ResultRow row = Iterables.getOnlyElement(result.getRows());
String plan = row.getValue("plan").getValue(Type.STRING);
return plan;
}
private void assertOrderedPlanAndQuery(String query, String planExpectation, List<String> paths) {
List<String> result = assertPlanAndQuery(query, planExpectation, paths);
assertEquals("Ordering doesn't match", paths, result);
}
private List<String> assertPlanAndQuery(String query, String planExpectation, List<String> paths){
assertThat(explain(query), containsString(planExpectation));
return assertQuery(query, paths);
}
protected Tree createIndex(String name, Set<String> propNames) {
Tree index = root.getTree("/");
return createIndex(index, name, propNames);
}
static Tree createIndex(Tree index, String name, Set<String> propNames) {
Tree def = index.addChild(INDEX_DEFINITIONS_NAME).addChild(name);
def.setProperty(JcrConstants.JCR_PRIMARYTYPE,
INDEX_DEFINITIONS_NODE_TYPE, Type.NAME);
def.setProperty(TYPE_PROPERTY_NAME, LuceneIndexConstants.TYPE_LUCENE);
def.setProperty(REINDEX_PROPERTY_NAME, true);
def.setProperty(FulltextIndexConstants.FULL_TEXT_ENABLED, false);
def.setProperty(PropertyStates.createProperty(FulltextIndexConstants.INCLUDE_PROPERTY_NAMES, propNames, Type.STRINGS));
def.setProperty(LuceneIndexConstants.SAVE_DIR_LISTING, true);
return index.getChild(INDEX_DEFINITIONS_NAME).getChild(name);
}
}