You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by be...@apache.org on 2015/03/18 12:05:00 UTC

[1/3] cassandra git commit: SSTableScanner enforces its bounds

Repository: cassandra
Updated Branches:
  refs/heads/cassandra-2.1 69ffd1fa0 -> 572ef50dd
  refs/heads/trunk c2dc31c1f -> 7aefd914c


SSTableScanner enforces its bounds

patch by benedict; reviewed by sylvain for CASSANDRA-8946


Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/572ef50d
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/572ef50d
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/572ef50d

Branch: refs/heads/cassandra-2.1
Commit: 572ef50dd11fcb501ebe46f1dde6656e42cb96bb
Parents: 69ffd1f
Author: Benedict Elliott Smith <be...@apache.org>
Authored: Wed Mar 18 11:02:35 2015 +0000
Committer: Benedict Elliott Smith <be...@apache.org>
Committed: Wed Mar 18 11:02:35 2015 +0000

----------------------------------------------------------------------
 CHANGES.txt                                     |   1 +
 .../apache/cassandra/dht/AbstractBounds.java    |  74 ++++++++++
 src/java/org/apache/cassandra/dht/Bounds.java   |  10 ++
 .../apache/cassandra/dht/ExcludingBounds.java   |  10 ++
 .../cassandra/dht/IncludingExcludingBounds.java |  10 ++
 src/java/org/apache/cassandra/dht/Range.java    |  10 ++
 .../cassandra/io/sstable/SSTableScanner.java    |  42 ++++--
 .../io/sstable/SSTableScannerTest.java          | 143 ++++++++++++++++---
 8 files changed, 270 insertions(+), 30 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/CHANGES.txt
----------------------------------------------------------------------
diff --git a/CHANGES.txt b/CHANGES.txt
index 2af8df6..36bdb39 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 2.1.4
+ * SSTableScanner enforces its bounds (CASSANDRA-8946)
  * Cleanup cell equality (CASSANDRA-8947)
  * Introduce intra-cluster message coalescing (CASSANDRA-8692)
  * DatabaseDescriptor throws NPE when rpc_interface is used (CASSANDRA-8839)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/src/java/org/apache/cassandra/dht/AbstractBounds.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/dht/AbstractBounds.java b/src/java/org/apache/cassandra/dht/AbstractBounds.java
index 90eb6b5..6d2ee43 100644
--- a/src/java/org/apache/cassandra/dht/AbstractBounds.java
+++ b/src/java/org/apache/cassandra/dht/AbstractBounds.java
@@ -68,6 +68,8 @@ public abstract class AbstractBounds<T extends RingPosition<T>> implements Seria
      * instead.
      */
     public abstract Pair<AbstractBounds<T>, AbstractBounds<T>> split(T position);
+    public abstract boolean inclusiveLeft();
+    public abstract boolean inclusiveRight();
 
     @Override
     public int hashCode()
@@ -193,4 +195,76 @@ public abstract class AbstractBounds<T extends RingPosition<T>> implements Seria
             return size;
         }
     }
+
+    public static <T extends RingPosition<T>> AbstractBounds<T> bounds(Boundary<T> min, Boundary<T> max)
+    {
+        return bounds(min.boundary, min.inclusive, max.boundary, max.inclusive);
+    }
+    public static <T extends RingPosition<T>> AbstractBounds<T> bounds(T min, boolean inclusiveMin, T max, boolean inclusiveMax)
+    {
+        if (inclusiveMin && inclusiveMax)
+            return new Bounds<T>(min, max);
+        else if (inclusiveMax)
+            return new Range<T>(min, max);
+        else if (inclusiveMin)
+            return new IncludingExcludingBounds<T>(min, max);
+        else
+            return new ExcludingBounds<T>(min, max);
+    }
+
+    // represents one side of a bounds (which side is not encoded)
+    public static class Boundary<T extends RingPosition<T>>
+    {
+        public final T boundary;
+        public final boolean inclusive;
+        public Boundary(T boundary, boolean inclusive)
+        {
+            this.boundary = boundary;
+            this.inclusive = inclusive;
+        }
+    }
+
+    public Boundary<T> leftBoundary()
+    {
+        return new Boundary<>(left, inclusiveLeft());
+    }
+
+    public Boundary<T> rightBoundary()
+    {
+        return new Boundary<>(right, inclusiveRight());
+    }
+
+    public static <T extends RingPosition<T>> boolean isEmpty(Boundary<T> left, Boundary<T> right)
+    {
+        int c = left.boundary.compareTo(right.boundary);
+        return c > 0 || (c == 0 && !(left.inclusive && right.inclusive));
+    }
+
+    public static <T extends RingPosition<T>> Boundary<T> minRight(Boundary<T> right1, T right2, boolean isInclusiveRight2)
+    {
+        return minRight(right1, new Boundary<T>(right2, isInclusiveRight2));
+    }
+
+    public static <T extends RingPosition<T>> Boundary<T> minRight(Boundary<T> right1, Boundary<T> right2)
+    {
+        int c = right1.boundary.compareTo(right2.boundary);
+        if (c != 0)
+            return c < 0 ? right1 : right2;
+        // return the exclusive version, if either
+        return right2.inclusive ? right1 : right2;
+    }
+
+    public static <T extends RingPosition<T>> Boundary<T> maxLeft(Boundary<T> left1, T left2, boolean isInclusiveLeft2)
+    {
+        return maxLeft(left1, new Boundary<T>(left2, isInclusiveLeft2));
+    }
+
+    public static <T extends RingPosition<T>> Boundary<T> maxLeft(Boundary<T> left1, Boundary<T> left2)
+    {
+        int c = left1.boundary.compareTo(left1.boundary);
+        if (c != 0)
+            return c > 0 ? left1 : left2;
+        // return the exclusive version, if either
+        return left2.inclusive ? left1 : left2;
+    }
 }

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/src/java/org/apache/cassandra/dht/Bounds.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/dht/Bounds.java b/src/java/org/apache/cassandra/dht/Bounds.java
index 396fc30..42eea77 100644
--- a/src/java/org/apache/cassandra/dht/Bounds.java
+++ b/src/java/org/apache/cassandra/dht/Bounds.java
@@ -61,6 +61,16 @@ public class Bounds<T extends RingPosition<T>> extends AbstractBounds<T>
         return Pair.create(lb, rb);
     }
 
+    public boolean inclusiveLeft()
+    {
+        return true;
+    }
+
+    public boolean inclusiveRight()
+    {
+        return true;
+    }
+
     public boolean intersects(Bounds<T> that)
     {
         // We either contains one of the that bounds, or we are fully contained into that.

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/src/java/org/apache/cassandra/dht/ExcludingBounds.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/dht/ExcludingBounds.java b/src/java/org/apache/cassandra/dht/ExcludingBounds.java
index 33e48b6..0d09f08 100644
--- a/src/java/org/apache/cassandra/dht/ExcludingBounds.java
+++ b/src/java/org/apache/cassandra/dht/ExcludingBounds.java
@@ -56,6 +56,16 @@ public class ExcludingBounds<T extends RingPosition<T>> extends AbstractBounds<T
         return Pair.create(lb, rb);
     }
 
+    public boolean inclusiveLeft()
+    {
+        return false;
+    }
+
+    public boolean inclusiveRight()
+    {
+        return false;
+    }
+
     public List<? extends AbstractBounds<T>> unwrap()
     {
         // ExcludingBounds objects never wrap

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java b/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java
index e8e9c74..278a806 100644
--- a/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java
+++ b/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java
@@ -55,6 +55,16 @@ public class IncludingExcludingBounds<T extends RingPosition<T>> extends Abstrac
         return Pair.create(lb, rb);
     }
 
+    public boolean inclusiveLeft()
+    {
+        return true;
+    }
+
+    public boolean inclusiveRight()
+    {
+        return false;
+    }
+
     public List<? extends AbstractBounds<T>> unwrap()
     {
         // IncludingExcludingBounds objects never wrap

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/src/java/org/apache/cassandra/dht/Range.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/dht/Range.java b/src/java/org/apache/cassandra/dht/Range.java
index 44b76d5..505f1f3 100644
--- a/src/java/org/apache/cassandra/dht/Range.java
+++ b/src/java/org/apache/cassandra/dht/Range.java
@@ -230,6 +230,16 @@ public class Range<T extends RingPosition<T>> extends AbstractBounds<T> implemen
         return Pair.create(lb, rb);
     }
 
+    public boolean inclusiveLeft()
+    {
+        return false;
+    }
+
+    public boolean inclusiveRight()
+    {
+        return true;
+    }
+
     public List<Range<T>> unwrap()
     {
         @SuppressWarnings("unchecked")

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/src/java/org/apache/cassandra/io/sstable/SSTableScanner.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableScanner.java b/src/java/org/apache/cassandra/io/sstable/SSTableScanner.java
index c05103b..46ddc24 100644
--- a/src/java/org/apache/cassandra/io/sstable/SSTableScanner.java
+++ b/src/java/org/apache/cassandra/io/sstable/SSTableScanner.java
@@ -32,6 +32,7 @@ import org.apache.cassandra.db.columniterator.IColumnIteratorFactory;
 import org.apache.cassandra.db.columniterator.LazyColumnIterator;
 import org.apache.cassandra.db.columniterator.OnDiskAtomIterator;
 import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.AbstractBounds.Boundary;
 import org.apache.cassandra.dht.Bounds;
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
@@ -40,6 +41,10 @@ import org.apache.cassandra.io.util.RandomAccessReader;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.Pair;
 
+import static org.apache.cassandra.dht.AbstractBounds.isEmpty;
+import static org.apache.cassandra.dht.AbstractBounds.maxLeft;
+import static org.apache.cassandra.dht.AbstractBounds.minRight;
+
 public class SSTableScanner implements ISSTableScanner
 {
     protected final RandomAccessReader dfile;
@@ -84,28 +89,39 @@ public class SSTableScanner implements ISSTableScanner
         List<AbstractBounds<RowPosition>> boundsList = new ArrayList<>(2);
         if (dataRange.isWrapAround())
         {
-            if (dataRange.stopKey().isMinimum(sstable.partitioner)
-                || dataRange.stopKey().compareTo(sstable.last) >= 0
-                || dataRange.startKey().compareTo(sstable.first) <= 0)
+            if (dataRange.stopKey().compareTo(sstable.first) >= 0)
             {
-                boundsList.add(new Bounds<RowPosition>(sstable.first, sstable.last, sstable.partitioner));
+                // since we wrap, we must contain the whole sstable prior to stopKey()
+                Boundary<RowPosition> left = new Boundary<RowPosition>(sstable.first, true);
+                Boundary<RowPosition> right;
+                right = dataRange.keyRange().rightBoundary();
+                right = minRight(right, sstable.last, true);
+                if (!isEmpty(left, right))
+                    boundsList.add(AbstractBounds.bounds(left, right));
             }
-            else
+            if (dataRange.startKey().compareTo(sstable.last) <= 0)
             {
-                if (dataRange.startKey().compareTo(sstable.last) <= 0)
-                    boundsList.add(new Bounds<>(dataRange.startKey(), sstable.last, sstable.partitioner));
-                if (dataRange.stopKey().compareTo(sstable.first) >= 0)
-                    boundsList.add(new Bounds<>(sstable.first, dataRange.stopKey(), sstable.partitioner));
+                // since we wrap, we must contain the whole sstable after dataRange.startKey()
+                Boundary<RowPosition> right = new Boundary<RowPosition>(sstable.last, true);
+                Boundary<RowPosition> left;
+                left = dataRange.keyRange().leftBoundary();
+                left = maxLeft(left, sstable.first, true);
+                if (!isEmpty(left, right))
+                    boundsList.add(AbstractBounds.bounds(left, right));
             }
         }
         else
         {
             assert dataRange.startKey().compareTo(dataRange.stopKey()) <= 0 || dataRange.stopKey().isMinimum();
-            RowPosition left = Ordering.natural().max(dataRange.startKey(), sstable.first);
+            Boundary<RowPosition> left, right;
+            left = dataRange.keyRange().leftBoundary();
+            right = dataRange.keyRange().rightBoundary();
+            left = maxLeft(left, sstable.first, true);
             // apparently isWrapAround() doesn't count Bounds that extend to the limit (min) as wrapping
-            RowPosition right = dataRange.stopKey().isMinimum() ? sstable.last : Ordering.natural().min(dataRange.stopKey(), sstable.last);
-            if (left.compareTo(right) <= 0)
-                boundsList.add(new Bounds<>(left, right, sstable.partitioner));
+            right = dataRange.stopKey().isMinimum() ? new Boundary<RowPosition>(sstable.last, true)
+                                                    : minRight(right, sstable.last, true);
+            if (!isEmpty(left, right))
+                boundsList.add(AbstractBounds.bounds(left, right));
         }
         this.rangeIterator = boundsList.iterator();
     }

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java b/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
index ff60481..91a820c 100644
--- a/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
@@ -18,21 +18,19 @@
 */
 package org.apache.cassandra.io.sstable;
 
-import java.util.ArrayList;
-import java.util.Collection;
+import java.util.*;
 
+import com.google.common.collect.Iterables;
 import org.junit.Test;
 
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.columniterator.IdentityQueryFilter;
-import org.apache.cassandra.dht.Bounds;
-import org.apache.cassandra.dht.BytesToken;
-import org.apache.cassandra.dht.Range;
-import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.dht.*;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
+import static org.apache.cassandra.dht.AbstractBounds.isEmpty;
 import static org.junit.Assert.*;
 
 public class SSTableScannerTest extends SchemaLoader
@@ -45,17 +43,85 @@ public class SSTableScannerTest extends SchemaLoader
         return String.format("%03d", key);
     }
 
-    private static Bounds<RowPosition> boundsFor(int start, int end)
+    // we produce all DataRange variations that produce an inclusive start and exclusive end range
+    private static Iterable<DataRange> dataRanges(int start, int end)
     {
-        return new Bounds<RowPosition>(new BytesToken(toKey(start).getBytes()).minKeyBound(),
-                                       new BytesToken(toKey(end).getBytes()).maxKeyBound());
+        if (end < start)
+            return dataRanges(start, end, false, true);
+        return Iterables.concat(dataRanges(start, end, false, false),
+                                dataRanges(start, end, false, true),
+                                dataRanges(start, end, true, false),
+                                dataRanges(start, end, true, true)
+        );
     }
 
+    private static Iterable<DataRange> dataRanges(int start, int end, boolean inclusiveStart, boolean inclusiveEnd)
+    {
+        List<DataRange> ranges = new ArrayList<>();
+        if (start == end + 1)
+        {
+            assert !inclusiveStart && inclusiveEnd;
+            ranges.add(dataRange(min(start), false, max(end), true));
+            ranges.add(dataRange(min(start), false, min(end + 1), true));
+            ranges.add(dataRange(max(start - 1), false, max(end), true));
+            ranges.add(dataRange(dk(start - 1), false, dk(start - 1), true));
+        }
+        else
+        {
+            for (RowPosition s : starts(start, inclusiveStart))
+            {
+                for (RowPosition e : ends(end, inclusiveEnd))
+                {
+                    if (end < start && e.compareTo(s) > 0)
+                        continue;
+                    if (!isEmpty(new AbstractBounds.Boundary<>(s, inclusiveStart), new AbstractBounds.Boundary<>(e, inclusiveEnd)))
+                        continue;
+                    ranges.add(dataRange(s, inclusiveStart, e, inclusiveEnd));
+                }
+            }
+        }
+        return ranges;
+    }
+
+    private static Iterable<RowPosition> starts(int key, boolean inclusive)
+    {
+        return Arrays.asList(min(key), max(key - 1), dk(inclusive ? key : key - 1));
+    }
+
+    private static Iterable<RowPosition> ends(int key, boolean inclusive)
+    {
+        return Arrays.asList(max(key), min(key + 1), dk(inclusive ? key : key + 1));
+    }
+
+    private static DecoratedKey dk(int key)
+    {
+        return Util.dk(toKey(key));
+    }
+
+    private static Token token(int key)
+    {
+        return key == Integer.MIN_VALUE ? ByteOrderedPartitioner.MINIMUM : new BytesToken(toKey(key).getBytes());
+    }
+
+    private static RowPosition min(int key)
+    {
+        return token(key).minKeyBound();
+    }
+
+    private static RowPosition max(int key)
+    {
+        return token(key).maxKeyBound();
+    }
+
+    private static DataRange dataRange(RowPosition start, boolean startInclusive, RowPosition end, boolean endInclusive)
+    {
+        return new DataRange(AbstractBounds.bounds(start, startInclusive, end, endInclusive), new IdentityQueryFilter());
+    }
 
     private static Range<Token> rangeFor(int start, int end)
     {
         return new Range<Token>(new BytesToken(toKey(start).getBytes()),
-                                new BytesToken(toKey(end).getBytes()));
+                                end == Integer.MIN_VALUE ? ByteOrderedPartitioner.MINIMUM : new BytesToken(toKey(end).getBytes()));
     }
 
     private static Collection<Range<Token>> makeRanges(int ... keys)
@@ -75,18 +141,22 @@ public class SSTableScannerTest extends SchemaLoader
         rm.apply();
     }
 
-    private static void assertScanMatches(SSTableReader sstable, int scanStart, int scanEnd, int expectedStart, int expectedEnd)
+    private static void assertScanMatches(SSTableReader sstable, int scanStart, int scanEnd, int ... boundaries)
     {
-        ISSTableScanner scanner = sstable.getScanner(new DataRange(boundsFor(scanStart, scanEnd), new IdentityQueryFilter()));
-        for (int i = expectedStart; i <= expectedEnd; i++)
-            assertEquals(toKey(i), new String(scanner.next().getKey().getKey().array()));
-        assertFalse(scanner.hasNext());
+        assert boundaries.length % 2 == 0;
+        for (DataRange range : dataRanges(scanStart, scanEnd))
+        {
+            ISSTableScanner scanner = sstable.getScanner(range);
+            for (int b = 0 ; b < boundaries.length ; b += 2)
+                for (int i = boundaries[b] ; i <= boundaries[b + 1] ; i++)
+                    assertEquals(toKey(i), new String(scanner.next().getKey().getKey().array()));
+            assertFalse(scanner.hasNext());
+        }
     }
 
     private static void assertScanEmpty(SSTableReader sstable, int scanStart, int scanEnd)
     {
-        ISSTableScanner scanner = sstable.getScanner(new DataRange(boundsFor(scanStart, scanEnd), new IdentityQueryFilter()));
-        assertFalse(String.format("scan of (%03d, %03d] should be empty", scanStart, scanEnd), scanner.hasNext());
+        assertScanMatches(sstable, scanStart, scanEnd);
     }
 
     @Test
@@ -132,6 +202,45 @@ public class SSTableScannerTest extends SchemaLoader
         // empty ranges
         assertScanEmpty(sstable, 0, 1);
         assertScanEmpty(sstable, 10, 11);
+
+        // wrapping, starts in middle
+        assertScanMatches(sstable, 5, 3, 2, 3, 5, 9);
+        assertScanMatches(sstable, 5, 2, 2, 2, 5, 9);
+        assertScanMatches(sstable, 5, 1, 5, 9);
+        assertScanMatches(sstable, 5, Integer.MIN_VALUE, 5, 9);
+        // wrapping, starts at end
+        assertScanMatches(sstable, 9, 8, 2, 8, 9, 9);
+        assertScanMatches(sstable, 9, 3, 2, 3, 9, 9);
+        assertScanMatches(sstable, 9, 2, 2, 2, 9, 9);
+        assertScanMatches(sstable, 9, 1, 9, 9);
+        assertScanMatches(sstable, 9, Integer.MIN_VALUE, 9, 9);
+        assertScanMatches(sstable, 8, 3, 2, 3, 8, 9);
+        assertScanMatches(sstable, 8, 2, 2, 2, 8, 9);
+        assertScanMatches(sstable, 8, 1, 8, 9);
+        assertScanMatches(sstable, 8, Integer.MIN_VALUE, 8, 9);
+        // wrapping, starts past end
+        assertScanMatches(sstable, 10, 9, 2, 9);
+        assertScanMatches(sstable, 10, 5, 2, 5);
+        assertScanMatches(sstable, 10, 2, 2, 2);
+        assertScanEmpty(sstable, 10, 1);
+        assertScanEmpty(sstable, 10, Integer.MIN_VALUE);
+        assertScanMatches(sstable, 11, 10, 2, 9);
+        assertScanMatches(sstable, 11, 9, 2, 9);
+        assertScanMatches(sstable, 11, 5, 2, 5);
+        assertScanMatches(sstable, 11, 2, 2, 2);
+        assertScanEmpty(sstable, 11, 1);
+        assertScanEmpty(sstable, 11, Integer.MIN_VALUE);
+        // wrapping, starts at start
+        assertScanMatches(sstable, 3, 1, 3, 9);
+        assertScanMatches(sstable, 3, Integer.MIN_VALUE, 3, 9);
+        assertScanMatches(sstable, 2, 1, 2, 9);
+        assertScanMatches(sstable, 2, Integer.MIN_VALUE, 2, 9);
+        assertScanMatches(sstable, 1, 0, 2, 9);
+        assertScanMatches(sstable, 1, Integer.MIN_VALUE, 2, 9);
+        // wrapping, starts before
+        assertScanMatches(sstable, 1, -1, 2, 9);
+        assertScanMatches(sstable, 1, Integer.MIN_VALUE, 2, 9);
+        assertScanMatches(sstable, 1, 0, 2, 9);
     }
 
     private static void assertScanContainsRanges(ISSTableScanner scanner, int ... rangePairs)


[2/3] cassandra git commit: SSTableScanner enforces its bounds

Posted by be...@apache.org.
SSTableScanner enforces its bounds

patch by benedict; reviewed by sylvain for CASSANDRA-8946


Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/572ef50d
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/572ef50d
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/572ef50d

Branch: refs/heads/trunk
Commit: 572ef50dd11fcb501ebe46f1dde6656e42cb96bb
Parents: 69ffd1f
Author: Benedict Elliott Smith <be...@apache.org>
Authored: Wed Mar 18 11:02:35 2015 +0000
Committer: Benedict Elliott Smith <be...@apache.org>
Committed: Wed Mar 18 11:02:35 2015 +0000

----------------------------------------------------------------------
 CHANGES.txt                                     |   1 +
 .../apache/cassandra/dht/AbstractBounds.java    |  74 ++++++++++
 src/java/org/apache/cassandra/dht/Bounds.java   |  10 ++
 .../apache/cassandra/dht/ExcludingBounds.java   |  10 ++
 .../cassandra/dht/IncludingExcludingBounds.java |  10 ++
 src/java/org/apache/cassandra/dht/Range.java    |  10 ++
 .../cassandra/io/sstable/SSTableScanner.java    |  42 ++++--
 .../io/sstable/SSTableScannerTest.java          | 143 ++++++++++++++++---
 8 files changed, 270 insertions(+), 30 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/CHANGES.txt
----------------------------------------------------------------------
diff --git a/CHANGES.txt b/CHANGES.txt
index 2af8df6..36bdb39 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 2.1.4
+ * SSTableScanner enforces its bounds (CASSANDRA-8946)
  * Cleanup cell equality (CASSANDRA-8947)
  * Introduce intra-cluster message coalescing (CASSANDRA-8692)
  * DatabaseDescriptor throws NPE when rpc_interface is used (CASSANDRA-8839)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/src/java/org/apache/cassandra/dht/AbstractBounds.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/dht/AbstractBounds.java b/src/java/org/apache/cassandra/dht/AbstractBounds.java
index 90eb6b5..6d2ee43 100644
--- a/src/java/org/apache/cassandra/dht/AbstractBounds.java
+++ b/src/java/org/apache/cassandra/dht/AbstractBounds.java
@@ -68,6 +68,8 @@ public abstract class AbstractBounds<T extends RingPosition<T>> implements Seria
      * instead.
      */
     public abstract Pair<AbstractBounds<T>, AbstractBounds<T>> split(T position);
+    public abstract boolean inclusiveLeft();
+    public abstract boolean inclusiveRight();
 
     @Override
     public int hashCode()
@@ -193,4 +195,76 @@ public abstract class AbstractBounds<T extends RingPosition<T>> implements Seria
             return size;
         }
     }
+
+    public static <T extends RingPosition<T>> AbstractBounds<T> bounds(Boundary<T> min, Boundary<T> max)
+    {
+        return bounds(min.boundary, min.inclusive, max.boundary, max.inclusive);
+    }
+    public static <T extends RingPosition<T>> AbstractBounds<T> bounds(T min, boolean inclusiveMin, T max, boolean inclusiveMax)
+    {
+        if (inclusiveMin && inclusiveMax)
+            return new Bounds<T>(min, max);
+        else if (inclusiveMax)
+            return new Range<T>(min, max);
+        else if (inclusiveMin)
+            return new IncludingExcludingBounds<T>(min, max);
+        else
+            return new ExcludingBounds<T>(min, max);
+    }
+
+    // represents one side of a bounds (which side is not encoded)
+    public static class Boundary<T extends RingPosition<T>>
+    {
+        public final T boundary;
+        public final boolean inclusive;
+        public Boundary(T boundary, boolean inclusive)
+        {
+            this.boundary = boundary;
+            this.inclusive = inclusive;
+        }
+    }
+
+    public Boundary<T> leftBoundary()
+    {
+        return new Boundary<>(left, inclusiveLeft());
+    }
+
+    public Boundary<T> rightBoundary()
+    {
+        return new Boundary<>(right, inclusiveRight());
+    }
+
+    public static <T extends RingPosition<T>> boolean isEmpty(Boundary<T> left, Boundary<T> right)
+    {
+        int c = left.boundary.compareTo(right.boundary);
+        return c > 0 || (c == 0 && !(left.inclusive && right.inclusive));
+    }
+
+    public static <T extends RingPosition<T>> Boundary<T> minRight(Boundary<T> right1, T right2, boolean isInclusiveRight2)
+    {
+        return minRight(right1, new Boundary<T>(right2, isInclusiveRight2));
+    }
+
+    public static <T extends RingPosition<T>> Boundary<T> minRight(Boundary<T> right1, Boundary<T> right2)
+    {
+        int c = right1.boundary.compareTo(right2.boundary);
+        if (c != 0)
+            return c < 0 ? right1 : right2;
+        // return the exclusive version, if either
+        return right2.inclusive ? right1 : right2;
+    }
+
+    public static <T extends RingPosition<T>> Boundary<T> maxLeft(Boundary<T> left1, T left2, boolean isInclusiveLeft2)
+    {
+        return maxLeft(left1, new Boundary<T>(left2, isInclusiveLeft2));
+    }
+
+    public static <T extends RingPosition<T>> Boundary<T> maxLeft(Boundary<T> left1, Boundary<T> left2)
+    {
+        int c = left1.boundary.compareTo(left1.boundary);
+        if (c != 0)
+            return c > 0 ? left1 : left2;
+        // return the exclusive version, if either
+        return left2.inclusive ? left1 : left2;
+    }
 }

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/src/java/org/apache/cassandra/dht/Bounds.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/dht/Bounds.java b/src/java/org/apache/cassandra/dht/Bounds.java
index 396fc30..42eea77 100644
--- a/src/java/org/apache/cassandra/dht/Bounds.java
+++ b/src/java/org/apache/cassandra/dht/Bounds.java
@@ -61,6 +61,16 @@ public class Bounds<T extends RingPosition<T>> extends AbstractBounds<T>
         return Pair.create(lb, rb);
     }
 
+    public boolean inclusiveLeft()
+    {
+        return true;
+    }
+
+    public boolean inclusiveRight()
+    {
+        return true;
+    }
+
     public boolean intersects(Bounds<T> that)
     {
         // We either contains one of the that bounds, or we are fully contained into that.

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/src/java/org/apache/cassandra/dht/ExcludingBounds.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/dht/ExcludingBounds.java b/src/java/org/apache/cassandra/dht/ExcludingBounds.java
index 33e48b6..0d09f08 100644
--- a/src/java/org/apache/cassandra/dht/ExcludingBounds.java
+++ b/src/java/org/apache/cassandra/dht/ExcludingBounds.java
@@ -56,6 +56,16 @@ public class ExcludingBounds<T extends RingPosition<T>> extends AbstractBounds<T
         return Pair.create(lb, rb);
     }
 
+    public boolean inclusiveLeft()
+    {
+        return false;
+    }
+
+    public boolean inclusiveRight()
+    {
+        return false;
+    }
+
     public List<? extends AbstractBounds<T>> unwrap()
     {
         // ExcludingBounds objects never wrap

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java b/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java
index e8e9c74..278a806 100644
--- a/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java
+++ b/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java
@@ -55,6 +55,16 @@ public class IncludingExcludingBounds<T extends RingPosition<T>> extends Abstrac
         return Pair.create(lb, rb);
     }
 
+    public boolean inclusiveLeft()
+    {
+        return true;
+    }
+
+    public boolean inclusiveRight()
+    {
+        return false;
+    }
+
     public List<? extends AbstractBounds<T>> unwrap()
     {
         // IncludingExcludingBounds objects never wrap

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/src/java/org/apache/cassandra/dht/Range.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/dht/Range.java b/src/java/org/apache/cassandra/dht/Range.java
index 44b76d5..505f1f3 100644
--- a/src/java/org/apache/cassandra/dht/Range.java
+++ b/src/java/org/apache/cassandra/dht/Range.java
@@ -230,6 +230,16 @@ public class Range<T extends RingPosition<T>> extends AbstractBounds<T> implemen
         return Pair.create(lb, rb);
     }
 
+    public boolean inclusiveLeft()
+    {
+        return false;
+    }
+
+    public boolean inclusiveRight()
+    {
+        return true;
+    }
+
     public List<Range<T>> unwrap()
     {
         @SuppressWarnings("unchecked")

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/src/java/org/apache/cassandra/io/sstable/SSTableScanner.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableScanner.java b/src/java/org/apache/cassandra/io/sstable/SSTableScanner.java
index c05103b..46ddc24 100644
--- a/src/java/org/apache/cassandra/io/sstable/SSTableScanner.java
+++ b/src/java/org/apache/cassandra/io/sstable/SSTableScanner.java
@@ -32,6 +32,7 @@ import org.apache.cassandra.db.columniterator.IColumnIteratorFactory;
 import org.apache.cassandra.db.columniterator.LazyColumnIterator;
 import org.apache.cassandra.db.columniterator.OnDiskAtomIterator;
 import org.apache.cassandra.dht.AbstractBounds;
+import org.apache.cassandra.dht.AbstractBounds.Boundary;
 import org.apache.cassandra.dht.Bounds;
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
@@ -40,6 +41,10 @@ import org.apache.cassandra.io.util.RandomAccessReader;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.Pair;
 
+import static org.apache.cassandra.dht.AbstractBounds.isEmpty;
+import static org.apache.cassandra.dht.AbstractBounds.maxLeft;
+import static org.apache.cassandra.dht.AbstractBounds.minRight;
+
 public class SSTableScanner implements ISSTableScanner
 {
     protected final RandomAccessReader dfile;
@@ -84,28 +89,39 @@ public class SSTableScanner implements ISSTableScanner
         List<AbstractBounds<RowPosition>> boundsList = new ArrayList<>(2);
         if (dataRange.isWrapAround())
         {
-            if (dataRange.stopKey().isMinimum(sstable.partitioner)
-                || dataRange.stopKey().compareTo(sstable.last) >= 0
-                || dataRange.startKey().compareTo(sstable.first) <= 0)
+            if (dataRange.stopKey().compareTo(sstable.first) >= 0)
             {
-                boundsList.add(new Bounds<RowPosition>(sstable.first, sstable.last, sstable.partitioner));
+                // since we wrap, we must contain the whole sstable prior to stopKey()
+                Boundary<RowPosition> left = new Boundary<RowPosition>(sstable.first, true);
+                Boundary<RowPosition> right;
+                right = dataRange.keyRange().rightBoundary();
+                right = minRight(right, sstable.last, true);
+                if (!isEmpty(left, right))
+                    boundsList.add(AbstractBounds.bounds(left, right));
             }
-            else
+            if (dataRange.startKey().compareTo(sstable.last) <= 0)
             {
-                if (dataRange.startKey().compareTo(sstable.last) <= 0)
-                    boundsList.add(new Bounds<>(dataRange.startKey(), sstable.last, sstable.partitioner));
-                if (dataRange.stopKey().compareTo(sstable.first) >= 0)
-                    boundsList.add(new Bounds<>(sstable.first, dataRange.stopKey(), sstable.partitioner));
+                // since we wrap, we must contain the whole sstable after dataRange.startKey()
+                Boundary<RowPosition> right = new Boundary<RowPosition>(sstable.last, true);
+                Boundary<RowPosition> left;
+                left = dataRange.keyRange().leftBoundary();
+                left = maxLeft(left, sstable.first, true);
+                if (!isEmpty(left, right))
+                    boundsList.add(AbstractBounds.bounds(left, right));
             }
         }
         else
         {
             assert dataRange.startKey().compareTo(dataRange.stopKey()) <= 0 || dataRange.stopKey().isMinimum();
-            RowPosition left = Ordering.natural().max(dataRange.startKey(), sstable.first);
+            Boundary<RowPosition> left, right;
+            left = dataRange.keyRange().leftBoundary();
+            right = dataRange.keyRange().rightBoundary();
+            left = maxLeft(left, sstable.first, true);
             // apparently isWrapAround() doesn't count Bounds that extend to the limit (min) as wrapping
-            RowPosition right = dataRange.stopKey().isMinimum() ? sstable.last : Ordering.natural().min(dataRange.stopKey(), sstable.last);
-            if (left.compareTo(right) <= 0)
-                boundsList.add(new Bounds<>(left, right, sstable.partitioner));
+            right = dataRange.stopKey().isMinimum() ? new Boundary<RowPosition>(sstable.last, true)
+                                                    : minRight(right, sstable.last, true);
+            if (!isEmpty(left, right))
+                boundsList.add(AbstractBounds.bounds(left, right));
         }
         this.rangeIterator = boundsList.iterator();
     }

http://git-wip-us.apache.org/repos/asf/cassandra/blob/572ef50d/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java b/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
index ff60481..91a820c 100644
--- a/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
@@ -18,21 +18,19 @@
 */
 package org.apache.cassandra.io.sstable;
 
-import java.util.ArrayList;
-import java.util.Collection;
+import java.util.*;
 
+import com.google.common.collect.Iterables;
 import org.junit.Test;
 
 import org.apache.cassandra.SchemaLoader;
 import org.apache.cassandra.Util;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.columniterator.IdentityQueryFilter;
-import org.apache.cassandra.dht.Bounds;
-import org.apache.cassandra.dht.BytesToken;
-import org.apache.cassandra.dht.Range;
-import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.dht.*;
 import org.apache.cassandra.utils.ByteBufferUtil;
 
+import static org.apache.cassandra.dht.AbstractBounds.isEmpty;
 import static org.junit.Assert.*;
 
 public class SSTableScannerTest extends SchemaLoader
@@ -45,17 +43,85 @@ public class SSTableScannerTest extends SchemaLoader
         return String.format("%03d", key);
     }
 
-    private static Bounds<RowPosition> boundsFor(int start, int end)
+    // we produce all DataRange variations that produce an inclusive start and exclusive end range
+    private static Iterable<DataRange> dataRanges(int start, int end)
     {
-        return new Bounds<RowPosition>(new BytesToken(toKey(start).getBytes()).minKeyBound(),
-                                       new BytesToken(toKey(end).getBytes()).maxKeyBound());
+        if (end < start)
+            return dataRanges(start, end, false, true);
+        return Iterables.concat(dataRanges(start, end, false, false),
+                                dataRanges(start, end, false, true),
+                                dataRanges(start, end, true, false),
+                                dataRanges(start, end, true, true)
+        );
     }
 
+    private static Iterable<DataRange> dataRanges(int start, int end, boolean inclusiveStart, boolean inclusiveEnd)
+    {
+        List<DataRange> ranges = new ArrayList<>();
+        if (start == end + 1)
+        {
+            assert !inclusiveStart && inclusiveEnd;
+            ranges.add(dataRange(min(start), false, max(end), true));
+            ranges.add(dataRange(min(start), false, min(end + 1), true));
+            ranges.add(dataRange(max(start - 1), false, max(end), true));
+            ranges.add(dataRange(dk(start - 1), false, dk(start - 1), true));
+        }
+        else
+        {
+            for (RowPosition s : starts(start, inclusiveStart))
+            {
+                for (RowPosition e : ends(end, inclusiveEnd))
+                {
+                    if (end < start && e.compareTo(s) > 0)
+                        continue;
+                    if (!isEmpty(new AbstractBounds.Boundary<>(s, inclusiveStart), new AbstractBounds.Boundary<>(e, inclusiveEnd)))
+                        continue;
+                    ranges.add(dataRange(s, inclusiveStart, e, inclusiveEnd));
+                }
+            }
+        }
+        return ranges;
+    }
+
+    private static Iterable<RowPosition> starts(int key, boolean inclusive)
+    {
+        return Arrays.asList(min(key), max(key - 1), dk(inclusive ? key : key - 1));
+    }
+
+    private static Iterable<RowPosition> ends(int key, boolean inclusive)
+    {
+        return Arrays.asList(max(key), min(key + 1), dk(inclusive ? key : key + 1));
+    }
+
+    private static DecoratedKey dk(int key)
+    {
+        return Util.dk(toKey(key));
+    }
+
+    private static Token token(int key)
+    {
+        return key == Integer.MIN_VALUE ? ByteOrderedPartitioner.MINIMUM : new BytesToken(toKey(key).getBytes());
+    }
+
+    private static RowPosition min(int key)
+    {
+        return token(key).minKeyBound();
+    }
+
+    private static RowPosition max(int key)
+    {
+        return token(key).maxKeyBound();
+    }
+
+    private static DataRange dataRange(RowPosition start, boolean startInclusive, RowPosition end, boolean endInclusive)
+    {
+        return new DataRange(AbstractBounds.bounds(start, startInclusive, end, endInclusive), new IdentityQueryFilter());
+    }
 
     private static Range<Token> rangeFor(int start, int end)
     {
         return new Range<Token>(new BytesToken(toKey(start).getBytes()),
-                                new BytesToken(toKey(end).getBytes()));
+                                end == Integer.MIN_VALUE ? ByteOrderedPartitioner.MINIMUM : new BytesToken(toKey(end).getBytes()));
     }
 
     private static Collection<Range<Token>> makeRanges(int ... keys)
@@ -75,18 +141,22 @@ public class SSTableScannerTest extends SchemaLoader
         rm.apply();
     }
 
-    private static void assertScanMatches(SSTableReader sstable, int scanStart, int scanEnd, int expectedStart, int expectedEnd)
+    private static void assertScanMatches(SSTableReader sstable, int scanStart, int scanEnd, int ... boundaries)
     {
-        ISSTableScanner scanner = sstable.getScanner(new DataRange(boundsFor(scanStart, scanEnd), new IdentityQueryFilter()));
-        for (int i = expectedStart; i <= expectedEnd; i++)
-            assertEquals(toKey(i), new String(scanner.next().getKey().getKey().array()));
-        assertFalse(scanner.hasNext());
+        assert boundaries.length % 2 == 0;
+        for (DataRange range : dataRanges(scanStart, scanEnd))
+        {
+            ISSTableScanner scanner = sstable.getScanner(range);
+            for (int b = 0 ; b < boundaries.length ; b += 2)
+                for (int i = boundaries[b] ; i <= boundaries[b + 1] ; i++)
+                    assertEquals(toKey(i), new String(scanner.next().getKey().getKey().array()));
+            assertFalse(scanner.hasNext());
+        }
     }
 
     private static void assertScanEmpty(SSTableReader sstable, int scanStart, int scanEnd)
     {
-        ISSTableScanner scanner = sstable.getScanner(new DataRange(boundsFor(scanStart, scanEnd), new IdentityQueryFilter()));
-        assertFalse(String.format("scan of (%03d, %03d] should be empty", scanStart, scanEnd), scanner.hasNext());
+        assertScanMatches(sstable, scanStart, scanEnd);
     }
 
     @Test
@@ -132,6 +202,45 @@ public class SSTableScannerTest extends SchemaLoader
         // empty ranges
         assertScanEmpty(sstable, 0, 1);
         assertScanEmpty(sstable, 10, 11);
+
+        // wrapping, starts in middle
+        assertScanMatches(sstable, 5, 3, 2, 3, 5, 9);
+        assertScanMatches(sstable, 5, 2, 2, 2, 5, 9);
+        assertScanMatches(sstable, 5, 1, 5, 9);
+        assertScanMatches(sstable, 5, Integer.MIN_VALUE, 5, 9);
+        // wrapping, starts at end
+        assertScanMatches(sstable, 9, 8, 2, 8, 9, 9);
+        assertScanMatches(sstable, 9, 3, 2, 3, 9, 9);
+        assertScanMatches(sstable, 9, 2, 2, 2, 9, 9);
+        assertScanMatches(sstable, 9, 1, 9, 9);
+        assertScanMatches(sstable, 9, Integer.MIN_VALUE, 9, 9);
+        assertScanMatches(sstable, 8, 3, 2, 3, 8, 9);
+        assertScanMatches(sstable, 8, 2, 2, 2, 8, 9);
+        assertScanMatches(sstable, 8, 1, 8, 9);
+        assertScanMatches(sstable, 8, Integer.MIN_VALUE, 8, 9);
+        // wrapping, starts past end
+        assertScanMatches(sstable, 10, 9, 2, 9);
+        assertScanMatches(sstable, 10, 5, 2, 5);
+        assertScanMatches(sstable, 10, 2, 2, 2);
+        assertScanEmpty(sstable, 10, 1);
+        assertScanEmpty(sstable, 10, Integer.MIN_VALUE);
+        assertScanMatches(sstable, 11, 10, 2, 9);
+        assertScanMatches(sstable, 11, 9, 2, 9);
+        assertScanMatches(sstable, 11, 5, 2, 5);
+        assertScanMatches(sstable, 11, 2, 2, 2);
+        assertScanEmpty(sstable, 11, 1);
+        assertScanEmpty(sstable, 11, Integer.MIN_VALUE);
+        // wrapping, starts at start
+        assertScanMatches(sstable, 3, 1, 3, 9);
+        assertScanMatches(sstable, 3, Integer.MIN_VALUE, 3, 9);
+        assertScanMatches(sstable, 2, 1, 2, 9);
+        assertScanMatches(sstable, 2, Integer.MIN_VALUE, 2, 9);
+        assertScanMatches(sstable, 1, 0, 2, 9);
+        assertScanMatches(sstable, 1, Integer.MIN_VALUE, 2, 9);
+        // wrapping, starts before
+        assertScanMatches(sstable, 1, -1, 2, 9);
+        assertScanMatches(sstable, 1, Integer.MIN_VALUE, 2, 9);
+        assertScanMatches(sstable, 1, 0, 2, 9);
     }
 
     private static void assertScanContainsRanges(ISSTableScanner scanner, int ... rangePairs)


[3/3] cassandra git commit: Merge branch 'cassandra-2.1' into trunk

Posted by be...@apache.org.
Merge branch 'cassandra-2.1' into trunk

Conflicts:
	src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java
	test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java


Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/7aefd914
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/7aefd914
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/7aefd914

Branch: refs/heads/trunk
Commit: 7aefd914c79a7a5d652abcc1531fec89d80c0adc
Parents: c2dc31c 572ef50
Author: Benedict Elliott Smith <be...@apache.org>
Authored: Wed Mar 18 11:04:48 2015 +0000
Committer: Benedict Elliott Smith <be...@apache.org>
Committed: Wed Mar 18 11:04:48 2015 +0000

----------------------------------------------------------------------
 CHANGES.txt                                     |   1 +
 .../apache/cassandra/dht/AbstractBounds.java    |  74 ++++++++++
 src/java/org/apache/cassandra/dht/Bounds.java   |  10 ++
 .../apache/cassandra/dht/ExcludingBounds.java   |  10 ++
 .../cassandra/dht/IncludingExcludingBounds.java |  10 ++
 src/java/org/apache/cassandra/dht/Range.java    |  10 ++
 .../io/sstable/format/big/BigTableScanner.java  |  42 ++++--
 .../io/sstable/SSTableScannerTest.java          | 143 ++++++++++++++++---
 8 files changed, 270 insertions(+), 30 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cassandra/blob/7aefd914/CHANGES.txt
----------------------------------------------------------------------
diff --cc CHANGES.txt
index 997bf04,36bdb39..62b1079
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@@ -1,78 -1,5 +1,79 @@@
 +3.0
 + * Partition intra-cluster message streams by size, not type (CASSANDRA-8789)
 + * Add nodetool command to validate all sstables in a node (CASSANDRA-5791)
 + * Add WriteFailureException to native protocol, notify coordinator of
 +   write failures (CASSANDRA-8592)
 + * Convert SequentialWriter to nio (CASSANDRA-8709)
 + * Add role based access control (CASSANDRA-7653, 8650, 7216, 8760, 8849, 8761, 8850)
 + * Record client ip address in tracing sessions (CASSANDRA-8162)
 + * Indicate partition key columns in response metadata for prepared
 +   statements (CASSANDRA-7660)
 + * Merge UUIDType and TimeUUIDType parse logic (CASSANDRA-8759)
 + * Avoid memory allocation when searching index summary (CASSANDRA-8793)
 + * Optimise (Time)?UUIDType Comparisons (CASSANDRA-8730)
 + * Make CRC32Ex into a separate maven dependency (CASSANDRA-8836)
 + * Use preloaded jemalloc w/ Unsafe (CASSANDRA-8714)
 + * Avoid accessing partitioner through StorageProxy (CASSANDRA-8244, 8268)
 + * Upgrade Metrics library and remove depricated metrics (CASSANDRA-5657)
 + * Serializing Row cache alternative, fully off heap (CASSANDRA-7438)
 + * Duplicate rows returned when in clause has repeated values (CASSANDRA-6707)
 + * Make CassandraException unchecked, extend RuntimeException (CASSANDRA-8560)
 + * Support direct buffer decompression for reads (CASSANDRA-8464)
 + * DirectByteBuffer compatible LZ4 methods (CASSANDRA-7039)
 + * Group sstables for anticompaction correctly (CASSANDRA-8578)
 + * Add ReadFailureException to native protocol, respond
 +   immediately when replicas encounter errors while handling
 +   a read request (CASSANDRA-7886)
 + * Switch CommitLogSegment from RandomAccessFile to nio (CASSANDRA-8308)
 + * Allow mixing token and partition key restrictions (CASSANDRA-7016)
 + * Support index key/value entries on map collections (CASSANDRA-8473)
 + * Modernize schema tables (CASSANDRA-8261)
 + * Support for user-defined aggregation functions (CASSANDRA-8053)
 + * Fix NPE in SelectStatement with empty IN values (CASSANDRA-8419)
 + * Refactor SelectStatement, return IN results in natural order instead
 +   of IN value list order and ignore duplicate values in partition key IN restrictions (CASSANDRA-7981)
 + * Support UDTs, tuples, and collections in user-defined
 +   functions (CASSANDRA-7563)
 + * Fix aggregate fn results on empty selection, result column name,
 +   and cqlsh parsing (CASSANDRA-8229)
 + * Mark sstables as repaired after full repair (CASSANDRA-7586)
 + * Extend Descriptor to include a format value and refactor reader/writer
 +   APIs (CASSANDRA-7443)
 + * Integrate JMH for microbenchmarks (CASSANDRA-8151)
 + * Keep sstable levels when bootstrapping (CASSANDRA-7460)
 + * Add Sigar library and perform basic OS settings check on startup (CASSANDRA-7838)
 + * Support for aggregation functions (CASSANDRA-4914)
 + * Remove cassandra-cli (CASSANDRA-7920)
 + * Accept dollar quoted strings in CQL (CASSANDRA-7769)
 + * Make assassinate a first class command (CASSANDRA-7935)
 + * Support IN clause on any partition key column (CASSANDRA-7855)
 + * Support IN clause on any clustering column (CASSANDRA-4762)
 + * Improve compaction logging (CASSANDRA-7818)
 + * Remove YamlFileNetworkTopologySnitch (CASSANDRA-7917)
 + * Do anticompaction in groups (CASSANDRA-6851)
 + * Support user-defined functions (CASSANDRA-7395, 7526, 7562, 7740, 7781, 7929,
 +   7924, 7812, 8063, 7813, 7708)
 + * Permit configurable timestamps with cassandra-stress (CASSANDRA-7416)
 + * Move sstable RandomAccessReader to nio2, which allows using the
 +   FILE_SHARE_DELETE flag on Windows (CASSANDRA-4050)
 + * Remove CQL2 (CASSANDRA-5918)
 + * Add Thrift get_multi_slice call (CASSANDRA-6757)
 + * Optimize fetching multiple cells by name (CASSANDRA-6933)
 + * Allow compilation in java 8 (CASSANDRA-7028)
 + * Make incremental repair default (CASSANDRA-7250)
 + * Enable code coverage thru JaCoCo (CASSANDRA-7226)
 + * Switch external naming of 'column families' to 'tables' (CASSANDRA-4369) 
 + * Shorten SSTable path (CASSANDRA-6962)
 + * Use unsafe mutations for most unit tests (CASSANDRA-6969)
 + * Fix race condition during calculation of pending ranges (CASSANDRA-7390)
 + * Fail on very large batch sizes (CASSANDRA-8011)
 + * Improve concurrency of repair (CASSANDRA-6455, 8208)
 + * Select optimal CRC32 implementation at runtime (CASSANDRA-8614)
 + * Evaluate MurmurHash of Token once per query (CASSANDRA-7096)
 + * Generalize progress reporting (CASSANDRA-8901)
 +
  2.1.4
+  * SSTableScanner enforces its bounds (CASSANDRA-8946)
   * Cleanup cell equality (CASSANDRA-8947)
   * Introduce intra-cluster message coalescing (CASSANDRA-8692)
   * DatabaseDescriptor throws NPE when rpc_interface is used (CASSANDRA-8839)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7aefd914/src/java/org/apache/cassandra/dht/AbstractBounds.java
----------------------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7aefd914/src/java/org/apache/cassandra/dht/Bounds.java
----------------------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7aefd914/src/java/org/apache/cassandra/dht/ExcludingBounds.java
----------------------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7aefd914/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java
----------------------------------------------------------------------

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7aefd914/src/java/org/apache/cassandra/dht/Range.java
----------------------------------------------------------------------
diff --cc src/java/org/apache/cassandra/dht/Range.java
index 224deac,505f1f3..936ad21
--- a/src/java/org/apache/cassandra/dht/Range.java
+++ b/src/java/org/apache/cassandra/dht/Range.java
@@@ -223,9 -230,20 +223,19 @@@ public class Range<T extends RingPositi
          return Pair.create(lb, rb);
      }
  
+     public boolean inclusiveLeft()
+     {
+         return false;
+     }
+ 
+     public boolean inclusiveRight()
+     {
+         return true;
+     }
+ 
      public List<Range<T>> unwrap()
      {
 -        @SuppressWarnings("unchecked")
 -        T minValue = (T) partitioner.minValue(right.getClass());
 +        T minValue = right.minValue();
          if (!isWrapAround() || right.equals(minValue))
              return Arrays.asList(this);
          List<Range<T>> unwrapped = new ArrayList<Range<T>>(2);

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7aefd914/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java
----------------------------------------------------------------------
diff --cc src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java
index b270b2d,0000000..8247a2d
mode 100644,000000..100644
--- a/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java
+++ b/src/java/org/apache/cassandra/io/sstable/format/big/BigTableScanner.java
@@@ -1,362 -1,0 +1,378 @@@
 +/*
 + * 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.cassandra.io.sstable.format.big;
 +
 +import java.io.IOException;
 +import java.util.*;
 +
 +import com.google.common.collect.AbstractIterator;
 +import com.google.common.collect.Ordering;
 +import com.google.common.util.concurrent.RateLimiter;
 +
 +import org.apache.cassandra.db.DataRange;
 +import org.apache.cassandra.db.DecoratedKey;
 +import org.apache.cassandra.db.RowIndexEntry;
 +import org.apache.cassandra.db.RowPosition;
 +import org.apache.cassandra.db.columniterator.IColumnIteratorFactory;
 +import org.apache.cassandra.db.columniterator.LazyColumnIterator;
 +import org.apache.cassandra.db.columniterator.OnDiskAtomIterator;
 +import org.apache.cassandra.dht.AbstractBounds;
++import org.apache.cassandra.dht.AbstractBounds.Boundary;
 +import org.apache.cassandra.dht.Bounds;
 +import org.apache.cassandra.dht.Range;
 +import org.apache.cassandra.dht.Token;
 +import org.apache.cassandra.io.sstable.CorruptSSTableException;
 +import org.apache.cassandra.io.sstable.ISSTableScanner;
 +import org.apache.cassandra.io.sstable.SSTableIdentityIterator;
 +import org.apache.cassandra.io.sstable.format.SSTableReader;
 +import org.apache.cassandra.io.util.FileUtils;
 +import org.apache.cassandra.io.util.RandomAccessReader;
 +import org.apache.cassandra.utils.ByteBufferUtil;
 +import org.apache.cassandra.utils.Pair;
 +
++import static org.apache.cassandra.dht.AbstractBounds.isEmpty;
++import static org.apache.cassandra.dht.AbstractBounds.maxLeft;
++import static org.apache.cassandra.dht.AbstractBounds.minRight;
++
 +public class BigTableScanner implements ISSTableScanner
 +{
 +    protected final RandomAccessReader dfile;
 +    protected final RandomAccessReader ifile;
 +    public final SSTableReader sstable;
 +
 +    private final Iterator<AbstractBounds<RowPosition>> rangeIterator;
 +    private AbstractBounds<RowPosition> currentRange;
 +
 +    private final DataRange dataRange;
 +    private final RowIndexEntry.IndexSerializer rowIndexEntrySerializer;
 +
 +    protected Iterator<OnDiskAtomIterator> iterator;
 +
 +    public static ISSTableScanner getScanner(SSTableReader sstable, DataRange dataRange, RateLimiter limiter)
 +    {
 +        return new BigTableScanner(sstable, dataRange, limiter);
 +    }
 +    public static ISSTableScanner getScanner(SSTableReader sstable, Collection<Range<Token>> tokenRanges, RateLimiter limiter)
 +    {
 +        // We want to avoid allocating a SSTableScanner if the range don't overlap the sstable (#5249)
 +        List<Pair<Long, Long>> positions = sstable.getPositionsForRanges(tokenRanges);
 +        if (positions.isEmpty())
 +            return new EmptySSTableScanner(sstable.getFilename());
 +
 +        return new BigTableScanner(sstable, tokenRanges, limiter);
 +    }
 +
 +    /**
 +     * @param sstable SSTable to scan; must not be null
 +     * @param dataRange a single range to scan; must not be null
 +     * @param limiter background i/o RateLimiter; may be null
 +     */
 +    private BigTableScanner(SSTableReader sstable, DataRange dataRange, RateLimiter limiter)
 +    {
 +        assert sstable != null;
 +
 +        this.dfile = limiter == null ? sstable.openDataReader() : sstable.openDataReader(limiter);
 +        this.ifile = sstable.openIndexReader();
 +        this.sstable = sstable;
 +        this.dataRange = dataRange;
 +        this.rowIndexEntrySerializer = sstable.descriptor.version.getSSTableFormat().getIndexSerializer(sstable.metadata);
 +
 +        List<AbstractBounds<RowPosition>> boundsList = new ArrayList<>(2);
 +        // we enforce the first/last keys of the sstablereader
 +        if (dataRange.isWrapAround())
 +        {
-             if (dataRange.stopKey().isMinimum()
-                 || dataRange.stopKey().compareTo(sstable.last) >= 0
-                 || dataRange.startKey().compareTo(sstable.first) <= 0)
++            if (dataRange.stopKey().compareTo(sstable.first) >= 0)
 +            {
-                 boundsList.add(new Bounds<RowPosition>(sstable.first, sstable.last));
++                // since we wrap, we must contain the whole sstable prior to stopKey()
++                Boundary<RowPosition> left = new Boundary<RowPosition>(sstable.first, true);
++                Boundary<RowPosition> right;
++                right = dataRange.keyRange().rightBoundary();
++                right = minRight(right, sstable.last, true);
++                if (!isEmpty(left, right))
++                    boundsList.add(AbstractBounds.bounds(left, right));
 +            }
-             else
++            if (dataRange.startKey().compareTo(sstable.last) <= 0)
 +            {
-                 if (dataRange.startKey().compareTo(sstable.last) <= 0)
-                     boundsList.add(new Bounds<>(dataRange.startKey(), sstable.last));
-                 if (dataRange.stopKey().compareTo(sstable.first) >= 0)
-                     boundsList.add(new Bounds<>(sstable.first, dataRange.stopKey()));
++                // since we wrap, we must contain the whole sstable after dataRange.startKey()
++                Boundary<RowPosition> right = new Boundary<RowPosition>(sstable.last, true);
++                Boundary<RowPosition> left;
++                left = dataRange.keyRange().leftBoundary();
++                left = maxLeft(left, sstable.first, true);
++                if (!isEmpty(left, right))
++                    boundsList.add(AbstractBounds.bounds(left, right));
 +            }
 +        }
 +        else
 +        {
 +            assert dataRange.startKey().compareTo(dataRange.stopKey()) <= 0 || dataRange.stopKey().isMinimum();
-             RowPosition left = Ordering.natural().max(dataRange.startKey(), sstable.first);
++            Boundary<RowPosition> left, right;
++            left = dataRange.keyRange().leftBoundary();
++            right = dataRange.keyRange().rightBoundary();
++            left = maxLeft(left, sstable.first, true);
 +            // apparently isWrapAround() doesn't count Bounds that extend to the limit (min) as wrapping
-             RowPosition right = dataRange.stopKey().isMinimum() ? sstable.last : Ordering.natural().min(dataRange.stopKey(), sstable.last);
-             if (left.compareTo(right) <= 0)
-                 boundsList.add(new Bounds<>(left, right));
++            right = dataRange.stopKey().isMinimum() ? new Boundary<RowPosition>(sstable.last, true)
++                                                    : minRight(right, sstable.last, true);
++            if (!isEmpty(left, right))
++                boundsList.add(AbstractBounds.bounds(left, right));
 +        }
 +        this.rangeIterator = boundsList.iterator();
 +    }
 +
 +    /**
 +     * @param sstable SSTable to scan; must not be null
 +     * @param tokenRanges A set of token ranges to scan
 +     * @param limiter background i/o RateLimiter; may be null
 +     */
 +    private BigTableScanner(SSTableReader sstable, Collection<Range<Token>> tokenRanges, RateLimiter limiter)
 +    {
 +        assert sstable != null;
 +
 +        this.dfile = limiter == null ? sstable.openDataReader() : sstable.openDataReader(limiter);
 +        this.ifile = sstable.openIndexReader();
 +        this.sstable = sstable;
 +        this.dataRange = null;
 +        this.rowIndexEntrySerializer = sstable.descriptor.version.getSSTableFormat().getIndexSerializer(sstable.metadata);
 +
 +        List<Range<Token>> normalized = Range.normalize(tokenRanges);
 +        List<AbstractBounds<RowPosition>> boundsList = new ArrayList<>(normalized.size());
 +        // we enforce the first/last keys of the sstablereader
 +        for (Range<Token> range : normalized)
 +        {
 +            // cap our ranges by the start/end of the sstable
 +            RowPosition right = range.right.maxKeyBound();
 +            if (right.compareTo(sstable.last) > 0)
 +                right = sstable.last;
 +
 +            RowPosition left = range.left.maxKeyBound();
 +            if (left.compareTo(sstable.first) < 0)
 +            {
 +                if (sstable.first.compareTo(right) <= 0)
 +                    boundsList.add(new Bounds<>(sstable.first, right));
 +            }
 +            else if (left.compareTo(right) < 0)
 +                boundsList.add(new Range<>(left, right));
 +        }
 +
 +        this.rangeIterator = boundsList.iterator();
 +    }
 +
 +    private void seekToCurrentRangeStart()
 +    {
 +        long indexPosition = sstable.getIndexScanPosition(currentRange.left);
 +        ifile.seek(indexPosition);
 +        try
 +        {
 +
 +            while (!ifile.isEOF())
 +            {
 +                indexPosition = ifile.getFilePointer();
 +                DecoratedKey indexDecoratedKey = sstable.partitioner.decorateKey(ByteBufferUtil.readWithShortLength(ifile));
 +                if (indexDecoratedKey.compareTo(currentRange.left) > 0 || currentRange.contains(indexDecoratedKey))
 +                {
 +                    // Found, just read the dataPosition and seek into index and data files
 +                    long dataPosition = ifile.readLong();
 +                    ifile.seek(indexPosition);
 +                    dfile.seek(dataPosition);
 +                    break;
 +                }
 +                else
 +                {
 +                    RowIndexEntry.Serializer.skip(ifile);
 +                }
 +            }
 +        }
 +        catch (IOException e)
 +        {
 +            sstable.markSuspect();
 +            throw new CorruptSSTableException(e, sstable.getFilename());
 +        }
 +    }
 +
 +    public void close() throws IOException
 +    {
 +        FileUtils.close(dfile, ifile);
 +    }
 +
 +    public long getLengthInBytes()
 +    {
 +        return dfile.length();
 +    }
 +
 +    public long getCurrentPosition()
 +    {
 +        return dfile.getFilePointer();
 +    }
 +
 +    public String getBackingFiles()
 +    {
 +        return sstable.toString();
 +    }
 +
 +    public boolean hasNext()
 +    {
 +        if (iterator == null)
 +            iterator = createIterator();
 +        return iterator.hasNext();
 +    }
 +
 +    public OnDiskAtomIterator next()
 +    {
 +        if (iterator == null)
 +            iterator = createIterator();
 +        return iterator.next();
 +    }
 +
 +    public void remove()
 +    {
 +        throw new UnsupportedOperationException();
 +    }
 +
 +    private Iterator<OnDiskAtomIterator> createIterator()
 +    {
 +        return new KeyScanningIterator();
 +    }
 +
 +    protected class KeyScanningIterator extends AbstractIterator<OnDiskAtomIterator>
 +    {
 +        private DecoratedKey nextKey;
 +        private RowIndexEntry nextEntry;
 +        private DecoratedKey currentKey;
 +        private RowIndexEntry currentEntry;
 +
 +        protected OnDiskAtomIterator computeNext()
 +        {
 +            try
 +            {
 +                if (nextEntry == null)
 +                {
 +                    do
 +                    {
 +                        // we're starting the first range or we just passed the end of the previous range
 +                        if (!rangeIterator.hasNext())
 +                            return endOfData();
 +
 +                        currentRange = rangeIterator.next();
 +                        seekToCurrentRangeStart();
 +
 +                        if (ifile.isEOF())
 +                            return endOfData();
 +
 +                        currentKey = sstable.partitioner.decorateKey(ByteBufferUtil.readWithShortLength(ifile));
 +                        currentEntry = rowIndexEntrySerializer.deserialize(ifile, sstable.descriptor.version);
 +                    } while (!currentRange.contains(currentKey));
 +                }
 +                else
 +                {
 +                    // we're in the middle of a range
 +                    currentKey = nextKey;
 +                    currentEntry = nextEntry;
 +                }
 +
 +                if (ifile.isEOF())
 +                {
 +                    nextEntry = null;
 +                    nextKey = null;
 +                }
 +                else
 +                {
 +                    // we need the position of the start of the next key, regardless of whether it falls in the current range
 +                    nextKey = sstable.partitioner.decorateKey(ByteBufferUtil.readWithShortLength(ifile));
 +                    nextEntry = rowIndexEntrySerializer.deserialize(ifile, sstable.descriptor.version);
 +
 +                    if (!currentRange.contains(nextKey))
 +                    {
 +                        nextKey = null;
 +                        nextEntry = null;
 +                    }
 +                }
 +
 +                if (dataRange == null || dataRange.selectsFullRowFor(currentKey.getKey()))
 +                {
 +                    dfile.seek(currentEntry.position + currentEntry.headerOffset());
 +                    ByteBufferUtil.readWithShortLength(dfile); // key
 +                    return new SSTableIdentityIterator(sstable, dfile, currentKey);
 +                }
 +
 +                return new LazyColumnIterator(currentKey, new IColumnIteratorFactory()
 +                {
 +                    public OnDiskAtomIterator create()
 +                    {
 +                        return dataRange.columnFilter(currentKey.getKey()).getSSTableColumnIterator(sstable, dfile, currentKey, currentEntry);
 +                    }
 +                });
 +
 +            }
 +            catch (IOException e)
 +            {
 +                sstable.markSuspect();
 +                throw new CorruptSSTableException(e, sstable.getFilename());
 +            }
 +        }
 +    }
 +
 +    @Override
 +    public String toString()
 +    {
 +        return getClass().getSimpleName() + "(" +
 +               "dfile=" + dfile +
 +               " ifile=" + ifile +
 +               " sstable=" + sstable +
 +               ")";
 +    }
 +
 +    public static class EmptySSTableScanner implements ISSTableScanner
 +    {
 +        private final String filename;
 +
 +        public EmptySSTableScanner(String filename)
 +        {
 +            this.filename = filename;
 +        }
 +
 +        public long getLengthInBytes()
 +        {
 +            return 0;
 +        }
 +
 +        public long getCurrentPosition()
 +        {
 +            return 0;
 +        }
 +
 +        public String getBackingFiles()
 +        {
 +            return filename;
 +        }
 +
 +        public boolean hasNext()
 +        {
 +            return false;
 +        }
 +
 +        public OnDiskAtomIterator next()
 +        {
 +            return null;
 +        }
 +
 +        public void close() throws IOException { }
 +
 +        public void remove() { }
 +    }
 +
 +
 +}

http://git-wip-us.apache.org/repos/asf/cassandra/blob/7aefd914/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
----------------------------------------------------------------------
diff --cc test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
index 9da895e,91a820c..22eb5a0
--- a/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
+++ b/test/unit/org/apache/cassandra/io/sstable/SSTableScannerTest.java
@@@ -18,42 -18,26 +18,40 @@@
  */
  package org.apache.cassandra.io.sstable;
  
- import java.util.ArrayList;
- import java.util.Collection;
+ import java.util.*;
  
 +import org.apache.cassandra.io.sstable.format.SSTableReader;
 +import org.junit.BeforeClass;
+ import com.google.common.collect.Iterables;
  import org.junit.Test;
  
  import org.apache.cassandra.SchemaLoader;
  import org.apache.cassandra.Util;
 +import org.apache.cassandra.config.KSMetaData;
  import org.apache.cassandra.db.*;
  import org.apache.cassandra.db.columniterator.IdentityQueryFilter;
- import org.apache.cassandra.dht.Bounds;
- import org.apache.cassandra.dht.ByteOrderedPartitioner.BytesToken;
- import org.apache.cassandra.dht.Range;
- import org.apache.cassandra.dht.Token;
+ import org.apache.cassandra.dht.*;
 +import org.apache.cassandra.locator.SimpleStrategy;
  import org.apache.cassandra.utils.ByteBufferUtil;
  
+ import static org.apache.cassandra.dht.AbstractBounds.isEmpty;
  import static org.junit.Assert.*;
  
 -public class SSTableScannerTest extends SchemaLoader
 +public class SSTableScannerTest
  {
 -    public static final String KEYSPACE = "Keyspace1";
 +    public static final String KEYSPACE = "SSTableScannerTest";
      public static final String TABLE = "Standard1";
  
 +    @BeforeClass
 +    public static void defineSchema() throws Exception
 +    {
 +        SchemaLoader.prepareServer();
 +        SchemaLoader.createKeyspace(KEYSPACE,
 +                                    SimpleStrategy.class,
 +                                    KSMetaData.optsWithRF(1),
 +                                    SchemaLoader.standardCFMD(KEYSPACE, TABLE));
 +    }
 +
      private static String toKey(int key)
      {
          return String.format("%03d", key);
@@@ -86,15 -138,20 +152,20 @@@
          DecoratedKey decoratedKey = Util.dk(toKey(key));
          Mutation rm = new Mutation(KEYSPACE, decoratedKey.getKey());
          rm.add(TABLE, Util.cellname("col"), ByteBufferUtil.EMPTY_BYTE_BUFFER, timestamp, 1000);
 -        rm.apply();
 +        rm.applyUnsafe();
      }
  
-     private static void assertScanMatches(SSTableReader sstable, int scanStart, int scanEnd, int expectedStart, int expectedEnd)
+     private static void assertScanMatches(SSTableReader sstable, int scanStart, int scanEnd, int ... boundaries)
      {
-         ISSTableScanner scanner = sstable.getScanner(new DataRange(boundsFor(scanStart, scanEnd), new IdentityQueryFilter()));
-         for (int i = expectedStart; i <= expectedEnd; i++)
-             assertEquals(toKey(i), new String(scanner.next().getKey().getKey().array()));
-         assertFalse(scanner.hasNext());
+         assert boundaries.length % 2 == 0;
+         for (DataRange range : dataRanges(scanStart, scanEnd))
+         {
+             ISSTableScanner scanner = sstable.getScanner(range);
+             for (int b = 0 ; b < boundaries.length ; b += 2)
+                 for (int i = boundaries[b] ; i <= boundaries[b + 1] ; i++)
+                     assertEquals(toKey(i), new String(scanner.next().getKey().getKey().array()));
+             assertFalse(scanner.hasNext());
+         }
      }
  
      private static void assertScanEmpty(SSTableReader sstable, int scanStart, int scanEnd)