From 0bf4ba1c9f40633783a84f7621b60da988a0a86b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Kel=C4=8D=C3=A1k?= Date: Wed, 5 Aug 2026 10:57:53 +0200 Subject: [PATCH] Added `OBJECT` and hash field expiration commands to ClusterStrategy (#1711) Co-authored-by: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> --- CHANGELOG.md | 1 + src/Cluster/ClusterStrategy.php | 30 ++++++++++ tests/Predis/Cluster/PredisStrategyTest.php | 63 +++++++++++++++++++++ tests/Predis/Cluster/RedisStrategyTest.php | 63 +++++++++++++++++++++ 4 files changed, 157 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc124f8f..8afe6ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Added support for `FT.ALIASLIST` command - Added stream commands to ClusterStrategy - Added vector sets commands to ClusterStrategy +- Added `OBJECT` and hash field expiration commands to ClusterStrategy ### Fixed - Fixed Sentinel does not wipe servers on exception caused (#1694) diff --git a/src/Cluster/ClusterStrategy.php b/src/Cluster/ClusterStrategy.php index d73a620a..d8b2c65f 100644 --- a/src/Cluster/ClusterStrategy.php +++ b/src/Cluster/ClusterStrategy.php @@ -54,6 +54,7 @@ abstract class ClusterStrategy implements StrategyInterface 'SORT' => [$this, 'getKeyFromSortCommand'], 'DUMP' => $getKeyFromFirstArgument, 'RESTORE' => $getKeyFromFirstArgument, + 'OBJECT' => [$this, 'getKeyFromObjectCommand'], 'FLUSHDB' => [$this, 'getFakeKey'], /* commands operating on string values */ @@ -179,6 +180,17 @@ abstract class ClusterStrategy implements StrategyInterface 'HVALS' => $getKeyFromFirstArgument, 'HSCAN' => $getKeyFromFirstArgument, 'HSTRLEN' => $getKeyFromFirstArgument, + 'HEXPIRE' => $getKeyFromFirstArgument, + 'HEXPIREAT' => $getKeyFromFirstArgument, + 'HPERSIST' => $getKeyFromFirstArgument, + 'HPEXPIRE' => $getKeyFromFirstArgument, + 'HPEXPIREAT' => $getKeyFromFirstArgument, + 'HTTL' => $getKeyFromFirstArgument, + 'HPTTL' => $getKeyFromFirstArgument, + 'HEXPIRETIME' => $getKeyFromFirstArgument, + 'HPEXPIRETIME' => $getKeyFromFirstArgument, + 'HGETEX' => $getKeyFromFirstArgument, + 'HGETDEL' => $getKeyFromFirstArgument, /* commands operating on streams */ 'XACK' => $getKeyFromFirstArgument, @@ -392,6 +404,24 @@ abstract class ClusterStrategy implements StrategyInterface return $firstKey; } + /** + * Extracts the key from the OBJECT command, where it follows the subcommand. + * + * @param CommandInterface $command Command instance. + * + * @return string|null + */ + protected function getKeyFromObjectCommand(CommandInterface $command) + { + $arguments = $command->getArguments(); + + if (!isset($arguments[1])) { + return null; + } + + return $arguments[1]; + } + /** * Extracts the key from commands where the first two arguments are keys, * followed by non-key arguments (e.g. LMOVEM). diff --git a/tests/Predis/Cluster/PredisStrategyTest.php b/tests/Predis/Cluster/PredisStrategyTest.php index c874202e..f3dcd877 100644 --- a/tests/Predis/Cluster/PredisStrategyTest.php +++ b/tests/Predis/Cluster/PredisStrategyTest.php @@ -350,6 +350,57 @@ class PredisStrategyTest extends PredisTestCase } } + /** + * @group disconnected + */ + public function testKeysForHashFieldCommands(): void + { + $strategy = $this->getClusterStrategy(); + $commands = $this->getCommandFactory(); + + $arguments = [ + 'HEXPIRE' => ['key', 60, ['field']], + 'HEXPIREAT' => ['key', 1893456000, ['field']], + 'HPERSIST' => ['key', ['field']], + 'HPEXPIRE' => ['key', 60000, ['field']], + 'HPEXPIREAT' => ['key', 1893456000000, ['field']], + 'HTTL' => ['key', ['field']], + 'HPTTL' => ['key', ['field']], + 'HEXPIRETIME' => ['key', ['field']], + 'HPEXPIRETIME' => ['key', ['field']], + 'HGETEX' => ['key', ['field']], + 'HGETDEL' => ['key', ['field']], + ]; + + foreach ($this->getExpectedCommands('keys-hash-field') as $commandID) { + $command = $commands->create($commandID, $arguments[$commandID]); + $this->assertSame($strategy->getSlotByKey('key'), $strategy->getSlot($command), $commandID); + } + } + + /** + * @group disconnected + */ + public function testKeysForObjectSubcommand(): void + { + $strategy = $this->getClusterStrategy(); + + $command = $this->getCommandFactory()->create('OBJECT', ['ENCODING', 'key']); + + $this->assertSame($strategy->getSlotByKey('key'), $strategy->getSlot($command)); + } + + /** + * @group disconnected + */ + public function testReturnsNullOnObjectSubcommandWithoutKey(): void + { + $strategy = $this->getClusterStrategy(); + $command = $this->getCommandFactory()->create('OBJECT', ['HELP']); + + $this->assertNull($strategy->getSlot($command)); + } + /** * @group disconnected */ @@ -539,6 +590,7 @@ class PredisStrategyTest extends PredisTestCase 'SORT' => 'variable', 'DUMP' => 'keys-first', 'RESTORE' => 'keys-first', + 'OBJECT' => 'keys-object-subcommand', 'FLUSHDB' => 'keys-fake', /* commands operating on string values */ @@ -664,6 +716,17 @@ class PredisStrategyTest extends PredisTestCase 'HVALS' => 'keys-first', 'HSCAN' => 'keys-first', 'HSTRLEN' => 'keys-first', + 'HEXPIRE' => 'keys-hash-field', + 'HEXPIREAT' => 'keys-hash-field', + 'HPERSIST' => 'keys-hash-field', + 'HPEXPIRE' => 'keys-hash-field', + 'HPEXPIREAT' => 'keys-hash-field', + 'HTTL' => 'keys-hash-field', + 'HPTTL' => 'keys-hash-field', + 'HEXPIRETIME' => 'keys-hash-field', + 'HPEXPIRETIME' => 'keys-hash-field', + 'HGETEX' => 'keys-hash-field', + 'HGETDEL' => 'keys-hash-field', /* commands operating on streams */ 'XACK' => 'keys-first', diff --git a/tests/Predis/Cluster/RedisStrategyTest.php b/tests/Predis/Cluster/RedisStrategyTest.php index 3601aa59..940d2bf7 100644 --- a/tests/Predis/Cluster/RedisStrategyTest.php +++ b/tests/Predis/Cluster/RedisStrategyTest.php @@ -360,6 +360,57 @@ class RedisStrategyTest extends PredisTestCase } } + /** + * @group disconnected + */ + public function testKeysForHashFieldCommands(): void + { + $strategy = $this->getClusterStrategy(); + $commands = $this->getCommandFactory(); + + $arguments = [ + 'HEXPIRE' => ['key', 60, ['field']], + 'HEXPIREAT' => ['key', 1893456000, ['field']], + 'HPERSIST' => ['key', ['field']], + 'HPEXPIRE' => ['key', 60000, ['field']], + 'HPEXPIREAT' => ['key', 1893456000000, ['field']], + 'HTTL' => ['key', ['field']], + 'HPTTL' => ['key', ['field']], + 'HEXPIRETIME' => ['key', ['field']], + 'HPEXPIRETIME' => ['key', ['field']], + 'HGETEX' => ['key', ['field']], + 'HGETDEL' => ['key', ['field']], + ]; + + foreach ($this->getExpectedCommands('keys-hash-field') as $commandID) { + $command = $commands->create($commandID, $arguments[$commandID]); + $this->assertSame($strategy->getSlotByKey('key'), $strategy->getSlot($command), $commandID); + } + } + + /** + * @group disconnected + */ + public function testKeysForObjectSubcommand(): void + { + $strategy = $this->getClusterStrategy(); + + $command = $this->getCommandFactory()->create('OBJECT', ['ENCODING', 'key']); + + $this->assertSame($strategy->getSlotByKey('key'), $strategy->getSlot($command)); + } + + /** + * @group disconnected + */ + public function testReturnsNullOnObjectSubcommandWithoutKey(): void + { + $strategy = $this->getClusterStrategy(); + $command = $this->getCommandFactory()->create('OBJECT', ['HELP']); + + $this->assertNull($strategy->getSlot($command)); + } + /** * @group disconnected */ @@ -559,6 +610,7 @@ class RedisStrategyTest extends PredisTestCase 'SORT' => 'keys-first', // TODO 'DUMP' => 'keys-first', 'RESTORE' => 'keys-first', + 'OBJECT' => 'keys-object-subcommand', 'FLUSHDB' => 'keys-fake', /* commands operating on string values */ @@ -684,6 +736,17 @@ class RedisStrategyTest extends PredisTestCase 'HVALS' => 'keys-first', 'HSCAN' => 'keys-first', 'HSTRLEN' => 'keys-first', + 'HEXPIRE' => 'keys-hash-field', + 'HEXPIREAT' => 'keys-hash-field', + 'HPERSIST' => 'keys-hash-field', + 'HPEXPIRE' => 'keys-hash-field', + 'HPEXPIREAT' => 'keys-hash-field', + 'HTTL' => 'keys-hash-field', + 'HPTTL' => 'keys-hash-field', + 'HEXPIRETIME' => 'keys-hash-field', + 'HPEXPIRETIME' => 'keys-hash-field', + 'HGETEX' => 'keys-hash-field', + 'HGETDEL' => 'keys-hash-field', /* commands operating on streams */ 'XACK' => 'keys-first',