From ce853ecec2d891aa9905fcba750975e230d6c4f4 Mon Sep 17 00:00:00 2001 From: Vladyslav Vildanov <117659936+vladvildanov@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:49:07 +0200 Subject: [PATCH] Added support for MSET command (#1610) * Added support for MSET command * Codestyle fixes --- CHANGELOG.md | 1 + src/ClientContextInterface.php | 1 + src/ClientInterface.php | 1 + src/Command/Redis/MSETEX.php | 80 +++++++++++ .../Processor/KeyPrefixProcessorTest.php | 4 + tests/Predis/Command/Redis/MSETEX_Test.php | 135 ++++++++++++++++++ 6 files changed, 222 insertions(+) create mode 100644 src/Command/Redis/MSETEX.php create mode 100644 tests/Predis/Command/Redis/MSETEX_Test.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a2db6ecf..9c3a74ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - Added cluster support for `XADD`, `XDEL` and `XRANGE` (#1587) - Added prefixable interface for `HEXPIRE` and `HEXPIRETIME` (#1592) +- Added support for MSET command (#1610) ### Changed - Refactor pipeline data writing depends on connection type (#1586) diff --git a/src/ClientContextInterface.php b/src/ClientContextInterface.php index a5eaa4ea..f69854e1 100644 --- a/src/ClientContextInterface.php +++ b/src/ClientContextInterface.php @@ -148,6 +148,7 @@ use Predis\Command\Redis\VADD; * @method $this incrbyfloat($key, $increment) * @method $this mget(array $keys) * @method $this mset(array $dictionary) + * @method $this msetex(array $dictionary, ?string $existModifier = null, ?string $expireResolution = null, ?int $expireTTL = null) * @method $this msetnx(array $dictionary) * @method $this psetex($key, $milliseconds, $value) * @method $this set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null) diff --git a/src/ClientInterface.php b/src/ClientInterface.php index cbaaaf27..148e2747 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -158,6 +158,7 @@ use Predis\Response\Status; * @method string incrbyfloat(string $key, int|float $increment) * @method array mget(string[]|string $keyOrKeys, string ...$keys = null) * @method mixed mset(array $dictionary) + * @method array msetex(array $dictionary, ?string $existModifier = null, ?string $expireResolution = null, ?int $expireTTL = null) * @method int msetnx(array $dictionary) * @method Status psetex(string $key, $milliseconds, $value) * @method Status|null set(string $key, $value, $expireResolution = null, $expireTTL = null, $flag = null) diff --git a/src/Command/Redis/MSETEX.php b/src/Command/Redis/MSETEX.php new file mode 100644 index 00000000..b1989fd7 --- /dev/null +++ b/src/Command/Redis/MSETEX.php @@ -0,0 +1,80 @@ +getArguments(); + $keysCount = $arguments[0]; + $currentKeyIndex = 1; + + while ($keysCount > 0) { + $arguments[$currentKeyIndex] = $prefix . $arguments[$currentKeyIndex]; + $keysCount--; + $currentKeyIndex += 2; + } + + parent::setRawArguments($arguments); + } +} diff --git a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php index ff349bf3..c6d1a627 100644 --- a/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php +++ b/tests/Predis/Command/Processor/KeyPrefixProcessorTest.php @@ -301,6 +301,10 @@ class KeyPrefixProcessorTest extends PredisTestCase ['foo', 'bar', 'hoge', 'piyo'], ['prefix:foo', 'bar', 'prefix:hoge', 'piyo'], ], + ['MSETEX', + [['key' => 'value', 'key1' => 'value', 'key2' => 'value'], 'NX', 'EX', 10], + [3, 'prefix:key', 'value', 'prefix:key1', 'value', 'prefix:key2', 'value', 'NX', 'EX', 10], + ], ['MSETNX', ['foo', 'bar', 'hoge', 'piyo'], ['prefix:foo', 'bar', 'prefix:hoge', 'piyo'], diff --git a/tests/Predis/Command/Redis/MSETEX_Test.php b/tests/Predis/Command/Redis/MSETEX_Test.php new file mode 100644 index 00000000..3a68fb99 --- /dev/null +++ b/tests/Predis/Command/Redis/MSETEX_Test.php @@ -0,0 +1,135 @@ +getCommand(); + $command->setArguments($actualArguments); + + $this->assertSame($expectedArguments, $command->getArguments()); + } + + /** + * @group connected + * @return void + * @requiresRedisVersion >= 8.3.224 + */ + public function testSetWithModifiers() + { + $redis = $this->getClient(); + + $this->assertEquals(1, $redis->msetex(['foo' => 'bar', 'bar' => 'baz'], 'nx')); + $this->assertEquals(0, $redis->msetex(['foo' => 'bar', 'bar' => 'baz'], 'nx')); + $this->assertEquals(1, $redis->msetex(['foo' => 'baz', 'bar' => 'bar'], 'xx')); + $this->assertEquals(0, $redis->msetex(['foo' => 'baz', 'baz' => 'bar'], 'xx')); + + $this->assertEquals(1, $redis->msetex(['foo' => 'baz', 'bar' => 'bar'], null, 'ex', 10)); + $this->assertEquals(1, $redis->msetex(['foo' => 'baz', 'bar' => 'bar'], null, 'px', 1000)); + $this->assertEquals(1, $redis->msetex(['foo' => 'baz', 'bar' => 'bar'], null, 'exat', time() + 10)); + $this->assertEquals(1, $redis->msetex(['foo' => 'baz', 'bar' => 'bar'], null, 'pxat', (time() * 1000) + 1000)); + $this->assertEquals(1, $redis->msetex(['foo' => 'baz', 'bar' => 'bar'], null, 'keepttl')); + + $this->assertGreaterThan(0, $redis->expiretime('foo')); + } + + /** + * @group disconnected + * @return void + */ + public function testThrowsExceptionOnInvalidArguments(): void + { + $command = $this->getCommand(); + + $this->expectException(ValueError::class); + $this->expectExceptionMessage('Incorrect exist modifier. Should be one of: NX, XX.'); + + $command->setArguments([['key' => 'value'], 'wrong']); + + $this->expectException(ValueError::class); + $this->expectExceptionMessage('TTL should be specified along with expire resolution parameter'); + + $command->setArguments([['key' => 'value'], null, 'EX']); + + $this->expectException(ValueError::class); + $this->expectExceptionMessage('Incorrect expire modifier. Should be one of: EX, PX, EXAT, PXAT, KEEPTTL'); + + $command->setArguments([['key' => 'value'], null, 'wrong', 10]); + } + + public function argumentsProvider(): array + { + return [ + 'with default arguments' => [ + [['field1' => 'value1', 'field2' => 'value2']], + [2, 'field1', 'value1', 'field2', 'value2'], + ], + 'with exist modifier - NX' => [ + [['field1' => 'value1', 'field2' => 'value2'], 'nx'], + [2, 'field1', 'value1', 'field2', 'value2', 'NX'], + ], + 'with exist modifier - XX' => [ + [['field1' => 'value1', 'field2' => 'value2'], 'xx'], + [2, 'field1', 'value1', 'field2', 'value2', 'XX'], + ], + 'with expire modifier - EX' => [ + [['field1' => 'value1', 'field2' => 'value2'], null, 'EX', 10], + [2, 'field1', 'value1', 'field2', 'value2', 'EX', 10], + ], + 'with expire modifier - PX' => [ + [['field1' => 'value1', 'field2' => 'value2'], null, 'PX', 10], + [2, 'field1', 'value1', 'field2', 'value2', 'PX', 10], + ], + 'with expire modifier - EXAT' => [ + [['field1' => 'value1', 'field2' => 'value2'], null, 'EXAT', 10], + [2, 'field1', 'value1', 'field2', 'value2', 'EXAT', 10], + ], + 'with expire modifier - PXAT' => [ + [['field1' => 'value1', 'field2' => 'value2'], null, 'PXAT', 10], + [2, 'field1', 'value1', 'field2', 'value2', 'PXAT', 10], + ], + 'with expire modifier - KEETTL' => [ + [['field1' => 'value1', 'field2' => 'value2'], null, 'KEEPTTL'], + [2, 'field1', 'value1', 'field2', 'value2', 'KEEPTTL'], + ], + 'with all modifiers' => [ + [['field1' => 'value1', 'field2' => 'value2'], 'xx', 'KEEPTTL'], + [2, 'field1', 'value1', 'field2', 'value2', 'XX', 'KEEPTTL'], + ], + ]; + } +}