Extend Sorted Set support by implementing ZRANGESTORE command (#829)

* Added support for ZRANGESTORE command

* Added missing test coverage, added new filter method

* Added test coverage for new traits

* Updated offset for unexpected value tests

* Added missing offset

* Removed version annotation for inconsistency

* Update ZDIFF.php

* Update ZRANGESTORE.php

Co-authored-by: Vladyslav Vildanov <vladyslavvildanov@Vladyslav-Vildanov-MacBook-Pro.local>
Co-authored-by: Till Krüss <tillkruss@users.noreply.github.com>
This commit is contained in:
Vladyslav Vildanov
2022-12-06 18:40:06 +02:00
committed by GitHub
parent 57a24413c2
commit 9f3bdd5616
11 changed files with 694 additions and 0 deletions
+1
View File
@@ -119,6 +119,7 @@ use Predis\Command\CommandInterface;
* @method $this zrandmember(string $key, int $count = 1, bool $withScores = false)
* @method $this zrange($key, $start, $stop, array $options = null)
* @method $this zrangebyscore($key, $min, $max, array $options = null)
* @method $this zrangestore(string $destination, string $source, int|string $min, string|int $max, string|bool $by = false, bool $reversed = false, bool $limit = false, int $offset = 0, int $count = 0)
* @method $this zrank($key, $member)
* @method $this zrem($key, $member)
* @method $this zremrangebyrank($key, $start, $stop)
+1
View File
@@ -137,6 +137,7 @@ use Predis\Response\Status;
* @method mixed zrandmember(string $key, int $count = 1, bool $withScores = false)
* @method array zrange(string $key, int|string $start, int|string $stop, array $options = null)
* @method array zrangebyscore(string $key, int|string $min, int|string $max, array $options = null)
* @method int zrangestore(string $destination, string $source, int|string $min, int|string $max, string|bool $by = false, bool $reversed = false, bool $limit = false, int $offset = 0, int $count = 0)
* @method int|null zrank(string $key, string $member)
* @method int zrem(string $key, string ...$member)
* @method int zremrangebyrank(string $key, int|string $start, int|string $stop)
+12
View File
@@ -114,4 +114,16 @@ abstract class Command implements CommandInterface
return $arguments;
}
/**
* Remove all false values from arguments.
*
* @return void
*/
public function filterArguments(): void
{
$this->arguments = array_filter($this->arguments, static function ($argument) {
return $argument !== false;
});
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
use Predis\Command\Traits\ByLexByScore;
use Predis\Command\Traits\Limit;
use Predis\Command\Traits\Rev;
/**
* @link https://redis.io/commands/zrangestore/
*
* This command is like ZRANGE, but stores the result in the destination key.
*/
class ZRANGESTORE extends RedisCommand
{
use ByLexByScore {
ByLexByScore::setArguments as setByLexByScoreArgument;
}
use Rev {
Rev::setArguments as setReversedArgument;
}
use Limit {
Limit::setArguments as setLimitArguments;
}
protected static $byLexByScoreArgumentPositionOffset = 4;
protected static $revArgumentPositionOffset = 5;
protected static $limitArgumentPositionOffset = 6;
public function getId()
{
return 'ZRANGESTORE';
}
public function setArguments(array $arguments)
{
$this->setByLexByScoreArgument($arguments);
$arguments = $this->getArguments();
$this->setReversedArgument($arguments);
$arguments = $this->getArguments();
$this->setLimitArguments($arguments);
$this->filterArguments();
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace Predis\Command\Traits;
use UnexpectedValueException;
trait ByLexByScore
{
private static $argumentsEnum = [
'bylex' => 'BYLEX',
'byscore' => 'BYSCORE',
];
public function setArguments(array $arguments)
{
$argument = $arguments[static::$byLexByScoreArgumentPositionOffset];
if (false === $argument) {
parent::setArguments($arguments);
return;
}
if (is_string($argument) && in_array(strtoupper($argument), self::$argumentsEnum)) {
$argument = self::$argumentsEnum[$argument];
} else {
throw new UnexpectedValueException("By argument accepts only \"bylex\" and \"byscore\" values");
}
$argumentsBefore = array_slice($arguments, 0, static::$byLexByScoreArgumentPositionOffset);
$argumentsAfter = array_slice($arguments, static::$byLexByScoreArgumentPositionOffset + 1);
parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter));
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Predis\Command\Traits;
use UnexpectedValueException;
trait Limit
{
public function setArguments(array $arguments)
{
$argument = $arguments[static::$limitArgumentPositionOffset];
$argumentsBefore = array_slice($arguments, 0, static::$limitArgumentPositionOffset);
if (false === $argument) {
parent::setArguments($argumentsBefore);
return;
}
if (true === $argument) {
$argument = 'LIMIT';
} else {
throw new UnexpectedValueException('Wrong limit argument type');
}
$argumentsAfter = array_slice($arguments, static::$limitArgumentPositionOffset + 1);
parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter));
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Predis\Command\Traits;
use UnexpectedValueException;
trait Rev
{
public function setArguments(array $arguments)
{
$argument = $arguments[static::$revArgumentPositionOffset];
if (false === $argument) {
parent::setArguments($arguments);
return;
}
if (true === $argument) {
$argument = 'REV';
} else {
throw new UnexpectedValueException('Wrong rev argument type');
}
$argumentsBefore = array_slice($arguments, 0, static::$revArgumentPositionOffset);
$argumentsAfter = array_slice($arguments, static::$revArgumentPositionOffset + 1);
parent::setArguments(array_merge($argumentsBefore, [$argument], $argumentsAfter));
}
}
@@ -0,0 +1,272 @@
<?php
namespace Predis\Command\Redis;
use UnexpectedValueException;
class ZRANGESTORE_Test extends PredisCommandTestCase
{
/**
* @inheritDoc
*/
protected function getExpectedCommand(): string
{
return ZRANGESTORE::class;
}
/**
* @inheritDoc
*/
protected function getExpectedId(): string
{
return 'ZRANGESTORE';
}
/**
* @group disconnected
* @dataProvider argumentsProvider
*/
public function testFilterArguments(array $actualArguments, array $expectedArguments): void
{
$command = $this->getCommand();
$command->setArguments($actualArguments);
$this->assertSameValues($expectedArguments, $command->getArguments());
}
/**
* @group disconnected
*/
public function testParseResponse(): void
{
$this->assertSame(1, $this->getCommand()->parseResponse(1));
}
/**
* @group connected
* @dataProvider rangesProvider
* @param array $actualSortedSet
* @param int|string $min
* @param int|string $max
* @param string|bool $by
* @param bool $rev
* @param bool $limit
* @param int $offset
* @param int $count
* @param int $expectedResultingElements
* @param array $expectedResponse
* @requiresRedisVersion >= 6.2.0
* @return void
*/
public function testStoresSortedSetRanges(
array $actualSortedSet,
$min,
$max,
$by,
bool $rev,
bool $limit,
int $offset,
int $count,
int $expectedResultingElements,
array $expectedResponse
): void {
$redis = $this->getClient();
$redis->zadd('source', ...$actualSortedSet);
$actualResponse = $redis->zrangestore(
'destination',
'source',
$min,
$max,
$by,
$rev,
$limit,
$offset,
$count
);
$this->assertSame($expectedResultingElements, $actualResponse);
$this->assertSame($expectedResponse, $redis->zrange('destination', 0, -1));
}
/**
* @group connected
* @dataProvider unexpectedValuesProvider
* @param int|string $min
* @param int|string $max
* @param string|bool $by
* @param $rev
* @param $limit
* @param int $offset
* @param int $count
* @param string $expectedExceptionMessage
* @return void
* @requiresRedisVersion >= 6.2.0
*/
public function testThrowsExceptionOnUnexpectedValuesGiven(
$min,
$max,
$by,
$rev,
$limit,
int $offset,
int $count,
string $expectedExceptionMessage
): void {
$redis = $this->getClient();
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage($expectedExceptionMessage);
$redis->zrangestore(
'destination',
'source',
$min,
$max,
$by,
$rev,
$limit,
$offset,
$count
);
}
public function argumentsProvider(): array
{
return [
'without optional arguments' => [
['destination', 'source', 0, -1, false, false, false, 0, 0],
['destination', 'source', 0, -1],
],
'with BYLEX argument' => [
['destination', 'source', 0, -1, 'bylex', false, false, 0, 0],
['destination', 'source', 0, -1, 'BYLEX'],
],
'with BYSCORE argument' => [
['destination', 'source', 0, -1, 'byscore', false, false, 0, 0],
['destination', 'source', 0, -1, 'BYSCORE'],
],
'with REV argument' => [
['destination', 'source', 0, -1, false, true, false, 0, 0],
['destination', 'source', 0, -1, 'REV'],
],
'with BYSCORE/BYLEX and LIMIT argument' => [
['destination', 'source', 0, -1, 'byscore', false, true, 0, 1],
['destination', 'source', 0, -1, 'BYSCORE', 'LIMIT', 0, 1],
],
'with BYSCORE/BYLEX argument and REV argument' => [
['destination', 'source', 0, -1, 'byscore', true, false, 0, 0],
['destination', 'source', 0, -1, 'BYSCORE', 'REV'],
],
'with BYSCORE/BYLEX argument, REV argument and LIMIT' => [
['destination', 'source', 0, -1, 'bylex', true, true, 0, 1],
['destination', 'source', 0, -1, 'BYLEX', 'REV', 'LIMIT', 0, 1],
]
];
}
public function rangesProvider(): array
{
return [
'without optional arguments' => [
[1, 'member1', 2, 'member2', 3, 'member3'],
0,
-1,
false,
false,
false,
0,
0,
3,
['member1', 'member2', 'member3'],
],
'with BYLEX argument' => [
[1, 'abc', 1, 'abb', 1, 'aaa'],
'[aaa',
'[abc',
'bylex',
false,
false,
0,
0,
3,
['aaa', 'abb', 'abc'],
],
'with BYSCORE argument' => [
[3, 'member1', 2, 'member2', 1, 'member3'],
'1',
'(4',
'byscore',
false,
false,
0,
0,
3,
['member3', 'member2', 'member1'],
],
'with REV argument' => [
[3, 'member1', 2, 'member2', 1, 'member3'],
0,
2,
false,
true,
false,
0,
0,
3,
['member3', 'member2', 'member1'],
],
'with BYSCORE/BYLEX and LIMIT argument' => [
[1, 'member1', 2, 'member2', 3, 'member3'],
'1',
'(4',
'byscore',
false,
true,
0,
1,
1,
['member1'],
],
'with BYSCORE/BYLEX argument and REV argument' => [
[3, 'member1', 2, 'member2', 1, 'member3'],
3,
0,
'byscore',
true,
false,
0,
0,
3,
['member3', 'member2', 'member1'],
],
'with BYSCORE/BYLEX argument, REV argument and LIMIT' => [
[3, 'member1', 2, 'member2', 1, 'member3'],
3,
0,
'byscore',
true,
true,
0,
2,
2,
['member2', 'member1'],
],
];
}
public function unexpectedValuesProvider(): array
{
return [
'wrong BY argument value' => [
0, -1, 'wrong value', false, false, 0, 0, "By argument accepts only \"bylex\" and \"byscore\" values",
],
'wrong REV argument type' => [
0, -1, false, 'wrong value', false, 0, 0, 'Wrong rev argument type',
],
'wrong LIMIT argument type' => [
0, -1, false, false, 'wrong value', 0, 0, 'Wrong limit argument type'
]
];
}
}
@@ -0,0 +1,93 @@
<?php
namespace Predis\Command\Traits;
use PredisTestCase;
use Predis\Command\Command as RedisCommand;
use UnexpectedValueException;
class ByLexByScoreTest extends PredisTestCase
{
private $testClass;
protected function setUp(): void
{
parent::setUp();
$this->testClass = new class extends RedisCommand {
use ByLexByScore;
public static $byLexByScoreArgumentPositionOffset = 0;
public function getId()
{
return 'test';
}
};
}
/**
* @dataProvider argumentsProvider
* @param int $offset
* @param array $actualArguments
* @param array $expectedArguments
* @return void
*/
public function testReturnsCorrectArguments(int $offset, array $actualArguments, array $expectedArguments): void
{
$this->testClass::$byLexByScoreArgumentPositionOffset = $offset;
$this->testClass->setArguments($actualArguments);
$this->assertSame($expectedArguments, $this->testClass->getArguments());
}
/**
* @dataProvider unexpectedValuesProvider
* @param array $actualArguments
* @return void
*/
public function testThrowsExceptionOnUnexpectedValue(array $actualArguments): void
{
$this->testClass::$byLexByScoreArgumentPositionOffset = 0;
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage("By argument accepts only \"bylex\" and \"byscore\" values");
$this->testClass->setArguments($actualArguments);
}
public function argumentsProvider(): array
{
return [
'by false argument' => [
0,
[false, 'second argument', 'third argument'],
[false, 'second argument', 'third argument']
],
'by argument first and there is arguments after' => [
0,
['bylex', 'second argument', 'third argument'],
['BYLEX', 'second argument', 'third argument']
],
'by argument last and there is arguments before' => [
2,
['first argument', 'second argument', 'byscore'],
['first argument', 'second argument', 'BYSCORE']
],
'by argument not the first and not the last' => [
1,
['first argument', 'byscore', 'third argument'],
['first argument', 'BYSCORE', 'third argument']
],
];
}
public function unexpectedValuesProvider(): array
{
return [
'true argument' => [[true]],
'string argument, not BYLEX/BYSCORE' => [['wrong argument']]
];
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace Predis\Command\Traits;
use PredisTestCase;
use Predis\Command\Command as RedisCommand;
use UnexpectedValueException;
class LimitTest extends PredisTestCase
{
private $testClass;
protected function setUp(): void
{
parent::setUp();
$this->testClass = new class extends RedisCommand {
use Limit;
public static $limitArgumentPositionOffset = 0;
public function getId()
{
return 'test';
}
};
}
/**
* @dataProvider argumentsProvider
* @param int $offset
* @param array $actualArguments
* @param array $expectedArguments
* @return void
*/
public function testReturnsCorrectArguments(int $offset, array $actualArguments, array $expectedArguments): void
{
$this->testClass::$limitArgumentPositionOffset = $offset;
$this->testClass->setArguments($actualArguments);
$this->assertSame($expectedArguments, $this->testClass->getArguments());
}
/**
* @return void
*/
public function testThrowsExceptionOnUnexpectedValue(): void
{
$this->testClass::$limitArgumentPositionOffset = 0;
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage("Wrong limit argument type");
$this->testClass->setArguments(['test']);
}
public function argumentsProvider(): array
{
return [
'limit false argument first and there is arguments after' => [
0,
[false, 'second argument', 'third argument'],
[]
],
'limit false argument last and there is arguments before' => [
2,
['first argument', 'second argument', false],
['first argument', 'second argument'],
],
'limit false argument not the first and not the last' => [
1,
['first argument', false, 'third argument'],
['first argument'],
],
'limit argument first and there is arguments after' => [
0,
[true, 'second argument', 'third argument'],
['LIMIT', 'second argument', 'third argument']
],
'limit argument last and there is arguments before' => [
2,
['first argument', 'second argument', true],
['first argument', 'second argument', 'LIMIT']
],
'limit argument not the first and not the last' => [
1,
['first argument', true, 'third argument'],
['first argument', 'LIMIT', 'third argument']
],
];
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
namespace Predis\Command\Traits;
use Predis\Command\Command as RedisCommand;
use PredisTestCase;
use UnexpectedValueException;
class RevTest extends PredisTestCase
{
private $testClass;
protected function setUp(): void
{
parent::setUp();
$this->testClass = new class extends RedisCommand {
use Rev;
public static $revArgumentPositionOffset = 0;
public function getId()
{
return 'test';
}
};
}
/**
* @dataProvider argumentsProvider
* @param int $offset
* @param array $actualArguments
* @param array $expectedArguments
* @return void
*/
public function testReturnsCorrectArguments(int $offset, array $actualArguments, array $expectedArguments): void
{
$this->testClass::$revArgumentPositionOffset = $offset;
$this->testClass->setArguments($actualArguments);
$this->assertSame($expectedArguments, $this->testClass->getArguments());
}
/**
* @return void
*/
public function testThrowsExceptionOnUnexpectedValue(): void
{
$this->testClass::$revArgumentPositionOffset = 0;
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage("Wrong rev argument type");
$this->testClass->setArguments(['test']);
}
public function argumentsProvider(): array
{
return [
'rev false argument' => [
0,
[false, 'second argument', 'third argument'],
[false, 'second argument', 'third argument']
],
'rev argument first and there is arguments after' => [
0,
[true, 'second argument', 'third argument'],
['REV', 'second argument', 'third argument']
],
'rev argument last and there is arguments before' => [
2,
['first argument', 'second argument', true],
['first argument', 'second argument', 'REV']
],
'rev argument not the first and not the last' => [
1,
['first argument', true, 'third argument'],
['first argument', 'REV', 'third argument']
],
];
}
}