Extended core support by implementing SORT_RO command (#1044)

* Added support for SORT_RO command

* Codestyle fixes

* Added command description

---------

Co-authored-by: Vladyslav Vildanov <vladyslavvildanov@Vladyslav-Vildanov-MacBook-Pro.local>
This commit is contained in:
Vladyslav Vildanov
2023-01-29 21:38:30 +02:00
committed by GitHub
parent 50e005bfe3
commit db65e5ebd3
18 changed files with 738 additions and 6 deletions
+2
View File
@@ -14,6 +14,7 @@ namespace Predis;
use Predis\Command\Argument\Geospatial\ByInterface;
use Predis\Command\Argument\Geospatial\FromInterface;
use Predis\Command\Argument\Server\LimitOffsetCount;
use Predis\Command\Argument\Server\To;
use Predis\Command\CommandInterface;
@@ -39,6 +40,7 @@ use Predis\Command\CommandInterface;
* @method $this renamenx($key, $target)
* @method $this scan($cursor, array $options = null)
* @method $this sort($key, array $options = null)
* @method $this sort_ro(string $key, ?string $byPattern = null, ?LimitOffsetCount $limit = null, array $getPatterns = [], ?string $sorting = null, bool $alpha = false)
* @method $this ttl($key)
* @method $this type($key)
* @method $this append($key, $value)
+2
View File
@@ -14,6 +14,7 @@ namespace Predis;
use Predis\Command\Argument\Geospatial\ByInterface;
use Predis\Command\Argument\Geospatial\FromInterface;
use Predis\Command\Argument\Server\LimitOffsetCount;
use Predis\Command\Argument\Server\To;
use Predis\Command\CommandInterface;
use Predis\Command\FactoryInterface;
@@ -48,6 +49,7 @@ use Predis\Response\Status;
* @method int renamenx(string $key, string $target)
* @method array scan($cursor, array $options = null)
* @method array sort(string $key, array $options = null)
* @method array sort_ro(string $key, ?string $byPattern = null, ?LimitOffsetCount $limit = null, array $getPatterns = [], ?string $sorting = null, bool $alpha = false)
* @method int ttl(string $key)
* @method mixed type(string $key)
* @method int append(string $key, $value)
@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Server;
use Predis\Command\Argument\ArrayableArgument;
interface LimitInterface extends ArrayableArgument
{
}
@@ -0,0 +1,42 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Argument\Server;
class LimitOffsetCount implements LimitInterface
{
private const KEYWORD = 'LIMIT';
/**
* @var int
*/
private $offset;
/**
* @var int
*/
private $count;
public function __construct(int $offset, int $count)
{
$this->offset = $offset;
$this->count = $count;
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
return [self::KEYWORD, $this->offset, $this->count];
}
}
+1 -1
View File
@@ -120,7 +120,7 @@ abstract class Command implements CommandInterface
public function filterArguments(): void
{
$this->arguments = array_filter($this->arguments, static function ($argument) {
return $argument !== false;
return $argument !== false && $argument !== null;
});
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
use Predis\Command\Traits\Keys;
use Predis\Command\Traits\Limit;
use Predis\Command\Traits\Limit\Limit;
class SINTERCARD extends RedisCommand
{
+74
View File
@@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
use Predis\Command\Traits\By\ByArgument;
use Predis\Command\Traits\Get\Get;
use Predis\Command\Traits\Limit\LimitObject;
use Predis\Command\Traits\Sorting;
/**
* @see https://redis.io/commands/sort_ro/
*
* Read-only variant of the SORT command.
* It is exactly like the original SORT but refuses the STORE option
* and can safely be used in read-only replicas.
*/
class SORT_RO extends RedisCommand
{
use ByArgument {
ByArgument::setArguments as setBy;
}
use LimitObject {
LimitObject::setArguments as setLimit;
}
use Get {
Get::setArguments as setGetArgument;
}
use Sorting {
Sorting::setArguments as setSorting;
}
protected static $byArgumentPositionOffset = 1;
protected static $getArgumentPositionOffset = 3;
protected static $sortArgumentPositionOffset = 4;
public function getId()
{
return 'SORT_RO';
}
public function setArguments(array $arguments)
{
$alpha = array_pop($arguments);
if (is_bool($alpha) && $alpha) {
$arguments[] = 'ALPHA';
} elseif (!is_bool($alpha)) {
$arguments[] = $alpha;
}
$this->setSorting($arguments);
$arguments = $this->getArguments();
$this->setGetArgument($arguments);
$arguments = $this->getArguments();
$this->setLimit($arguments);
$arguments = $this->getArguments();
$this->setBy($arguments);
$this->filterArguments();
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
use Predis\Command\Traits\Keys;
use Predis\Command\Traits\Limit;
use Predis\Command\Traits\Limit\Limit;
/**
* @see https://redis.io/commands/zintercard/
+1 -1
View File
@@ -14,7 +14,7 @@ namespace Predis\Command\Redis;
use Predis\Command\Command as RedisCommand;
use Predis\Command\Traits\By\ByLexByScore;
use Predis\Command\Traits\Limit;
use Predis\Command\Traits\Limit\Limit;
use Predis\Command\Traits\Rev;
/**
+40
View File
@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Traits\By;
use Predis\Command\Command;
/**
* @mixin Command
*/
trait ByArgument
{
private $byModifier = 'BY';
public function setArguments(array $arguments)
{
$argumentsLength = count($arguments);
if (static::$byArgumentPositionOffset >= $argumentsLength || null === $arguments[static::$byArgumentPositionOffset]) {
parent::setArguments($arguments);
return;
}
$argument = $arguments[static::$byArgumentPositionOffset];
$argumentsBefore = array_slice($arguments, 0, static::$byArgumentPositionOffset);
$argumentsAfter = array_slice($arguments, static::$byArgumentPositionOffset + 1);
parent::setArguments(array_merge($argumentsBefore, [$this->byModifier, $argument], $argumentsAfter));
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Traits\Get;
use UnexpectedValueException;
trait Get
{
private static $getModifier = 'GET';
public function setArguments(array $arguments)
{
$argumentsLength = count($arguments);
if (static::$getArgumentPositionOffset >= $argumentsLength) {
parent::setArguments($arguments);
return;
}
if (!is_array($arguments[static::$getArgumentPositionOffset])) {
throw new UnexpectedValueException('Wrong get argument type');
}
$patterns = [];
foreach ($arguments[static::$getArgumentPositionOffset] as $pattern) {
$patterns[] = self::$getModifier;
$patterns[] = $pattern;
}
$argumentsBeforeKeys = array_slice($arguments, 0, static::$getArgumentPositionOffset);
$argumentsAfterKeys = array_slice($arguments, static::$getArgumentPositionOffset + 1);
parent::setArguments(array_merge($argumentsBeforeKeys, $patterns, $argumentsAfterKeys));
}
}
@@ -10,7 +10,7 @@
* file that was distributed with this source code.
*/
namespace Predis\Command\Traits;
namespace Predis\Command\Traits\Limit;
use Predis\Command\Command;
use UnexpectedValueException;
+50
View File
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Traits\Limit;
use Predis\Command\Argument\Server\LimitInterface;
trait LimitObject
{
public function setArguments(array $arguments)
{
$argumentPositionOffset = $this->getLimitArgumentPositionOffset($arguments);
if (null === $argumentPositionOffset) {
parent::setArguments($arguments);
return;
}
$limitObject = $arguments[$argumentPositionOffset];
$argumentsBefore = array_slice($arguments, 0, $argumentPositionOffset);
$argumentsAfter = array_slice($arguments, $argumentPositionOffset + 1);
parent::setArguments(array_merge(
$argumentsBefore,
$limitObject->toArray(),
$argumentsAfter
));
}
private function getLimitArgumentPositionOffset(array $arguments): ?int
{
foreach ($arguments as $i => $value) {
if ($value instanceof LimitInterface) {
return $i;
}
}
return null;
}
}
+233
View File
@@ -0,0 +1,233 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Redis;
use Predis\Command\Argument\Server\LimitOffsetCount;
use UnexpectedValueException;
/**
* @group commands
* @group realm-set
*/
class SORT_RO_Test extends PredisCommandTestCase
{
/**
* {@inheritdoc}
*/
protected function getExpectedCommand(): string
{
return SORT_RO::class;
}
/**
* {@inheritdoc}
*/
protected function getExpectedId(): string
{
return 'SORT_RO';
}
/**
* @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 listProvider
* @param array $listArguments
* @param array $sortArguments
* @param array $expectedSortedResponse
* @return void
* @requiresRedisVersion >= 7.0.0
*/
public function testSortItemsWithinGivenList(
array $listArguments,
array $sortArguments,
array $expectedSortedResponse
): void {
$redis = $this->getClient();
$redis->lpush(...$listArguments);
$this->assertSame($expectedSortedResponse, $redis->sort_ro(...$sortArguments));
}
/**
* @group connected
* @dataProvider listsProvider
* @param array $localKeys
* @param array $externalKeys
* @param array $sortArguments
* @param array $expectedSortedResponse
* @return void
* @requiresRedisVersion >= 7.0.0
*/
public function testSortItemsWithExternalKeysWithinGivenList(
array $localKeys,
array $externalKeys,
array $sortArguments,
array $expectedSortedResponse
): void {
$redis = $this->getClient();
$redis->lpush(...$localKeys);
$redis->mset(...$externalKeys);
$this->assertSame($expectedSortedResponse, $redis->sort_ro(...$sortArguments));
}
/**
* @group connected
* @dataProvider unexpectedValuesProvider
* @param array $arguments
* @param string $expectedExceptionMessage
* @return void
* @requiresRedisVersion >= 7.0.0
*/
public function testThrowsExceptionOnUnexpectedValueGiven(
array $arguments,
string $expectedExceptionMessage
): void {
$redis = $this->getClient();
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage($expectedExceptionMessage);
$redis->sort_ro(...$arguments);
}
public function argumentsProvider(): array
{
return [
'with default arguments' => [
['key'],
['key'],
],
'with BY argument' => [
['key', 'pattern'],
['key', 'BY', 'pattern'],
],
'with LIMIT argument' => [
['key', null, new LimitOffsetCount(0, 1)],
['key', 'LIMIT', 0, 1],
],
'with GET patterns' => [
['key', null, null, ['pattern1', 'pattern2']],
['key', 'GET', 'pattern1', 'GET', 'pattern2'],
],
'with sorting argument - ASC' => [
['key', null, null, [], 'asc'],
['key', 'ASC'],
],
'with sorting argument - DESC' => [
['key', null, null, [], 'desc'],
['key', 'DESC'],
],
'with ALPHA argument' => [
['key', null, null, [], null, true],
['key', 'ALPHA'],
],
'with all arguments argument' => [
['key', 'pattern', new LimitOffsetCount(0, 1), ['pattern1', 'pattern2'], 'asc', true],
['key', 'BY', 'pattern', 'LIMIT', 0, 1, 'GET', 'pattern1', 'GET', 'pattern2', 'ASC', 'ALPHA'],
],
];
}
public function listProvider(): array
{
return [
'without any modifiers' => [
['key', 2, 1],
['key', null, null, [], null, false],
['1', '2'],
],
'with LIMIT modifier' => [
['key', 2, 1, 4, 15, 3, 36],
['key', null, new LimitOffsetCount(1, 2), [], null, false],
['2', '3'],
],
'with sorting - ASC' => [
['key', 2, 1],
['key', null, null, [], 'asc', false],
['1', '2'],
],
'with sorting - DESC' => [
['key', 2, 1],
['key', null, null, [], 'desc', false],
['2', '1'],
],
'with sorting lexicographically' => [
['key', 'abc', 'aab', 'abb'],
['key', null, null, [], null, true],
['aab', 'abb', 'abc'],
],
'with all arguments for single list' => [
['key', 'abc', 'aab', 'abb'],
['key', null, new LimitOffsetCount(0, 2), [], 'desc', true],
['abc', 'abb'],
],
];
}
public function listsProvider(): array
{
return [
'sorted by external keys - returns local keys' => [
['uid', 1, 2, 3, 4, 5],
['points_1', 500, 'points_2', 200, 'points_3', 300, 'points_4', 400, 'points_5', 100],
['uid', 'points_*', null, [], null, false],
['5', '2', '3', '4', '1'],
],
'sorted by external keys - returns external keys' => [
['uid', 1, 2, 3, 4, 5],
[
'points_1', 500, 'points_2', 200, 'points_3', 300, 'points_4', 400, 'points_5', 100,
'user_1', 'User1', 'user_2', 'User2', 'user_3', 'User3', 'user_4', 'User4', 'user_5', 'User5',
],
['uid', 'points_*', null, ['user_*'], null, false],
['User5', 'User2', 'User3', 'User4', 'User1'],
],
];
}
public function unexpectedValuesProvider(): array
{
return [
'wrong GET argument type' => [
['key', null, null, 'wrong', null, false],
'Wrong get argument type',
],
'wrong sorting argument type' => [
['key', null, null, [], 'wrong', false],
'Sorting argument accepts only: asc, desc values',
],
];
}
}
@@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Traits\By;
use Predis\Command\Command as RedisCommand;
use PredisTestCase;
class ByArgumentTest extends PredisTestCase
{
private $testClass;
protected function setUp(): void
{
parent::setUp();
$this->testClass = new class() extends RedisCommand {
use ByArgument;
public static $byArgumentPositionOffset = 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::$byArgumentPositionOffset = $offset;
$this->testClass->setArguments($actualArguments);
$this->assertSame($expectedArguments, $this->testClass->getArguments());
}
public function argumentsProvider(): array
{
return [
'with wrong offset' => [
1,
['key'],
['key'],
],
'with null value' => [
0,
[null],
[null],
],
'with any value' => [
0,
['value'],
['BY', 'value'],
],
];
}
}
@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Traits\Get;
use Predis\Command\Command as RedisCommand;
use PredisTestCase;
use UnexpectedValueException;
class GetTest extends PredisTestCase
{
private $testClass;
protected function setUp(): void
{
parent::setUp();
$this->testClass = new class() extends RedisCommand {
use Get;
public static $getArgumentPositionOffset = 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::$getArgumentPositionOffset = $offset;
$this->testClass->setArguments($actualArguments);
$this->assertSame($expectedArguments, $this->testClass->getArguments());
}
/**
* @return void
*/
public function testThrowsExceptionOnUnexpectedValue(): void
{
$this->expectException(UnexpectedValueException::class);
$this->expectExceptionMessage('Wrong get argument type');
$this->testClass->setArguments(['wrong']);
}
public function argumentsProvider(): array
{
return [
'with wrong offset' => [
1,
['value'],
['value'],
],
'with single value' => [
0,
[['value']],
['GET', 'value'],
],
'with multiple values' => [
0,
[['value1', 'value2']],
['GET', 'value1', 'GET', 'value2'],
],
];
}
}
@@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2023 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Command\Traits\Limit;
use Predis\Command\Argument\Server\LimitOffsetCount;
use Predis\Command\Command as RedisCommand;
use PredisTestCase;
class LimitObjectTest extends PredisTestCase
{
private $testClass;
protected function setUp(): void
{
parent::setUp();
$this->testClass = new class() extends RedisCommand {
use LimitObject;
public function getId()
{
return 'test';
}
};
}
/**
* @dataProvider argumentsProvider
* @param array $actualArguments
* @param array $expectedArguments
* @return void
*/
public function testReturnsCorrectArguments(array $actualArguments, array $expectedArguments): void
{
$this->testClass->setArguments($actualArguments);
$this->assertSame($expectedArguments, $this->testClass->getArguments());
}
public function argumentsProvider(): array
{
return [
'with non-existing LimitInterface' => [
['value'],
['value'],
],
'with existing LimitInterface' => [
[new LimitOffsetCount(0, 1)],
['LIMIT', 0, 1],
],
];
}
}
@@ -10,7 +10,7 @@
* file that was distributed with this source code.
*/
namespace Predis\Command\Traits;
namespace Predis\Command\Traits\Limit;
use Predis\Command\Command as RedisCommand;
use PredisTestCase;