-
Notifications
You must be signed in to change notification settings - Fork 695
GEODE-9378: Implement ZRANGEBYSCORE #6700
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GEODE-9378: Implement ZRANGEBYSCORE #6700
Conversation
| public List<byte[]> zrangebyscore(RedisKey key, SortedSetRangeOptions rangeOptions, | ||
| boolean withScores) { | ||
| return stripedExecute(key, | ||
| () -> getRedisSortedSet(key, true).zrangebyscore(rangeOptions, withScores)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed you are calling getRedisSortedSet with updatesStats set to true. This seems reasonable. But I also noticed that a bunch of other methods on this class (zadd, zincrby, zrange, zrem) call it with false. Do you know when to set it to true vs false?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that Redis' approach is to update the stats for commands that don't update data. In your sample list, zrange actually does call it with true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see ZRANGE calling it with true. In general, operations that read data update stats, operations that change data do not. So zadd, zrem, zincrby change data and therefore don't update stats.
| List<byte[]> result = new ArrayList<>(); | ||
| AbstractOrderedSetEntry minEntry = | ||
| new DummyOrderedSetEntry(rangeOptions.getMinDouble(), rangeOptions.isMinExclusive(), true); | ||
| long minIndex = scoreSet.indexOf(minEntry); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is minIndex and maxIndex typed as long? indexOf returns an int and later we cast it to an int. Seems like if you typed it as "int" you could get rid of the cast later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're typed as long because they're essentially the same code as in ZCOUNT. But it makes more sense for them to be int here, as you say.
| count = Integer.MAX_VALUE; | ||
| } | ||
| } else { | ||
| throw new Exception(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you make this a more specific exception? When the higher level catches Exception it could be catching all kinds of things (like NullPointerException) and reporting them as something else
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IllegalArgumentException seems to fit. Done.
|
|
||
| // Count 1 <= score <= 1 | ||
| assertThat(jedis.zrangeByScore(KEY, score, score)) | ||
| .containsExactlyInAnyOrderElementsOf(map.keySet()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should also be using containsExactly since the order of the returned values should be lexicographically sorted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tweaked; unfortunately the map.keySet() isn't in the order we want, but it's fixable.
|
|
||
| import org.apache.geode.redis.RedisIntegrationTest; | ||
|
|
||
| public abstract class AbstractZRangeByScoreIntegrationTest implements RedisIntegrationTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a lot of boundary cases tested, but please would you also add a basic test for exclusivity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also add tests to confirm the behaviour of ZRANGEBYSCORE with multiple instances of WITHSCORES and LIMIT, and with incorrectly formatted LIMIT arguments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hope the new one is the kind of thing you have in mind...
| if (skip < offset) { | ||
| skip++; | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could avoid use of skip here and just count down on offset:
if (offset > 0) {
offset--;
continue;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice; I think I can do that with 'count' as well!
| } | ||
|
|
||
| @Test | ||
| public void shouldReturnCount_givenRangeIncludingScore() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test name should probably be "shouldReturnMember_givenRangeIncludingScore" or something similar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, missed one.
| public void shouldReturnZero_givenMinGreaterThanMax() { | ||
| jedis.zadd(KEY, 1, "member"); | ||
|
|
||
| // Count +inf <= score <= -inf |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment (and others in this class) is a little misleading, as these tests aren't returning a count, but rather a range.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, thought I got all those. Quick fix.
|
|
||
| // Count -inf < score <= +inf | ||
| assertThat(jedis.zrangeByScore(KEY, "(-inf", "+inf")) | ||
| .containsExactlyElementsOf(Arrays.asList("member2", "member3")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be simplified to .containsExactly("member2", "member3"). This also applies to other places in this test where Arrays.asList() and containsExactlyElementsOf() are used together.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got a few of them earlier, all done now!
| ZREVRANGE(new ZRevRangeExecutor(), SUPPORTED, new MinimumParameterRequirements(4) | ||
| .and(new MaximumParameterRequirements(5, ERROR_SYNTAX))), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change should be reverted as ZREVRANGE should return a syntax error if more than 5 arguments are passed to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| Iterator<AbstractOrderedSetEntry> entryIterator = | ||
| scoreSet.getIndexRange(minIndex, maxIndex, false); | ||
| if (rangeOptions.hasLimit()) { | ||
| count = rangeOptions.getCount(); | ||
| offset = rangeOptions.getOffset(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than getting the range iterator and then skipping to the offset, might it be neater to add the offset to minIndex and then retrieve the range iterator? Similarly, I think you should be able to pass Math.min(count, maxIndex - minIndex) as the second argument of getIndexRange() so that you get an iterator that's automatically the right size. Then you can just iterate the entire length and return that with no further checks needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Short answer: Yes.
Long answer: Yes, and we can eliminate another variable, too.
|
|
||
| import org.apache.geode.redis.RedisIntegrationTest; | ||
|
|
||
| public abstract class AbstractZRangeByScoreIntegrationTest implements RedisIntegrationTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also add tests to confirm the behaviour of ZRANGEBYSCORE with multiple instances of WITHSCORES and LIMIT, and with incorrectly formatted LIMIT arguments?
| offset = narrowLongToInt(bytesToLong(commandElements.get(commandIndex + 1))); | ||
| count = narrowLongToInt(bytesToLong(commandElements.get(commandIndex + 2))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With an improperly provided LIMIT argument (one or both of offset or count not supplied) this code will throw an ArrayIndexOutOfBoundsException which is not handled, so no syntax error will be returned to the client.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests and fixes added for these.
| public static final byte[] bRADISH_DUMP_HEADER = stringToBytes("RADISH"); | ||
|
|
||
| @MakeImmutable | ||
| public static final byte[] bRADISH_WITHSCORES = stringToBytes("WITHSCORES"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is already a byte array constant for WITHSCORES further up in this class (line 180), so this one is not necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, fixed!
| public static final byte[] bRADISH_WITHSCORES = stringToBytes("WITHSCORES"); | ||
|
|
||
| @MakeImmutable | ||
| public static final byte[] bRADISH_LIMIT = stringToBytes("LIMIT"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be renamed "bLIMIT" to match the naming of other byte array constants used in executors, and moved further up in this class to be with the other executor-related constants?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also corrected!
jdeppe-pivotal
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work on this!
Addresses GEODE-9378 to implement the ZRANGEBYSCORE command for Geode Radish, the compatible-with-Redis API.