diff --git a/lib/Predis/Cluster/PredisStrategy.php b/lib/Predis/Cluster/PredisStrategy.php index e239dae7..580cd912 100644 --- a/lib/Predis/Cluster/PredisStrategy.php +++ b/lib/Predis/Cluster/PredisStrategy.php @@ -140,6 +140,7 @@ class PredisStrategy implements StrategyInterface 'ZUNIONSTORE' => array($this, 'getKeyFromZsetAggregationCommands'), 'ZSCAN' => $keyIsFirstArgument, 'ZLEXCOUNT' => $keyIsFirstArgument, + 'ZRANGEBYLEX' => $keyIsFirstArgument, /* commands operating on hashes */ 'HDEL' => $keyIsFirstArgument, diff --git a/lib/Predis/Cluster/RedisStrategy.php b/lib/Predis/Cluster/RedisStrategy.php index e3fcd9fe..9430de43 100644 --- a/lib/Predis/Cluster/RedisStrategy.php +++ b/lib/Predis/Cluster/RedisStrategy.php @@ -125,6 +125,7 @@ class RedisStrategy implements StrategyInterface 'ZSCORE' => $keyIsFirstArgument, 'ZSCAN' => $keyIsFirstArgument, 'ZLEXCOUNT' => $keyIsFirstArgument, + 'ZRANGEBYLEX' => $keyIsFirstArgument, /* commands operating on hashes */ 'HDEL' => $keyIsFirstArgument, diff --git a/lib/Predis/Command/Processor/KeyPrefixProcessor.php b/lib/Predis/Command/Processor/KeyPrefixProcessor.php index dd19d9af..6010252d 100644 --- a/lib/Predis/Command/Processor/KeyPrefixProcessor.php +++ b/lib/Predis/Command/Processor/KeyPrefixProcessor.php @@ -153,6 +153,7 @@ class KeyPrefixProcessor implements ProcessorInterface 'PFCOUNT' => 'self::all', 'PFMERGE' => 'self::all', 'ZLEXCOUNT' => 'self::first', + 'ZRANGEBYLEX' => 'self::first', ); } 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/RedisVersion280.php b/lib/Predis/Profile/RedisVersion280.php index 4c40a755..a241bdc9 100644 --- a/lib/Predis/Profile/RedisVersion280.php +++ b/lib/Predis/Profile/RedisVersion280.php @@ -241,6 +241,7 @@ class RedisVersion280 extends RedisProfile /* 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 d4006209..ef56d440 100644 --- a/lib/Predis/Replication/ReplicationStrategy.php +++ b/lib/Predis/Replication/ReplicationStrategy.php @@ -205,6 +205,7 @@ class ReplicationStrategy 'ZREVRANK' => true, 'ZSCAN' => true, 'ZLEXCOUNT' => true, + 'ZRANGEBYLEX' => true, 'HGET' => true, 'HMGET' => true, 'HEXISTS' => true, diff --git a/tests/Predis/Cluster/PredisStrategyTest.php b/tests/Predis/Cluster/PredisStrategyTest.php index 488f0afd..dec9e768 100644 --- a/tests/Predis/Cluster/PredisStrategyTest.php +++ b/tests/Predis/Cluster/PredisStrategyTest.php @@ -346,6 +346,7 @@ class PredisStrategyTest 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/RedisStrategyTest.php b/tests/Predis/Cluster/RedisStrategyTest.php index 73f5a516..ab6e047c 100644 --- a/tests/Predis/Cluster/RedisStrategyTest.php +++ b/tests/Predis/Cluster/RedisStrategyTest.php @@ -343,6 +343,7 @@ class RedisStrategyTest 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/Processor/KeyPrefixProcessorTest.php b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php index 4ad1c674..b2626ee8 100644 --- a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php +++ b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php @@ -815,6 +815,10 @@ class KeyPrefixProcessorTest extends PredisTestCase array('key', '-', '+'), array('prefix:key', '-', '+'), ), + array('ZRANGEBYLEX', + array('key', '-', '+', 'LIMIT', '0', '10'), + array('prefix:key', '-', '+', 'LIMIT', '0', '10'), + ), ); } } diff --git a/tests/Predis/Command/ZSetRangeByLexTest.php b/tests/Predis/Command/ZSetRangeByLexTest.php new file mode 100644 index 00000000..a26c06ff --- /dev/null +++ b/tests/Predis/Command/ZSetRangeByLexTest.php @@ -0,0 +1,201 @@ + + * + * 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 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\Response\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\Response\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/RedisUnstableTest.php b/tests/Predis/Profile/RedisUnstableTest.php index 9f80dc5b..19a267ff 100644 --- a/tests/Predis/Profile/RedisUnstableTest.php +++ b/tests/Predis/Profile/RedisUnstableTest.php @@ -178,11 +178,12 @@ class RedisUnstableTest extends PredisProfileTestCase 137 => 'SSCAN', 138 => 'ZSCAN', 139 => 'ZLEXCOUNT', - 140 => 'HSCAN', - 141 => 'PUBSUB', - 142 => 'PFADD', - 143 => 'PFCOUNT', - 144 => 'PFMERGE', + 140 => 'ZRANGEBYLEX', + 141 => 'HSCAN', + 142 => 'PUBSUB', + 143 => 'PFADD', + 144 => 'PFCOUNT', + 145 => 'PFMERGE', ); } } diff --git a/tests/Predis/Profile/RedisVersion280Test.php b/tests/Predis/Profile/RedisVersion280Test.php index 954f5156..0dd39bac 100644 --- a/tests/Predis/Profile/RedisVersion280Test.php +++ b/tests/Predis/Profile/RedisVersion280Test.php @@ -178,11 +178,12 @@ class RedisVersion280Test extends PredisProfileTestCase 137 => 'SSCAN', 138 => 'ZSCAN', 139 => 'ZLEXCOUNT', - 140 => 'HSCAN', - 141 => 'PUBSUB', - 142 => 'PFADD', - 143 => 'PFCOUNT', - 144 => 'PFMERGE', + 140 => 'ZRANGEBYLEX', + 141 => 'HSCAN', + 142 => 'PUBSUB', + 143 => 'PFADD', + 144 => 'PFCOUNT', + 145 => 'PFMERGE', ); } } diff --git a/tests/Predis/Replication/ReplicationStrategyTest.php b/tests/Predis/Replication/ReplicationStrategyTest.php index c306d522..d460ea44 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',