From 3362e474b66c4de8b5639991ec61bae8984a0566 Mon Sep 17 00:00:00 2001 From: Daniele Alessandri Date: Mon, 23 May 2016 16:16:54 +0200 Subject: [PATCH] Add new command: BITFIELD (Redis 3.2.0). --- src/ClientContextInterface.php | 1 + src/ClientInterface.php | 1 + src/Cluster/ClusterStrategy.php | 1 + src/Command/Processor/KeyPrefixProcessor.php | 1 + src/Command/StringBitField.php | 28 ++++ src/Profile/RedisVersion320.php | 1 + src/Replication/ReplicationStrategy.php | 26 +++ tests/Predis/Cluster/PredisStrategyTest.php | 1 + tests/Predis/Cluster/RedisStrategyTest.php | 1 + .../Processor/KeyPrefixProcessorTest.php | 5 + tests/Predis/Command/StringBitFieldTest.php | 155 ++++++++++++++++++ tests/Predis/Profile/RedisUnstableTest.php | 1 + tests/Predis/Profile/RedisVersion320Test.php | 1 + .../Replication/ReplicationStrategyTest.php | 46 ++++++ 14 files changed, 269 insertions(+) create mode 100644 src/Command/StringBitField.php create mode 100644 tests/Predis/Command/StringBitFieldTest.php diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 9c69f5cb..e2625bcc 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -38,6 +38,7 @@ use Predis\Command\CommandInterface; * @method $this append($key, $value) * @method $this bitcount($key, $start = null, $end = null) * @method $this bitop($operation, $destkey, $key) + * @method $this bitfield($key, ...) * @method $this decr($key) * @method $this decrby($key, $decrement) * @method $this get($key) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 8126ffec..4c65df0e 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -46,6 +46,7 @@ use Predis\Profile\ProfileInterface; * @method int append($key, $value) * @method int bitcount($key, $start = null, $end = null) * @method int bitop($operation, $destkey, $key) + * @method array bitfield($key, ...) * @method int decr($key) * @method int decrby($key, $decrement) * @method string get($key) diff --git a/src/Cluster/ClusterStrategy.php b/src/Cluster/ClusterStrategy.php index 2e845521..b07eb333 100644 --- a/src/Cluster/ClusterStrategy.php +++ b/src/Cluster/ClusterStrategy.php @@ -80,6 +80,7 @@ abstract class ClusterStrategy implements StrategyInterface 'SUBSTR' => $getKeyFromFirstArgument, 'BITOP' => array($this, 'getKeyFromBitOp'), 'BITCOUNT' => $getKeyFromFirstArgument, + 'BITFIELD' => $getKeyFromFirstArgument, /* commands operating on lists */ 'LINSERT' => $getKeyFromFirstArgument, diff --git a/src/Command/Processor/KeyPrefixProcessor.php b/src/Command/Processor/KeyPrefixProcessor.php index f982a8fe..d20078df 100644 --- a/src/Command/Processor/KeyPrefixProcessor.php +++ b/src/Command/Processor/KeyPrefixProcessor.php @@ -159,6 +159,7 @@ class KeyPrefixProcessor implements ProcessorInterface 'BITPOS' => 'static::first', /* ---------------- Redis 3.2 ---------------- */ 'HSTRLEN' => 'static::first', + 'BITFIELD' => 'static::first', ); } diff --git a/src/Command/StringBitField.php b/src/Command/StringBitField.php new file mode 100644 index 00000000..9f4deaa6 --- /dev/null +++ b/src/Command/StringBitField.php @@ -0,0 +1,28 @@ + + * + * 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/bitfield + * + * @author Daniele Alessandri + */ +class StringBitField extends Command +{ + /** + * {@inheritdoc} + */ + public function getId() + { + return 'BITFIELD'; + } +} diff --git a/src/Profile/RedisVersion320.php b/src/Profile/RedisVersion320.php index 4c01fb39..7c7a4342 100644 --- a/src/Profile/RedisVersion320.php +++ b/src/Profile/RedisVersion320.php @@ -267,6 +267,7 @@ class RedisVersion320 extends RedisProfile /* commands operating on hashes */ 'HSTRLEN' => 'Predis\Command\HashStringLength', + 'BITFIELD' => 'Predis\Command\StringBitField', ); } } diff --git a/src/Replication/ReplicationStrategy.php b/src/Replication/ReplicationStrategy.php index 0b769405..a1c5840f 100644 --- a/src/Replication/ReplicationStrategy.php +++ b/src/Replication/ReplicationStrategy.php @@ -104,6 +104,31 @@ class ReplicationStrategy return ($c = count($arguments)) === 1 ? true : $arguments[$c - 2] !== 'STORE'; } + /** + * Checks if BITFIELD performs a read-only operation by looking for certain + * SET and INCRYBY modifiers in the arguments array of the command. + * + * @param CommandInterface $command Command instance. + * + * @return bool + */ + protected function isBitfieldReadOnly(CommandInterface $command) + { + $arguments = $command->getArguments(); + $argc = count($arguments); + + if ($argc >= 2) { + for ($i = 1; $i < $argc; $i++) { + $argument = strtoupper($arguments[$i]); + if ($argument === 'SET' || $argument === 'INCRBY') { + return false; + } + } + } + + return true; + } + /** * Marks a command as a read-only operation. * @@ -232,6 +257,7 @@ class ReplicationStrategy 'TIME' => true, 'PFCOUNT' => true, 'SORT' => array($this, 'isSortReadOnly'), + 'BITFIELD' => array($this, 'isBitfieldReadOnly'), ); } } diff --git a/tests/Predis/Cluster/PredisStrategyTest.php b/tests/Predis/Cluster/PredisStrategyTest.php index 6e944fe4..43d6b155 100644 --- a/tests/Predis/Cluster/PredisStrategyTest.php +++ b/tests/Predis/Cluster/PredisStrategyTest.php @@ -292,6 +292,7 @@ class PredisStrategyTest extends PredisTestCase 'SUBSTR' => 'keys-first', 'BITOP' => 'keys-bitop', 'BITCOUNT' => 'keys-first', + 'BITFIELD' => 'keys-first', /* commands operating on lists */ 'LINSERT' => 'keys-first', diff --git a/tests/Predis/Cluster/RedisStrategyTest.php b/tests/Predis/Cluster/RedisStrategyTest.php index d8b9a544..3a03a4ab 100644 --- a/tests/Predis/Cluster/RedisStrategyTest.php +++ b/tests/Predis/Cluster/RedisStrategyTest.php @@ -302,6 +302,7 @@ class RedisStrategyTest extends PredisTestCase 'SUBSTR' => 'keys-first', 'BITOP' => 'keys-bitop', 'BITCOUNT' => 'keys-first', + 'BITFIELD' => 'keys-first', /* commands operating on lists */ 'LINSERT' => 'keys-first', diff --git a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php index 0f415cd8..8a754659 100644 --- a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php +++ b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php @@ -868,6 +868,11 @@ class KeyPrefixProcessorTest extends PredisTestCase array('key1', 'key2', 'key3'), array('prefix:key1', 'prefix:key2', 'prefix:key3'), ), + /* ---------------- Redis 3.2 ---------------- */ + array('BITFIELD', + array('key', 'GET', 'u8', '0', 'SET', 'u8', '0', '1'), + array('prefix:key', 'GET', 'u8', '0', 'SET', 'u8', '0', '1'), + ), ); } } diff --git a/tests/Predis/Command/StringBitFieldTest.php b/tests/Predis/Command/StringBitFieldTest.php new file mode 100644 index 00000000..244ed0e0 --- /dev/null +++ b/tests/Predis/Command/StringBitFieldTest.php @@ -0,0 +1,155 @@ + + * + * 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-string + */ +class StringBitFieldTest extends PredisCommandTestCase +{ + /** + * {@inheritdoc} + */ + protected function getExpectedCommand() + { + return 'Predis\Command\StringBitField'; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedId() + { + return 'BITFIELD'; + } + + /** + * @group disconnected + */ + public function testFilterArguments() + { + $arguments = array('key'); + $expected = array('key'); + + $command = $this->getCommand(); + $command->setArguments($arguments); + + $this->assertSame($expected, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testFilterMultipleArguments() + { + $arguments = array('key', 'incrby', 'u2', '100', '1', 'OVERFLOW', 'SAT', 'incrby', 'u2', '102', '1', 'GET', 'u2', '100'); + $expected = array('key', 'incrby', 'u2', '100', '1', 'OVERFLOW', 'SAT', 'incrby', 'u2', '102', '1', 'GET', 'u2', '100'); + + $command = $this->getCommand(); + $command->setArguments($arguments); + + $this->assertSame($expected, $command->getArguments()); + } + + /** + * @group disconnected + */ + public function testParseResponse() + { + $raw = array(1); + $expected = array(1); + + $command = $this->getCommand(); + + $this->assertSame($expected, $command->parseResponse($raw)); + } + + /** + * @group disconnected + */ + public function testParseResponseComplex() + { + $raw = array(1, 0, 3); + $expected = array(1, 0, 3); + + $command = $this->getCommand(); + + $this->assertSame($expected, $command->parseResponse($raw)); + } + + /** + * @group connected + * @requiresRedisVersion >= 3.2.0 + */ + public function testBitfieldWithGetModifier() + { + $redis = $this->getClient(); + + $redis->setbit('string', 0, 1); + $redis->setbit('string', 8, 1); + + $this->assertSame(array(128), $redis->bitfield('string', 'GET', 'u8', 0)); + $this->assertSame(array(128, 1, 128), $redis->bitfield('string', 'GET', 'u8', 0, 'GET', 'u8', 1, 'GET', 'u8', 8)); + } + + /** + * @group connected + * @requiresRedisVersion >= 3.2.0 + */ + public function testBitfieldWithSetModifier() + { + $redis = $this->getClient(); + + $redis->setbit('string', 0, 1); + $redis->setbit('string', 8, 1); + + $this->assertSame(array(128), $redis->bitfield('string', 'SET', 'u8', 0, 1)); + $this->assertSame(array(1, 128), $redis->bitfield('string', 'SET', 'u8', 0, 128, 'SET', 'u8', 8, 1)); + $this->assertSame(array(1, 128), $redis->bitfield('string', 'SET', 'u8', 8, 128, 'GET', 'u8', 8)); + + $this->assertSame("\x80\x80", $redis->get('string')); + } + + /** + * @group connected + * @requiresRedisVersion >= 3.2.0 + */ + public function testBitfieldWithIncrbyModifier() + { + $redis = $this->getClient(); + + $redis->setbit('string', 0, 1); + $redis->setbit('string', 8, 1); + + $this->assertSame(array(138), $redis->bitfield('string', 'INCRBY', 'u8', 0, 10)); + $this->assertSame(array(143, 128), $redis->bitfield('string', 'INCRBY', 'u8', 0, 5, 'INCRBY', 'u8', 0, -15)); + + $this->assertSame("\x80\x80", $redis->get('string')); + } + + /** + * @group connected + * @requiresRedisVersion >= 3.2.0 + * @expectedException \Predis\Response\ServerException + * @expectedExceptionMessage Operation against a key holding the wrong kind of value + */ + public function testThrowsExceptionOnWrongType() + { + $this->markTestSkipped('Currently skipped due issues in Redis (see antirez/redis#3259).'); + + $redis = $this->getClient(); + + $redis->lpush('metavars', 'foo'); + $redis->bitfield('metavars', 'SET', 'u4', '0', '1'); + } +} diff --git a/tests/Predis/Profile/RedisUnstableTest.php b/tests/Predis/Profile/RedisUnstableTest.php index 77059386..2bf6eadc 100644 --- a/tests/Predis/Profile/RedisUnstableTest.php +++ b/tests/Predis/Profile/RedisUnstableTest.php @@ -190,6 +190,7 @@ class RedisUnstableTest extends PredisProfileTestCase 149 => 'PFMERGE', 150 => 'COMMAND', 151 => 'HSTRLEN', + 152 => 'BITFIELD', ); } } diff --git a/tests/Predis/Profile/RedisVersion320Test.php b/tests/Predis/Profile/RedisVersion320Test.php index d5f641f5..31bee7b8 100644 --- a/tests/Predis/Profile/RedisVersion320Test.php +++ b/tests/Predis/Profile/RedisVersion320Test.php @@ -190,6 +190,7 @@ class RedisVersion320Test extends PredisProfileTestCase 149 => 'PFMERGE', 150 => 'COMMAND', 151 => 'HSTRLEN', + 152 => 'BITFIELD', ); } } diff --git a/tests/Predis/Replication/ReplicationStrategyTest.php b/tests/Predis/Replication/ReplicationStrategyTest.php index 02d00472..d07b3e53 100644 --- a/tests/Predis/Replication/ReplicationStrategyTest.php +++ b/tests/Predis/Replication/ReplicationStrategyTest.php @@ -94,6 +94,51 @@ class ReplicationStrategyTest extends PredisTestCase ); } + /** + * @group disconnected + */ + public function testBitFieldCommand() + { + $profile = Profile\Factory::getDevelopment(); + $strategy = new ReplicationStrategy(); + + $command = $profile->createCommand('BITFIELD', array('key')); + $this->assertTrue( + $strategy->isReadOperation($command), + 'BITFIELD with no modifiers is expected to be a read operation.' + ); + + $command = $profile->createCommand('BITFIELD', array('key', 'GET', 'u4', '0')); + $this->assertTrue( + $strategy->isReadOperation($command), + 'BITFIELD with GET only is expected to be a read operation.' + ); + + $command = $profile->createCommand('BITFIELD', array('key', 'SET', 'u4', '0', 1)); + $this->assertFalse( + $strategy->isReadOperation($command), + 'BITFIELD with SET is expected to be a write operation.' + ); + + $command = $profile->createCommand('BITFIELD', array('key', 'INCRBY', 'u4', '0', 1)); + $this->assertFalse( + $strategy->isReadOperation($command), + 'BITFIELD with INCRBY is expected to be a write operation.' + ); + + $command = $profile->createCommand('BITFIELD', array('key', 'GET', 'u4', '0', 'INCRBY', 'u4', '0', 1)); + $this->assertFalse( + $strategy->isReadOperation($command), + 'BITFIELD with GET and INCRBY is expected to be a write operation.' + ); + + $command = $profile->createCommand('BITFIELD', array('key', 'GET', 'u4', '0', 'SET', 'u4', '0', 1)); + $this->assertFalse( + $strategy->isReadOperation($command), + 'BITFIELD with GET and SET is expected to be a write operation.' + ); + } + /** * @group disconnected * @expectedException \Predis\NotSupportedException @@ -316,6 +361,7 @@ class ReplicationStrategyTest extends PredisTestCase 'SETRANGE' => 'write', 'STRLEN' => 'read', 'SUBSTR' => 'read', + 'BITFIELD' => 'variable', /* commands operating on lists */ 'LINSERT' => 'write',