Updated Branches: refs/heads/trunk bce44ff32 -> 35f7bc597
Allow rangeSlice queries to be start/end inclusive/exclusive patch by slebresne; reviewed by jbellis for CASSANDRA-3749 Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/35f7bc59 Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/35f7bc59 Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/35f7bc59 Branch: refs/heads/trunk Commit: 35f7bc597a8d26ca88526cf0d5208c13bdda523d Parents: bce44ff Author: Sylvain Lebresne <sylv...@datastax.com> Authored: Thu Jan 19 17:05:45 2012 +0100 Committer: Sylvain Lebresne <sylv...@datastax.com> Committed: Thu Jan 19 17:05:45 2012 +0100 ---------------------------------------------------------------------- CHANGES.txt | 1 + .../org/apache/cassandra/db/ColumnFamilyStore.java | 30 ++-- .../org/apache/cassandra/dht/AbstractBounds.java | 105 ++---------- src/java/org/apache/cassandra/dht/Bounds.java | 15 ++- .../org/apache/cassandra/dht/ExcludingBounds.java | 98 ++++++++++ .../cassandra/dht/IncludingExcludingBounds.java | 97 ++++++++++ src/java/org/apache/cassandra/dht/Range.java | 91 +++++++++- src/java/org/apache/cassandra/dht/Token.java | 27 ++-- .../apache/cassandra/io/sstable/SSTableReader.java | 4 +- .../org/apache/cassandra/service/StorageProxy.java | 20 +- .../apache/cassandra/db/ColumnFamilyStoreTest.java | 70 +++++++- .../apache/cassandra/dht/AbstractBoundsTest.java | 137 --------------- test/unit/org/apache/cassandra/dht/RangeTest.java | 77 ++++++++ .../apache/cassandra/service/StorageProxyTest.java | 32 ++++- 14 files changed, 522 insertions(+), 282 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/CHANGES.txt ---------------------------------------------------------------------- diff --git a/CHANGES.txt b/CHANGES.txt index 68147d7..4a4bf35 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -45,6 +45,7 @@ * Make stress.java insert operation to use microseconds (CASSANDRA-3725) * Allows (internally) doing a range query with a limit of columns instead of rows (CASSANDRA-3742) + * Allow rangeSlice queries to be start/end inclusive/exclusive (CASSANDRA-3749) 1.0.7 http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/src/java/org/apache/cassandra/db/ColumnFamilyStore.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java index 805c55c..0238344 100644 --- a/src/java/org/apache/cassandra/db/ColumnFamilyStore.java +++ b/src/java/org/apache/cassandra/db/ColumnFamilyStore.java @@ -1218,9 +1218,7 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean */ public AbstractScanIterator getSequentialIterator(ByteBuffer superColumn, final AbstractBounds<RowPosition> range, IFilter columnFilter) { - assert range instanceof Bounds - || !((Range)range).isWrapAround() || range.right.isMinimum() - : range; + assert !(range instanceof Range) || !((Range)range).isWrapAround() || range.right.isMinimum() : range; final RowPosition startWith = range.left; final RowPosition stopAt = range.right; @@ -1236,8 +1234,6 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean return new AbstractScanIterator() { - boolean first = true; - protected Row computeNext() { // pull a row out of the iterator @@ -1250,20 +1246,18 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean if (!stopAt.isMinimum() && stopAt.compareTo(key) < 0) return endOfData(); - // skip first one - if (range instanceof Bounds || !first || !key.equals(startWith)) - { - if (logger.isDebugEnabled()) - logger.debug("scanned " + key); - // TODO this is necessary because when we collate supercolumns together, we don't check - // their subcolumns for relevance, so we need to do a second prune post facto here. - return current.cf != null && current.cf.isSuper() - ? new Row(current.key, removeDeleted(current.cf, gcBefore)) - : current; - } - first = false; + // skipping outside of assigned range + if (!range.contains(key)) + return computeNext(); + + if (logger.isDebugEnabled()) + logger.debug("scanned " + key); - return computeNext(); + // TODO this is necessary because when we collate supercolumns together, we don't check + // their subcolumns for relevance, so we need to do a second prune post facto here. + return current.cf != null && current.cf.isSuper() + ? new Row(current.key, removeDeleted(current.cf, gcBefore)) + : current; } public void close() throws IOException http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/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 da218ef..b85fc65 100644 --- a/src/java/org/apache/cassandra/dht/AbstractBounds.java +++ b/src/java/org/apache/cassandra/dht/AbstractBounds.java @@ -61,19 +61,20 @@ public abstract class AbstractBounds<T extends RingPosition> implements Serializ } /** - * Given token T and AbstractBounds ?L,R], returns Pair(?L,T], ]T,R]) - * (where ? means that the same type of Bounds is returned -- Range or Bounds -- as the original.) - * The original AbstractBounds must contain the token T. - * If the split would cause one of the left or right side to be empty, it will be null in the result pair. + * Given token T and AbstractBounds ?L,R?, returns Pair(?L,T], (T,R?), + * where ? means that the same type of AbstractBounds is returned as the original. + * + * Put another way, returns a Pair of everything this AbstractBounds contains + * up to and including the split position, and everything it contains after + * (not including the split position). + * + * The original AbstractBounds must either contain the position T, or T + * should be equals to the left bound L. + * + * If the split would only yield the same AbstractBound, null is returned + * instead. */ - public Pair<AbstractBounds<T>,AbstractBounds<T>> split(T pos) - { - assert left.equals(pos) || contains(pos); - AbstractBounds<T> lb = createFrom(pos); - // we contain this token, so only one of the left or right can be empty - AbstractBounds<T> rb = lb != null && pos.equals(right) ? null : new Range<T>(pos, right); - return new Pair<AbstractBounds<T>,AbstractBounds<T>>(lb, rb); - } + public abstract Pair<AbstractBounds<T>, AbstractBounds<T>> split(T position); @Override public int hashCode() @@ -83,89 +84,9 @@ public abstract class AbstractBounds<T extends RingPosition> implements Serializ public abstract boolean contains(T start); - /** @return A clone of this AbstractBounds with a new right T, or null if an identical range would be created. */ - public abstract AbstractBounds<T> createFrom(T right); - public abstract List<? extends AbstractBounds<T>> unwrap(); /** - * @return A copy of the given list of with all bounds unwrapped, sorted by bound.left and with overlapping bounds merged. - * This method does not allow allow to mix Range and Bound in the input list. - */ - public static <T extends RingPosition> List<AbstractBounds<T>> normalize(Collection<? extends AbstractBounds<T>> bounds) - { - // unwrap all - List<AbstractBounds<T>> output = new ArrayList<AbstractBounds<T>>(); - for (AbstractBounds<T> bound : bounds) - output.addAll(bound.unwrap()); - - // sort by left - Collections.sort(output, new Comparator<AbstractBounds<T>>() - { - public int compare(AbstractBounds<T> b1, AbstractBounds<T> b2) - { - return b1.left.compareTo(b2.left); - } - }); - - // deoverlap - return deoverlap(output); - } - - /** - * Given a list of unwrapped bounds sorted by left token, return a list a equivalent - * list of bounds but with no overlapping bounds. - */ - private static <T extends RingPosition> List<AbstractBounds<T>> deoverlap(List<AbstractBounds<T>> bounds) - { - if (bounds.isEmpty()) - return bounds; - - List<AbstractBounds<T>> output = new ArrayList<AbstractBounds<T>>(); - - Iterator<AbstractBounds<T>> iter = bounds.iterator(); - AbstractBounds<T> current = iter.next(); - boolean isRange = current instanceof Range; - - T min = (T) current.partitioner.minValue(current.left.getClass()); - while (iter.hasNext()) - { - if (current.right.equals(min)) - { - // If one of the bound is the full range, we return only that - if (current.left.equals(min)) - return Collections.<AbstractBounds<T>>singletonList(current); - - output.add(current.createFrom(min)); - return output; - } - - AbstractBounds<T> next = iter.next(); - assert isRange ? next instanceof Range : next instanceof Bounds; - - // For Ranges, if next left is equal to current right, we do not intersect per se, but replacing (A, B] and (B, C] by (A, C] is - // legit, and since this actually avoid special casing and will result in more "optimal" ranges, we do this transformation - if (next.left.compareTo(current.right) <= 0) - { - // We do overlap - // (we've handler current.right.equals(min) already) - T newRight = next.right.equals(min) || current.right.compareTo(next.right) < 0 ? next.right : current.right; - current = current.createFrom(newRight); - if (current == null) - // current is the full ring, can only happen for Range - return Collections.<AbstractBounds<T>>singletonList(new Range<T>(min, min)); - } - else - { - output.add(current); - current = next; - } - } - output.add(current); - return output; - } - - /** * Transform this abstract bounds to equivalent covering bounds of row positions. * If this abstract bounds was already an abstractBounds of row positions, this is a noop. */ http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/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 f2cbeff..b521178 100644 --- a/src/java/org/apache/cassandra/dht/Bounds.java +++ b/src/java/org/apache/cassandra/dht/Bounds.java @@ -26,7 +26,11 @@ import java.util.List; import org.apache.cassandra.db.RowPosition; import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.utils.Pair; +/** + * AbstractBounds containing both its endpoints: [left, right]. Used by "classic" by-key range scans. + */ public class Bounds<T extends RingPosition> extends AbstractBounds<T> { public Bounds(T left, T right) @@ -46,9 +50,16 @@ public class Bounds<T extends RingPosition> extends AbstractBounds<T> return Range.contains(left, right, position) || left.equals(position); } - public AbstractBounds<T> createFrom(T position) + public Pair<AbstractBounds<T>, AbstractBounds<T>> split(T position) { - return new Bounds<T>(left, position, partitioner); + assert contains(position); + // Check if the split would have no effect on the range + if (position.equals(right)) + return null; + + AbstractBounds<T> lb = new Bounds<T>(left, position, partitioner); + AbstractBounds<T> rb = new Range<T>(position, right, partitioner); + return new Pair<AbstractBounds<T>, AbstractBounds<T>>(lb, rb); } public List<? extends AbstractBounds<T>> unwrap() http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/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 new file mode 100644 index 0000000..5b53dd0 --- /dev/null +++ b/src/java/org/apache/cassandra/dht/ExcludingBounds.java @@ -0,0 +1,98 @@ +/* + * 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.dht; + +import java.util.Collections; +import java.util.List; + +import org.apache.cassandra.db.RowPosition; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.utils.Pair; + +/** + * AbstractBounds containing neither of its endpoints: (left, right). Used by CQL key > X AND key < Y range scans. + */ +public class ExcludingBounds<T extends RingPosition> extends AbstractBounds<T> +{ + public ExcludingBounds(T left, T right) + { + this(left, right, StorageService.getPartitioner()); + } + + ExcludingBounds(T left, T right, IPartitioner partitioner) + { + super(left, right, partitioner); + // unlike a Range, an ExcludingBounds may not wrap, nor be empty + assert left.compareTo(right) < 0 || right.isMinimum(partitioner) : "(" + left + "," + right + ")"; + } + + public boolean contains(T position) + { + return Range.contains(left, right, position) && !right.equals(position); + } + + public Pair<AbstractBounds<T>, AbstractBounds<T>> split(T position) + { + assert contains(position) || left.equals(position); + if (left.equals(position)) + return null; + AbstractBounds<T> lb = new Range<T>(left, position, partitioner); + AbstractBounds<T> rb = new ExcludingBounds<T>(position, right, partitioner); + return new Pair<AbstractBounds<T>, AbstractBounds<T>>(lb, rb); + } + + public List<? extends AbstractBounds<T>> unwrap() + { + // ExcludingBounds objects never wrap + return Collections.<AbstractBounds<T>>singletonList(this); + } + + @Override + public boolean equals(Object o) + { + if (!(o instanceof ExcludingBounds)) + return false; + ExcludingBounds<T> rhs = (ExcludingBounds<T>)o; + return left.equals(rhs.left) && right.equals(rhs.right); + } + + @Override + public String toString() + { + return "(" + left + "," + right + ")"; + } + + /** + * Compute a bounds of keys corresponding to a given bounds of token. + */ + private static ExcludingBounds<RowPosition> makeRowBounds(Token left, Token right, IPartitioner partitioner) + { + return new ExcludingBounds<RowPosition>(left.maxKeyBound(partitioner), right.minKeyBound(partitioner), partitioner); + } + + public AbstractBounds<RowPosition> toRowBounds() + { + return (left instanceof Token) ? makeRowBounds((Token)left, (Token)right, partitioner) : (ExcludingBounds<RowPosition>)this; + } + + public AbstractBounds<Token> toTokenBounds() + { + return (left instanceof RowPosition) ? new ExcludingBounds<Token>(((RowPosition)left).getToken(), ((RowPosition)right).getToken(), partitioner) : (ExcludingBounds<Token>)this; + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/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 new file mode 100644 index 0000000..61bc22f --- /dev/null +++ b/src/java/org/apache/cassandra/dht/IncludingExcludingBounds.java @@ -0,0 +1,97 @@ +/* + * 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.dht; + +import java.util.Collections; +import java.util.List; + +import org.apache.cassandra.db.RowPosition; +import org.apache.cassandra.service.StorageService; +import org.apache.cassandra.utils.Pair; + +/** + * AbstractBounds containing only its left endpoint: [left, right). Used by CQL key >= X AND key < Y range scans. + */ +public class IncludingExcludingBounds<T extends RingPosition> extends AbstractBounds<T> +{ + public IncludingExcludingBounds(T left, T right) + { + this(left, right, StorageService.getPartitioner()); + } + + IncludingExcludingBounds(T left, T right, IPartitioner partitioner) + { + super(left, right, partitioner); + // unlike a Range, an IncludingExcludingBounds may not wrap, nor have + // right == left unless the right is the min token + assert left.compareTo(right) < 0 || right.isMinimum(partitioner) : "[" + left + "," + right + ")"; + } + + public boolean contains(T position) + { + return (Range.contains(left, right, position) || left.equals(position)) && !right.equals(position); + } + + public Pair<AbstractBounds<T>, AbstractBounds<T>> split(T position) + { + assert contains(position); + AbstractBounds<T> lb = new Bounds<T>(left, position, partitioner); + AbstractBounds<T> rb = new ExcludingBounds<T>(position, right, partitioner); + return new Pair<AbstractBounds<T>, AbstractBounds<T>>(lb, rb); + } + + public List<? extends AbstractBounds<T>> unwrap() + { + // IncludingExcludingBounds objects never wrap + return Collections.<AbstractBounds<T>>singletonList(this); + } + + @Override + public boolean equals(Object o) + { + if (!(o instanceof IncludingExcludingBounds)) + return false; + IncludingExcludingBounds<T> rhs = (IncludingExcludingBounds<T>)o; + return left.equals(rhs.left) && right.equals(rhs.right); + } + + @Override + public String toString() + { + return "(" + left + "," + right + ")"; + } + + /** + * Compute a bounds of keys corresponding to a given bounds of token. + */ + private static IncludingExcludingBounds<RowPosition> makeRowBounds(Token left, Token right, IPartitioner partitioner) + { + return new IncludingExcludingBounds<RowPosition>(left.maxKeyBound(partitioner), right.minKeyBound(partitioner), partitioner); + } + + public AbstractBounds<RowPosition> toRowBounds() + { + return (left instanceof Token) ? makeRowBounds((Token)left, (Token)right, partitioner) : (IncludingExcludingBounds<RowPosition>)this; + } + + public AbstractBounds<Token> toTokenBounds() + { + return (left instanceof RowPosition) ? new IncludingExcludingBounds<Token>(((RowPosition)left).getToken(), ((RowPosition)right).getToken(), partitioner) : (IncludingExcludingBounds<Token>)this; + } +} http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/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 3caa6d0..13335d1 100644 --- a/src/java/org/apache/cassandra/dht/Range.java +++ b/src/java/org/apache/cassandra/dht/Range.java @@ -28,11 +28,14 @@ import org.apache.commons.lang.ObjectUtils; import org.apache.cassandra.db.RowPosition; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; +import org.apache.cassandra.utils.Pair; /** * A representation of the range that a node is responsible for on the DHT ring. * * A Range is responsible for the tokens between (left, right]. + * + * Used by the partitioner and by map/reduce by-token range scans. */ public class Range<T extends RingPosition> extends AbstractBounds<T> implements Comparable<Range<T>>, Serializable { @@ -197,19 +200,24 @@ public class Range<T extends RingPosition> extends AbstractBounds<T> implements return Collections.unmodifiableSet(intersection); } - public AbstractBounds<T> createFrom(T pos) + public Pair<AbstractBounds<T>, AbstractBounds<T>> split(T position) { - if (pos.equals(left)) + assert contains(position) || left.equals(position); + // Check if the split would have no effect on the range + if (position.equals(left) || position.equals(right)) return null; - return new Range<T>(left, pos, partitioner); + + AbstractBounds<T> lb = new Range<T>(left, position, partitioner); + AbstractBounds<T> rb = new Range<T>(position, right, partitioner); + return new Pair<AbstractBounds<T>, AbstractBounds<T>>(lb, rb); } - public List<? extends AbstractBounds<T>> unwrap() + public List<Range<T>> unwrap() { T minValue = (T) partitioner.minValue(right.getClass()); if (!isWrapAround() || right.equals(minValue)) return Arrays.asList(this); - List<AbstractBounds<T>> unwrapped = new ArrayList<AbstractBounds<T>>(2); + List<Range<T>> unwrapped = new ArrayList<Range<T>>(2); unwrapped.add(new Range<T>(left, minValue, partitioner)); unwrapped.add(new Range<T>(minValue, right, partitioner)); return unwrapped; @@ -340,6 +348,79 @@ public class Range<T extends RingPosition> extends AbstractBounds<T> implements } /** + * @return A copy of the given list of with all ranges unwrapped, sorted by left bound and with overlapping bounds merged. + */ + public static <T extends RingPosition> List<Range<T>> normalize(Collection<Range<T>> ranges) + { + // unwrap all + List<Range<T>> output = new ArrayList<Range<T>>(); + for (Range<T> range : ranges) + output.addAll(range.unwrap()); + + // sort by left + Collections.sort(output, new Comparator<Range<T>>() + { + public int compare(Range<T> b1, Range<T> b2) + { + return b1.left.compareTo(b2.left); + } + }); + + // deoverlap + return deoverlap(output); + } + + /** + * Given a list of unwrapped ranges sorted by left position, return an + * equivalent list of ranges but with no overlapping ranges. + */ + private static <T extends RingPosition> List<Range<T>> deoverlap(List<Range<T>> ranges) + { + if (ranges.isEmpty()) + return ranges; + + List<Range<T>> output = new ArrayList<Range<T>>(); + + Iterator<Range<T>> iter = ranges.iterator(); + Range<T> current = iter.next(); + + T min = (T) current.partitioner.minValue(current.left.getClass()); + while (iter.hasNext()) + { + // If current goes to the end of the ring, we're done + if (current.right.equals(min)) + { + // If one range is the full range, we return only that + if (current.left.equals(min)) + return Collections.<Range<T>>singletonList(current); + + output.add(new Range<T>(current.left, min)); + return output; + } + + Range<T> next = iter.next(); + + // if next left is equal to current right, we do not intersect per se, but replacing (A, B] and (B, C] by (A, C] is + // legit, and since this avoid special casing and will result in more "optimal" ranges, we do the transformation + if (next.left.compareTo(current.right) <= 0) + { + // We do overlap + // (we've handled current.right.equals(min) already) + if (next.right.equals(min) || current.right.compareTo(next.right) < 0) + current = new Range<T>(current.left, next.right); + } + else + { + output.add(current); + current = next; + } + } + output.add(current); + return output; + } + + + /** * Compute a range of keys corresponding to a given range of token. */ public static Range<RowPosition> makeRowRange(Token left, Token right, IPartitioner partitioner) http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/src/java/org/apache/cassandra/dht/Token.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/dht/Token.java b/src/java/org/apache/cassandra/dht/Token.java index dcbea16..e9969e1 100644 --- a/src/java/org/apache/cassandra/dht/Token.java +++ b/src/java/org/apache/cassandra/dht/Token.java @@ -42,9 +42,6 @@ public abstract class Token<T> implements RingPosition<Token<T>>, Serializable public final T token; - private final transient KeyBound minimumBound = new KeyBound(true); - private final transient KeyBound maximumBound = new KeyBound(false); - protected Token(T token) { this.token = token; @@ -143,7 +140,7 @@ public abstract class Token<T> implements RingPosition<Token<T>>, Serializable */ public KeyBound minKeyBound(IPartitioner partitioner) { - return minimumBound; + return new KeyBound(this, true); } public KeyBound minKeyBound() @@ -161,8 +158,8 @@ public abstract class Token<T> implements RingPosition<Token<T>>, Serializable * maxKeyBound for the minimun token. */ if (isMinimum(partitioner)) - return minimumBound; - return maximumBound; + return minKeyBound(); + return new KeyBound(this, false); } public KeyBound maxKeyBound() @@ -170,7 +167,7 @@ public abstract class Token<T> implements RingPosition<Token<T>>, Serializable return maxKeyBound(StorageService.getPartitioner()); } - public <T extends RingPosition> T asSplitValue(Class<T> klass) + public <T extends RingPosition> T upperBound(Class<T> klass) { if (klass.equals(getClass())) return (T)this; @@ -178,18 +175,20 @@ public abstract class Token<T> implements RingPosition<Token<T>>, Serializable return (T)maxKeyBound(); } - public class KeyBound extends RowPosition + public static class KeyBound extends RowPosition { + private final Token token; public final boolean isMinimumBound; - private KeyBound(boolean isMinimumBound) + private KeyBound(Token t, boolean isMinimumBound) { + this.token = t; this.isMinimumBound = isMinimumBound; } public Token getToken() { - return Token.this; + return token; } public int compareTo(RowPosition pos) @@ -201,8 +200,10 @@ public abstract class Token<T> implements RingPosition<Token<T>>, Serializable if (cmp != 0) return cmp; - // We've already eliminated the == case - return isMinimumBound ? -1 : 1; + if (isMinimumBound) + return ((pos instanceof KeyBound) && ((KeyBound)pos).isMinimumBound) ? 0 : -1; + else + return ((pos instanceof KeyBound) && !((KeyBound)pos).isMinimumBound) ? 0 : 1; } public boolean isMinimum(IPartitioner partitioner) @@ -224,7 +225,7 @@ public abstract class Token<T> implements RingPosition<Token<T>>, Serializable return false; KeyBound other = (KeyBound)obj; - return getToken().equals(other.getToken()); + return token.equals(other.token) && isMinimumBound == other.isMinimumBound; } @Override http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/src/java/org/apache/cassandra/io/sstable/SSTableReader.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/io/sstable/SSTableReader.java b/src/java/org/apache/cassandra/io/sstable/SSTableReader.java index 6f9fcda..6eb0c4e 100644 --- a/src/java/org/apache/cassandra/io/sstable/SSTableReader.java +++ b/src/java/org/apache/cassandra/io/sstable/SSTableReader.java @@ -490,7 +490,7 @@ public class SSTableReader extends SSTable if (samples.isEmpty()) return positions; - for (AbstractBounds<Token> range : AbstractBounds.<Token>normalize(ranges)) + for (Range<Token> range : Range.<Token>normalize(ranges)) { RowPosition leftPosition = range.left.maxKeyBound(); RowPosition rightPosition = range.left.maxKeyBound(); @@ -587,7 +587,7 @@ public class SSTableReader extends SSTable { // use the index to determine a minimal section for each range List<Pair<Long,Long>> positions = new ArrayList<Pair<Long,Long>>(); - for (AbstractBounds<Token> range : AbstractBounds.normalize(ranges)) + for (Range<Token> range : Range.normalize(ranges)) { AbstractBounds<RowPosition> keyRange = range.toRowBounds(); long left = getPosition(keyRange.left, Operator.GT); http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/src/java/org/apache/cassandra/service/StorageProxy.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/service/StorageProxy.java b/src/java/org/apache/cassandra/service/StorageProxy.java index 4137632..632f6fc 100644 --- a/src/java/org/apache/cassandra/service/StorageProxy.java +++ b/src/java/org/apache/cassandra/service/StorageProxy.java @@ -46,7 +46,6 @@ import org.apache.cassandra.concurrent.StageManager; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.Schema; import org.apache.cassandra.db.*; -import org.apache.cassandra.db.filter.QueryFilter; import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Bounds; import org.apache.cassandra.dht.IPartitioner; @@ -1025,28 +1024,29 @@ public class StorageProxy implements StorageProxyMBean AbstractBounds<T> remainder = queryRange; while (ringIter.hasNext()) { - Token token = ringIter.next(); /* * remainder can be a range/bounds of token _or_ keys and we want to split it with a token: * - if remainder is tokens, then we'll just split using the provided token. - * - if reaminer is keys, we want to split using token.upperBoundKey. For instance, if remainder + * - if remainder is keys, we want to split using token.upperBoundKey. For instance, if remainder * is [DK(10, 'foo'), DK(20, 'bar')], and we have 3 nodes with tokens 0, 15, 30. We want to * split remainder to A=[DK(10, 'foo'), 15] and B=(15, DK(20, 'bar')]. But since we can't mix * tokens and keys at the same time in a range, we uses 15.upperBoundKey() to have A include all * keys having 15 as token and B include none of those (since that is what our node owns). * asSplitValue() abstracts that choice. */ - T splitValue = (T)token.asSplitValue(queryRange.left.getClass()); - if (remainder == null || !(remainder.left.equals(splitValue) || remainder.contains(splitValue))) + Token upperBoundToken = ringIter.next(); + T upperBound = (T)upperBoundToken.upperBound(queryRange.left.getClass()); + if (!remainder.left.equals(upperBound) && !remainder.contains(upperBound)) // no more splits break; - Pair<AbstractBounds<T>,AbstractBounds<T>> splits = remainder.split(splitValue); - if (splits.left != null) - ranges.add(splits.left); + Pair<AbstractBounds<T>,AbstractBounds<T>> splits = remainder.split(upperBound); + if (splits == null) + continue; + + ranges.add(splits.left); remainder = splits.right; } - if (remainder != null) - ranges.add(remainder); + ranges.add(remainder); if (logger.isDebugEnabled()) logger.debug("restricted ranges for query {} are {}", queryRange, ranges); http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java b/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java index d2b0a0a..9c871bf 100644 --- a/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java +++ b/test/unit/org/apache/cassandra/db/ColumnFamilyStoreTest.java @@ -36,8 +36,7 @@ import org.apache.cassandra.db.filter.*; import org.apache.cassandra.db.index.SecondaryIndex; import org.apache.cassandra.db.marshal.LongType; import org.apache.cassandra.db.marshal.LexicalUUIDType; -import org.apache.cassandra.dht.IPartitioner; -import org.apache.cassandra.dht.Range; +import org.apache.cassandra.dht.*; import org.apache.cassandra.io.sstable.Component; import org.apache.cassandra.io.sstable.Descriptor; import org.apache.cassandra.io.sstable.SSTableReader; @@ -53,6 +52,7 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; import static org.apache.cassandra.Util.column; import static org.apache.cassandra.Util.getBytes; +import static org.apache.cassandra.Util.rp; import static org.apache.cassandra.db.TableTest.assertColumns; import static org.junit.Assert.assertNull; @@ -842,4 +842,70 @@ public class ColumnFamilyStoreTest extends CleanupHelper assertTotalColCount(cfs.getRangeSlice(null, Util.range("", ""), 6, QueryFilter.getFilter(sp, cfs.getComparator()), null, true), 8); assertTotalColCount(cfs.getRangeSlice(null, Util.range("", ""), 100, QueryFilter.getFilter(sp, cfs.getComparator()), null, true), 8); } + + private static DecoratedKey idk(int i) + { + return Util.dk(String.valueOf(i)); + } + + @Test + public void testRangeSliceInclusionExclusion() throws Throwable + { + String tableName = "Keyspace1"; + String cfName = "Standard1"; + Table table = Table.open(tableName); + ColumnFamilyStore cfs = table.getColumnFamilyStore(cfName); + cfs.clearUnsafe(); + + Column[] cols = new Column[5]; + for (int i = 0; i < 5; i++) + cols[i] = column("c" + i, "value", 1); + + for (int i = 0; i <= 9; i++) + { + putColsStandard(cfs, idk(i), column("name", "value", 1)); + } + cfs.forceBlockingFlush(); + + SlicePredicate sp = new SlicePredicate(); + sp.setSlice_range(new SliceRange()); + sp.getSlice_range().setCount(1); + sp.getSlice_range().setStart(ArrayUtils.EMPTY_BYTE_ARRAY); + sp.getSlice_range().setFinish(ArrayUtils.EMPTY_BYTE_ARRAY); + IFilter qf = QueryFilter.getFilter(sp, cfs.getComparator()); + + List<Row> rows; + + // Start and end inclusive + rows = cfs.getRangeSlice(null, new Bounds<RowPosition>(rp("2"), rp("7")), 100, qf, null); + assert rows.size() == 6; + assert rows.get(0).key.equals(idk(2)); + assert rows.get(rows.size() - 1).key.equals(idk(7)); + + // Start and end excluded + rows = cfs.getRangeSlice(null, new ExcludingBounds<RowPosition>(rp("2"), rp("7")), 100, qf, null); + assert rows.size() == 4; + assert rows.get(0).key.equals(idk(3)); + assert rows.get(rows.size() - 1).key.equals(idk(6)); + + // Start excluded, end included + rows = cfs.getRangeSlice(null, new Range<RowPosition>(rp("2"), rp("7")), 100, qf, null); + assert rows.size() == 5; + assert rows.get(0).key.equals(idk(3)); + assert rows.get(rows.size() - 1).key.equals(idk(7)); + + // Start included, end excluded + rows = cfs.getRangeSlice(null, new IncludingExcludingBounds<RowPosition>(rp("2"), rp("7")), 100, qf, null); + assert rows.size() == 5; + assert rows.get(0).key.equals(idk(2)); + assert rows.get(rows.size() - 1).key.equals(idk(6)); + } + + private static String keys(List<Row> rows) throws Throwable + { + String k = ""; + for (Row r : rows) + k += " " + ByteBufferUtil.string(r.key.key); + return k; + } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/test/unit/org/apache/cassandra/dht/AbstractBoundsTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/dht/AbstractBoundsTest.java b/test/unit/org/apache/cassandra/dht/AbstractBoundsTest.java deleted file mode 100644 index 5e007ac..0000000 --- a/test/unit/org/apache/cassandra/dht/AbstractBoundsTest.java +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.cassandra.dht; - -import java.util.List; -import static java.util.Arrays.asList; - -import org.junit.Test; - -import org.apache.cassandra.db.RowPosition; -import org.apache.cassandra.dht.RingPosition; -import static org.apache.cassandra.Util.range; -import static org.apache.cassandra.Util.bounds; - -public class AbstractBoundsTest -{ - private <T extends RingPosition> void assertNormalize(List<? extends AbstractBounds<T>> input, List<? extends AbstractBounds<T>> expected) - { - List<AbstractBounds<T>> result = AbstractBounds.normalize(input); - assert result.equals(expected) : "Expecting " + expected + " but got " + result; - } - - @Test - public void testNormalizeNoop() - { - List<? extends AbstractBounds<RowPosition>> l; - - l = asList(range("1", "3"), range("4", "5")); - assert AbstractBounds.normalize(l).equals(l); - - l = asList(bounds("1", "3"), bounds("4", "5")); - assertNormalize(l, l); - } - - @Test - public void testNormalizeSimpleOverlap() - { - List<? extends AbstractBounds<RowPosition>> input, expected; - - input = asList(range("1", "4"), range("3", "5")); - expected = asList(range("1", "5")); - assertNormalize(input, expected); - - input = asList(range("1", "4"), range("1", "4")); - expected = asList(range("1", "4")); - assertNormalize(input, expected); - - input = asList(bounds("1", "4"), bounds("3", "5")); - expected = asList(bounds("1", "5")); - assertNormalize(input, expected); - - input = asList(bounds("1", "4"), bounds("1", "4")); - expected = asList(bounds("1", "4")); - assertNormalize(input, expected); - - input = asList(bounds("1", "1"), bounds("1", "1")); - expected = asList(bounds("1", "1")); - assertNormalize(input, expected); - } - - @Test - public void testNormalizeSort() - { - List<? extends AbstractBounds<RowPosition>> input, expected; - - input = asList(range("4", "5"), range("1", "3")); - expected = asList(range("1", "3"), range("4", "5")); - assertNormalize(input, expected); - - input = asList(bounds("4", "5"), bounds("1", "3")); - expected = asList(bounds("1", "3"), bounds("4", "5")); - assertNormalize(input, expected); - } - - @Test - public void testNormalizeUnwrap() - { - List<? extends AbstractBounds<RowPosition>> input, expected; - - input = asList(range("9", "2")); - expected = asList(range("", "2"), range("9", "")); - assertNormalize(input, expected); - - // Bounds cannot wrap - } - - @Test - public void testNormalizeComplex() - { - List<? extends AbstractBounds<RowPosition>> input, expected; - - input = asList(range("8", "2"), range("7", "9"), range("4", "5")); - expected = asList(range("", "2"), range("4", "5"), range("7", "")); - assertNormalize(input, expected); - - input = asList(range("5", "9"), range("2", "5")); - expected = asList(range("2", "9")); - assertNormalize(input, expected); - - input = asList(range ("", "1"), range("9", "2"), range("4", "5"), range("", "")); - expected = asList(range("", "")); - assertNormalize(input, expected); - - input = asList(range ("", "1"), range("1", "4"), range("4", "5"), range("5", "")); - expected = asList(range("", "")); - assertNormalize(input, expected); - - // bounds - input = asList(bounds("5", "9"), bounds("2", "5")); - expected = asList(bounds("2", "9")); - assertNormalize(input, expected); - - input = asList(bounds ("", "1"), bounds("", "9"), bounds("4", "5"), bounds("", "")); - expected = asList(bounds("", "")); - assertNormalize(input, expected); - - input = asList(bounds ("", "1"), bounds("1", "4"), bounds("4", "5"), bounds("5", "")); - expected = asList(bounds("", "")); - assertNormalize(input, expected); - } -} http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/test/unit/org/apache/cassandra/dht/RangeTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/dht/RangeTest.java b/test/unit/org/apache/cassandra/dht/RangeTest.java index 7c27787..dc951f8 100644 --- a/test/unit/org/apache/cassandra/dht/RangeTest.java +++ b/test/unit/org/apache/cassandra/dht/RangeTest.java @@ -20,11 +20,17 @@ package org.apache.cassandra.dht; import java.nio.ByteBuffer; import java.util.HashSet; +import java.util.List; import java.util.Set; +import static java.util.Arrays.asList; import org.apache.commons.lang.StringUtils; import org.junit.Test; +import org.apache.cassandra.db.RowPosition; +import static org.apache.cassandra.Util.range; + + public class RangeTest { @Test @@ -459,4 +465,75 @@ public class RangeTest String[][] expected6 = { { "40", "20" } }; checkDifference(oldRange, newTokens6, expected6); } + + private <T extends RingPosition> void assertNormalize(List<Range<T>> input, List<Range<T>> expected) + { + List<Range<T>> result = Range.normalize(input); + assert result.equals(expected) : "Expecting " + expected + " but got " + result; + } + + @Test + public void testNormalizeNoop() + { + List<Range<RowPosition>> l; + + l = asList(range("1", "3"), range("4", "5")); + assertNormalize(l, l); + } + + @Test + public void testNormalizeSimpleOverlap() + { + List<Range<RowPosition>> input, expected; + + input = asList(range("1", "4"), range("3", "5")); + expected = asList(range("1", "5")); + assertNormalize(input, expected); + + input = asList(range("1", "4"), range("1", "4")); + expected = asList(range("1", "4")); + assertNormalize(input, expected); + } + + @Test + public void testNormalizeSort() + { + List<Range<RowPosition>> input, expected; + + input = asList(range("4", "5"), range("1", "3")); + expected = asList(range("1", "3"), range("4", "5")); + assertNormalize(input, expected); + } + + @Test + public void testNormalizeUnwrap() + { + List<Range<RowPosition>> input, expected; + + input = asList(range("9", "2")); + expected = asList(range("", "2"), range("9", "")); + assertNormalize(input, expected); + } + + @Test + public void testNormalizeComplex() + { + List<Range<RowPosition>> input, expected; + + input = asList(range("8", "2"), range("7", "9"), range("4", "5")); + expected = asList(range("", "2"), range("4", "5"), range("7", "")); + assertNormalize(input, expected); + + input = asList(range("5", "9"), range("2", "5")); + expected = asList(range("2", "9")); + assertNormalize(input, expected); + + input = asList(range ("", "1"), range("9", "2"), range("4", "5"), range("", "")); + expected = asList(range("", "")); + assertNormalize(input, expected); + + input = asList(range ("", "1"), range("1", "4"), range("4", "5"), range("5", "")); + expected = asList(range("", "")); + assertNormalize(input, expected); + } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/35f7bc59/test/unit/org/apache/cassandra/service/StorageProxyTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/service/StorageProxyTest.java b/test/unit/org/apache/cassandra/service/StorageProxyTest.java index 6b90d87..1c89cfb 100644 --- a/test/unit/org/apache/cassandra/service/StorageProxyTest.java +++ b/test/unit/org/apache/cassandra/service/StorageProxyTest.java @@ -35,6 +35,8 @@ import org.apache.cassandra.dht.AbstractBounds; import org.apache.cassandra.dht.Bounds; import org.apache.cassandra.dht.Range; import org.apache.cassandra.dht.Token; +import org.apache.cassandra.dht.ExcludingBounds; +import org.apache.cassandra.dht.IncludingExcludingBounds; import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.utils.ByteBufferUtil; @@ -52,6 +54,16 @@ public class StorageProxyTest extends CleanupHelper return new Bounds<RowPosition>(left, right); } + private static ExcludingBounds<RowPosition> exBounds(RowPosition left, RowPosition right) + { + return new ExcludingBounds<RowPosition>(left, right); + } + + private static IncludingExcludingBounds<RowPosition> incExBounds(RowPosition left, RowPosition right) + { + return new IncludingExcludingBounds<RowPosition>(left, right); + } + private static RowPosition startOf(String key) { return StorageService.getPartitioner().getToken(ByteBufferUtil.bytes(key)).minKeyBound(); @@ -125,18 +137,26 @@ public class StorageProxyTest extends CleanupHelper // no splits testGRRKeys(range(rp("2"), rp("5")), range(rp("2"), rp("5"))); testGRRKeys(bounds(rp("2"), rp("5")), bounds(rp("2"), rp("5"))); + testGRRKeys(exBounds(rp("2"), rp("5")), exBounds(rp("2"), rp("5"))); // single split testGRRKeys(range("2", "7"), range(rp("2"), endOf("6")), range(endOf("6"), rp("7"))); testGRRKeys(bounds(rp("2"), rp("7")), bounds(rp("2"), endOf("6")), range(endOf("6"), rp("7"))); + testGRRKeys(exBounds(rp("2"), rp("7")), range(rp("2"), endOf("6")), exBounds(endOf("6"), rp("7"))); + testGRRKeys(incExBounds(rp("2"), rp("7")), bounds(rp("2"), endOf("6")), exBounds(endOf("6"), rp("7"))); // single split starting from min testGRRKeys(range(rp(""), rp("2")), range(rp(""), endOf("1")), range(endOf("1"), rp("2"))); testGRRKeys(bounds(rp(""), rp("2")), bounds(rp(""), endOf("1")), range(endOf("1"), rp("2"))); + testGRRKeys(exBounds(rp(""), rp("2")), range(rp(""), endOf("1")), exBounds(endOf("1"), rp("2"))); + testGRRKeys(incExBounds(rp(""), rp("2")), bounds(rp(""), endOf("1")), exBounds(endOf("1"), rp("2"))); // single split ending with max testGRRKeys(range(rp("5"), rp("")), range(rp("5"), endOf("6")), range(endOf("6"), rp(""))); testGRRKeys(bounds(rp("5"), rp("")), bounds(rp("5"), endOf("6")), range(endOf("6"), rp(""))); + testGRRKeys(exBounds(rp("5"), rp("")), range(rp("5"), endOf("6")), exBounds(endOf("6"), rp(""))); + testGRRKeys(incExBounds(rp("5"), rp("")), bounds(rp("5"), endOf("6")), exBounds(endOf("6"), rp(""))); // two splits testGRRKeys(range(rp("0"), rp("7")), range(rp("0"), endOf("1")), range(endOf("1"), endOf("6")), range(endOf("6"), rp("7"))); testGRRKeys(bounds(rp("0"), rp("7")), bounds(rp("0"), endOf("1")), range(endOf("1"), endOf("6")), range(endOf("6"), rp("7"))); - + testGRRKeys(exBounds(rp("0"), rp("7")), range(rp("0"), endOf("1")), range(endOf("1"), endOf("6")), exBounds(endOf("6"), rp("7"))); + testGRRKeys(incExBounds(rp("0"), rp("7")), bounds(rp("0"), endOf("1")), range(endOf("1"), endOf("6")), exBounds(endOf("6"), rp("7"))); } @Test @@ -158,12 +178,20 @@ public class StorageProxyTest extends CleanupHelper testGRRKeys(range(endOf("1"), endOf("5")), range(endOf("1"), endOf("5"))); testGRRKeys(range(rp("1"), endOf("5")), range(rp("1"), endOf("1")), range(endOf("1"), endOf("5"))); testGRRKeys(bounds(startOf("1"), endOf("5")), bounds(startOf("1"), endOf("1")), range(endOf("1"), endOf("5"))); + testGRRKeys(exBounds(endOf("1"), rp("5")), exBounds(endOf("1"), rp("5"))); + testGRRKeys(exBounds(rp("1"), rp("5")), range(rp("1"), endOf("1")), exBounds(endOf("1"), rp("5"))); + testGRRKeys(exBounds(startOf("1"), endOf("5")), range(startOf("1"), endOf("1")), exBounds(endOf("1"), endOf("5"))); + testGRRKeys(incExBounds(rp("1"), rp("5")), bounds(rp("1"), endOf("1")), exBounds(endOf("1"), rp("5"))); // max testGRRKeys(range(endOf("2"), endOf("6")), range(endOf("2"), endOf("6"))); testGRRKeys(bounds(startOf("2"), endOf("6")), bounds(startOf("2"), endOf("6"))); + testGRRKeys(exBounds(rp("2"), rp("6")), exBounds(rp("2"), rp("6"))); + testGRRKeys(incExBounds(rp("2"), rp("6")), incExBounds(rp("2"), rp("6"))); // bothKeys testGRRKeys(range(rp("1"), rp("6")), range(rp("1"), endOf("1")), range(endOf("1"), rp("6"))); testGRRKeys(bounds(rp("1"), rp("6")), bounds(rp("1"), endOf("1")), range(endOf("1"), rp("6"))); + testGRRKeys(exBounds(rp("1"), rp("6")), range(rp("1"), endOf("1")), exBounds(endOf("1"), rp("6"))); + testGRRKeys(incExBounds(rp("1"), rp("6")), bounds(rp("1"), endOf("1")), exBounds(endOf("1"), rp("6"))); } @Test @@ -212,5 +240,7 @@ public class StorageProxyTest extends CleanupHelper testGRRKeys(bounds(rp("0"), rp("0")), bounds(rp("0"), rp("0"))); // completely empty bounds match everything testGRRKeys(bounds(rp(""), rp("")), bounds(rp(""), endOf("1")), range(endOf("1"), endOf("6")), range(endOf("6"), rp(""))); + testGRRKeys(exBounds(rp(""), rp("")), range(rp(""), endOf("1")), range(endOf("1"), endOf("6")), exBounds(endOf("6"), rp(""))); + testGRRKeys(incExBounds(rp(""), rp("")), bounds(rp(""), endOf("1")), range(endOf("1"), endOf("6")), exBounds(endOf("6"), rp(""))); } }