From bc5892e2609d2d70b435c3c74fe4e3ced23307ad Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Mon, 12 Dec 2022 18:38:26 +0200 Subject: [PATCH] Extended Sorted Set support by adding BZPOPMIN command (#862) * Refactored zinterstore, zunionstore commands and command traits * Merge conflicts resolve, update cluster strategy test with new arguments * Updated assertion in case if command executed faster then duration minimal threshold * Updated Keys trait to handle cases when no numkeys modifier needed * Added support for BZPOPMIN command * Added command link and description Co-authored-by: Vladyslav Vildanov --- src/ClientContextInterface.php | 1 + src/ClientInterface.php | 1 + src/Command/Redis/BZPOPMIN.php | 46 +++++++ src/Command/Traits/Keys.php | 11 +- tests/Predis/Command/Redis/BZPOPMIN_Test.php | 122 +++++++++++++++++++ tests/Predis/Command/Traits/KeysTest.php | 23 +++- 6 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 src/Command/Redis/BZPOPMIN.php create mode 100644 tests/Predis/Command/Redis/BZPOPMIN_Test.php diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index 8600a2fd..9a884fb3 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -40,6 +40,7 @@ use Predis\Command\CommandInterface; * @method $this bitop($operation, $destkey, $key) * @method $this bitfield($key, $subcommand, ...$subcommandArg) * @method $this bitpos($key, $bit, $start = null, $end = null) + * @method $this bzpopmin(array $keys, int $timeout) * @method $this bzmpop(int $timeout, array $keys, string $modifier = 'min', int $count = 1) * @method $this decr($key) * @method $this decrby($key, $decrement) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 31c731ea..463d3eae 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -49,6 +49,7 @@ use Predis\Response\Status; * @method int bitop($operation, $destkey, $key) * @method array|null bitfield(string $key, $subcommand, ...$subcommandArg) * @method int bitpos(string $key, $bit, $start = null, $end = null) + * @method array bzpopmin(array $keys, int $timeout) * @method array bzmpop(int $timeout, array $keys, string $modifier = 'min', int $count = 1) * @method int decr(string $key) * @method int decrby(string $key, int $decrement) diff --git a/src/Command/Redis/BZPOPMIN.php b/src/Command/Redis/BZPOPMIN.php new file mode 100644 index 00000000..2f89a2c5 --- /dev/null +++ b/src/Command/Redis/BZPOPMIN.php @@ -0,0 +1,46 @@ +setKeys($arguments, false); + } + + public function parseResponse($data) + { + $key = array_shift($data); + + if (null === $key) { + return [$key]; + } + + return array_combine([$key], [[$data[0] => $data[1]]]); + } +} diff --git a/src/Command/Traits/Keys.php b/src/Command/Traits/Keys.php index ce76aa88..1bbe758f 100644 --- a/src/Command/Traits/Keys.php +++ b/src/Command/Traits/Keys.php @@ -10,7 +10,7 @@ use UnexpectedValueException; */ trait Keys { - public function setArguments(array $arguments) + public function setArguments(array $arguments, bool $withNumkeys = true) { $argumentsLength = count($arguments); @@ -22,10 +22,15 @@ trait Keys } $keysArgument = $arguments[static::$keysArgumentPositionOffset]; - $numkeys = count($keysArgument); $argumentsBeforeKeys = array_slice($arguments, 0, static::$keysArgumentPositionOffset); $argumentsAfterKeys = array_slice($arguments, static::$keysArgumentPositionOffset + 1); - parent::setArguments(array_merge($argumentsBeforeKeys, [$numkeys], $keysArgument, $argumentsAfterKeys)); + if ($withNumkeys) { + $numkeys = count($keysArgument); + parent::setArguments(array_merge($argumentsBeforeKeys, [$numkeys], $keysArgument, $argumentsAfterKeys)); + return; + } + + parent::setArguments(array_merge($argumentsBeforeKeys, $keysArgument, $argumentsAfterKeys)); } } diff --git a/tests/Predis/Command/Redis/BZPOPMIN_Test.php b/tests/Predis/Command/Redis/BZPOPMIN_Test.php new file mode 100644 index 00000000..d58e0142 --- /dev/null +++ b/tests/Predis/Command/Redis/BZPOPMIN_Test.php @@ -0,0 +1,122 @@ +getCommand(); + $command->setArguments($actualArguments); + + $this->assertSame($expectedArguments, $command->getArguments()); + } + + /** + * @group disconnected + * @dataProvider responsesProvider + */ + public function testParseResponse(array $actualResponse, array $expectedResponse): void + { + $this->assertSame($expectedResponse, $this->getCommand()->parseResponse($actualResponse)); + } + + /** + * @group connected + * @return void + * @requiresRedisVersion >= 5.0.0 + */ + public function testReturnsPoppedMinElementFromGivenNonEmptySortedSet(): void + { + $redis = $this->getClient(); + $sortedSetDictionary = [1, 'member1', 2, 'member2', 3, 'member3']; + $expectedResponse = ['test-bzpopmin' => ['member1' => '1']]; + $expectedModifiedSortedSet = ['member2', 'member3']; + + $redis->zadd('test-bzpopmin', ...$sortedSetDictionary); + + $this->assertSame($expectedResponse, $redis->bzpopmin(['empty sorted set','test-bzpopmin'], 0)); + $this->assertSame($expectedModifiedSortedSet, $redis->zrange('test-bzpopmin', 0, -1)); + } + + /** + * @group connected + * @return void + * @requiresRedisVersion >= 5.0.0 + */ + public function testThrowsExceptionOnUnexpectedValueGiven(): void + { + $redis = $this->getClient(); + + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Wrong keys argument type or position offset'); + + $redis->bzpopmin(1, 0); + } + + /** + * @group connected + * @requiresRedisVersion >= 5.0.0 + */ + public function testThrowsExceptionOnWrongType(): void + { + $this->expectException(ServerException::class); + $this->expectExceptionMessage('Operation against a key holding the wrong kind of value'); + + $redis = $this->getClient(); + + $redis->set('bzpopmin_foo', 'bar'); + $redis->bzpopmin(['bzpopmin_foo'], 0); + } + + public function argumentsProvider(): array + { + return [ + 'with one key' => [ + [['key1'], 1], + ['key1', 1] + ], + 'with multiple keys' => [ + [['key1', 'key2', 'key3'], 1], + ['key1', 'key2', 'key3', 1] + ], + ]; + } + + public function responsesProvider(): array + { + return [ + 'null-element array' => [ + [null], + [null] + ], + 'three-element array' => [ + ['key', 'member', 'score'], + ['key' => ['member' => 'score']] + ], + ]; + } +} diff --git a/tests/Predis/Command/Traits/KeysTest.php b/tests/Predis/Command/Traits/KeysTest.php index e74d4ef9..3cc9add3 100644 --- a/tests/Predis/Command/Traits/KeysTest.php +++ b/tests/Predis/Command/Traits/KeysTest.php @@ -29,15 +29,20 @@ class KeysTest extends PredisTestCase /** * @dataProvider argumentsProvider * @param int $offset + * @param bool $withNumkeys * @param array $actualArguments * @param array $expectedArguments * @return void */ - public function testReturnsCorrectArguments(int $offset, array $actualArguments, array $expectedArguments): void - { + public function testReturnsCorrectArguments( + int $offset, + bool $withNumkeys, + array $actualArguments, + array $expectedArguments + ): void { $this->testClass::$keysArgumentPositionOffset = $offset; - $this->testClass->setArguments($actualArguments); + $this->testClass->setArguments($actualArguments, $withNumkeys); $this->assertSame($expectedArguments, $this->testClass->getArguments()); } @@ -63,24 +68,34 @@ class KeysTest extends PredisTestCase return [ 'keys argument first and there is arguments after' => [ 0, + true, [['key1', 'key2'], 'second argument', 'third argument'], [2, 'key1', 'key2', 'second argument', 'third argument'] ], 'keys argument last and there is arguments before' => [ 2, + true, ['first argument', 'second argument', ['key1', 'key2']], ['first argument', 'second argument', 2, 'key1', 'key2'] ], 'keys argument not the first and not the last' => [ 1, + true, ['first argument', ['key1', 'key2'], 'third argument'], ['first argument', 2, 'key1', 'key2', 'third argument'] ], 'keys argument the only argument' => [ 0, + true, [['key1', 'key2']], [2, 'key1', 'key2'] - ] + ], + 'without numkeys modifier' => [ + 0, + false, + [['key1', 'key2']], + ['key1', 'key2'], + ], ]; }