diff --git a/lib/Predis/Cluster/PredisClusterHashStrategy.php b/lib/Predis/Cluster/PredisClusterHashStrategy.php index bfa445f7..5e0330df 100644 --- a/lib/Predis/Cluster/PredisClusterHashStrategy.php +++ b/lib/Predis/Cluster/PredisClusterHashStrategy.php @@ -139,6 +139,7 @@ class PredisClusterHashStrategy implements CommandHashStrategyInterface 'ZUNIONSTORE' => array($this, 'getKeyFromZsetAggregationCommands'), 'ZSCAN' => $keyIsFirstArgument, 'ZLEXCOUNT' => $keyIsFirstArgument, + 'ZRANGEBYLEX' => $keyIsFirstArgument, /* commands operating on hashes */ 'HDEL' => $keyIsFirstArgument, diff --git a/lib/Predis/Cluster/RedisClusterHashStrategy.php b/lib/Predis/Cluster/RedisClusterHashStrategy.php index ebb42727..acdd235e 100644 --- a/lib/Predis/Cluster/RedisClusterHashStrategy.php +++ b/lib/Predis/Cluster/RedisClusterHashStrategy.php @@ -125,6 +125,7 @@ class RedisClusterHashStrategy implements CommandHashStrategyInterface 'ZSCORE' => $keyIsFirstArgument, 'ZSCAN' => $keyIsFirstArgument, 'ZLEXCOUNT' => $keyIsFirstArgument, + 'ZRANGEBYLEX' => $keyIsFirstArgument, /* commands operating on hashes */ 'HDEL' => $keyIsFirstArgument, diff --git a/lib/Predis/Command/ZSetRangeByLex.php b/lib/Predis/Command/ZSetRangeByLex.php new file mode 100644 index 00000000..58b6768d --- /dev/null +++ b/lib/Predis/Command/ZSetRangeByLex.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @link http://redis.io/commands/zrangebylex + * @author Daniele Alessandri + */ +class ZSetRangeByLex extends ZSetRange +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'ZRANGEBYLEX'; + } + + /** + * {@inheritdoc} + */ + protected function prepareOptions($options) + { + $opts = array_change_key_case($options, CASE_UPPER); + $finalizedOpts = array(); + + if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) { + $limit = array_change_key_case($opts['LIMIT'], CASE_UPPER); + + $finalizedOpts[] = 'LIMIT'; + $finalizedOpts[] = isset($limit['OFFSET']) ? $limit['OFFSET'] : $limit[0]; + $finalizedOpts[] = isset($limit['COUNT']) ? $limit['COUNT'] : $limit[1]; + } + + return $finalizedOpts; + } + + /** + * {@inheritdoc} + */ + protected function withScores() + { + return false; + } +} diff --git a/lib/Predis/Profile/ServerVersion28.php b/lib/Predis/Profile/ServerVersion28.php index b53522e8..bc523a22 100644 --- a/lib/Predis/Profile/ServerVersion28.php +++ b/lib/Predis/Profile/ServerVersion28.php @@ -240,6 +240,7 @@ class ServerVersion28 extends ServerProfile /* commands operating on sorted sets */ 'zscan' => 'Predis\Command\ZSetScan', 'zlexcount' => 'Predis\Command\ZSetLexCount', + 'zrangebylex' => 'Predis\Command\ZSetRangeByLex', /* commands operating on hashes */ 'hscan' => 'Predis\Command\HashScan', diff --git a/lib/Predis/Replication/ReplicationStrategy.php b/lib/Predis/Replication/ReplicationStrategy.php index d02980af..d05880b3 100644 --- a/lib/Predis/Replication/ReplicationStrategy.php +++ b/lib/Predis/Replication/ReplicationStrategy.php @@ -202,6 +202,7 @@ class ReplicationStrategy 'ZREVRANK' => true, 'ZSCAN' => true, 'ZLEXCOUNT' => true, + 'ZRANGEBYLEX' => true, 'HGET' => true, 'HMGET' => true, 'HEXISTS' => true, diff --git a/tests/Predis/Cluster/PredisClusterHashStrategyTest.php b/tests/Predis/Cluster/PredisClusterHashStrategyTest.php index 9c0f3d2b..38531926 100644 --- a/tests/Predis/Cluster/PredisClusterHashStrategyTest.php +++ b/tests/Predis/Cluster/PredisClusterHashStrategyTest.php @@ -343,6 +343,7 @@ class PredisClusterHashStrategyTest extends PredisTestCase 'ZUNIONSTORE' => 'keys-zaggregated', 'ZSCAN' => 'keys-first', 'ZLEXCOUNT' => 'keys-first', + 'ZRANGEBYLEX' => 'keys-first', /* commands operating on hashes */ 'HDEL' => 'keys-first', diff --git a/tests/Predis/Cluster/RedisClusterHashStrategyTest.php b/tests/Predis/Cluster/RedisClusterHashStrategyTest.php index b2298cf9..2d5228e6 100644 --- a/tests/Predis/Cluster/RedisClusterHashStrategyTest.php +++ b/tests/Predis/Cluster/RedisClusterHashStrategyTest.php @@ -343,6 +343,7 @@ class RedisClusterHashStrategyTest extends PredisTestCase 'ZSCORE' => 'keys-first', 'ZSCAN' => 'keys-first', 'ZLEXCOUNT' => 'keys-first', + 'ZRANGEBYLEX' => 'keys-first', /* commands operating on hashes */ 'HDEL' => 'keys-first', diff --git a/tests/Predis/Command/ZSetRangeByLexTest.php b/tests/Predis/Command/ZSetRangeByLexTest.php new file mode 100644 index 00000000..3283448b --- /dev/null +++ b/tests/Predis/Command/ZSetRangeByLexTest.php @@ -0,0 +1,230 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Predis\Command; + +/** + * @group commands + * @group realm-zset + */ +class ZSetRangeByLexTest extends PredisCommandTestCase +{ + /** + * {@inheritdoc} + */ + protected function getExpectedCommand() + { + return 'Predis\Command\ZSetRangeByLex'; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedId() + { + return 'ZRANGEBYLEX'; + } + + /** + * @group disconnected + */ + public function testFilterArguments() + { + $modifiers = array( + 'limit' => array(0, 100), + ); + + $arguments = array('zset', '[a', '[z', $modifiers); + $expected = array('zset', '[a', '[z', 'LIMIT', 0, 100); + + $command = $this->getCommand(); + $command->setArguments($arguments); + + $this->assertSame($expected, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testFilterArgumentsWithNamedLimit() + { + $arguments = array('zset', '[a', '[z', array('limit' => array('offset' => 1, 'count' => 2))); + $expected = array('zset', '[a', '[z', 'LIMIT', 1, 2); + + $command = $this->getCommand(); + $command->setArguments($arguments); + + $this->assertSame($expected, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testParseResponse() + { + $raw = array('a', 'b', 'c'); + $expected = array('a', 'b', 'c'); + + $command = $this->getCommand(); + + $this->assertSame($expected, $command->parseResponse($raw)); + } + + /** + * @group disconnected + */ + public function testPrefixKeys() + { + $modifiers = array( + 'limit' => array(0, 100), + ); + + $arguments = array('zset', '[a', '[z', $modifiers); + $expected = array('prefix:zset', '[a', '[z', 'LIMIT', 0, 100); + + $command = $this->getCommandWithArgumentsArray($arguments); + $command->prefixKeys('prefix:'); + + $this->assertSame($expected, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testPrefixKeysIgnoredOnEmptyArguments() + { + $command = $this->getCommand(); + $command->prefixKeys('prefix:'); + + $this->assertSame(array(), $command->getArguments()); + } + + /** + * @group connected + */ + public function testReturnsElementsInWholeRange() + { + $this->markTestSkippedOnRedisVersionBelow('2.8.9', 'Lexicographical operations on sorted sets require Redis >= 2.8.9.', true); + + $redis = $this->getClient(); + + $redis->zadd('letters', 0, 'a', 0, 'b', 0, 'c', 0, 'd', 0, 'e', 0, 'f', 0, 'g'); + + $this->assertSame(array('a', 'b', 'c', 'd', 'e', 'f', 'g'), $redis->zrangebylex('letters', '-', '+')); + $this->assertSame(array(), $redis->zrangebylex('letters', '+', '-')); + $this->assertSame(array(), $redis->zrangebylex('unknown', '-', '+')); + $this->assertSame(array(), $redis->zrangebylex('unknown', '+', '-')); + } + + /** + * @group connected + */ + public function testReturnsElementsInInclusiveRange() + { + $this->markTestSkippedOnRedisVersionBelow('2.8.9', 'Lexicographical operations on sorted sets require Redis >= 2.8.9.', true); + + $redis = $this->getClient(); + + $redis->zadd('letters', 0, 'a', 0, 'b', 0, 'c', 0, 'd', 0, 'e', 0, 'f', 0, 'g'); + + $this->assertSame(array('a'), $redis->zrangebylex('letters', '[a', '[a')); + $this->assertSame(array('c', 'd', 'e', 'f'), $redis->zrangebylex('letters', '[c', '[f')); + $this->assertSame(array('a', 'b', 'c'), $redis->zrangebylex('letters', '-', '[c')); + $this->assertSame(array(), $redis->zrangebylex('letters', '+', '[c')); + $this->assertSame(array(), $redis->zrangebylex('letters', '[x', '[z')); + $this->assertSame(array(), $redis->zrangebylex('unknown', '[0', '[1')); + } + + /** + * @group connected + */ + public function testReturnsElementsInExclusiveRange() + { + $this->markTestSkippedOnRedisVersionBelow('2.8.9', 'Lexicographical operations on sorted sets require Redis >= 2.8.9.', true); + + $redis = $this->getClient(); + + $redis->zadd('letters', 0, 'a', 0, 'b', 0, 'c', 0, 'd', 0, 'e', 0, 'f', 0, 'g'); + + $this->assertSame(array(), $redis->zrangebylex('letters', '(a', '(a')); + $this->assertSame(array('d', 'e'), $redis->zrangebylex('letters', '(c', '(f')); + $this->assertSame(array('a', 'b'), $redis->zrangebylex('letters', '-', '(c')); + $this->assertSame(array(), $redis->zrangebylex('letters', '+', '(c')); + $this->assertSame(array(), $redis->zrangebylex('letters', '(x', '(z')); + $this->assertSame(array(), $redis->zrangebylex('unknown', '(0', '(1')); + } + + /** + * @group connected + */ + public function testReturnsElementsInMixedRange() + { + $this->markTestSkippedOnRedisVersionBelow('2.8.9', 'Lexicographical operations on sorted sets require Redis >= 2.8.9.', true); + + $redis = $this->getClient(); + + $redis->zadd('letters', 0, 'a', 0, 'b', 0, 'c', 0, 'd', 0, 'e', 0, 'f', 0, 'g'); + + $this->assertSame(array(), $redis->zrangebylex('letters', '[a', '(a')); + $this->assertSame(array(), $redis->zrangebylex('letters', '(a', '[a')); + $this->assertSame(array('c', 'd', 'e'), $redis->zrangebylex('letters', '[c', '(f')); + $this->assertSame(array('d', 'e', 'f'), $redis->zrangebylex('letters', '(c', '[f')); + $this->assertSame(array(), $redis->zrangebylex('unknown', '[0', '(5')); + } + + /** + * @group connected + */ + public function testRangeWithLimitModifier() + { + $this->markTestSkippedOnRedisVersionBelow('2.8.9', 'Lexicographical operations on sorted sets require Redis >= 2.8.9.', true); + + $redis = $this->getClient(); + + $redis->zadd('letters', 0, 'a', 0, 'b', 0, 'c', 0, 'd', 0, 'e', 0, 'f', 0, 'g'); + + $this->assertSame(array('c', 'd', 'e'), $redis->zrangebylex('letters', '-', '+', 'LIMIT', '2', '3')); + $this->assertSame(array('c', 'd', 'e'), $redis->zrangebylex('letters', '-', '+', array('limit' => array(2, 3)))); + $this->assertSame(array('c', 'd', 'e'), $redis->zrangebylex('letters', '-', '+', array('limit' => array('offset' => 2, 'count' => 3)))); + $this->assertSame(array(), $redis->zrangebylex('letters', '[a', '[f', 'LIMIT', '2', '0')); + $this->assertSame(array(), $redis->zrangebylex('letters', '[a', '[f', 'LIMIT', '-4', '2')); + } + + /** + * @group connected + * @expectedException Predis\ServerException + * @expectedExceptionMessage min or max not valid string range item + */ + public function testThrowsExceptionOnInvalidRangeFormat() + { + $this->markTestSkippedOnRedisVersionBelow('2.8.9', 'Lexicographical operations on sorted sets require Redis >= 2.8.9.', true); + + $redis = $this->getClient(); + + $redis->zadd('letters', 0, 'a', 0, 'b', 0, 'c', 0, 'd', 0, 'e', 0, 'f', 0, 'g'); + $redis->zrangebylex('letters', 'b', 'f'); + } + + /** + * @group connected + * @expectedException Predis\ServerException + * @expectedExceptionMessage Operation against a key holding the wrong kind of value + */ + public function testThrowsExceptionOnWrongType() + { + $this->markTestSkippedOnRedisVersionBelow('2.8.9', 'Lexicographical operations on sorted sets require Redis >= 2.8.9.', true); + + $redis = $this->getClient(); + + $redis->set('foo', 'bar'); + $redis->zrangebylex('foo', '-', '+'); + } +} diff --git a/tests/Predis/Profile/ServerVersion28Test.php b/tests/Predis/Profile/ServerVersion28Test.php index f1b3711e..bae884f4 100644 --- a/tests/Predis/Profile/ServerVersion28Test.php +++ b/tests/Predis/Profile/ServerVersion28Test.php @@ -177,10 +177,11 @@ class ServerVersion28Test extends PredisProfileTestCase 136 => 'sscan', 137 => 'zscan', 138 => 'zlexcount', - 139 => 'hscan', - 140 => 'pfadd', - 141 => 'pfcount', - 142 => 'pfmerge', + 139 => 'zrangebylex', + 140 => 'hscan', + 141 => 'pfadd', + 142 => 'pfcount', + 143 => 'pfmerge', ); } } diff --git a/tests/Predis/Profile/ServerVersionNextTest.php b/tests/Predis/Profile/ServerVersionNextTest.php index 1023e50d..076112f4 100644 --- a/tests/Predis/Profile/ServerVersionNextTest.php +++ b/tests/Predis/Profile/ServerVersionNextTest.php @@ -177,10 +177,11 @@ class ServerVersionNextTest extends PredisProfileTestCase 136 => 'sscan', 137 => 'zscan', 138 => 'zlexcount', - 139 => 'hscan', - 140 => 'pfadd', - 141 => 'pfcount', - 142 => 'pfmerge', + 139 => 'zrangebylex', + 140 => 'hscan', + 141 => 'pfadd', + 142 => 'pfcount', + 143 => 'pfmerge', ); } } diff --git a/tests/Predis/Replication/ReplicationStrategyTest.php b/tests/Predis/Replication/ReplicationStrategyTest.php index bc02851f..270d180e 100644 --- a/tests/Predis/Replication/ReplicationStrategyTest.php +++ b/tests/Predis/Replication/ReplicationStrategyTest.php @@ -343,6 +343,7 @@ class ReplicationStrategyTest extends PredisTestCase 'ZSCORE' => 'read', 'ZSCAN' => 'read', 'ZLEXCOUNT' => 'read', + 'ZRANGEBYLEX' => 'read', /* commands operating on hashes */ 'HDEL' => 'write',