You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by da...@apache.org on 2014/08/18 16:51:13 UTC

svn commit: r1618624 - in /jackrabbit/oak/trunk: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/ oak-jcr/src/test/java/org/apache/jackra...

Author: davide
Date: Mon Aug 18 14:51:13 2014
New Revision: 1618624

URL: http://svn.apache.org/r1618624
Log:
OAK-2035 - OrderedIndex fails to keep a correct skipList on TarMK

fixed the predicate that was double-encoding the values and added IT for covering the use case.

Added:
    jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OrderedIndexIT.java
    jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/util/
    jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/util/ValuePathTuple.java
Modified:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java?rev=1618624&r1=1618623&r2=1618624&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStoreStrategy.java Mon Aug 18 14:51:13 2014
@@ -114,8 +114,20 @@ public class OrderedContentMirrorStoreSt
         this.direction = direction;
     }
     
+    private static void printWalkedLanes(final String msg, final String[] walked) {
+        String m = (msg == null) ? "" : msg;
+        if (walked == null) {
+            LOG.debug(m + " walked: null");
+        } else {
+            for (int i = 0; i < walked.length; i++) {
+                LOG.debug("{}walked[{}]: {}", new Object[] { m, i, walked[i] });
+            }
+        }
+    }
+    
     @Override
     NodeBuilder fetchKeyNode(@Nonnull NodeBuilder index, @Nonnull String key) {
+        LOG.debug("fetchKeyNode() - new item '{}' -----------------------------------------", key);
         // this is where the actual adding and maintenance of index's keys happen
         NodeBuilder node = null;
         NodeBuilder start = index.child(START);
@@ -132,8 +144,14 @@ public class OrderedContentMirrorStoreSt
         // we use the seek for seeking the right spot. The walkedLanes will have all our
         // predecessors
         String entry = seek(index, condition, walked);
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("fetchKeyNode() - entry: {} ", entry);
+            printWalkedLanes("fetchKeyNode() - ", walked);
+        }
+        
         if (entry != null && entry.equals(key)) {
             // it's an existing node. We should not need to update anything around pointers
+            LOG.debug("fetchKeyNode() - node already there.");
             node = index.getChildNode(key);
         } else {
             // the entry does not exits yet
@@ -141,6 +159,7 @@ public class OrderedContentMirrorStoreSt
             // it's a brand new node. let's start setting an empty next
             setPropertyNext(node, EMPTY_NEXT_ARRAY);
             int lane = getLane();
+            LOG.debug("fetchKeyNode() - extracted lane: {}", lane);
             String next;
             NodeBuilder predecessor;
             for (int l = lane; l >= 0; l--) {
@@ -149,6 +168,12 @@ public class OrderedContentMirrorStoreSt
                 next = getPropertyNext(predecessor, l);
                 setPropertyNext(predecessor, key, l);
                 setPropertyNext(node, next, l);
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug("fetchKeyNode() - on lane: {}", l);
+                    LOG.debug("fetchKeyNode() - next from previous: {}", next);
+                    LOG.debug("fetchKeyNode() - new status of predecessor name: {} - {} ", walked[l], predecessor.getProperty(NEXT));
+                    LOG.debug("fetchKeyNode() - new node name: {} - {}", key, node.getProperty(NEXT));
+                }
             }
         }
         return node;
@@ -268,21 +293,33 @@ public class OrderedContentMirrorStoreSt
     public Iterable<String> query(final Filter filter, final String indexName,
                                   final NodeState indexMeta, final String indexStorageNodeName,
                                   final PropertyRestriction pr) {
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("query() - filter: {}", filter);            
+            LOG.debug("query() - indexName: {}", indexName);            
+            LOG.debug("query() - indexMeta: {}", indexMeta);            
+            LOG.debug("query() - indexStorageNodeName: {}", indexStorageNodeName);            
+            LOG.debug("query() - pr: {}", pr);            
+        }
         
         final NodeState indexState = indexMeta.getChildNode(indexStorageNodeName);
         final NodeBuilder index = new ReadOnlyBuilder(indexState);
-
-        if (pr.first != null && !pr.first.equals(pr.last)) {
+        final String firstEncoded = (pr.first == null) ? null 
+                                                       : encode(pr.first.getValue(Type.STRING));
+        final String lastEncoded = (pr.last == null) ? null
+                                                     : encode(pr.last.getValue(Type.STRING));
+        
+        if (firstEncoded != null && !firstEncoded.equals(lastEncoded)) {
             // '>' & '>=' and between use case
+            LOG.debug("'>' & '>=' and between use case");
             ChildNodeEntry firstValueableItem;
             String firstValuableItemKey;
             Iterable<String> it = Collections.emptyList();
             Iterable<ChildNodeEntry> childrenIterable;
             
-            if (pr.last == null) {
+            if (lastEncoded == null) {
                 LOG.debug("> & >= case.");
                 firstValuableItemKey = seek(index,
-                    new PredicateGreaterThan(pr.first.getValue(Type.STRING), pr.firstIncluding));
+                    new PredicateGreaterThan(firstEncoded, pr.firstIncluding));
                 if (firstValuableItemKey != null) {
                     firstValueableItem = new OrderedChildNodeEntry(firstValuableItemKey,
                         indexState.getChildNode(firstValuableItemKey));
@@ -291,15 +328,15 @@ public class OrderedContentMirrorStoreSt
                         it = new QueryResultsWrapper(filter, indexName, childrenIterable);
                     } else {
                         it = new QueryResultsWrapper(filter, indexName, new BetweenIterable(
-                            indexState, firstValueableItem, pr.first.getValue(Type.STRING),
+                            indexState, firstValueableItem, firstEncoded,
                             pr.firstIncluding, direction));
                     }
                 }
             } else {
                 String first, last;
                 boolean includeFirst, includeLast;
-                first = pr.first.getValue(Type.STRING);
-                last = pr.last.getValue(Type.STRING);
+                first = firstEncoded;
+                last = lastEncoded;
                 includeFirst = pr.firstIncluding;
                 includeLast = pr.lastIncluding;
 
@@ -330,9 +367,10 @@ public class OrderedContentMirrorStoreSt
             }
 
             return it;
-        } else if (pr.last != null && !pr.last.equals(pr.first)) {
+        } else if (lastEncoded != null && !lastEncoded.equals(firstEncoded)) {
             // '<' & '<=' use case
-            final String searchfor = pr.last.getValue(Type.STRING);
+            LOG.debug("'<' & '<=' use case");
+            final String searchfor = lastEncoded;
             final boolean include = pr.lastIncluding;
             Predicate<String> predicate = new PredicateLessThan(searchfor, include);
             
@@ -359,8 +397,8 @@ public class OrderedContentMirrorStoreSt
             return it;
         } else {
             // property is not null. AKA "open query"
-            Iterable<String> values = null;
-            return query(filter, indexName, indexMeta, values);
+            LOG.debug("property is not null. AKA 'open query'. FullIterable");
+            return new QueryResultsWrapper(filter, indexName, new FullIterable(indexState, false));
         }
     }
     
@@ -602,8 +640,10 @@ public class OrderedContentMirrorStoreSt
 
         @Override
         public boolean hasNext() {
-            return (includeStart && start.equals(current))
-                   || (!includeStart && !Strings.isNullOrEmpty(getPropertyNext(current)));
+            boolean hasNext = (includeStart && start.equals(current))
+                || (!includeStart && !Strings.isNullOrEmpty(getPropertyNext(current)));
+                        
+            return hasNext;
         }
 
         @Override
@@ -622,6 +662,7 @@ public class OrderedContentMirrorStoreSt
                     throw new NoSuchElementException();
                 }
             }
+            
             return entry;
         }
 
@@ -751,7 +792,10 @@ public class OrderedContentMirrorStoreSt
                                @Nullable final String[] walkedLanes) {
         boolean keepWalked = false;
         String searchfor = condition.getSearchFor();
-        LOG.debug("seek() - Searching for: {}", condition.getSearchFor());        
+        if (LOG.isDebugEnabled()) {
+            LOG.debug("seek() - Searching for: {}", condition.getSearchFor());        
+            LOG.debug("seek() - condition: {}", condition);
+        }
         Predicate<String> walkingPredicate = direction.isAscending() 
                                                              ? new PredicateLessThan(searchfor, true)
                                                              : new PredicateGreaterThan(searchfor, true);
@@ -781,6 +825,8 @@ public class OrderedContentMirrorStoreSt
             // we're asking for a <, <= query from ascending index or >, >= from descending
             // we have to walk the lanes from bottom to up rather than up to bottom.
             
+            LOG.debug("seek() - cross case");
+            
             lane = 0;
             do {
                 stillLaning = lane < OrderedIndex.LANES;
@@ -801,6 +847,8 @@ public class OrderedContentMirrorStoreSt
                 }
             } while (((!Strings.isNullOrEmpty(nextkey) && walkingPredicate.apply(nextkey)) || stillLaning) && (found == null));
         } else {
+            LOG.debug("seek() - plain case");
+            
             lane = OrderedIndex.LANES - 1;
             
             do {
@@ -854,7 +902,6 @@ public class OrderedContentMirrorStoreSt
      * {@code searchfor}
      */
     static class PredicateGreaterThan implements Predicate<String> {
-        private String searchforEncoded;
         private String searchforDecoded;
         private boolean include;
         
@@ -863,7 +910,6 @@ public class OrderedContentMirrorStoreSt
         }
         
         public PredicateGreaterThan(@Nonnull String searchfor, boolean include) {
-            this.searchforEncoded = encode(searchfor);
             this.searchforDecoded = searchfor;
             this.include = include;
         }
@@ -873,8 +919,8 @@ public class OrderedContentMirrorStoreSt
             boolean b = false;
             if (!Strings.isNullOrEmpty(arg0)) {
                 String name = arg0;
-                b = searchforEncoded.compareTo(name) < 0 || (include && searchforEncoded
-                        .equals(name));
+                b = searchforDecoded.compareTo(name) < 0 || 
+                    (include && searchforDecoded.equals(name));
             }
             
             return b;
@@ -890,7 +936,6 @@ public class OrderedContentMirrorStoreSt
      * evaluates when the current element is less than (<) and less than equal {@code searchfor}
      */
     static class PredicateLessThan implements Predicate<String> {
-        private String searchforEncoded;
         private String searchforOriginal;
         private boolean include;
 
@@ -899,7 +944,6 @@ public class OrderedContentMirrorStoreSt
         }
 
         public PredicateLessThan(@Nonnull String searchfor, boolean include) {
-            this.searchforEncoded = encode(searchfor);
             this.searchforOriginal = searchfor;
             this.include = include;
         }
@@ -909,10 +953,10 @@ public class OrderedContentMirrorStoreSt
             boolean b = false;
             if (!Strings.isNullOrEmpty(arg0)) {
                 String name = arg0;
-                b = searchforEncoded.compareTo(name) > 0
-                    || (include && searchforEncoded.equals(name));
+                b = searchforOriginal.compareTo(name) > 0
+                    || (include && searchforOriginal.equals(name));
             }
-
+            
             return b;
         }
         

Modified: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java?rev=1618624&r1=1618623&r2=1618624&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/index/property/strategy/OrderedContentMirrorStorageStrategyTest.java Mon Aug 18 14:51:13 2014
@@ -3252,7 +3252,6 @@ public class OrderedContentMirrorStorage
         
         // testing the exception in case of wrong parameters
         String searchFor = "wedontcareaswetesttheexception";
-        NodeState node = index.getChildNode(searchFor);
         String entry = searchFor;
         String[] wl = new String[0];
         String item = null;
@@ -3389,6 +3388,12 @@ public class OrderedContentMirrorStorage
         predicate = new PredicateLessThan(searchfor, true);
         entry = null;
         assertFalse(predicate.apply(entry));
+        
+        // equality matching
+        searchfor = "2012-11-25T21:00:45.967-07:00";
+        entry = "2012-11-25T21%3A00%3A45.967-07%3A00";
+        predicate = new PredicateLessThan(searchfor, true);
+        assertTrue("this should have matched the equality flag", predicate.apply(entry));
     }
 
     /**

Added: jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OrderedIndexIT.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OrderedIndexIT.java?rev=1618624&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OrderedIndexIT.java (added)
+++ jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/OrderedIndexIT.java Mon Aug 18 14:51:13 2014
@@ -0,0 +1,323 @@
+/*
+ * 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.jcr;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.ImmutableList.of;
+import static com.google.common.collect.Lists.newArrayList;
+import static java.util.Calendar.DAY_OF_MONTH;
+import static java.util.Calendar.HOUR_OF_DAY;
+import static java.util.Collections.reverseOrder;
+import static java.util.Collections.sort;
+import static javax.jcr.PropertyType.DATE;
+import static javax.jcr.PropertyType.NAME;
+import static javax.jcr.PropertyType.STRING;
+import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NODE_TYPE;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.PROPERTY_NAMES;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.REINDEX_PROPERTY_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_PROPERTY_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.DIRECTION;
+import static org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.TYPE;
+import static org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection.DESC;
+import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.NT_OAK_UNSTRUCTURED;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Properties;
+import java.util.Random;
+import java.util.Set;
+import java.util.TimeZone;
+
+import javax.annotation.Nonnull;
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryManager;
+import javax.jcr.query.QueryResult;
+import javax.jcr.query.Row;
+import javax.jcr.query.RowIterator;
+
+import org.apache.jackrabbit.oak.jcr.util.ValuePathTuple;
+import org.apache.jackrabbit.oak.plugins.index.property.OrderedIndex.OrderDirection;
+import org.apache.jackrabbit.test.RepositoryStub;
+import org.apache.jackrabbit.test.RepositoryStubException;
+import org.apache.jackrabbit.util.ISO8601;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableSet;
+
+public class OrderedIndexIT {
+    private static final String INDEX_DEF_NODE = "indexdef";
+    private static final String ORDERED_PROPERTY = "foo";
+    private static final String CONTENT = "content";
+    private static final String NODE_TYPE = NT_UNSTRUCTURED;
+    
+    private static final Logger LOG = LoggerFactory.getLogger(OrderedIndexIT.class);
+
+    private static final List<TimeZone> TZS = of(
+        TimeZone.getTimeZone("GMT+01:00"),
+        TimeZone.getTimeZone("GMT+02:00"), 
+        TimeZone.getTimeZone("GMT+03:00"),
+        TimeZone.getTimeZone("GMT+05:00"), 
+        TimeZone.getTimeZone("GMT-02:00"),
+        TimeZone.getTimeZone("GMT-04:00"), 
+        TimeZone.getTimeZone("GMT-05:00"),
+        TimeZone.getTimeZone("GMT-07:00"), 
+        TimeZone.getTimeZone("GMT-08:00"),
+        TimeZone.getTimeZone("GMT")
+    );
+
+    /** 
+     * define the index
+     * 
+     * @param session
+     * @throws RepositoryException
+     */
+    private void createIndexDefinition(@Nonnull final Session session) throws RepositoryException {
+        checkNotNull(session);
+        
+        Node oakIndex = session.getRootNode().getNode(INDEX_DEFINITIONS_NAME);
+        Node indexDef = oakIndex.addNode(INDEX_DEF_NODE, INDEX_DEFINITIONS_NODE_TYPE);
+        indexDef.setProperty(TYPE_PROPERTY_NAME, TYPE, STRING);
+        indexDef.setProperty(PROPERTY_NAMES, new String[]{ORDERED_PROPERTY} , NAME);
+        indexDef.setProperty(DIRECTION, DESC.getDirection(), STRING);
+        indexDef.setProperty(REINDEX_PROPERTY_NAME, true);
+        session.save();
+    }
+    
+    /**
+     * add a bunch of sequential nodes with the provided property values pick-up randomly and start
+     * numerating nodes from {@code startFrom}.
+     * 
+     * The caller will have to perform the {@link Session#save()} after the method call.
+     * 
+     * @param values the property values to set
+     * @param father under whom we should add the nodes
+     * @param propertyType the type of the property to be stored
+     * @return
+     * @throws RepositoryException 
+     * @throws LockException 
+     * @throws ConstraintViolationException 
+     * @throws VersionException 
+     * @throws PathNotFoundException 
+     * @throws ItemExistsException 
+     */
+    private List<ValuePathTuple> addNodes(@Nonnull final List<String> values,
+                                          @Nonnull final Node father,
+                                          final int propertyType,
+                                          final int startFrom) throws RepositoryException {
+        
+        checkNotNull(father);
+        checkArgument(startFrom >= 0, "startFrom must be >= 0");
+        
+        List<String> working =  newArrayList(checkNotNull(values));
+        Random rnd = new Random(1);
+        int counter = startFrom;
+        Node n;
+        List<ValuePathTuple> vpt = newArrayList();
+        
+        while (!working.isEmpty()) {
+            String v = working.remove(rnd.nextInt(working.size()));
+            n = father.addNode("n" + counter++, NODE_TYPE);
+            n.setProperty(ORDERED_PROPERTY, v, propertyType);
+            vpt.add(new ValuePathTuple(v, n.getPath()));            
+        }
+        
+        return vpt;
+    }
+    
+    @SuppressWarnings("deprecation")
+    @Test
+    public void oak2035() throws IOException, RepositoryException, RepositoryStubException  {
+        final int numberOfNodes = 1500;
+        final String statement = String.format(
+            "/jcr:root/content//element(*, %s) order by @%s descending", NODE_TYPE,
+            ORDERED_PROPERTY);
+        
+        Properties env = new Properties();
+        env.load(getClass().getResourceAsStream("/repositoryStubImpl.properties"));
+        RepositoryStub stub = RepositoryStub.getInstance(env);
+        Repository repo = stub.getRepository();
+        Session session = null;
+        Node root, content;
+        
+        try {
+            session = repo.login(stub.getSuperuserCredentials());
+
+            createIndexDefinition(session);
+            
+            root = session.getRootNode();
+            if (!root.hasNode(CONTENT)) {
+                root.addNode(CONTENT, NT_OAK_UNSTRUCTURED);
+            }
+            session.save();
+            content = root.getNode(CONTENT);
+
+            Calendar start = midnightFirstJan2013();
+            List<String> dates = generateOrderedDates(String.class, numberOfNodes, DESC, start,
+                DAY_OF_MONTH, 1, TZS, true);
+            
+            List<ValuePathTuple> nodes = addNodes(dates, content, DATE, 0);
+            session.save();
+            
+            // ensuring the correct order for checks
+            sort(nodes, reverseOrder());
+            
+            if (LOG.isDebugEnabled()) {
+                for (ValuePathTuple node : nodes) {
+                    LOG.debug(node.toString());
+                }
+            }
+            
+            QueryManager qm = session.getWorkspace().getQueryManager();
+            Query query = qm.createQuery(statement, Query.XPATH);
+            QueryResult result = query.execute();
+
+            assertRightOrder(nodes, result.getRows());
+            
+        } finally {
+            if (session != null) {
+                session.logout();
+            }
+        }                
+    }
+ 
+    private void assertRightOrder(final List<ValuePathTuple> expected, 
+                                  final RowIterator obtained) throws RepositoryException {
+        checkNotNull(expected);
+        checkNotNull(obtained);
+        
+        assertTrue("the obtained result is empty", obtained.hasNext());
+        
+        Iterator<ValuePathTuple> exp = expected.iterator();
+        
+        while (exp.hasNext() && obtained.hasNext()) {
+            ValuePathTuple vpt = exp.next();
+            Row row = obtained.nextRow();
+            
+            // check manually about paths and dates
+            // if paths don't match maybe the date does. It's the date we care, in case of multiple
+            // paths under the same date the order of them is non-deterministic dependent on
+            // persistence rules
+            if (!vpt.getPath().equals(row.getPath())) {
+                String property = row.getNode().getProperty(ORDERED_PROPERTY).getString();
+                if (!vpt.getValue().equals(property)) {
+                    fail(String.format(
+                        "both path and date failed to match. Expected: %s - %s. Obtained: %s, %s",
+                        vpt.getPath(),
+                        vpt.getValue(),
+                        row.getPath(),
+                        property
+                        ));
+                }
+            }
+        }
+        
+        assertFalse("we should have processed all the expected", exp.hasNext());
+        assertFalse("we should have processed all the obtained", obtained.hasNext());
+    }
+
+    // ------------------------------------- < copied over from BasicOrderedPropertyIndexQueryTest>
+    // TODO should we have anything in commons?
+    
+    /**
+     * generates a list of sorted dates as ISO8601 formatted strings.
+     * 
+     * @param returnType Allowed values: {@link String} and {@link Long}. When String is specified
+     *            it will return ISO8601 formatted dates, otherwise the milliseconds.
+     * @param amount the amount of dates to be generated
+     * @param direction the direction of the sorting for the dates
+     * @param start the dates from where to start generating.
+     * @param increaseOf the {@link Calendar} field to increase while generating
+     * @param increaseBy the amount of increase to be used.
+     * @param timezones available timezones to be used in a random manner
+     * @param generateDuplicates if true it will generate a duplicate with a probability of 10%
+     * @return
+     */
+    public static List<String> generateOrderedDates(Class<?> returnType, 
+                                                    int amount,
+                                                   @Nonnull OrderDirection direction,
+                                                   @Nonnull final Calendar start,
+                                                   int increaseOf,
+                                                   int increaseBy,
+                                                   @Nonnull List<TimeZone> timezones,
+                                                   boolean generateDuplicates) {
+        final Set<Integer> allowedIncrease = ImmutableSet.of(HOUR_OF_DAY, DAY_OF_MONTH);
+        
+        checkArgument(amount > 0, "the amount must be > 0");
+        checkNotNull(direction);
+        checkNotNull(start);
+        checkArgument(allowedIncrease.contains(increaseOf), "Wrong increaseOf. Allowed values: "
+                                                            + allowedIncrease);
+        checkArgument(increaseBy > 0, "increaseBy must be a positive number");
+        checkNotNull(timezones);
+        checkArgument(returnType.equals(String.class) || returnType.equals(Long.class),
+            "only String and Long accepted as return type");
+
+        final int tzsSize = timezones.size();
+        final boolean tzsExtract = tzsSize > 0;
+        final Random rnd = new Random(1);
+        final Random duplicate = new Random(2);
+        
+        List<String> values = new ArrayList<String>(amount);
+        Calendar lstart = (Calendar) start.clone();
+        int hours = (OrderDirection.DESC.equals(direction)) ? -increaseBy : increaseBy;
+        
+        for (int i = 0; i < amount; i++) {
+            if (tzsExtract) {
+                lstart.setTimeZone(timezones.get(rnd.nextInt(tzsSize)));
+            }
+            if (returnType.equals(String.class)) {
+                values.add(ISO8601.format(lstart));                
+            } else if (returnType.equals(Long.class)) {
+                values.add(String.valueOf(lstart.getTimeInMillis()));
+            }
+            if (generateDuplicates && duplicate.nextDouble() < 0.1) {
+                // let's not increase the date
+            } else {
+                lstart.add(increaseOf, hours);
+            }
+            
+        }
+        
+        return values;        
+    }
+    
+    /**
+     * @return a Calendar set for midnight of 1st January 2013
+     */
+    public static Calendar midnightFirstJan2013() {
+        Calendar c = Calendar.getInstance();
+        c.set(2013, Calendar.JANUARY, 1, 0, 0, 0);
+        c.set(Calendar.MILLISECOND, 0);
+        return c;
+    }
+
+}

Added: jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/util/ValuePathTuple.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/util/ValuePathTuple.java?rev=1618624&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/util/ValuePathTuple.java (added)
+++ jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/util/ValuePathTuple.java Mon Aug 18 14:51:13 2014
@@ -0,0 +1,194 @@
+/*
+ * 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.jcr.util;
+
+import com.google.common.base.Predicate;
+
+// TODO copied over from oak-core. Should we have something common?
+
+/**
+ * convenience orderable object that represents a tuple of values and paths
+ *
+ * where the values are the indexed keys from the index and the paths are the path which hold the
+ * key
+ */
+public class ValuePathTuple implements Comparable<ValuePathTuple> {
+    private String value;
+    private String path;
+
+    /**
+     * convenience Predicate for easing the testing
+     */
+    public static class GreaterThanPredicate implements Predicate<ValuePathTuple> {
+        /**
+         * the value for comparison
+         */
+        private String value;
+
+        /**
+         * whether we should include the value in the result
+         */
+        private boolean include;
+
+        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<ValuePathTuple> {
+        /**
+         * the value for comparison
+         */
+        private String value;
+
+        /**
+         * whether we should include the value in the result
+         */
+        private boolean include;
+
+        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()));
+        }
+
+    }
+
+    public static class BetweenPredicate implements Predicate<ValuePathTuple> {
+        private String start;
+        private String end;
+        private boolean includeStart;
+        private boolean includeEnd;
+        
+        public BetweenPredicate(String start, String end, boolean includeStart, boolean includeEnd) {
+            this.start = start;
+            this.end = end;
+            this.includeStart = includeStart;
+            this.includeEnd = includeEnd;
+        }
+
+        @Override
+        public boolean apply(ValuePathTuple arg0) {
+            String other = arg0.getValue();
+            return 
+                (start.compareTo(other) < 0 || (includeStart && start.equals(other)))
+                && (end.compareTo(other) > 0 || (includeEnd && end.equals(other)));
+        }
+    }
+
+    public ValuePathTuple(String value, String path) {
+        this.value = value;
+        this.path = path;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + this.getClass().hashCode();
+        result = prime * result + ((path == null) ? 0 : path.hashCode());
+        result = prime * result + ((value == null) ? 0 : value.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        ValuePathTuple other = (ValuePathTuple) obj;
+        if (path == null) {
+            if (other.getPath() != null) {
+                return false;
+            }
+        } else if (!path.equals(other.getPath())) {
+            return false;
+        }
+        if (value == null) {
+            if (other.getValue() != null) {
+                return false;
+            }
+        } else if (!value.equals(other.getValue())) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int compareTo(ValuePathTuple o) {
+        if (this.equals(o)) {
+            return 0;
+        }
+        if (this.value.compareTo(o.getValue()) < 0) {
+            return -1;
+        }
+        if (this.value.compareTo(o.getValue()) > 0) {
+            return 1;
+        }
+        if (this.path.compareTo(o.getPath()) < 0) {
+            return -1;
+        }
+        if (this.path.compareTo(o.getPath()) > 0) {
+            return 1;
+        }
+        return 0;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+            "value: %s - path: %s - hash: %s",
+            value,
+            path,
+            super.toString()
+        );
+    }
+}