From a2eef77f4419cda815052e75def3af81b0ccd80f Mon Sep 17 00:00:00 2001 From: michael-grunder Date: Sun, 19 Jan 2025 09:15:41 -0800 Subject: [PATCH] Implement Valkey >= 8.1 IFEQ set option Implement the new `IFEQ` `SET` option that will be included in `Valkey` 8.1. See: valkey-io/valkey#1324 --- redis_commands.c | 29 +++++++++++++++++++++++++---- tests/RedisClusterTest.php | 1 + tests/RedisTest.php | 18 ++++++++++++++++++ tests/TestSuite.php | 1 + 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/redis_commands.c b/redis_commands.c index c49f5cd..0c2aaa1 100644 --- a/redis_commands.c +++ b/redis_commands.c @@ -2293,7 +2293,8 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char **cmd, int *cmd_len, short *slot, void **ctx) { char *key = NULL, *exp_type = NULL, *set_type = NULL; - zval *z_value, *z_opts=NULL; + zend_string *ifeq = NULL, *tmp = NULL; + zval *z_value, *z_opts = NULL; smart_string cmdstr = {0}; zend_long expire = -1; zend_bool get = 0; @@ -2312,7 +2313,6 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, return FAILURE; } - // Check for an options array if (z_opts && Z_TYPE_P(z_opts) == IS_ARRAY) { HashTable *kt = Z_ARRVAL_P(z_opts); @@ -2329,11 +2329,14 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zend_string_equals_literal_ci(zkey, "PXAT")) ) { if (redis_try_get_expiry(v, &expire) == FAILURE || expire < 1) { + zend_tmp_string_release(tmp); setExpiryWarning(v); return FAILURE; } exp_type = ZSTR_VAL(zkey); + } else if (zkey && !ifeq && zend_string_equals_literal_ci(zkey, "IFEQ")) { + ifeq = zval_get_tmp_string(v, &tmp); } else if (Z_TYPE_P(v) == IS_STRING) { if (zend_string_equals_literal_ci(Z_STR_P(v), "KEEPTTL")) { keep_ttl = 1; @@ -2348,6 +2351,7 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, } ZEND_HASH_FOREACH_END(); } else if (z_opts && Z_TYPE_P(z_opts) != IS_NULL) { if (redis_try_get_expiry(z_opts, &expire) == FAILURE || expire < 1) { + zend_tmp_string_release(tmp); setExpiryWarning(z_opts); return FAILURE; } @@ -2356,6 +2360,14 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, /* Protect the user from syntax errors but give them some info about what's wrong */ if (exp_type && keep_ttl) { php_error_docref(NULL, E_WARNING, "KEEPTTL can't be combined with EX or PX option"); + zend_tmp_string_release(tmp); + return FAILURE; + } + + /* You can't use IFEQ with NX or XX */ + if (set_type && ifeq) { + php_error_docref(NULL, E_WARNING, "IFEQ can't be combined with NX or XX option"); + zend_tmp_string_release(tmp); return FAILURE; } @@ -2363,11 +2375,13 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, * actually execute a SETEX command */ if (expire > 0 && !exp_type && !set_type && !keep_ttl) { *cmd_len = REDIS_CMD_SPPRINTF(cmd, "SETEX", "klv", key, key_len, expire, z_value); + zend_tmp_string_release(tmp); return SUCCESS; } /* Calculate argc based on options set */ - int argc = 2 + (exp_type ? 2 : 0) + (set_type != NULL) + (keep_ttl != 0) + get; + int argc = 2 + (ifeq ? 2 : 0) + (exp_type ? 2 : 0) + (set_type != NULL) + + (keep_ttl != 0) + get; /* Initial SET */ redis_cmd_init_sstr(&cmdstr, argc, "SET", 3); @@ -2379,8 +2393,13 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, redis_cmd_append_sstr_long(&cmdstr, (long)expire); } - if (set_type) + if (ifeq) { + REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "IFEQ"); + redis_cmd_append_sstr_zstr(&cmdstr, ifeq); + } else if (set_type) { redis_cmd_append_sstr(&cmdstr, set_type, strlen(set_type)); + } + if (keep_ttl) redis_cmd_append_sstr(&cmdstr, "KEEPTTL", 7); if (get) { @@ -2388,6 +2407,8 @@ int redis_set_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, *ctx = PHPREDIS_CTX_PTR; } + zend_tmp_string_release(tmp); + /* Push command and length to the caller */ *cmd = cmdstr.c; *cmd_len = cmdstr.len; diff --git a/tests/RedisClusterTest.php b/tests/RedisClusterTest.php index aceebf8..1be83c2 100644 --- a/tests/RedisClusterTest.php +++ b/tests/RedisClusterTest.php @@ -133,6 +133,7 @@ class Redis_Cluster_Test extends Redis_Test { $info = $this->redis->info(uniqid()); $this->version = $info['redis_version'] ?? '0.0.0'; $this->is_keydb = $this->detectKeyDB($info); + $this->is_valkey = $this->detectValkey($info); } /* Override newInstance as we want a RedisCluster object */ diff --git a/tests/RedisTest.php b/tests/RedisTest.php index 61eecd7..c9b16c7 100644 --- a/tests/RedisTest.php +++ b/tests/RedisTest.php @@ -67,11 +67,16 @@ class Redis_Test extends TestSuite { isset($info['mvcc_depth']); } + protected function detectValkey(array $info) { + return isset($info['server_name']) && $info['server_name'] === 'valkey'; + } + public function setUp() { $this->redis = $this->newInstance(); $info = $this->redis->info(); $this->version = (isset($info['redis_version'])?$info['redis_version']:'0.0.0'); $this->is_keydb = $this->detectKeyDB($info); + $this->is_valkey = $this->detectValKey($info); } protected function minVersionCheck($version) { @@ -629,6 +634,19 @@ class Redis_Test extends TestSuite { $this->assertEquals('bar', $this->redis->set('foo', 'baz', ['GET'])); } + /* Test Valkey >= 8.1 IFEQ SET option */ + public function testValkeyIfEq() { + if ( ! $this->is_valkey || ! $this->minVersionCheck('8.1.0')) + $this->markTestSkipped(); + + $this->redis->del('foo'); + $this->assertTrue($this->redis->set('foo', 'bar')); + $this->assertTrue($this->redis->set('foo', 'bar2', ['IFEQ' => 'bar'])); + $this->assertFalse($this->redis->set('foo', 'bar4', ['IFEQ' => 'bar3'])); + + $this->assertEquals('bar2', $this->redis->set('foo', 'bar3', ['IFEQ' => 'bar2', 'GET'])); + } + public function testGetSet() { $this->redis->del('key'); $this->assertFalse($this->redis->getSet('key', '42')); diff --git a/tests/TestSuite.php b/tests/TestSuite.php index f5135d3..c3fe7f7 100644 --- a/tests/TestSuite.php +++ b/tests/TestSuite.php @@ -16,6 +16,7 @@ class TestSuite /* Redis server version */ protected $version; protected bool $is_keydb; + protected bool $is_valkey; private static bool $colorize = false;