diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java index 003a560..ed4604e 100644 --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/IndexUtils.java @@ -191,4 +191,45 @@ public class IndexUtils { } } } + + /** + * Create a new property index definition below the given {@code indexNode} of the provided + * {@code propertyIndexType}. + * + * @param indexNode the oak:index + * @param indexDefName the node for the index definition + * @param unique true if uniqueness + * @param propertyNames the list of properties to be indexed + * @param declaringNodeTypeNames + * @param propertyIndexType the type of the PropertyIndex + * @param properties any additional property to be added to the index definition. + * @throws RepositoryException + */ + public static void createIndexDefinition(@Nonnull NodeBuilder indexNode, + @Nonnull String indexDefName, + boolean unique, + @Nonnull Iterable propertyNames, + @Nullable String[] declaringNodeTypeNames, + @Nonnull String propertyIndexType, + Map properties) throws RepositoryException { + + NodeBuilder entry = indexNode.child(indexDefName) + .setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME) + .setProperty(TYPE_PROPERTY_NAME, propertyIndexType) + .setProperty(REINDEX_PROPERTY_NAME, false); + if (unique) { + entry.setProperty(UNIQUE_PROPERTY_NAME, unique); + } + entry.setProperty(PropertyStates.createProperty(PROPERTY_NAMES, propertyNames, NAMES)); + if (declaringNodeTypeNames != null && declaringNodeTypeNames.length > 0) { + entry.setProperty(PropertyStates.createProperty(DECLARING_NODE_TYPES, + declaringNodeTypeNames, NAMES)); + } + // additional properties + if (properties != null) { + for (String k : properties.keySet()) { + entry.setProperty(k, properties.get(k)); + } + } + } } diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java index fd0b85d..d309017 100644 --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndex.java @@ -21,6 +21,8 @@ import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.apache.jackrabbit.oak.spi.state.NodeState; + /** * interface for shared constants around different actors: QueryIndex, IndexEditors, * IndexEditorProviders, ... @@ -49,12 +51,14 @@ public interface OrderedIndex { } /** - * retrieve an {@code OrderDirection} from a provided String. Will return null in case of no-match + * retrieve an {@code OrderDirection} from a provided String. Will return null in case of + * no-match * * @param direction the direction of the sorting: ascending or descending - * @return the order + * @return */ - @Nullable @CheckForNull + @Nullable + @CheckForNull public static OrderDirection fromString(@Nonnull final String direction) { for (OrderDirection d : OrderDirection.values()) { if (d.getDirection().equalsIgnoreCase(direction)) { @@ -63,6 +67,40 @@ public interface OrderedIndex { } return null; } + + /** + * tells whether the provided index definition is ascending or descending + * + * @param indexMeta + * @return the direction + */ + public static OrderDirection fromIndexMeta(final NodeState indexMeta) { + OrderDirection direction = ASC; + if (indexMeta != null && DESC.getDirection().equals(indexMeta.getString(DIRECTION))) { + direction = DESC; + } + return direction; + } + + /** + * convenience method that tells if the provided index definition is descending + * + * @param indexMeta + * @return true if descending + */ + public static boolean isDescending(NodeState indexMeta) { + return DESC.equals(fromIndexMeta(indexMeta)); + } + + /** + * convenience method that tells if the provided index definition is ascending + * + * @param indexMeta + * @return true if ascending + */ + public static boolean isAscending(NodeState indexMeta) { + return ASC.equals(fromIndexMeta(indexMeta)); + } }; String TYPE = "ordered"; @@ -70,7 +108,7 @@ public interface OrderedIndex { /** * the 'key' used for specifying the direction of the index when providing the configuration * - * {@code "propertyNames"="foobar", "direction"="ascending" } + * {@code { "propertyNames"="foobar", "direction"="ascending" } } */ String DIRECTION = "direction"; diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java index 1ebaaaf..2ef2d2d 100644 --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndex.java @@ -19,20 +19,30 @@ package org.apache.jackrabbit.oak.plugins.index.property; import static org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.TYPE; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.spi.query.Cursor; +import org.apache.jackrabbit.oak.spi.query.Cursors; import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.Filter.PropertyRestriction; +import org.apache.jackrabbit.oak.spi.query.QueryIndex.AdvancedQueryIndex; import org.apache.jackrabbit.oak.spi.state.NodeState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.collect.ImmutableList; + /** * A property index that supports ordering keys. */ -public class OrderedPropertyIndex extends PropertyIndex { - +public class OrderedPropertyIndex extends PropertyIndex implements AdvancedQueryIndex { private static final Logger LOG = LoggerFactory.getLogger(OrderedPropertyIndex.class); - private static boolean warned; - @Override public String getIndexName() { return TYPE; @@ -43,13 +53,175 @@ public class OrderedPropertyIndex extends PropertyIndex { return new OrderedPropertyIndexLookup(root); } + /** + * retrieve the cost for the query. + * + * !!! for now we want to skip the use-case of NON range-queries !!! + */ @Override public double getCost(Filter filter, NodeState root) { - //we don't want the index to be used yet - if (!warned) { - warned = true; - LOG.warn("This index always returns Double.POSITIVE_INFINITY and is therefore never used"); + throw new UnsupportedOperationException("Not supported as implementing AdvancedQueryIndex"); + } + + /** + * @return an builder with some initial common settings + */ + private static IndexPlan.Builder getIndexPlanBuilder(final Filter filter) { + IndexPlan.Builder b = new IndexPlan.Builder(); + b.setCostPerExecution(1); // we're local. Low-cost + // we're local but slightly more expensive than a standard PropertyIndex + b.setCostPerEntry(1.3); + b.setFulltextIndex(false); // we're never full-text + b.setIncludesNodeData(false); // we should not include node data + b.setFilter(filter); + // TODO it's synch for now but we should investigate the indexMeta + b.setDelayed(false); + return b; + } + + @Override + public List getPlans(Filter filter, List sortOrder, NodeState root) { + LOG.debug("getPlans(Filter, List, NodeState)"); + LOG.debug("getPlans() - filter: {} - ", filter); + LOG.debug("getPlans() - sortOrder: {} - ", sortOrder); + LOG.debug("getPlans() - rootState: {} - ", root); + List plans = new ArrayList(); + + PropertyIndexLookup pil = getLookup(root); + if (pil instanceof OrderedPropertyIndexLookup) { + OrderedPropertyIndexLookup lookup = (OrderedPropertyIndexLookup) pil; + Collection restrictions = filter.getPropertyRestrictions(); + + // first we process the sole orders as we could be in a situation where we don't have + // a where condition indexed but we do for order. In that case we will return always the + // whole index + if(sortOrder!=null){ + for (OrderEntry oe : sortOrder) { + String propertyName = PathUtils.getName(oe.getPropertyName()); + if (lookup.isIndexed(propertyName, "/", filter)) { + IndexPlan.Builder b = getIndexPlanBuilder(filter); + b.setSortOrder(ImmutableList.of(new OrderEntry( + propertyName, + Type.UNDEFINED, + (lookup.isAscending(root, propertyName, filter) ? OrderEntry.Order.ASCENDING + : OrderEntry.Order.DESCENDING)))); + b.setEstimatedEntryCount(lookup.getEstimatedEntryCount(propertyName, null, + filter, null)); + IndexPlan plan = b.build(); + LOG.debug("plan: {}", plan); + plans.add(plan); + } + } + } + + // then we add plans for each restriction that could apply to us + for (Filter.PropertyRestriction pr : restrictions) { + String propertyName = PathUtils.getName(pr.propertyName); + if (lookup.isIndexed(propertyName, "/", filter)) { + PropertyValue value = null; + boolean createPlan = true; + if (pr.first == null && pr.last == null) { + // open query: [property] is not null + value = null; + } else if (pr.first != null && pr.first.equals(pr.last) && pr.firstIncluding + && pr.lastIncluding) { + // [property]=[value] + value = pr.first; +// ----------- DISABLING RANGE QUERIES FOR NOW. EASYING THE INTEGRATION WITH OAK-622 [BEGIN] +// } else if (pr.first != null && !pr.first.equals(pr.last)) { +// // '>' & '>=' use cases +// if (lookup.isAscending(root, propertyName, filter)) { +// value = pr.first; +// } else { +// createPlan = false; +// } +// } else if (pr.last != null && !pr.last.equals(pr.first)) { +// // '<' & '<=' +// if (!lookup.isAscending(root, propertyName, filter)) { +// value = pr.last; +// } else { +// createPlan = false; +// } +// ----------- DISABLING RANGE QUERIES FOR NOW. EASYING THE INTEGRATION WITH OAK-622 [ END ] + } + if (createPlan) { + // we always return a sorted set + IndexPlan.Builder b = getIndexPlanBuilder(filter); + b.setSortOrder(ImmutableList.of(new OrderEntry( + propertyName, + Type.UNDEFINED, + (lookup.isAscending(root, propertyName, filter) ? OrderEntry.Order.ASCENDING + : OrderEntry.Order.DESCENDING)))); + long count = lookup.getEstimatedEntryCount(propertyName, value, filter, pr); + b.setEstimatedEntryCount(count); + LOG.debug("estimatedCount: {}", count); + + IndexPlan plan = b.build(); + LOG.debug("plan: {}", plan); + plans.add(plan); + } + } + } + } else { + LOG.error("Without an OrderedPropertyIndexLookup you should not end here"); + } + + return plans; + } + + @Override + public String getPlanDescription(IndexPlan plan) { + LOG.debug("getPlanDescription() - plan: {}", plan); + LOG.error("Not implemented yet"); + throw new UnsupportedOperationException("Not implemented yet."); +// return null; + } + + @Override + public Cursor query(IndexPlan plan, NodeState root) { + LOG.debug("query(IndexPlan, NodeState)"); + LOG.debug("query() - plan: {}", plan); + LOG.debug("query() - rootState: {}", root); + + Filter filter = plan.getFilter(); + List sortOrder = plan.getSortOrder(); + Iterable paths = null; + Cursor cursor = null; + PropertyIndexLookup pil = getLookup(root); + if (pil instanceof OrderedPropertyIndexLookup) { + OrderedPropertyIndexLookup lookup = (OrderedPropertyIndexLookup) pil; + Collection prs = filter.getPropertyRestrictions(); + int depth = 1; + for (PropertyRestriction pr : prs) { + String propertyName = PathUtils.getName(pr.propertyName); + depth = PathUtils.getDepth(pr.propertyName); + if (lookup.isIndexed(propertyName, "/", filter)) { + paths = lookup.query(filter, propertyName, pr); + } + } + if(paths==null && sortOrder!=null && !sortOrder.isEmpty()){ + // we could be here if we have a query where the ORDER BY makes us play it. + for (OrderEntry oe : sortOrder) { + String propertyName = PathUtils.getName(oe.getPropertyName()); + if (lookup.isIndexed(propertyName, "/", null)) { + paths = lookup.query(null, propertyName, new PropertyRestriction()); + } + } + } + if (paths == null) { + // if still here then something went wrong. + throw new IllegalStateException( + "OrderedPropertyIndex index is used even when no index is available for filter " + + filter); + } + cursor = Cursors.newPathCursor(paths); + if (depth > 1) { + cursor = Cursors.newAncestorCursor(cursor, depth - 1); + } + } else { + // if for some reasons it's not an Ordered Lookup we delegate up the chain + cursor = super.query(filter, root); } - return Double.POSITIVE_INFINITY; + return cursor; } } diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexLookup.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexLookup.java index 9a84e4f..ec28e19 100644 --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexLookup.java +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexLookup.java @@ -17,28 +17,101 @@ package org.apache.jackrabbit.oak.plugins.index.property; +import org.apache.jackrabbit.oak.api.PropertyValue; +import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; import org.apache.jackrabbit.oak.plugins.index.property.strategy.IndexStoreStrategy; import org.apache.jackrabbit.oak.plugins.index.property.strategy.OrderedContentMirrorStoreStrategy; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.Filter.PropertyRestriction; import org.apache.jackrabbit.oak.spi.state.NodeState; /** * */ public class OrderedPropertyIndexLookup extends PropertyIndexLookup { + private NodeState root; + /** + * the standard Ascending ordered index + */ private static final IndexStoreStrategy STORE = new OrderedContentMirrorStoreStrategy(); + /** + * the descending ordered index + */ + private static final IndexStoreStrategy REVERSED_STORE = new OrderedContentMirrorStoreStrategy(OrderDirection.DESC); + + /** + * we're slightly more expensive than the standard PropertyIndex. + */ + private static final int COST_OVERHEAD = 3; + public OrderedPropertyIndexLookup(NodeState root) { super(root); + this.root = root; } @Override IndexStoreStrategy getStrategy(NodeState indexMeta) { + if (OrderDirection.isAscending(indexMeta)) { return STORE; + } else { + return REVERSED_STORE; + } + } + + public boolean isAscending(NodeState root, String propertyName, Filter filter){ + NodeState indexMeta = getIndexNode(root, propertyName, filter); + return OrderDirection.isAscending(indexMeta); } @Override String getType() { return OrderedIndex.TYPE; } + + @Override + public double getCost(Filter filter, String propertyName, PropertyValue value) { + double cost = Double.POSITIVE_INFINITY; + NodeState indexMeta = getIndexNode(root, propertyName, filter); + if (indexMeta != null) { + // we relay then on the standard property index for the cost + cost = COST_OVERHEAD + + getStrategy(indexMeta).count(indexMeta, PropertyIndex.encode(value), MAX_COST); + } + return cost; + } + + /** + * query the strategy for the provided constrains + * + * @param filter + * @param propertyName + * @param pr + * @return the resultset + */ + public Iterable query(Filter filter, String propertyName, PropertyRestriction pr) { + NodeState indexMeta = getIndexNode(root, propertyName, filter); + if (indexMeta == null) { + throw new IllegalArgumentException("No index for " + propertyName); + } + return ((OrderedContentMirrorStoreStrategy) getStrategy(indexMeta)).query(filter, + propertyName, indexMeta, pr); + } + + /** + * return an estimated count to be used in IndexPlans. + * + * @param propertyName + * @param value + * @param filter + * @param pr + * @return + */ + public long getEstimatedEntryCount(String propertyName, PropertyValue value, Filter filter, + PropertyRestriction pr) { + NodeState indexMeta = getIndexNode(root, propertyName, filter); + OrderedContentMirrorStoreStrategy strategy = (OrderedContentMirrorStoreStrategy) getStrategy(indexMeta); + return strategy.count(indexMeta, pr, MAX_COST); + } } diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexLookup.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexLookup.java index 706de4a..f72c439 100644 --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexLookup.java +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/PropertyIndexLookup.java @@ -67,7 +67,7 @@ public class PropertyIndexLookup { /** * The maximum cost when the index can be used. */ - private static final int MAX_COST = 100; + static final int MAX_COST = 100; /** Index storage strategy */ private static final IndexStoreStrategy MIRROR = @@ -144,7 +144,7 @@ public class PropertyIndexLookup { * node was found */ @Nullable - private NodeState getIndexNode(NodeState node, String propertyName, Filter filter) { + NodeState getIndexNode(NodeState node, String propertyName, Filter filter) { // keep a fallback to a matching index def that has *no* node type constraints // (initially, there is no fallback) NodeState fallback = null; diff --git oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java index e0b94af..c9ce6c7 100644 --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java @@ -17,18 +17,24 @@ package org.apache.jackrabbit.oak.plugins.index.property.strategy; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.ENTRY_COUNT_PROPERTY_NAME; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_CONTENT_NODE_NAME; + import java.util.Collections; import java.util.Deque; import java.util.Iterator; import java.util.NoSuchElementException; -import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex; import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.Filter.PropertyRestriction; import org.apache.jackrabbit.oak.spi.state.AbstractChildNodeEntry; import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; import org.apache.jackrabbit.oak.spi.state.NodeBuilder; @@ -36,7 +42,9 @@ import org.apache.jackrabbit.oak.spi.state.NodeState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.Predicate; import com.google.common.base.Strings; +import com.google.common.collect.Iterators; /** * Same as for {@link ContentMirrorStoreStrategy} but the order of the keys is kept by using the @@ -89,7 +97,6 @@ public class OrderedContentMirrorStoreStrategy extends ContentMirrorStoreStrateg @Override NodeBuilder fetchKeyNode(@Nonnull NodeBuilder index, @Nonnull String key) { - LOG.debug("fetchKeyNode() - index: {} - key: {}", index, key); NodeBuilder localkey = null; NodeBuilder start = index.child(START); @@ -203,16 +210,6 @@ public class OrderedContentMirrorStoreStrategy extends ContentMirrorStoreStrateg return found ? previous : null; } - @Override - public void update(NodeBuilder index, String path, Set beforeKeys, - Set afterKeys) { - LOG.debug("update() - index : {}", index); - LOG.debug("update() - path : {}", path); - LOG.debug("update() - beforeKeys: {}", beforeKeys); - LOG.debug("update() - afterKeys : {}", afterKeys); - super.update(index, path, beforeKeys, afterKeys); - } - /** * retrieve an Iterable for going through the index in the right order without the :start node * @@ -289,6 +286,95 @@ public class OrderedContentMirrorStoreStrategy extends ContentMirrorStoreStrateg return cne; } + /** + * search the index for the provided PropertyRestriction + * + * @param filter + * @param indexName + * @param indexMeta + * @param pr + * @return + */ + public Iterable query(final Filter filter, final String indexName, + final NodeState indexMeta, final PropertyRestriction pr) { + return query(filter, indexName, indexMeta, INDEX_CONTENT_NODE_NAME, pr); + } + + /** + * queries through the index as other query() but provides the PropertyRestriction to be applied + * for advanced cases like range queries + * + * @param filter + * @param indexName + * @param indexMeta + * @param indexStorageNodeName + * @param pr + * @return + */ + public Iterable query(final Filter filter, final String indexName, + final NodeState indexMeta, final String indexStorageNodeName, + final PropertyRestriction pr) { + + final NodeState index = indexMeta.getChildNode(indexStorageNodeName); + + if (pr.first != null && !pr.first.equals(pr.last)) { + // '>' & '>=' use case + return new Iterable() { + private PropertyRestriction lpr = pr; + + @Override + public Iterator iterator() { + PathIterator pi = new PathIterator(filter, indexName); + Iterator children = getChildNodeEntries(index) + .iterator(); + pi.setPathContainsValue(true); + pi.enqueue(Iterators.filter(children, new Predicate() { + @Override + public boolean apply(ChildNodeEntry entry) { + String value = lpr.first.getValue(Type.STRING); + String name = convert(entry.getName()); + return (value.compareTo(name) < 0 || (lpr.firstIncluding && value + .equals(name))); + } + })); + return pi; + } + }; + } + else if (pr.last != null && !pr.last.equals(pr.first)) { + // '<' & '<=' use case + return new Iterable() { + private PropertyRestriction lpr = pr; + + @Override + public Iterator iterator() { + PathIterator pi = new PathIterator(filter, indexName); + Iterator children = getChildNodeEntries(index) + .iterator(); + pi.setPathContainsValue(true); + pi.enqueue(Iterators.filter(children, new Predicate() { + @Override + public boolean apply(ChildNodeEntry entry) { + String value = lpr.last.getValue(Type.STRING); + String name = convert(entry.getName()); + return (value.compareTo(name) > 0) + || (lpr.lastIncluding && value.equals(name)); + } + })); + return pi; + } + }; + } else { + // property is not null. AKA "open query" + Iterable values = null; + return query(filter, indexName, indexMeta, values); + } + } + + private static String convert(String value){ + return value.replaceAll("%3A", ":"); + } + private static final class OrderedChildNodeEntry extends AbstractChildNodeEntry { private final String name; private final NodeState state; @@ -312,4 +398,102 @@ public class OrderedContentMirrorStoreStrategy extends ContentMirrorStoreStrateg return state; } } + + /** + * estimate the number of nodes given the provided PropertyRestriction + * + * @param indexMeta + * @param pr + * @param max + * @return the estimated number of nodes + */ + public long count(NodeState indexMeta, Filter.PropertyRestriction pr, int max) { + long count = 0; + NodeState content = indexMeta.getChildNode(INDEX_CONTENT_NODE_NAME); + Filter.PropertyRestriction lpr = pr; + + if (content.exists()) { + if(lpr==null){ + // it means we have no restriction and we should return the whole lot + lpr = new Filter.PropertyRestriction(); + } + // the index is not empty + String value; + if (lpr.firstIncluding && lpr.lastIncluding && lpr.first != null + && lpr.first.equals(lpr.last)) { + // property==value case + value = lpr.first.getValue(Type.STRING); + NodeState n = content.getChildNode(value); + if (n.exists()) { + CountingNodeVisitor v = new CountingNodeVisitor(max); + v.visit(n); + count = v.getEstimatedCount(); + } + } else if (lpr.first == null && lpr.last == null) { + // property not null case + PropertyState ec = indexMeta.getProperty(ENTRY_COUNT_PROPERTY_NAME); + if (ec != null) { + count = ec.getValue(Type.LONG); + } else { + CountingNodeVisitor v = new CountingNodeVisitor(max); + v.visit(content); + count = v.getEstimatedCount(); + } + } else if (lpr.first != null && !lpr.first.equals(lpr.last) + && OrderDirection.ASC.equals(direction)) { + // > & >= in ascending index + Iterable children = getChildNodeEntries(content); + CountingNodeVisitor v; + value = lpr.first.getValue(Type.STRING); + int depthTotal = 0; + // seeking the right starting point + for (ChildNodeEntry child : children) { + String converted = convert(child.getName()); + if (value.compareTo(converted) < 0 + || (lpr.firstIncluding && value.equals(converted))) { + // here we are let's start counting + v = new CountingNodeVisitor(max); + v.visit(content.getChildNode(child.getName())); + count += v.getCount(); + depthTotal += v.depthTotal; + if (count > max) + break; + } + } + // small hack for having a common way of counting + v = new CountingNodeVisitor(max); + v.depthTotal = depthTotal; + v.count = (int) Math.min(count, Integer.MAX_VALUE); + count = v.getEstimatedCount(); + } else if (lpr.last != null && !lpr.last.equals(lpr.first) + && OrderDirection.DESC.equals(direction)) { + // > & >= in ascending index + Iterable children = getChildNodeEntries(content); + CountingNodeVisitor v; + value = lpr.last.getValue(Type.STRING); + int depthTotal = 0; + // seeking the right starting point + for (ChildNodeEntry child : children) { + String converted = convert(child.getName()); + if (value.compareTo(converted) > 0 + || (lpr.lastIncluding && value.equals(converted))) { + // here we are let's start counting + v = new CountingNodeVisitor(max); + v.visit(content.getChildNode(child.getName())); + count += v.getCount(); + depthTotal += v.depthTotal; + if (count > max) + break; + } + } + // small hack for having a common way of counting + v = new CountingNodeVisitor(max); + v.depthTotal = depthTotal; + v.count = (int) Math.min(count, Integer.MAX_VALUE); + count = v.getEstimatedCount(); + } + + } + return count; + } } diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java index ea51961..ca65550 100644 --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/BasicOrderedPropertyIndexQueryTest.java @@ -23,7 +23,9 @@ import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Calendar; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -40,6 +42,7 @@ import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirect import org.apache.jackrabbit.oak.plugins.nodetype.write.InitialContent; import org.apache.jackrabbit.oak.query.AbstractQueryTest; import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; public abstract class BasicOrderedPropertyIndexQueryTest extends AbstractQueryTest { /** @@ -58,6 +61,12 @@ public abstract class BasicOrderedPropertyIndexQueryTest extends AbstractQueryTe protected static final int NUMBER_OF_NODES = 50; /** + * formatter for date conversions + */ + protected static final SimpleDateFormat ISO_8601_2000 = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + + /** * generate a list of values to be used as ordered set. Will return something like * {@code value000, value001, value002, ...} * @@ -96,22 +105,11 @@ public abstract class BasicOrderedPropertyIndexQueryTest extends AbstractQueryTe } /** - * create a child node for the provided father - * - * @param father - * @param name - * the name of the node to create - * @param propName - * the name of the property to assign - * @param propValue - * the value of the property to assign - * @return + * As {@link #child(Tree, String, String, String, Type)} but forces {@code String} as + * {@code Type}. */ static Tree child(Tree father, String name, String propName, String propValue) { - Tree child = father.addChild(name); - child.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME); - child.setProperty(propName, propValue, Type.STRING); - return child; + return child(father, name, propName, propValue, Type.STRING); } @Override @@ -132,14 +130,17 @@ public abstract class BasicOrderedPropertyIndexQueryTest extends AbstractQueryTe * @param direction the direction of the items to be added. * @return */ + @SuppressWarnings("rawtypes") protected List addChildNodes(final List values, final Tree father, - OrderDirection direction) { + OrderDirection direction, + @Nonnull final Type propertyType) { List nodes = new ArrayList(); Random rnd = new Random(); int counter = 0; while (!values.isEmpty()) { String v = values.remove(rnd.nextInt(values.size())); - Tree t = child(father, String.format("n%s", counter++), ORDERED_PROPERTY, v); + Tree t = child(father, String.format("n%s", counter++), ORDERED_PROPERTY, v, + propertyType); nodes.add(new ValuePathTuple(v, t.getPath())); } @@ -171,4 +172,101 @@ public abstract class BasicOrderedPropertyIndexQueryTest extends AbstractQueryTe counter++; } } + + /** + * convenience method for generating a list of ordered dates as string in ISO + * 8601:2000-compliant format. + * + * {@link http ://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.6.4.3%20From%20DATE%20To}. + * + * it will add or remove depending of the {@code direction} provided, 12hrs for every entry to + * be generated. + * + * + * @param amount + * @param direction the direction of the sorting. If null the {@code OrderDirection.ASC} will be + * used + * @param start the date from which to start from in the generation + * @return a list of {@code amount} values ordered as specified by {@code direction} + */ + protected static List generateOrderedDates(int amount, + OrderDirection direction, + @Nonnull final Calendar start) { + if (amount > 1000) { + throw new RuntimeException("amount cannot be greater than 1000"); + } + List values = new ArrayList(amount); + Calendar lstart = (Calendar)start.clone(); + int hours = (OrderDirection.DESC.equals(direction)) ? -12 : 12; + for (int i = 0; i < amount; i++) { + values.add(ISO_8601_2000.format(lstart.getTime())); + lstart.add(Calendar.HOUR_OF_DAY, hours); + } + + return values; + } + + /** + * create a child node for the provided father + * + * @param father + * @param name the name of the node to create + * @param propName the name of the property to assign + * @param propValue the value of the property to assign + * @param type the type of the property + * @return the just added child + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + static Tree child(@Nonnull Tree father, @Nonnull String name, @Nonnull String propName, + @Nonnull String propValue, @Nonnull Type type) { + Tree child = father.addChild(name); + child.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME); + child.setProperty(propName, propValue, type); + return child; + } + + /** + * @return a Calendar set for midnight of 1st January 2013 + */ + protected static Calendar midnightFirstJan2013() { + Calendar c = Calendar.getInstance(); + c.set(2013, Calendar.JANUARY, 1, 0, 0); + return c; + } + + /** + * convenience method that adds a bunch of nodes in random order and return the order in which + * they should be presented by the OrderedIndex + * + * @param values the values of the property that will be indexed + * @param father the father under which add the nodes + * @return + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected void addChildNodes(final List values, + final NodeBuilder father, + @Nonnull final Type propertyType) { + Random rnd = new Random(); + int counter = 0; + while (!values.isEmpty()) { + String v = values.remove(rnd.nextInt(values.size())); + father.child(String.format("n%s", counter++)) + .setProperty(ORDERED_PROPERTY, v, propertyType); + } + } + + /** + * initiate the environment for testing with proper OrderedPropertyIndexProvider + * @throws Exception + */ + protected void initWithProperProvider() throws Exception { + session = new Oak().with(new InitialContent()) + .with(new OpenSecurityProvider()) + .with(new OrderedPropertyIndexProvider()) + .with(new OrderedPropertyIndexEditorProvider()) + .createContentRepository().login(null, null); + root = session.getLatestRoot(); + qe = root.getQueryEngine(); + createTestIndexNode(); + } } diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderDirectionEnumTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderDirectionEnumTest.java new file mode 100644 index 0000000..e43b023 --- /dev/null +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderDirectionEnumTest.java @@ -0,0 +1,87 @@ +/* + * 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.property; + +import org.junit.Test; +import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; +import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.assertNull; + +/** + * tests the Enumeration for the index direction + */ +public class OrderDirectionEnumTest { + @Test + public void fromIndexMeta() { + assertEquals(OrderDirection.ASC, OrderDirection.fromIndexMeta(null)); + + NodeState indexMeta = EmptyNodeState.EMPTY_NODE; + assertEquals(OrderDirection.ASC, OrderDirection.fromIndexMeta(indexMeta)); + + indexMeta = EmptyNodeState.EMPTY_NODE.builder() + .setProperty(OrderedIndex.DIRECTION, OrderDirection.ASC.getDirection()).getNodeState(); + assertEquals(OrderDirection.ASC, OrderDirection.fromIndexMeta(indexMeta)); + + indexMeta = EmptyNodeState.EMPTY_NODE.builder() + .setProperty(OrderedIndex.DIRECTION, OrderDirection.DESC.getDirection()).getNodeState(); + assertEquals(OrderDirection.DESC, OrderDirection.fromIndexMeta(indexMeta)); + } + + @Test + public void isDescending(){ + NodeState indexMeta = null; + assertFalse(OrderDirection.isDescending(indexMeta)); + + indexMeta = EmptyNodeState.EMPTY_NODE.builder() + .setProperty(OrderedIndex.DIRECTION, OrderDirection.ASC.getDirection()).getNodeState(); + assertFalse(OrderDirection.isDescending(indexMeta)); + + indexMeta = EmptyNodeState.EMPTY_NODE.builder() + .setProperty(OrderedIndex.DIRECTION, OrderDirection.DESC.getDirection()).getNodeState(); + assertTrue(OrderDirection.isDescending(indexMeta)); + } + + @Test + public void isAscending(){ + NodeState indexMeta = null; + assertTrue(OrderDirection.isAscending(indexMeta)); + + indexMeta = EmptyNodeState.EMPTY_NODE.builder() + .setProperty(OrderedIndex.DIRECTION, OrderDirection.ASC.getDirection()).getNodeState(); + assertTrue(OrderDirection.isAscending(indexMeta)); + + indexMeta = EmptyNodeState.EMPTY_NODE.builder() + .setProperty(OrderedIndex.DIRECTION, OrderDirection.DESC.getDirection()).getNodeState(); + assertFalse(OrderDirection.isAscending(indexMeta)); + } + + @Test + public void orderedDirectionFromString() { + assertNull("A non-existing order direction should result in null", + OrderDirection.fromString("foobar")); + assertEquals(OrderDirection.ASC, OrderDirection.fromString("ascending")); + assertFalse(OrderDirection.ASC.equals(OrderDirection.fromString("descending"))); + assertEquals(OrderDirection.DESC, OrderDirection.fromString("descending")); + assertFalse(OrderDirection.DESC.equals(OrderDirection.fromString("ascending"))); + } + +} diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndexCostTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndexCostTest.java new file mode 100644 index 0000000..4f217a2 --- /dev/null +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedIndexCostTest.java @@ -0,0 +1,363 @@ +/* + * 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.property; + +import static org.easymock.EasyMock.createNiceMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.oak.plugins.index.IndexConstants; +import org.apache.jackrabbit.oak.plugins.index.IndexUtils; +import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; +import org.apache.jackrabbit.oak.plugins.nodetype.write.InitialContent; +import org.apache.jackrabbit.oak.query.fulltext.FullTextExpression; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.easymock.EasyMock; +import org.junit.Ignore; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +/** + * tests the Cost-related part of the provider/strategy + */ +public class OrderedIndexCostTest extends BasicOrderedPropertyIndexQueryTest { + /** + * convenience class that return an always indexed strategy + */ + private static class AlwaysIndexedOrderedPropertyIndex extends OrderedPropertyIndex { + @Override + PropertyIndexLookup getLookup(NodeState root) { + return new AlwaysIndexedLookup(root); + } + + /** + * convenience class that always return true at the isIndexed test + */ + private static class AlwaysIndexedLookup extends OrderedPropertyIndexLookup { + public AlwaysIndexedLookup(NodeState root) { + super(root); + } + + @Override + public boolean isIndexed(String propertyName, String path, Filter filter) { + return true; + } + } + } + + @Override + protected void createTestIndexNode() throws Exception { + // intentionally left blank. Each test will have to define its own index configuration + } + + private static void defineIndex(NodeBuilder root, OrderDirection direction) throws IllegalArgumentException, RepositoryException { + IndexUtils + .createIndexDefinition( + root.child(IndexConstants.INDEX_DEFINITIONS_NAME), + TEST_INDEX_NAME, + false, + ImmutableList.of(ORDERED_PROPERTY), + null, + OrderedIndex.TYPE, + ImmutableMap.of(OrderedIndex.DIRECTION, + direction.getDirection())); + // forcing the existence of :index + root.getChildNode(IndexConstants.INDEX_DEFINITIONS_NAME).getChildNode(TEST_INDEX_NAME) + .child(IndexConstants.INDEX_CONTENT_NODE_NAME); + } + + /** + * define e descending ordered index in the provided root + * + * @param root + * @throws IllegalArgumentException + * @throws RepositoryException + */ + private static void defineDescendingIndex(NodeBuilder root) throws IllegalArgumentException, + RepositoryException { + defineIndex(root, OrderDirection.DESC); + } + + /** + * define e Ascending ordered index in the provided root + * + * @param root + * @throws IllegalArgumentException + * @throws RepositoryException + */ + private static void defineAscendingIndex(NodeBuilder root) throws IllegalArgumentException, + RepositoryException { + defineIndex(root, OrderDirection.ASC); + } + + @Test @Ignore("As of OAK-622 this should no longer be used. Removing later.") + public void costFullTextConstraint() { + OrderedPropertyIndex index = new OrderedPropertyIndex(); + NodeState root = InitialContent.INITIAL_CONTENT; + Filter filter = EasyMock.createNiceMock(Filter.class); + FullTextExpression fte = EasyMock.createNiceMock(FullTextExpression.class); + EasyMock.expect(filter.getFullTextConstraint()).andReturn(fte).anyTimes(); + EasyMock.replay(fte); + EasyMock.replay(filter); + + assertEquals("if it contains FullText we don't serve", Double.POSITIVE_INFINITY, + index.getCost(filter, root), 0); + } + + @Test @Ignore("As of OAK-622 this should no longer be used. Removing later.") + public void costContainsNativeConstraints(){ + OrderedPropertyIndex index = new OrderedPropertyIndex(); + NodeState root = InitialContent.INITIAL_CONTENT; + Filter filter = EasyMock.createNiceMock(Filter.class); + EasyMock.expect(filter.containsNativeConstraint()).andReturn(true).anyTimes(); + EasyMock.replay(filter); + + assertEquals("If it contains Natives we don't serve", Double.POSITIVE_INFINITY, + index.getCost(filter, root), 0); + } + + /** + * tests the use-case where we ask for '>' of a date. + * + * As we're not testing the actual algorithm, part of {@code IndexLookup} we want to make sure + * the Index doesn't reply with "dont' serve" in special cases + */ + @Test @Ignore("As of OAK-622 this should no longer be used. Removing later.") + public void costGreaterThanAscendingDirection() throws Exception { + OrderedPropertyIndex index = new AlwaysIndexedOrderedPropertyIndex(); + NodeBuilder builder = InitialContent.INITIAL_CONTENT.builder(); + defineAscendingIndex(builder); + NodeState root = builder.getNodeState(); + Filter filter = createNiceMock(Filter.class); + Filter.PropertyRestriction restriction = new Filter.PropertyRestriction(); + restriction.first = PropertyValues.newDate("2013-01-01"); + restriction.propertyName = ORDERED_PROPERTY; + expect(filter.getPropertyRestrictions()).andReturn(ImmutableList.of(restriction)) + .anyTimes(); + expect(filter.containsNativeConstraint()).andReturn(false).anyTimes(); + replay(filter); + + assertFalse("In ascending order we're expeting to serve this kind of queries", + Double.POSITIVE_INFINITY == index.getCost(filter, root)); + } + + /** + * test that the '>=' use case is served from the index + * @throws RepositoryException + * @throws IllegalArgumentException + */ + @Test @Ignore("As of OAK-622 this should no longer be used. Removing later.") + public void costGreaterThanEqualAscendingDirection() throws IllegalArgumentException, RepositoryException { + OrderedPropertyIndex index = new AlwaysIndexedOrderedPropertyIndex(); + NodeBuilder builder = InitialContent.INITIAL_CONTENT.builder(); + defineAscendingIndex(builder); + NodeState root = builder.getNodeState(); + Filter filter = createNiceMock(Filter.class); + Filter.PropertyRestriction restriction = new Filter.PropertyRestriction(); + restriction.first = PropertyValues.newDate("2013-01-01"); + restriction.firstIncluding = true; + restriction.propertyName = ORDERED_PROPERTY; + expect(filter.getPropertyRestrictions()).andReturn(ImmutableList.of(restriction)) + .anyTimes(); + expect(filter.containsNativeConstraint()).andReturn(false).anyTimes(); + replay(filter); + + assertFalse("In ascending order we're expeting to serve this kind of queries", + Double.POSITIVE_INFINITY == index.getCost(filter, root)); + } + + /** + * when we run a '<' in an Ascending index it should not serve it + * @throws RepositoryException + * @throws IllegalArgumentException + */ + @Test @Ignore("As of OAK-622 this should no longer be used. Removing later.") + public void costLessThanAscendingDirection() throws IllegalArgumentException, RepositoryException { + OrderedPropertyIndex index = new AlwaysIndexedOrderedPropertyIndex(); + NodeBuilder builder = InitialContent.INITIAL_CONTENT.builder(); + defineAscendingIndex(builder); + NodeState root = builder.getNodeState(); + Filter filter = createNiceMock(Filter.class); + Filter.PropertyRestriction restriction = new Filter.PropertyRestriction(); + restriction.last = PropertyValues.newDate("2013-01-01"); + restriction.propertyName = ORDERED_PROPERTY; + expect(filter.getPropertyRestrictions()).andReturn(ImmutableList.of(restriction)) + .anyTimes(); + expect(filter.containsNativeConstraint()).andReturn(false).anyTimes(); + replay(filter); + + assertEquals("in ascending index we're not expecting to serve '<' queries", + Double.POSITIVE_INFINITY, index.getCost(filter, root), 0); + } + + @Test @Ignore("As of OAK-622 this should no longer be used. Removing later.") + public void costLessThanEqualsAscendingDirection() throws IllegalArgumentException, RepositoryException { + OrderedPropertyIndex index = new AlwaysIndexedOrderedPropertyIndex(); + NodeBuilder builder = InitialContent.INITIAL_CONTENT.builder(); + defineAscendingIndex(builder); + NodeState root = builder.getNodeState(); + Filter filter = createNiceMock(Filter.class); + Filter.PropertyRestriction restriction = new Filter.PropertyRestriction(); + restriction.last = PropertyValues.newDate("2013-01-01"); + restriction.lastIncluding = true; + restriction.propertyName = ORDERED_PROPERTY; + expect(filter.getPropertyRestrictions()).andReturn(ImmutableList.of(restriction)) + .anyTimes(); + expect(filter.containsNativeConstraint()).andReturn(false).anyTimes(); + replay(filter); + + assertEquals("in ascending index we're not expecting to serve '<=' queries", + Double.POSITIVE_INFINITY, index.getCost(filter, root), 0); + } + + @Test @Ignore("As of OAK-622 this should no longer be used. Removing later.") + public void costGreaterThanDescendingDirection() throws IllegalArgumentException, RepositoryException { + OrderedPropertyIndex index = new AlwaysIndexedOrderedPropertyIndex(); + NodeBuilder builder = InitialContent.INITIAL_CONTENT.builder(); + defineDescendingIndex(builder); + NodeState root = builder.getNodeState(); + Filter filter = createNiceMock(Filter.class); + Filter.PropertyRestriction restriction = new Filter.PropertyRestriction(); + restriction.first = PropertyValues.newDate("2013-01-01"); + restriction.propertyName = ORDERED_PROPERTY; + expect(filter.getPropertyRestrictions()).andReturn(ImmutableList.of(restriction)) + .anyTimes(); + expect(filter.containsNativeConstraint()).andReturn(false).anyTimes(); + replay(filter); + + assertEquals("in descending index we're not expecting to serve '>' queries", + Double.POSITIVE_INFINITY, index.getCost(filter, root), 0); + + } + + @Test @Ignore("As of OAK-622 this should no longer be used. Removing later.") + public void costGreaterEqualThanDescendingDirection() throws IllegalArgumentException, RepositoryException { + OrderedPropertyIndex index = new AlwaysIndexedOrderedPropertyIndex(); + NodeBuilder builder = InitialContent.INITIAL_CONTENT.builder(); + defineDescendingIndex(builder); + NodeState root = builder.getNodeState(); + Filter filter = createNiceMock(Filter.class); + Filter.PropertyRestriction restriction = new Filter.PropertyRestriction(); + restriction.first = PropertyValues.newDate("2013-01-01"); + restriction.firstIncluding = true; + restriction.propertyName = ORDERED_PROPERTY; + expect(filter.getPropertyRestrictions()).andReturn(ImmutableList.of(restriction)) + .anyTimes(); + expect(filter.containsNativeConstraint()).andReturn(false).anyTimes(); + replay(filter); + + assertEquals("in descending index we're not expecting to serve '>' queries", + Double.POSITIVE_INFINITY, index.getCost(filter, root), 0); + + } + + @Test @Ignore("As of OAK-622 this should no longer be used. Removing later.") + public void costLessThanDescendingDirection() throws IllegalArgumentException, RepositoryException { + OrderedPropertyIndex index = new AlwaysIndexedOrderedPropertyIndex(); + NodeBuilder builder = InitialContent.INITIAL_CONTENT.builder(); + defineDescendingIndex(builder); + NodeState root = builder.getNodeState(); + Filter filter = createNiceMock(Filter.class); + Filter.PropertyRestriction restriction = new Filter.PropertyRestriction(); + restriction.last = PropertyValues.newDate("2013-01-01"); + restriction.propertyName = ORDERED_PROPERTY; + expect(filter.getPropertyRestrictions()).andReturn(ImmutableList.of(restriction)) + .anyTimes(); + expect(filter.containsNativeConstraint()).andReturn(false).anyTimes(); + replay(filter); + + assertFalse("In descending order we're expeting to serve this kind of queries", + Double.POSITIVE_INFINITY == index.getCost(filter, root)); + + } + + @Test @Ignore("As of OAK-622 this should no longer be used. Removing later.") + public void costLessThanEqualDescendingDirection() throws IllegalArgumentException, RepositoryException { + OrderedPropertyIndex index = new AlwaysIndexedOrderedPropertyIndex(); + NodeBuilder builder = InitialContent.INITIAL_CONTENT.builder(); + defineDescendingIndex(builder); + NodeState root = builder.getNodeState(); + Filter filter = createNiceMock(Filter.class); + Filter.PropertyRestriction restriction = new Filter.PropertyRestriction(); + restriction.last = PropertyValues.newDate("2013-01-01"); + restriction.lastIncluding = true; + restriction.propertyName = ORDERED_PROPERTY; + expect(filter.getPropertyRestrictions()).andReturn(ImmutableList.of(restriction)) + .anyTimes(); + expect(filter.containsNativeConstraint()).andReturn(false).anyTimes(); + replay(filter); + + assertFalse("In descending order we're expeting to serve this kind of queries", + Double.POSITIVE_INFINITY == index.getCost(filter, root)); + + } + + @Test @Ignore("As of OAK-622 this should no longer be used. Removing later.") + public void costBetweenDescendingDirection() throws IllegalArgumentException, RepositoryException { + OrderedPropertyIndex index = new AlwaysIndexedOrderedPropertyIndex(); + NodeBuilder builder = InitialContent.INITIAL_CONTENT.builder(); + defineDescendingIndex(builder); + NodeState root = builder.getNodeState(); + Filter filter = createNiceMock(Filter.class); + Filter.PropertyRestriction restriction = new Filter.PropertyRestriction(); + restriction.first = PropertyValues.newDate("2013-01-01"); + restriction.last = PropertyValues.newDate("2013-01-02"); + restriction.firstIncluding = true; + restriction.lastIncluding = true; + restriction.propertyName = ORDERED_PROPERTY; + expect(filter.getPropertyRestrictions()).andReturn(ImmutableList.of(restriction)) + .anyTimes(); + expect(filter.containsNativeConstraint()).andReturn(false).anyTimes(); + replay(filter); + + assertFalse("In descending order we're expeting to serve this kind of queries", + Double.POSITIVE_INFINITY == index.getCost(filter, root)); + + } + + @Test @Ignore("As of OAK-622 this should no longer be used. Removing later.") + public void costBetweenAscendingDirection() throws IllegalArgumentException, RepositoryException { + OrderedPropertyIndex index = new AlwaysIndexedOrderedPropertyIndex(); + NodeBuilder builder = InitialContent.INITIAL_CONTENT.builder(); + defineAscendingIndex(builder); + NodeState root = builder.getNodeState(); + Filter filter = createNiceMock(Filter.class); + Filter.PropertyRestriction restriction = new Filter.PropertyRestriction(); + restriction.first = PropertyValues.newDate("2013-01-01"); + restriction.last = PropertyValues.newDate("2013-01-02"); + restriction.firstIncluding = true; + restriction.lastIncluding = true; + restriction.propertyName = ORDERED_PROPERTY; + expect(filter.getPropertyRestrictions()).andReturn(ImmutableList.of(restriction)) + .anyTimes(); + expect(filter.containsNativeConstraint()).andReturn(false).anyTimes(); + replay(filter); + + assertFalse("In descending order we're expeting to serve this kind of queries", + Double.POSITIVE_INFINITY == index.getCost(filter, root)); + + } +} diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexDescendingQueryTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexDescendingQueryTest.java index 9180e8f..542dc4e 100644 --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexDescendingQueryTest.java +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexDescendingQueryTest.java @@ -22,6 +22,7 @@ import static junit.framework.Assert.assertTrue; import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED; import java.text.ParseException; +import java.util.Calendar; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -32,14 +33,18 @@ import org.apache.jackrabbit.oak.api.CommitFailedException; import org.apache.jackrabbit.oak.api.PropertyValue; 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.IndexConstants; import org.apache.jackrabbit.oak.plugins.index.IndexUtils; import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; import org.apache.jackrabbit.oak.spi.query.PropertyValues; import org.apache.jackrabbit.oak.util.NodeUtil; +import org.junit.Ignore; import org.junit.Test; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; public class OrderedPropertyIndexDescendingQueryTest extends BasicOrderedPropertyIndexQueryTest { @@ -77,7 +82,8 @@ public class OrderedPropertyIndexDescendingQueryTest extends BasicOrderedPropert Tree rTree = root.getTree("/"); Tree test = rTree.addChild("test"); List nodes = addChildNodes( - generateOrderedValues(NUMBER_OF_NODES, OrderDirection.DESC), test, OrderDirection.DESC); + generateOrderedValues(NUMBER_OF_NODES, OrderDirection.DESC), test, OrderDirection.DESC, + Type.STRING); root.commit(); // querying @@ -106,7 +112,7 @@ public class OrderedPropertyIndexDescendingQueryTest extends BasicOrderedPropert Tree rTree = root.getTree("/"); Tree test = rTree.addChild("test"); List nodes = addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test, - OrderDirection.DESC); + OrderDirection.DESC, Type.STRING); root.commit(); // getting the middle of the random list of nodes @@ -125,4 +131,157 @@ public class OrderedPropertyIndexDescendingQueryTest extends BasicOrderedPropert setTravesalEnabled(true); } + + /** + * test the range query in case of '>' condition + * @throws Exception + */ + @Test @Ignore("Disabling for now. Integration with OAK-622 and prioritising.") + public void queryGreaterThan() throws Exception { + initWithProperProvider(); + setTravesalEnabled(false); + + final OrderDirection direction = OrderDirection.ASC; + final String query = "SELECT * FROM [nt:base] AS n WHERE n.%s > $%s"; + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + // initialising the data + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + Calendar start = midnightFirstJan2013(); + addChildNodes( + generateOrderedDates(NUMBER_OF_NODES, direction, start), test, direction, Type.DATE); + root.commit(); + + Calendar searchForCalendar = (Calendar) start.clone(); + searchForCalendar.add(Calendar.HOUR_OF_DAY, 36); + String searchFor = ISO_8601_2000.format(searchForCalendar.getTime()); + Map filter = ImmutableMap.of(ORDERED_PROPERTY, + PropertyValues.newDate(searchFor)); + Iterator results = executeQuery( + String.format(query, ORDERED_PROPERTY, ORDERED_PROPERTY), SQL2, filter).getRows() + .iterator(); + assertFalse("We should not return any results as of the cost", results.hasNext()); + + setTravesalEnabled(true); + } + + /** + * test the range query in case of '>=' condition + * @throws Exception + */ + @Test @Ignore("Disabling for now. Integration with OAK-622 and prioritising.") + public void queryGreaterEqualThan() throws Exception { + initWithProperProvider(); + setTravesalEnabled(false); + + final OrderDirection direction = OrderDirection.ASC; + final String query = "SELECT * FROM [nt:base] AS n WHERE n.%s >= $%s"; + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + // initialising the data + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + Calendar start = midnightFirstJan2013(); + addChildNodes( + generateOrderedDates(NUMBER_OF_NODES, direction, start), test, direction, Type.DATE); + root.commit(); + + Calendar searchForCalendar = (Calendar) start.clone(); + searchForCalendar.add(Calendar.HOUR_OF_DAY, 36); + String searchFor = ISO_8601_2000.format(searchForCalendar.getTime()); + Map filter = ImmutableMap.of(ORDERED_PROPERTY, + PropertyValues.newDate(searchFor)); + Iterator results = executeQuery( + String.format(query, ORDERED_PROPERTY, ORDERED_PROPERTY), SQL2, filter).getRows() + .iterator(); + assertFalse("We should not return any results as of the cost", results.hasNext()); + + setTravesalEnabled(true); + } + + /** + * test the range query in case of '<' condition + * + * in this case as we're ascending we're expecting an empty resultset with the proper + * provider. not the lowcost one. + * @throws Exception + */ + @Test + public void queryLessThan() throws Exception { + setTravesalEnabled(false); + final OrderDirection direction = OrderDirection.DESC; + final String query = "SELECT * FROM [nt:base] AS n WHERE n.%s < $%s"; + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + // initialising the data + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + Calendar start = midnightFirstJan2013(); + List nodes = addChildNodes( + generateOrderedDates(10, direction, start), test, direction, Type.DATE); + root.commit(); + + Calendar searchForCalendar = (Calendar) start.clone(); + searchForCalendar.add(Calendar.HOUR_OF_DAY, -36); + String searchFor = ISO_8601_2000.format(searchForCalendar.getTime()); + Map filter = ImmutableMap.of(ORDERED_PROPERTY, + PropertyValues.newDate(searchFor)); + Iterator results = executeQuery( + String.format(query, ORDERED_PROPERTY, ORDERED_PROPERTY), SQL2, filter).getRows() + .iterator(); + Iterator filtered = Iterables.filter(nodes, + new ValuePathTuple.LessThanPredicate(searchFor)).iterator(); + assertRightOrder(Lists.newArrayList(filtered), results); + assertFalse("no more results expected", results.hasNext()); + + setTravesalEnabled(true); + } + + /** + * test the range query in case of '<=' condition + * + * in this case as we're ascending we're expecting an empty resultset with the proper + * provider. not the lowcost one. + * @throws Exception + */ + @Test + public void queryLessEqualThan() throws Exception { + setTravesalEnabled(false); + final OrderDirection direction = OrderDirection.DESC; + final String query = "SELECT * FROM [nt:base] AS n WHERE n.%s <= $%s"; + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + // initialising the data + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + Calendar start = midnightFirstJan2013(); + List nodes = addChildNodes( + generateOrderedDates(10, direction, start), test, direction, Type.DATE); + root.commit(); + + Calendar searchForCalendar = (Calendar) start.clone(); + searchForCalendar.add(Calendar.HOUR_OF_DAY, -36); + String searchFor = ISO_8601_2000.format(searchForCalendar.getTime()); + Map filter = ImmutableMap.of(ORDERED_PROPERTY, + PropertyValues.newDate(searchFor)); + Iterator results = executeQuery( + String.format(query, ORDERED_PROPERTY, ORDERED_PROPERTY), SQL2, filter).getRows() + .iterator(); + Iterator filtered = Iterables.filter(nodes, + new ValuePathTuple.LessThanPredicate(searchFor,true)).iterator(); + assertRightOrder(Lists.newArrayList(filtered), results); + assertFalse("no more results expecrted", results.hasNext()); + + setTravesalEnabled(true); + } + } diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorTest.java index 549eaba..f2e8396 100644 --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorTest.java +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexEditorTest.java @@ -22,7 +22,6 @@ import static org.easymock.EasyMock.replay; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.util.Arrays; @@ -88,15 +87,6 @@ public class OrderedPropertyIndexEditorTest { } @Test - public void orderedDirectionFromString() { - assertNull("A non-existing order direction should result in null", OrderDirection.fromString("foobar")); - assertEquals(OrderDirection.ASC, OrderDirection.fromString("ascending")); - assertFalse(OrderDirection.ASC.equals(OrderDirection.fromString("descending"))); - assertEquals(OrderDirection.DESC, OrderDirection.fromString("descending")); - assertFalse(OrderDirection.DESC.equals(OrderDirection.fromString("ascending"))); - } - - @Test public void orderDirectionDefinitionNotSpecified(){ final String property = "foobar"; NodeBuilder definition = EmptyNodeState.EMPTY_NODE.builder(); diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexQueryTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexQueryTest.java index 0612897..461e757 100644 --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexQueryTest.java +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/OrderedPropertyIndexQueryTest.java @@ -19,52 +19,53 @@ package org.apache.jackrabbit.oak.plugins.index.property; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; -import static junit.framework.Assert.fail; +import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM; import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_NODE_TYPES; +import static org.junit.Assert.assertNotNull; import java.text.ParseException; +import java.util.Calendar; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.jcr.RepositoryException; +import org.apache.jackrabbit.JcrConstants; import org.apache.jackrabbit.oak.api.CommitFailedException; import org.apache.jackrabbit.oak.api.PropertyValue; 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.IndexConstants; +import org.apache.jackrabbit.oak.plugins.index.IndexUpdateProvider; import org.apache.jackrabbit.oak.plugins.index.IndexUtils; import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; +import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState; +import org.apache.jackrabbit.oak.query.ast.Operator; +import org.apache.jackrabbit.oak.query.ast.SelectorImpl; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.commit.CommitInfo; +import org.apache.jackrabbit.oak.spi.commit.EditorHook; +import org.apache.jackrabbit.oak.spi.query.Filter; import org.apache.jackrabbit.oak.spi.query.PropertyValues; +import org.apache.jackrabbit.oak.spi.query.QueryIndex; +import org.apache.jackrabbit.oak.spi.query.QueryIndex.IndexPlan; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; import org.apache.jackrabbit.oak.util.NodeUtil; +import org.junit.Ignore; import org.junit.Test; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; public class OrderedPropertyIndexQueryTest extends BasicOrderedPropertyIndexQueryTest { - /** - * testing for asserting the right comparison behaviour of the custom class - */ - @Test - public void valuePathTupleComparison() { - try { - new ValuePathTuple("value", "path").compareTo(null); - fail("It should have raised a NPE"); - } catch (NullPointerException e) { - // so far so good - } - assertEquals(0, (new ValuePathTuple("value", "path")).compareTo(new ValuePathTuple("value", "path"))); - assertEquals(-1, (new ValuePathTuple("value", "path")).compareTo(new ValuePathTuple("value1", "path"))); - assertEquals(-1, (new ValuePathTuple("value1", "path")).compareTo(new ValuePathTuple("value1", "path1"))); - assertEquals(1, (new ValuePathTuple("value1", "path")).compareTo(new ValuePathTuple("value", "path"))); - assertEquals(1, (new ValuePathTuple("value1", "path1")).compareTo(new ValuePathTuple("value1", "path"))); - - assertEquals(-1, - (new ValuePathTuple("value000", "/test/n1")).compareTo(new ValuePathTuple("value001", "/test/n0"))); - assertEquals(1, - (new ValuePathTuple("value001", "/test/n0")).compareTo(new ValuePathTuple("value000", "/test/n1"))); - } + private static final EditorHook HOOK = new EditorHook(new IndexUpdateProvider( + new OrderedPropertyIndexEditorProvider())); @Override protected void createTestIndexNode() throws Exception { @@ -90,7 +91,8 @@ public class OrderedPropertyIndexQueryTest extends BasicOrderedPropertyIndexQuer Tree rTree = root.getTree("/"); Tree test = rTree.addChild("test"); - List nodes = addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test, OrderDirection.ASC); + List nodes = addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test, + OrderDirection.ASC, Type.STRING); root.commit(); // querying @@ -117,21 +119,439 @@ public class OrderedPropertyIndexQueryTest extends BasicOrderedPropertyIndexQuer Tree rTree = root.getTree("/"); Tree test = rTree.addChild("test"); - List nodes = addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test, OrderDirection.ASC); + List nodes = addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test, + OrderDirection.ASC, Type.STRING); root.commit(); // getting the middle of the random list of nodes. ValuePathTuple searchfor = nodes.get(NUMBER_OF_NODES / 2); - Map filter = ImmutableMap - .of(ORDERED_PROPERTY, PropertyValues.newString(searchfor.getValue())); + + Map filter = ImmutableMap.of(ORDERED_PROPERTY, + PropertyValues.newString(searchfor.getValue())); String query = "SELECT * FROM [%s] WHERE %s=$%s"; Iterator results = executeQuery( - String.format(query, NT_UNSTRUCTURED, ORDERED_PROPERTY, ORDERED_PROPERTY), SQL2, filter).getRows() - .iterator(); + String.format(query, NT_UNSTRUCTURED, ORDERED_PROPERTY, ORDERED_PROPERTY), SQL2, filter) + .getRows().iterator(); assertTrue("one element is expected", results.hasNext()); assertEquals("wrong path returned", searchfor.getPath(), results.next().getPath()); assertFalse("there should be not any more items", results.hasNext()); setTravesalEnabled(true); } + + /** + * test the range query in case of '>' condition + * + * @throws CommitFailedException + * @throws ParseException + */ + @Test @Ignore("Disabling for now. Integration with OAK-622 and prioritising.") + public void queryGreaterThan() throws CommitFailedException, ParseException { + setTravesalEnabled(false); + + final OrderDirection direction = OrderDirection.ASC; + final String query = "SELECT * FROM [nt:base] AS n WHERE n.%s > $%s"; + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + // initialising the data + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + Calendar start = midnightFirstJan2013(); + List nodes = addChildNodes( + generateOrderedDates(NUMBER_OF_NODES, direction, start), test, direction, Type.DATE); + root.commit(); + + Calendar searchForCalendar = (Calendar) start.clone(); + searchForCalendar.add(Calendar.HOUR_OF_DAY, 36); + String searchFor = ISO_8601_2000.format(searchForCalendar.getTime()); + Map filter = ImmutableMap.of(ORDERED_PROPERTY, + PropertyValues.newDate(searchFor)); + Iterator results = executeQuery( + String.format(query, ORDERED_PROPERTY, ORDERED_PROPERTY), SQL2, filter).getRows() + .iterator(); + Iterator filtered = Iterables.filter(nodes, + new ValuePathTuple.GreaterThanPredicate(searchFor)).iterator(); + assertRightOrder(Lists.newArrayList(filtered), results); + assertFalse("We should have looped throuhg all the results", results.hasNext()); + + setTravesalEnabled(true); + } + + /** + * test the range query in case of '>=' condition + * @throws CommitFailedException + * @throws ParseException + */ + @Test @Ignore("Disabling for now. Integration with OAK-622 and prioritising.") + public void queryGreaterEqualThan() throws CommitFailedException, ParseException { + setTravesalEnabled(false); + + final OrderDirection direction = OrderDirection.ASC; + final String query = "SELECT * FROM [nt:base] AS n WHERE n.%s >= $%s"; + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + // initialising the data + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + Calendar start = midnightFirstJan2013(); + List nodes = addChildNodes( + generateOrderedDates(NUMBER_OF_NODES, direction, start), test, direction, Type.DATE); + root.commit(); + + Calendar searchForCalendar = (Calendar) start.clone(); + searchForCalendar.add(Calendar.HOUR_OF_DAY, 36); + String searchFor = ISO_8601_2000.format(searchForCalendar.getTime()); + Map filter = ImmutableMap.of(ORDERED_PROPERTY, + PropertyValues.newDate(searchFor)); + Iterator results = executeQuery( + String.format(query, ORDERED_PROPERTY, ORDERED_PROPERTY), SQL2, filter).getRows() + .iterator(); + Iterator filtered = Iterables.filter(nodes, + new ValuePathTuple.GreaterThanPredicate(searchFor,true)).iterator(); + assertRightOrder(Lists.newArrayList(filtered), results); + assertFalse("We should have looped throuhg all the results", results.hasNext()); + + setTravesalEnabled(true); + } + + /** + * test the range query in case of '<' condition + * + * in this case as we're ascending we're expecting an empty resultset with the proper + * provider. not the lowcost one. + * @throws Exception + */ + @Test @Ignore("Disabling for now. Integration with OAK-622 and prioritising.") + public void queryLessThan() throws Exception { + initWithProperProvider(); + setTravesalEnabled(false); + final OrderDirection direction = OrderDirection.DESC; + final String query = "SELECT * FROM [nt:base] AS n WHERE n.%s < $%s"; + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + // initialising the data + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + Calendar start = midnightFirstJan2013(); + addChildNodes( + generateOrderedDates(NUMBER_OF_NODES, direction, start), test, direction, Type.DATE); + root.commit(); + + Calendar searchForCalendar = (Calendar) start.clone(); + searchForCalendar.add(Calendar.HOUR_OF_DAY, -36); + String searchFor = ISO_8601_2000.format(searchForCalendar.getTime()); + Map filter = ImmutableMap.of(ORDERED_PROPERTY, + PropertyValues.newDate(searchFor)); + Iterator results = executeQuery( + String.format(query, ORDERED_PROPERTY, ORDERED_PROPERTY), SQL2, filter).getRows() + .iterator(); + assertFalse("We should have no results as of the cost and index direction", results.hasNext()); + + setTravesalEnabled(true); + } + + /** + * test the range query in case of '<=' condition + * + * in this case as we're ascending we're expecting an empty resultset with the proper + * provider. not the lowcost one. + * @throws Exception + */ + @Test @Ignore("Disabling for now. Integration with OAK-622 and prioritising.") + public void queryLessEqualThan() throws Exception { + initWithProperProvider(); + initWithProperProvider(); + setTravesalEnabled(false); + final OrderDirection direction = OrderDirection.DESC; + final String query = "SELECT * FROM [nt:base] AS n WHERE n.%s <= $%s"; + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + // initialising the data + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + Calendar start = midnightFirstJan2013(); + addChildNodes( + generateOrderedDates(NUMBER_OF_NODES, direction, start), test, direction, Type.DATE); + root.commit(); + + Calendar searchForCalendar = (Calendar) start.clone(); + searchForCalendar.add(Calendar.HOUR_OF_DAY, -36); + String searchFor = ISO_8601_2000.format(searchForCalendar.getTime()); + Map filter = ImmutableMap.of(ORDERED_PROPERTY, + PropertyValues.newDate(searchFor)); + Iterator results = executeQuery( + String.format(query, ORDERED_PROPERTY, ORDERED_PROPERTY), SQL2, filter).getRows() + .iterator(); + assertFalse("We should have no results as of the cost and index direction", results.hasNext()); + + setTravesalEnabled(true); + } + + @Test + public void queryAllEntriesWithOrderBy() throws CommitFailedException, ParseException, RepositoryException { + setTravesalEnabled(false); + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + List nodes = addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test, + OrderDirection.ASC, Type.STRING); + root.commit(); + + // querying + Iterator results; + String query = String.format( + "SELECT * from [nt:base] WHERE %s IS NOT NULL ORDER BY %s", + ORDERED_PROPERTY, + ORDERED_PROPERTY); + results = executeQuery(query, SQL2, null) + .getRows().iterator(); + assertRightOrder(nodes, results); + + setTravesalEnabled(true); + } + + @Test + public void orderByQueryNoWhere() throws CommitFailedException, ParseException { + setTravesalEnabled(false); + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + List nodes = addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test, + OrderDirection.ASC, Type.STRING); + root.commit(); + + // querying + Iterator results; + String query = String.format( + "SELECT * from [nt:base] ORDER BY %s", + ORDERED_PROPERTY); + results = executeQuery(query, SQL2, null) + .getRows().iterator(); + assertRightOrder(nodes, results); + + setTravesalEnabled(true); + } + + @Test + public void planOderByNoWhere() throws IllegalArgumentException, RepositoryException, + CommitFailedException { + + NodeBuilder root = EmptyNodeState.EMPTY_NODE.builder(); + + IndexUtils.createIndexDefinition(root.child(IndexConstants.INDEX_DEFINITIONS_NAME), + TEST_INDEX_NAME, false, ImmutableList.of(ORDERED_PROPERTY), null, OrderedIndex.TYPE, + ImmutableMap. of()); + + NodeState before = root.getNodeState(); + final OrderDirection direction = OrderDirection.ASC; + final QueryIndex.OrderEntry.Order order = OrderDirection.ASC.equals(direction) ? QueryIndex.OrderEntry.Order.ASCENDING + : QueryIndex.OrderEntry.Order.DESCENDING; + + List values = generateOrderedValues(NUMBER_OF_NODES, direction); + addChildNodes(values, root, Type.STRING); + NodeState after = root.getNodeState(); + + NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY); + + final OrderedPropertyIndex index = new OrderedPropertyIndex(); + final String nodeTypeName = JcrConstants.NT_BASE; + + Filter filter = createFilter(indexed, nodeTypeName); + + List sortOrder = ImmutableList.of(createOrderEntry(ORDERED_PROPERTY, + order)); + List plans = index.getPlans(filter, sortOrder, indexed); + + assertNotNull(plans); + assertEquals(1, plans.size()); + IndexPlan p = plans.get(0); + assertTrue(p.getEstimatedEntryCount() > 0); + assertNotNull(p.getSortOrder()); + assertEquals(1, p.getSortOrder().size()); + QueryIndex.OrderEntry oe = p.getSortOrder().get(0); + assertNotNull(oe); + assertEquals(ORDERED_PROPERTY, oe.getPropertyName()); + assertEquals(QueryIndex.OrderEntry.Order.ASCENDING, oe.getOrder()); + } + + @Test + public void queryOrderByNonIndexedProperty() throws CommitFailedException, ParseException { + setTravesalEnabled(false); + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test, + OrderDirection.ASC, Type.STRING); + root.commit(); + + // querying + Iterator results; + String query = "SELECT * from [nt:base] ORDER BY somethingnotindexed"; + results = executeQuery(query, SQL2, null) + .getRows().iterator(); + assertFalse("An empty resultset is expected",results.hasNext()); + + setTravesalEnabled(true); + } + + private static FilterImpl createFilter(NodeState indexed, String nodeTypeName) { + NodeState system = indexed.getChildNode(JCR_SYSTEM); + NodeState types = system.getChildNode(JCR_NODE_TYPES); + NodeState type = types.getChildNode(nodeTypeName); + SelectorImpl selector = new SelectorImpl(type, nodeTypeName); + return new FilterImpl(selector, "SELECT * FROM [" + nodeTypeName + "]"); + } + + private static QueryIndex.OrderEntry createOrderEntry(String property, + QueryIndex.OrderEntry.Order order) { + return new QueryIndex.OrderEntry(property, Type.UNDEFINED, order); + } + + @Test + public void planOrderByNonIndexedProperty() throws IllegalArgumentException, + RepositoryException, CommitFailedException { + NodeBuilder root = EmptyNodeState.EMPTY_NODE.builder(); + + IndexUtils.createIndexDefinition(root.child(IndexConstants.INDEX_DEFINITIONS_NAME), + TEST_INDEX_NAME, false, ImmutableList.of(ORDERED_PROPERTY), null, OrderedIndex.TYPE, + ImmutableMap. of()); + + NodeState before = root.getNodeState(); + final OrderDirection direction = OrderDirection.ASC; + final QueryIndex.OrderEntry.Order order = OrderDirection.ASC.equals(direction) ? QueryIndex.OrderEntry.Order.ASCENDING + : QueryIndex.OrderEntry.Order.DESCENDING; + List values = generateOrderedValues(NUMBER_OF_NODES, direction); + addChildNodes(values, root, Type.STRING); + NodeState after = root.getNodeState(); + + NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY); + + final OrderedPropertyIndex index = new OrderedPropertyIndex(); + final String nodeTypeName = JcrConstants.NT_BASE; + Filter filter = createFilter(indexed, nodeTypeName); + + List sortOrder = ImmutableList.of(createOrderEntry( + "somethingnotindexed", order)); + + List plans = index.getPlans(filter, sortOrder, indexed); + + assertNotNull(plans); + assertEquals(0, plans.size()); + } + + /** + * tests the output of a plan where the query is asked with where conditions that are not indexed + * but the ORDER BY are on the indexed property + * + * eg: SELECT * FROM [nt:base] WHERE pinned=1 ORDER BY lastModified + * + * @throws RepositoryException + * @throws IllegalArgumentException + * @throws CommitFailedException + */ + @Test + public void planOrderAndWhereMixed() throws IllegalArgumentException, RepositoryException, CommitFailedException { + NodeBuilder root = EmptyNodeState.EMPTY_NODE.builder(); + + IndexUtils.createIndexDefinition(root.child(IndexConstants.INDEX_DEFINITIONS_NAME), + TEST_INDEX_NAME, false, ImmutableList.of(ORDERED_PROPERTY), null, OrderedIndex.TYPE, + ImmutableMap. of()); + + NodeState before = root.getNodeState(); + final OrderDirection direction = OrderDirection.ASC; + final QueryIndex.OrderEntry.Order order = OrderDirection.ASC.equals(direction) ? QueryIndex.OrderEntry.Order.ASCENDING + : QueryIndex.OrderEntry.Order.DESCENDING; + List values = generateOrderedValues(NUMBER_OF_NODES, direction); + addChildNodes(values, root, Type.STRING); + NodeState after = root.getNodeState(); + + NodeState indexed = HOOK.processCommit(before, after, CommitInfo.EMPTY); + + final OrderedPropertyIndex index = new OrderedPropertyIndex(); + final String nodeTypeName = JcrConstants.NT_BASE; + FilterImpl filter = createFilter(indexed, nodeTypeName); + filter.restrictProperty("somethingNotIndexed", Operator.EQUAL, PropertyValues.newLong(1L)); + + List sortOrder = ImmutableList.of(createOrderEntry( + ORDERED_PROPERTY, order)); + + List plans = index.getPlans(filter, sortOrder, indexed); + assertNotNull(plans); + assertEquals(1,plans.size()); + IndexPlan p = plans.get(0); + assertTrue(p.getEstimatedEntryCount()>0); + assertNotNull(p.getSortOrder()); + assertEquals(1, p.getSortOrder().size()); + assertEquals(QueryIndex.OrderEntry.Order.ASCENDING,p.getSortOrder().get(0).getOrder()); + } + + /** + * query the index in case of mixed situation + * + * eg: SELECT * FROM [nt:base] WHERE pinned=1 ORDER BY lastModified + * + * @throws RepositoryException + * @throws IllegalArgumentException + * @throws CommitFailedException + * @throws ParseException + */ + @Test + public void queryOrderAndWhereMixed() throws IllegalArgumentException, RepositoryException, CommitFailedException, ParseException { + setTravesalEnabled(false); + + // index automatically created by the framework: + // {@code createTestIndexNode()} + + // initiate the repo with some data + Tree rTree = root.getTree("/"); + Tree test = rTree.addChild("test"); + List nodes = addChildNodes(generateOrderedValues(NUMBER_OF_NODES), test, + OrderDirection.ASC, Type.STRING); + root.commit(); + + String where = "wholetthedogsout"; + String value = "woof-woof-woof-woof"; + + // let's set the property that will have to be queried only on 2 nodes + Tree t = root.getTree(nodes.get(0).getPath()); + t.setProperty(where, value); + t = root.getTree(nodes.get(1).getPath()); + t.setProperty(where, value); + root.commit(); + + // querying + Iterator results; + String query = String.format( + "SELECT * from [nt:base] WHERE %s=$%s ORDER BY %s", + where, + where, + ORDERED_PROPERTY + ); + Map filter = ImmutableMap.of( + where, PropertyValues.newString(value) + ); + results = executeQuery(query, SQL2, filter) + .getRows().iterator(); + assertTrue(results.hasNext()); + + setTravesalEnabled(true); + } + } diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/ValuePathTuple.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/ValuePathTuple.java index 13d8038..2627eec 100644 --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/ValuePathTuple.java +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/ValuePathTuple.java @@ -16,6 +16,8 @@ */ package org.apache.jackrabbit.oak.plugins.index.property; +import com.google.common.base.Predicate; + /** * convenience orderable object that represents a tuple of values and paths @@ -27,6 +29,64 @@ public class ValuePathTuple implements Comparable { private String value; private String path; + /** + * convenience Predicate for easing the testing + */ + public static class GreaterThanPredicate implements Predicate { + /** + * the value for comparison + */ + private String value; + + /** + * whether we should include the value in the result + */ + private boolean include = false; + + public GreaterThanPredicate(String value) { + this.value = value; + } + + public GreaterThanPredicate(String value, boolean include) { + this.value = value; + this.include = include; + } + + @Override + public boolean apply(ValuePathTuple arg0) { + return (value.compareTo(arg0.getValue()) < 0) + || (include && value.equals(arg0.getValue())); + } + }; + + public static class LessThanPredicate implements Predicate { + /** + * the value for comparison + */ + private String value; + + /** + * whether we should include the value in the result + */ + private boolean include = false; + + public LessThanPredicate(String value){ + this.value = value; + } + + public LessThanPredicate(String value, boolean include){ + this.value = value; + this.include = include; + } + + @Override + public boolean apply(ValuePathTuple arg0) { + return (value.compareTo(arg0.getValue()) > 0) + || (include && value.equals(arg0.getValue())); + } + + } + ValuePathTuple(String value, String path) { this.value = value; this.path = path; @@ -98,4 +158,14 @@ public class ValuePathTuple implements Comparable { public String getPath() { return path; } + + @Override + public String toString() { + return String.format( + "value: %s - path: %s - hash: %s", + value, + path, + super.toString() + ); + } } diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/ValuePathTupleTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/ValuePathTupleTest.java new file mode 100644 index 0000000..0571cce --- /dev/null +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/ValuePathTupleTest.java @@ -0,0 +1,152 @@ +/* + * 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.property; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.fail; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Iterator; +import java.util.List; + +import org.junit.Test; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +/** + * tests the utility class ValuePathTuple + */ +public class ValuePathTupleTest { + /** + * testing for asserting the right comparison behaviour of the custom class + */ + @Test + public void valuePathTupleComparison() { + try { + new ValuePathTuple("value", "path").compareTo(null); + fail("It should have raised a NPE"); + } catch (NullPointerException e) { + // so far so good + } + assertEquals(0, + (new ValuePathTuple("value", "path")).compareTo(new ValuePathTuple("value", "path"))); + assertEquals(-1, + (new ValuePathTuple("value", "path")).compareTo(new ValuePathTuple("value1", "path"))); + assertEquals(-1, + (new ValuePathTuple("value1", "path")).compareTo(new ValuePathTuple("value1", "path1"))); + assertEquals(1, + (new ValuePathTuple("value1", "path")).compareTo(new ValuePathTuple("value", "path"))); + assertEquals(1, + (new ValuePathTuple("value1", "path1")).compareTo(new ValuePathTuple("value1", "path"))); + + assertEquals(-1, (new ValuePathTuple("value000", "/test/n1")).compareTo(new ValuePathTuple( + "value001", "/test/n0"))); + assertEquals(1, (new ValuePathTuple("value001", "/test/n0")).compareTo(new ValuePathTuple( + "value000", "/test/n1"))); + } + + @Test + public void greaterThanPredicate() { + List data = ImmutableList.of(new ValuePathTuple("a", "foobar"), + new ValuePathTuple("b", "foobar"), new ValuePathTuple("c", "foobar"), + new ValuePathTuple("d", "foobar"), new ValuePathTuple("e", "foobar"), + new ValuePathTuple("f", "foobar")); + Iterator filtered = Iterables.filter(data, + new ValuePathTuple.GreaterThanPredicate("b")).iterator(); + assertTrue(filtered.hasNext()); + assertEquals("c", filtered.next().getValue()); + assertEquals("d", filtered.next().getValue()); + assertEquals("e", filtered.next().getValue()); + assertEquals("f", filtered.next().getValue()); + assertFalse(filtered.hasNext()); + } + + @Test + public void greaterThanEqualaPredicate() { + List data = ImmutableList.of(new ValuePathTuple("a", "foobar"), + new ValuePathTuple("b", "foobar"), new ValuePathTuple("c", "foobar"), + new ValuePathTuple("d", "foobar"), new ValuePathTuple("e", "foobar"), + new ValuePathTuple("f", "foobar")); + Iterator filtered = Iterables.filter(data, + new ValuePathTuple.GreaterThanPredicate("b", true)).iterator(); + assertTrue(filtered.hasNext()); + assertEquals("b", filtered.next().getValue()); + assertEquals("c", filtered.next().getValue()); + assertEquals("d", filtered.next().getValue()); + assertEquals("e", filtered.next().getValue()); + assertEquals("f", filtered.next().getValue()); + assertFalse(filtered.hasNext()); + } + + @Test + public void lessThanPredicate() { + List data = ImmutableList.of( + new ValuePathTuple("a", "foobar"), + new ValuePathTuple("b", "foobar"), + new ValuePathTuple("c", "foobar"), + new ValuePathTuple("d", "foobar"), + new ValuePathTuple("e", "foobar"), + new ValuePathTuple("f", "foobar")); + Iterator filtered = Iterables.filter(data, + new ValuePathTuple.LessThanPredicate("e")).iterator(); + assertTrue(filtered.hasNext()); + assertEquals("a", filtered.next().getValue()); + assertEquals("b", filtered.next().getValue()); + assertEquals("c", filtered.next().getValue()); + assertEquals("d", filtered.next().getValue()); + assertFalse(filtered.hasNext()); + + data = ImmutableList.of( + new ValuePathTuple("f", "foobar"), + new ValuePathTuple("e", "foobar"), + new ValuePathTuple("d", "foobar"), + new ValuePathTuple("c", "foobar"), + new ValuePathTuple("b", "foobar"), + new ValuePathTuple("a", "foobar") + ); + filtered = Iterables.filter(data, + new ValuePathTuple.LessThanPredicate("e")).iterator(); + assertTrue(filtered.hasNext()); + assertEquals("d", filtered.next().getValue()); + assertEquals("c", filtered.next().getValue()); + assertEquals("b", filtered.next().getValue()); + assertEquals("a", filtered.next().getValue()); + assertFalse(filtered.hasNext()); + } + + @Test + public void lessThanEqualPredicate() { + List data = ImmutableList.of( + new ValuePathTuple("a", "foobar"), + new ValuePathTuple("b", "foobar"), + new ValuePathTuple("c", "foobar"), + new ValuePathTuple("d", "foobar"), + new ValuePathTuple("e", "foobar"), + new ValuePathTuple("f", "foobar")); + Iterator filtered = Iterables.filter(data, + new ValuePathTuple.LessThanPredicate("e",true)).iterator(); + assertTrue(filtered.hasNext()); + assertEquals("a", filtered.next().getValue()); + assertEquals("b", filtered.next().getValue()); + assertEquals("c", filtered.next().getValue()); + assertEquals("d", filtered.next().getValue()); + assertEquals("e", filtered.next().getValue()); + assertFalse(filtered.hasNext()); + } +} diff --git oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java index e3722d4..ed81f93 100644 --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java @@ -25,18 +25,29 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import java.text.DecimalFormat; +import java.text.NumberFormat; import java.util.Iterator; import java.util.Set; +import javax.jcr.RepositoryException; + import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.index.IndexConstants; +import org.apache.jackrabbit.oak.plugins.index.IndexUtils; +import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex; import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection; import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState; +import org.apache.jackrabbit.oak.spi.query.Filter; +import org.apache.jackrabbit.oak.spi.query.PropertyValues; import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry; import org.apache.jackrabbit.oak.spi.state.NodeBuilder; import org.apache.jackrabbit.oak.spi.state.NodeState; import org.junit.Test; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; @@ -49,6 +60,7 @@ public class OrderedContentMirrorStorageStrategyTest { */ private static final String[] KEYS = new String[] { "donald", "goofy", "mickey", "minnie" }; private static final Set EMPTY_KEY_SET = newHashSet(); + private static final NumberFormat NF = new DecimalFormat("00000"); /** * checks that the fist item/key is inserted with an empty property 'next' @@ -974,32 +986,6 @@ public class OrderedContentMirrorStorageStrategyTest { } /** - * test the insert of 1 item in a descending order index. it should not really matter but just - * checking we don't break anything - * - * expecting - * - * - * :index : { - * :start : { :next = n0 }, - * n0 : { :next = } - * } - * - */ - @Test - public void descendingOrderInsert1item() { - IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(OrderDirection.DESC); - NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); - String n0 = KEYS[1]; - - store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n0)); - assertEquals(":start should point to the first node", n0, index.getChildNode(START) - .getString(NEXT)); - assertTrue("the first node should point nowhere", - Strings.isNullOrEmpty(index.getChildNode(n0).getString(NEXT))); - } - - /** *

test the insertion of 2 already ordered items

* *

expected

@@ -1029,42 +1015,6 @@ public class OrderedContentMirrorStorageStrategyTest { } /** - * test the insert of 4 shuffled items in a descending ordered index - * - * expected: - * - * - * :index : { - * :start : { :next= n1}, - * n0 : { :next= n3}, - * n1 : { :next= n2}, - * n2: { :next= n0}, - * n3 : { :next= }, - * } - * - */ - @Test - public void descendingOrderInsert4ShuffledItems() { - IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(OrderDirection.DESC); - NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); - String n0 = KEYS[1]; - String n1 = KEYS[3]; - String n2 = KEYS[2]; - String n3 = KEYS[0]; - - store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n0)); - store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n1)); - store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n2)); - store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n3)); - assertEquals(":start should point to n1", n1, index.getChildNode(START).getString(NEXT)); - assertEquals("n0 should point to n3", n3, index.getChildNode(n0).getString(NEXT)); - assertEquals("n1 should point to n2", n2, index.getChildNode(n1).getString(NEXT)); - assertEquals("n2 should point to n1", n0, index.getChildNode(n2).getString(NEXT)); - assertTrue("n3 should point nowhere", - Strings.isNullOrEmpty(index.getChildNode(n3).getString(NEXT))); - } - - /** * Tests the insert of 4 items that will always have to be added at the beginning of the list. * Just to simulate the use-case of lastModified DESC. * @@ -1200,6 +1150,68 @@ public class OrderedContentMirrorStorageStrategyTest { } /** + * test the insert of 1 item in a descending order index. it should not really matter but just + * checking we don't break anything + * + * expecting + * + * + * :index : { + * :start : { :next = n0 }, + * n0 : { :next = } + * } + * + */ + @Test + public void descendingOrderInsert1item() { + IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(OrderDirection.DESC); + NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); + String n0 = KEYS[1]; + + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n0)); + assertEquals(":start should point to the first node", n0, index.getChildNode(START) + .getString(NEXT)); + assertTrue("the first node should point nowhere", + Strings.isNullOrEmpty(index.getChildNode(n0).getString(NEXT))); + } + + /** + * test the insert of 4 shuffled items in a descending ordered index + * + * expected: + * + * + * :index : { + * :start : { :next= n1}, + * n0 : { :next= n3}, + * n1 : { :next= n2}, + * n2: { :next= n0}, + * n3 : { :next= }, + * } + * + */ + @Test + public void descendingOrderInsert4ShuffledItems() { + IndexStoreStrategy store = new OrderedContentMirrorStoreStrategy(OrderDirection.DESC); + NodeBuilder index = EmptyNodeState.EMPTY_NODE.builder(); + String n0 = KEYS[1]; + String n1 = KEYS[3]; + String n2 = KEYS[2]; + String n3 = KEYS[0]; + + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n0)); + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n1)); + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n2)); + store.update(index, "/a/b", EMPTY_KEY_SET, newHashSet(n3)); + assertEquals(":start should point to n1", n1, index.getChildNode(START).getString(NEXT)); + assertEquals("n0 should point to n3", n3, index.getChildNode(n0).getString(NEXT)); + assertEquals("n1 should point to n2", n2, index.getChildNode(n1).getString(NEXT)); + assertEquals("n2 should point to n1", n0, index.getChildNode(n2).getString(NEXT)); + assertTrue("n3 should point nowhere", + Strings.isNullOrEmpty(index.getChildNode(n3).getString(NEXT))); + } + + /** * test the iteration of the descending index with 2 shuffled items * * @@ -1244,4 +1256,108 @@ public class OrderedContentMirrorStorageStrategyTest { assertEquals("Wrong entry returned", node0, entry.getNodeState()); assertFalse("We should have be at the end of the list", it.hasNext()); } + + @Test + public void count() throws IllegalArgumentException, RepositoryException { + OrderedContentMirrorStoreStrategy store = new OrderedContentMirrorStoreStrategy(); + OrderedContentMirrorStoreStrategy descendingStore = new OrderedContentMirrorStoreStrategy(OrderDirection.DESC); + final String orderedProperty = "fooprop"; + final String testAscendingName = "testascending"; + final String testDescendingName = "testdescending"; + final int numberOfNodes = 1000; + final int maxNodeCount = 100; + + NodeBuilder builder = EmptyNodeState.EMPTY_NODE.builder(); + + IndexUtils.createIndexDefinition(builder.child(IndexConstants.INDEX_DEFINITIONS_NAME), + testAscendingName, false, ImmutableList.of(orderedProperty), null, OrderedIndex.TYPE, + ImmutableMap. of()); + IndexUtils.createIndexDefinition(builder.child(IndexConstants.INDEX_DEFINITIONS_NAME), + testDescendingName, false, ImmutableList.of(orderedProperty), null, OrderedIndex.TYPE, + ImmutableMap. of(OrderedIndex.DIRECTION,OrderDirection.DESC.getDirection())); + + NodeBuilder ascendingContent = builder.getChildNode(IndexConstants.INDEX_DEFINITIONS_NAME) + .getChildNode(testAscendingName).child(IndexConstants.INDEX_CONTENT_NODE_NAME); + NodeBuilder descendingContent = builder.getChildNode(IndexConstants.INDEX_DEFINITIONS_NAME) + .getChildNode(testDescendingName).child(IndexConstants.INDEX_CONTENT_NODE_NAME); + + // adding some content under the index + for (int i = 0; i < numberOfNodes; i++) { + store.update(ascendingContent, "/foo/bar", EMPTY_KEY_SET, newHashSet("x" + NF.format(i))); + descendingStore.update(descendingContent, "/foo/bar", EMPTY_KEY_SET, newHashSet("x" + NF.format(i))); + } + + assertEquals("wrong number of nodes encountered", numberOfNodes, + Iterables.size(store.getChildNodeEntries(ascendingContent.getNodeState()))); + assertEquals("wrong number of nodes encountered", numberOfNodes, + Iterables.size(descendingStore.getChildNodeEntries(descendingContent.getNodeState()))); + + NodeState ascendingMeta = builder.getChildNode(IndexConstants.INDEX_DEFINITIONS_NAME) + .getChildNode(testAscendingName).getNodeState(); + NodeState descendingMeta = builder.getChildNode(IndexConstants.INDEX_DEFINITIONS_NAME) + .getChildNode(testDescendingName).getNodeState(); + + Filter.PropertyRestriction pr = null; + + // equality + String value = "x" + NF.format(11); + pr = new Filter.PropertyRestriction(); + pr.first = PropertyValues.newString(value); + pr.last = PropertyValues.newString(value); + pr.firstIncluding = true; + pr.lastIncluding = true; + assertEquals(1, store.count(ascendingMeta, pr, maxNodeCount)); + assertEquals(1, descendingStore.count(descendingMeta, pr, maxNodeCount)); + + // property not null + pr = new Filter.PropertyRestriction(); + pr.first = null; + pr.last = null; + pr.firstIncluding = false; + pr.lastIncluding = false; + // don't care about the actual results as long as we have something. We're reusing existing + // code + assertTrue(store.count(ascendingMeta, pr, maxNodeCount) > 0); + assertEquals(store.count(ascendingMeta, pr, maxNodeCount), + descendingStore.count(descendingMeta, pr, maxNodeCount)); + + // '>' + pr = new Filter.PropertyRestriction(); + pr.first = PropertyValues.newString(value); + pr.last = null; + pr.firstIncluding = false; + pr.lastIncluding = false; + // don't care about the actual results as long as we have something. We're reusing existing + // code + assertTrue(store.count(ascendingMeta, pr, maxNodeCount) > 0); + assertEquals(0, descendingStore.count(descendingMeta, pr, maxNodeCount)); + + // '>=' + pr = new Filter.PropertyRestriction(); + pr.first = PropertyValues.newString(value); + pr.last = null; + pr.firstIncluding = true; + pr.lastIncluding = false; + // don't care about the actual results as long as we have something. We're reusing existing + // code + assertTrue(store.count(ascendingMeta, pr, maxNodeCount) > 0); + assertEquals(0, descendingStore.count(descendingMeta, pr, maxNodeCount)); + + // '<' + pr = new Filter.PropertyRestriction(); + pr.first = null; + pr.last = PropertyValues.newString(value);; + pr.firstIncluding = false; + pr.lastIncluding = false; + // don't care about the actual results as long as we have something. We're reusing existing + // code + assertTrue(descendingStore.count(descendingMeta, pr, maxNodeCount) > 0); + assertEquals(0, store.count(ascendingMeta, pr, maxNodeCount)); + + // when no conditions has been asked but just an ORDER BY + pr = null; + assertTrue(store.count(ascendingMeta, pr, maxNodeCount) > 0); + assertEquals(store.count(ascendingMeta, pr, maxNodeCount), + descendingStore.count(descendingMeta, pr, maxNodeCount)); + } } diff --git oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/BenchmarkRunner.java oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/BenchmarkRunner.java index be694b3..ca2c9c9 100644 --- oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/BenchmarkRunner.java +++ oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/BenchmarkRunner.java @@ -103,7 +103,6 @@ public class BenchmarkRunner { new OrderedIndexInsertOrderedPropertyTest(), new OrderedIndexInsertStandardPropertyTest(), new OrderedIndexInsertNoIndexTest(), - new OrderByQueryTest(), new LoginTest(), new LoginLogoutTest(), new NamespaceTest(), diff --git oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderByQueryTest.java oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderByQueryTest.java deleted file mode 100644 index a6cabdb..0000000 --- oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderByQueryTest.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.benchmark; - -import java.util.Random; -import java.util.UUID; - -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.query.Query; -import javax.jcr.query.QueryManager; -import javax.jcr.query.QueryResult; - -/** - * This benchmark measures the read performance of child nodes using - * an ORDER BY query. - *

- * This is related to OAK-1263. - * - */ -public class OrderByQueryTest extends AbstractTest { - - private static final String NT = "oak:unstructured"; - - private static final String ROOT_NODE_NAME = "test" + TEST_ID; - private static final int NUM_NODES = 10000; - private static final String PROPERTY_NAME = "testProperty"; - private static final Random random = new Random(); // doesn't have to be very secure, just some randomness - - @Override - protected void beforeSuite() throws Exception { - Session session = loginWriter(); - Node rootNode = session.getRootNode(); - if (rootNode.hasNode(ROOT_NODE_NAME)) { - Node root = rootNode.getNode(ROOT_NODE_NAME); - root.remove(); - } - rootNode = session.getRootNode().addNode(ROOT_NODE_NAME, NT); - - for (int i = 0; i < NUM_NODES; i++) { - if (i%1000==0) { - session.save(); - } - Node newNode = rootNode.addNode(UUID.randomUUID().toString(), NT); - newNode.setProperty(PROPERTY_NAME, random.nextLong()); - } - session.save(); - } - - @Override - public void runTest() throws Exception { - final Session session = loginWriter(); - try { - // run the query - final QueryManager qm = session.getWorkspace().getQueryManager(); - - final Query q = - qm.createQuery("SELECT * FROM [oak:unstructured] AS s WHERE " - + "ISDESCENDANTNODE(s, [/"+ROOT_NODE_NAME+"/]) ORDER BY s."+PROPERTY_NAME+"]", - Query.JCR_SQL2); - final QueryResult res = q.execute(); - - final NodeIterator nit = res.getNodes(); -// while(nit.hasNext()) { -// Node node = nit.nextNode(); -//// System.out.println("node: "+node.getPath()+", prop="+node.getProperty(PROPERTY_NAME).getLong()); -// } - } catch (RepositoryException e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } -} diff --git oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderedIndexBaseTest.java oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderedIndexBaseTest.java index 674ab89..6ee3757 100644 --- oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderedIndexBaseTest.java +++ oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderedIndexBaseTest.java @@ -54,6 +54,11 @@ public abstract class OrderedIndexBaseTest extends AbstractTest { static final String INDEXED_PROPERTY = "indexedProperty"; /** + * size of the batch for saving + */ + static final int BATCH_SAVING_SIZE = 1024; + + /** * node name below which creating the test data */ final String DUMP_NODE = this.getClass().getSimpleName() + TEST_ID; @@ -68,11 +73,27 @@ public abstract class OrderedIndexBaseTest extends AbstractTest { */ Node dump; + + /** + * insert a {@code numberOfNode} random nodes in the repository + * + * @param numberOfNodes + */ void insertRandomNodes(int numberOfNodes) { try { for (int i = 0; i < numberOfNodes; i++) { String uuid = UUID.randomUUID().toString(); dump.addNode(uuid, NODE_TYPE).setProperty(INDEXED_PROPERTY, uuid); + if (isBatchSaving()) { + if (i % BATCH_SAVING_SIZE == 0) { + session.save(); + } + } else { + session.save(); + } + } + if (isBatchSaving()) { + // an extra save to catch any pending operations. session.save(); } } catch (RepositoryException e) { @@ -109,4 +130,13 @@ public abstract class OrderedIndexBaseTest extends AbstractTest { session.save(); return index; } + + /** + * + * @return true if you want batch saving during {@code insertRandomNodes} by + * {@code BATCH_SAVE_SIZE} + */ + boolean isBatchSaving() { + return false; + } } diff --git oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderedIndexQueryBaseTest.java oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderedIndexQueryBaseTest.java index 28338ff..df04ef8 100644 --- oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderedIndexQueryBaseTest.java +++ oak-run/src/main/java/org/apache/jackrabbit/oak/benchmark/OrderedIndexQueryBaseTest.java @@ -77,4 +77,9 @@ public abstract class OrderedIndexQueryBaseTest extends OrderedIndexBaseTest { } abstract String getQuery(); + + @Override + boolean isBatchSaving() { + return true; + } }