Skip to content

Commit

Permalink
Implement Valkey >= 8.1 IFEQ set option
Browse files Browse the repository at this point in the history
Implement the new `IFEQ` `SET` option that will be included in `Valkey`
8.1.

See: valkey-io/valkey#1324
  • Loading branch information
michael-grunder committed Jan 20, 2025
1 parent faa4bc2 commit a2eef77
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 4 deletions.
29 changes: 25 additions & 4 deletions redis_commands.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -2356,18 +2360,28 @@ 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;
}

/* Backward compatibility: If we are passed no options except an EXPIRE ttl, we
* 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 <key> <value> */
redis_cmd_init_sstr(&cmdstr, argc, "SET", 3);
Expand All @@ -2379,15 +2393,22 @@ 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) {
REDIS_CMD_APPEND_SSTR_STATIC(&cmdstr, "GET");
*ctx = PHPREDIS_CTX_PTR;
}

zend_tmp_string_release(tmp);

/* Push command and length to the caller */
*cmd = cmdstr.c;
*cmd_len = cmdstr.len;
Expand Down
1 change: 1 addition & 0 deletions tests/RedisClusterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ public function setUp() {
$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 */
Expand Down
18 changes: 18 additions & 0 deletions tests/RedisTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,16 @@ protected function detectKeyDB(array $info) {
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) {
Expand Down Expand Up @@ -629,6 +634,19 @@ public function testExtendedSet() {
$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'));
Expand Down
1 change: 1 addition & 0 deletions tests/TestSuite.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class TestSuite
/* Redis server version */
protected $version;
protected bool $is_keydb;
protected bool $is_valkey;

private static bool $colorize = false;

Expand Down

0 comments on commit a2eef77

Please sign in to comment.