mirror of
https://github.com/predis/predis.git
synced 2026-11-08 18:56:33 +00:00
feat: bless commands family (#1733)
* feat: bless commands family * fix: bless scan args processing
This commit is contained in:
@@ -43,7 +43,7 @@ jobs:
|
||||
run: |
|
||||
# Mapping of original redis versions to client test containers
|
||||
declare -A redis_clients_version_mapping=(
|
||||
["8.12"]="unstable-35791556614-debian"
|
||||
["8.12"]="8.12-m02-int"
|
||||
["8.10"]="8.10.0"
|
||||
["8.8"]="8.8.0"
|
||||
["8.6"]="8.6.1"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## Unreleased
|
||||
### Added
|
||||
- Added optional `CELL_SIZE` argument for `CMS.INITBYDIM` and `CMS.INITBYPROB`
|
||||
- Added `BLESS GET`, `BLESS SET`, `BLESS CLEAR` and `BLESS SCAN` commands (`$client->bless->get($key)`, `->set($key, $flag)`, `->clear($key, $flag)`, `->scan($cursor, $flag, ['COUNT' => $count])`)
|
||||
### Changed
|
||||
### Fixed
|
||||
- Fixed client iteration with master/slave and Sentinel replication (#923)
|
||||
|
||||
@@ -42,6 +42,7 @@ use Predis\Command\Argument\TimeSeries\RangeArguments;
|
||||
use Predis\Command\Argument\TimeSeries\ReadArguments;
|
||||
use Predis\Command\CommandInterface;
|
||||
use Predis\Command\Container\ACL;
|
||||
use Predis\Command\Container\BLESS;
|
||||
use Predis\Command\Container\CLIENT;
|
||||
use Predis\Command\Container\FUNCTIONS;
|
||||
use Predis\Command\Container\HIMPORT;
|
||||
@@ -431,6 +432,7 @@ use Predis\Command\Redis\VADD;
|
||||
* @method $this geosearchstore(string $destination, string $source, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $storeDist = false)
|
||||
*
|
||||
* Container commands
|
||||
* @property BLESS $bless
|
||||
* @property CLIENT $client
|
||||
* @property HIMPORT $himport
|
||||
* @property HOTKEYS $hotkeys
|
||||
|
||||
@@ -42,6 +42,7 @@ use Predis\Command\Argument\TimeSeries\RangeArguments;
|
||||
use Predis\Command\Argument\TimeSeries\ReadArguments;
|
||||
use Predis\Command\CommandInterface;
|
||||
use Predis\Command\Container\ACL;
|
||||
use Predis\Command\Container\BLESS;
|
||||
use Predis\Command\Container\CLIENT;
|
||||
use Predis\Command\Container\FUNCTIONS;
|
||||
use Predis\Command\Container\HIMPORT;
|
||||
@@ -446,6 +447,7 @@ use Predis\Response\Status;
|
||||
* @method int geosearchstore(string $destination, string $source, FromInterface $from, ByInterface $by, ?string $sorting = null, int $count = -1, bool $any = false, bool $storeDist = false)
|
||||
*
|
||||
* Container commands
|
||||
* @property BLESS $bless
|
||||
* @property CLIENT $client
|
||||
* @property HIMPORT $himport
|
||||
* @property HOTKEYS $hotkeys
|
||||
|
||||
@@ -214,6 +214,9 @@ abstract class ClusterStrategy implements StrategyInterface
|
||||
'XSETID' => $getKeyFromFirstArgument,
|
||||
'XTRIM' => $getKeyFromFirstArgument,
|
||||
|
||||
/* commands operating on blessings */
|
||||
'BLESS' => [$this, 'getKeyFromBlessCommands'],
|
||||
|
||||
/* commands operating on vector sets */
|
||||
'VADD' => $getKeyFromFirstArgument,
|
||||
'VCARD' => $getKeyFromFirstArgument,
|
||||
@@ -482,6 +485,28 @@ abstract class ClusterStrategy implements StrategyInterface
|
||||
return $arguments[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the key from BLESS commands.
|
||||
*
|
||||
* BLESS GET/SET/CLEAR operate on a key at position 1, after the subcommand.
|
||||
* BLESS SCAN takes a cursor and a flag instead of a key, so it has no slot
|
||||
* and this returns null.
|
||||
*
|
||||
* @param CommandInterface $command Command instance.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getKeyFromBlessCommands(CommandInterface $command)
|
||||
{
|
||||
$arguments = $command->getArguments();
|
||||
|
||||
if (isset($arguments[0], $arguments[1]) && in_array(strtoupper($arguments[0]), ['GET', 'SET', 'CLEAR'], true)) {
|
||||
return $arguments[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the key from HIMPORT commands.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2026 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\Container;
|
||||
|
||||
/**
|
||||
* @method array get(string $key) Returns the flags currently set on the key.
|
||||
* @method int set(string $key, string $flag) Returns 1 if the flag was added, 0 if it was already set.
|
||||
* @method int clear(string $key, string $flag) Returns 1 if the flag was removed, 0 if it was not set.
|
||||
*/
|
||||
class BLESS extends AbstractContainer
|
||||
{
|
||||
public function getContainerCommandId(): string
|
||||
{
|
||||
return 'BLESS';
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over the keys carrying the given flag.
|
||||
*
|
||||
* Mirrors the plain SCAN command signature: options are passed as an
|
||||
* array, e.g. ['COUNT' => 10].
|
||||
*
|
||||
* @param string|int $cursor Cursor returned by the previous call, 0 to start a new iteration.
|
||||
* @param string $flag Flag to scan for.
|
||||
* @param array|null $options Scan options, supports COUNT.
|
||||
* @return array|mixed [next cursor, list of keys]; an error response when the client runs with exceptions disabled
|
||||
*/
|
||||
public function scan($cursor, string $flag, ?array $options = null)
|
||||
{
|
||||
return $this->__call('SCAN', func_get_args());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2026 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\PrefixableCommand as RedisCommand;
|
||||
|
||||
/**
|
||||
* @see https://redis.io/commands/?name=bless
|
||||
*
|
||||
* Container command corresponds to any BLESS *.
|
||||
* Represents any BLESS command with subcommand as first argument.
|
||||
*/
|
||||
class BLESS extends RedisCommand
|
||||
{
|
||||
private const KEYED_SUBCOMMANDS = ['GET', 'SET', 'CLEAR'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return 'BLESS';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setArguments(array $arguments)
|
||||
{
|
||||
if (isset($arguments[0]) && strtoupper($arguments[0]) === 'SCAN') {
|
||||
$this->setScanArguments($arguments);
|
||||
} else {
|
||||
parent::setArguments($arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BLESS SCAN cursor flag [COUNT count].
|
||||
*
|
||||
* Mirrors the plain SCAN command: options are passed as an array
|
||||
* (e.g. ['COUNT' => 10]) and expanded into Redis modifiers.
|
||||
*
|
||||
* @param array $arguments
|
||||
* @return void
|
||||
*/
|
||||
private function setScanArguments(array $arguments): void
|
||||
{
|
||||
if (count($arguments) === 4) {
|
||||
$options = array_pop($arguments);
|
||||
if (is_array($options)) {
|
||||
$options = $this->prepareScanOptions($options);
|
||||
$arguments = array_merge($arguments, $options);
|
||||
}
|
||||
}
|
||||
|
||||
parent::setArguments($arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of SCAN options and modifiers compatible with Redis.
|
||||
*
|
||||
* @param array $options List of options.
|
||||
* @return array
|
||||
*/
|
||||
private function prepareScanOptions(array $options): array
|
||||
{
|
||||
$options = array_change_key_case($options, CASE_UPPER);
|
||||
$normalized = [];
|
||||
|
||||
if (!empty($options['COUNT'])) {
|
||||
$normalized[] = 'COUNT';
|
||||
$normalized[] = $options['COUNT'];
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
public function prefixKeys($prefix)
|
||||
{
|
||||
$arguments = $this->getArguments();
|
||||
|
||||
// BLESS GET/SET/CLEAR operate on a key at index 1, right after the subcommand.
|
||||
// BLESS SCAN takes a cursor and a flag, never a key.
|
||||
if (isset($arguments[0], $arguments[1]) && in_array(strtoupper($arguments[0]), self::KEYED_SUBCOMMANDS, true)) {
|
||||
$arguments[1] = $prefix . $arguments[1];
|
||||
$this->setRawArguments($arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,6 +257,7 @@ class PredisStrategyTest extends PredisTestCase
|
||||
$arguments = [
|
||||
'XGROUP' => ['CREATE', 'key', 'group', '$'],
|
||||
'XINFO' => ['GROUPS', 'key'],
|
||||
'BLESS' => ['GET', 'key'],
|
||||
];
|
||||
|
||||
foreach ($this->getExpectedCommands('keys-stream-subcommand') as $commandID) {
|
||||
@@ -773,6 +774,9 @@ class PredisStrategyTest extends PredisTestCase
|
||||
'XSETID' => 'keys-first',
|
||||
'XTRIM' => 'keys-first',
|
||||
|
||||
/* commands operating on blessings */
|
||||
'BLESS' => 'keys-stream-subcommand',
|
||||
|
||||
/* commands operating on vector sets */
|
||||
'VADD' => 'keys-vector',
|
||||
'VCARD' => 'keys-first',
|
||||
|
||||
@@ -240,6 +240,7 @@ class RedisStrategyTest extends PredisTestCase
|
||||
$arguments = [
|
||||
'XGROUP' => ['CREATE', 'key', 'group', '$'],
|
||||
'XINFO' => ['GROUPS', 'key'],
|
||||
'BLESS' => ['GET', 'key'],
|
||||
];
|
||||
|
||||
foreach ($this->getExpectedCommands('keys-stream-subcommand') as $commandID) {
|
||||
@@ -793,6 +794,9 @@ class RedisStrategyTest extends PredisTestCase
|
||||
'XSETID' => 'keys-first',
|
||||
'XTRIM' => 'keys-first',
|
||||
|
||||
/* commands operating on blessings */
|
||||
'BLESS' => 'keys-stream-subcommand',
|
||||
|
||||
/* commands operating on vector sets */
|
||||
'VADD' => 'keys-vector',
|
||||
'VCARD' => 'keys-first',
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Predis package.
|
||||
*
|
||||
* (c) 2009-2020 Daniele Alessandri
|
||||
* (c) 2021-2026 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\Container\BLESS as Container;
|
||||
use Predis\Response\ServerException;
|
||||
|
||||
/**
|
||||
* @group commands
|
||||
* @group realm-generic
|
||||
*/
|
||||
class BLESS_Test extends PredisCommandTestCase
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedCommand(): string
|
||||
{
|
||||
return BLESS::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedId(): string
|
||||
{
|
||||
return 'BLESS';
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @dataProvider argumentsProvider
|
||||
*/
|
||||
public function testFilterArguments(array $actualArguments, array $expectedArguments): void
|
||||
{
|
||||
$command = $this->getCommand();
|
||||
$command->setArguments($actualArguments);
|
||||
|
||||
$this->assertSame($expectedArguments, $command->getArguments());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
*/
|
||||
public function testParseResponse(): void
|
||||
{
|
||||
$this->assertSame([], $this->getCommand()->parseResponse([]));
|
||||
$this->assertSame(['a', 'b'], $this->getCommand()->parseResponse(['a', 'b']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @dataProvider prefixKeysProvider
|
||||
*/
|
||||
public function testPrefixKeys(array $arguments, array $expectedArguments): void
|
||||
{
|
||||
$command = $this->getCommand();
|
||||
$command->setArguments($arguments);
|
||||
$command->prefixKeys('prefix:');
|
||||
|
||||
$this->assertSame($expectedArguments, $command->getArguments());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group disconnected
|
||||
* @dataProvider containerProvider
|
||||
*/
|
||||
public function testContainerBuildsSubcommand(string $method, array $methodArguments, array $expectedArguments, $response): void
|
||||
{
|
||||
$client = $this->getMockBuilder('Predis\ClientInterface')->getMock();
|
||||
$command = $this->getCommand();
|
||||
|
||||
// The container forwards the subcommand followed by the raw method arguments;
|
||||
// argument normalisation (e.g. COUNT expansion) happens inside the command.
|
||||
$rawArguments = array_merge([strtoupper($method)], $methodArguments);
|
||||
|
||||
$client
|
||||
->expects($this->once())
|
||||
->method('createCommand')
|
||||
->with('BLESS', $rawArguments)
|
||||
->willReturnCallback(function () use ($command, $rawArguments) {
|
||||
$command->setArguments($rawArguments);
|
||||
|
||||
return $command;
|
||||
});
|
||||
|
||||
$client
|
||||
->expects($this->once())
|
||||
->method('executeCommand')
|
||||
->with($command)
|
||||
->willReturn($response);
|
||||
|
||||
$container = new Container($client);
|
||||
|
||||
$this->assertSame('BLESS', $container->getContainerCommandId());
|
||||
$this->assertSame($response, $container->$method(...$methodArguments));
|
||||
$this->assertSame($expectedArguments, $command->getArguments());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @requiresRedisVersion >= 8.12.0
|
||||
*/
|
||||
public function testSetGetScanAndClearFlags(): void
|
||||
{
|
||||
$this->assertBlessRoundTrip($this->getClient());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @requiresRedisVersion >= 8.12.0
|
||||
*/
|
||||
public function testSetGetScanAndClearFlagsResp3(): void
|
||||
{
|
||||
$this->assertBlessRoundTrip($this->getResp3Client());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @requiresRedisVersion >= 8.12.0
|
||||
*/
|
||||
public function testThrowsExceptionOnMissingKey(): void
|
||||
{
|
||||
$redis = $this->getClient();
|
||||
|
||||
$this->expectException(ServerException::class);
|
||||
$this->expectExceptionMessage('ERR no such key');
|
||||
|
||||
$redis->bless->get('missing');
|
||||
}
|
||||
|
||||
/**
|
||||
* @group connected
|
||||
* @requiresRedisVersion >= 8.12.0
|
||||
*/
|
||||
public function testThrowsExceptionOnUnknownFlag(): void
|
||||
{
|
||||
$redis = $this->getClient();
|
||||
$redis->set('foo', 'bar');
|
||||
|
||||
$this->expectException(ServerException::class);
|
||||
$this->expectExceptionMessage('ERR syntax error');
|
||||
|
||||
$redis->bless->set('foo', 'BOGUS');
|
||||
}
|
||||
|
||||
private function assertBlessRoundTrip($redis): void
|
||||
{
|
||||
$redis->set('foo', 'bar');
|
||||
$redis->set('baz', 'qux');
|
||||
$redis->set('plain', 'value');
|
||||
|
||||
$this->assertSame([], $redis->bless->get('foo'));
|
||||
|
||||
$this->assertSame(1, $redis->bless->set('foo', 'NO-EVICT'));
|
||||
$this->assertSame(0, $redis->bless->set('foo', 'NO-EVICT'));
|
||||
$this->assertSame(1, $redis->bless->set('baz', 'NO-EVICT'));
|
||||
|
||||
$this->assertSame(['NO-EVICT'], $redis->bless->get('foo'));
|
||||
$this->assertSame([], $redis->bless->get('plain'));
|
||||
|
||||
// Full scan in one call.
|
||||
[$cursor, $keys] = $redis->bless->scan(0, 'NO-EVICT');
|
||||
$this->assertSame('0', $cursor);
|
||||
$this->assertEqualsCanonicalizing(['foo', 'baz'], $keys);
|
||||
|
||||
// Iterating with COUNT must eventually visit every flagged key (duplicates are allowed, as with SCAN).
|
||||
$cursor = 0;
|
||||
$seen = [];
|
||||
do {
|
||||
[$cursor, $keys] = $redis->bless->scan($cursor, 'NO-EVICT', ['COUNT' => 1]);
|
||||
$seen = array_merge($seen, $keys);
|
||||
} while ($cursor !== '0');
|
||||
$this->assertEqualsCanonicalizing(['foo', 'baz'], array_unique($seen));
|
||||
|
||||
$this->assertSame(1, $redis->bless->clear('foo', 'NO-EVICT'));
|
||||
$this->assertSame(0, $redis->bless->clear('foo', 'NO-EVICT'));
|
||||
$this->assertSame([], $redis->bless->get('foo'));
|
||||
|
||||
[$cursor, $keys] = $redis->bless->scan(0, 'NO-EVICT');
|
||||
$this->assertSame('0', $cursor);
|
||||
$this->assertSame(['baz'], $keys);
|
||||
}
|
||||
|
||||
public function argumentsProvider(): array
|
||||
{
|
||||
return [
|
||||
'with GET subcommand' => [
|
||||
['GET', 'key'],
|
||||
['GET', 'key'],
|
||||
],
|
||||
'with SET subcommand' => [
|
||||
['SET', 'key', 'flag'],
|
||||
['SET', 'key', 'flag'],
|
||||
],
|
||||
'with CLEAR subcommand' => [
|
||||
['CLEAR', 'key', 'flag'],
|
||||
['CLEAR', 'key', 'flag'],
|
||||
],
|
||||
'with lowercase subcommand' => [
|
||||
['get', 'key'],
|
||||
['get', 'key'],
|
||||
],
|
||||
'with SCAN subcommand' => [
|
||||
['SCAN', 0, 'flag'],
|
||||
['SCAN', 0, 'flag'],
|
||||
],
|
||||
'with SCAN subcommand and COUNT option' => [
|
||||
['SCAN', 0, 'flag', ['COUNT' => 10]],
|
||||
['SCAN', 0, 'flag', 'COUNT', 10],
|
||||
],
|
||||
'with SCAN subcommand and lowercase option key' => [
|
||||
['SCAN', '17', 'flag', ['count' => 10]],
|
||||
['SCAN', '17', 'flag', 'COUNT', 10],
|
||||
],
|
||||
'with SCAN subcommand and empty options' => [
|
||||
['SCAN', 0, 'flag', []],
|
||||
['SCAN', 0, 'flag'],
|
||||
],
|
||||
'with SCAN subcommand and unknown option' => [
|
||||
['SCAN', 0, 'flag', ['MATCH' => 'x*']],
|
||||
['SCAN', 0, 'flag'],
|
||||
],
|
||||
'with lowercase SCAN subcommand and COUNT option' => [
|
||||
['scan', 0, 'flag', ['COUNT' => 10]],
|
||||
['scan', 0, 'flag', 'COUNT', 10],
|
||||
],
|
||||
'with SCAN subcommand and null options' => [
|
||||
['SCAN', 0, 'flag', null],
|
||||
['SCAN', 0, 'flag'],
|
||||
],
|
||||
'with SCAN subcommand and raw modifiers' => [
|
||||
['SCAN', 0, 'flag', 'COUNT', 10],
|
||||
['SCAN', 0, 'flag', 'COUNT', 10],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function prefixKeysProvider(): array
|
||||
{
|
||||
return [
|
||||
'with GET subcommand' => [
|
||||
['GET', 'key'],
|
||||
['GET', 'prefix:key'],
|
||||
],
|
||||
'with SET subcommand' => [
|
||||
['SET', 'key', 'flag'],
|
||||
['SET', 'prefix:key', 'flag'],
|
||||
],
|
||||
'with CLEAR subcommand' => [
|
||||
['CLEAR', 'key', 'flag'],
|
||||
['CLEAR', 'prefix:key', 'flag'],
|
||||
],
|
||||
'with lowercase subcommand' => [
|
||||
['set', 'key', 'flag'],
|
||||
['set', 'prefix:key', 'flag'],
|
||||
],
|
||||
'with keyed subcommand and no key' => [
|
||||
['GET'],
|
||||
['GET'],
|
||||
],
|
||||
'with SCAN subcommand (cursor is not a key)' => [
|
||||
['SCAN', 0, 'flag', ['COUNT' => 10]],
|
||||
['SCAN', 0, 'flag', 'COUNT', 10],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function containerProvider(): array
|
||||
{
|
||||
return [
|
||||
'get' => ['get', ['key'], ['GET', 'key'], []],
|
||||
'set' => ['set', ['key', 'flag'], ['SET', 'key', 'flag'], 1],
|
||||
'clear' => ['clear', ['key', 'flag'], ['CLEAR', 'key', 'flag'], 1],
|
||||
'scan' => ['scan', [0, 'flag'], ['SCAN', 0, 'flag'], ['0', ['key1', 'key2']]],
|
||||
'scan with count' => ['scan', [0, 'flag', ['COUNT' => 10]], ['SCAN', 0, 'flag', 'COUNT', 10], ['0', ['key1']]],
|
||||
'scan with explicit null options' => ['scan', [0, 'flag', null], ['SCAN', 0, 'flag'], ['0', ['key1', 'key2']]],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user