Merge pull request #2306 from phpredis/issue-2068-function

Add Redis::function command
This commit is contained in:
Pavlo Yatsukhnenko
2022-12-21 22:53:16 +02:00
committed by GitHub
10 changed files with 239 additions and 3 deletions
+88
View File
@@ -1719,6 +1719,50 @@ static void array_zip_values_and_scores(RedisSock *redis_sock, zval *z_tab,
ZVAL_ZVAL(z_tab, &z_ret, 0, 0);
}
static int
array_zip_values_recursive(zval *z_tab)
{
zend_string *zkey;
zval z_ret, z_sub, *zv;
array_init(&z_ret);
for (zend_hash_internal_pointer_reset(Z_ARRVAL_P(z_tab));
zend_hash_has_more_elements(Z_ARRVAL_P(z_tab)) == SUCCESS;
zend_hash_move_forward(Z_ARRVAL_P(z_tab))
) {
if ((zv = zend_hash_get_current_data(Z_ARRVAL_P(z_tab))) == NULL) {
zval_dtor(&z_ret);
return FAILURE;
}
if (Z_TYPE_P(zv) == IS_STRING) {
zkey = zval_get_string(zv);
zend_hash_move_forward(Z_ARRVAL_P(z_tab));
if ((zv = zend_hash_get_current_data(Z_ARRVAL_P(z_tab))) == NULL) {
zend_string_release(zkey);
zval_dtor(&z_ret);
return FAILURE;
}
if (Z_TYPE_P(zv) == IS_ARRAY && array_zip_values_recursive(zv) != SUCCESS) {
zend_string_release(zkey);
zval_dtor(&z_ret);
return FAILURE;
}
ZVAL_ZVAL(&z_sub, zv, 1, 0);
add_assoc_zval_ex(&z_ret, ZSTR_VAL(zkey), ZSTR_LEN(zkey), &z_sub);
zend_string_release(zkey);
} else {
if (Z_TYPE_P(zv) == IS_ARRAY && array_zip_values_recursive(zv) != SUCCESS) {
zval_dtor(&z_ret);
return FAILURE;
}
ZVAL_ZVAL(&z_sub, zv, 1, 0);
add_next_index_zval(&z_ret, &z_sub);
}
}
zval_dtor(z_tab);
ZVAL_ZVAL(z_tab, &z_ret, 0, 0);
return SUCCESS;
}
static int
redis_mbulk_reply_zipped(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
@@ -1977,6 +2021,50 @@ redis_client_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval
}
}
static int
redis_function_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx)
{
int numElems;
zval z_ret;
if (read_mbulk_header(redis_sock, &numElems) < 0) {
if (IS_ATOMIC(redis_sock)) {
RETVAL_FALSE;
} else {
add_next_index_bool(z_tab, 0);
}
return FAILURE;
}
array_init(&z_ret);
redis_read_multibulk_recursive(redis_sock, numElems, 0, &z_ret);
array_zip_values_recursive(&z_ret);
if (IS_ATOMIC(redis_sock)) {
RETVAL_ZVAL(&z_ret, 0, 1);
} else {
add_next_index_zval(z_tab, &z_ret);
}
return SUCCESS;
}
PHP_REDIS_API int
redis_function_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx)
{
if (ctx == NULL) {
return redis_boolean_response(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, z_tab, NULL);
} else if (ctx == PHPREDIS_CTX_PTR) {
return redis_string_response(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, z_tab, NULL);
} else if (ctx == PHPREDIS_CTX_PTR + 1) {
return redis_function_reply(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, z_tab, NULL);
} else {
ZEND_ASSERT(!"memory corruption?");
return FAILURE;
}
}
static int
redis_command_info_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx)
{
+1 -1
View File
@@ -196,8 +196,8 @@ PHP_REDIS_API int redis_lpos_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *r
PHP_REDIS_API int redis_object_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx);
PHP_REDIS_API int redis_read_lpos_response(zval *zdst, RedisSock *redis_sock, char reply_type, long long elements, void *ctx);
PHP_REDIS_API int redis_client_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx);
PHP_REDIS_API int redis_function_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx);
PHP_REDIS_API int redis_command_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx);
PHP_REDIS_API int redis_select_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx);
+6
View File
@@ -1494,6 +1494,12 @@ PHP_METHOD(Redis, flushAll)
}
/* }}} */
/* {{{ proto mixed Redis::function(string op, mixed ...args) */
PHP_METHOD(Redis, function)
{
REDIS_PROCESS_CMD(function, redis_function_response)
}
/* {{{ proto int Redis::dbSize() */
PHP_METHOD(Redis, dbSize)
{
+20
View File
@@ -1184,6 +1184,26 @@ class Redis {
*/
public function flushDB(?bool $sync = null): Redis|bool;
/**
* Functions is an API for managing code to be executed on the server.
*
* @param string $operation The subcommand you intend to execute. Valid options are as follows
* 'LOAD' - Create a new library with the given library name and code.
* 'DELETE' - Delete the given library.
* 'LIST' - Return general information on all the libraries
* 'STATS' - Return information about the current function running
* 'KILL' - Kill the current running function
* 'FLUSH' - Delete all the libraries
* 'DUMP' - Return a serialized payload representing the current libraries
* 'RESTORE' - Restore the libraries represented by the given payload
* @param member $args Additional arguments
*
* @return Redis|bool|string|array Depends on subcommand.
*
* @see https://redis.io/commands/function
*/
public function function(string $operation, mixed ...$args): Redis|bool|string|array;
/**
* Add one or more members to a geospacial sorted set
*
+8 -1
View File
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: b0d5c56084a89230807e6ba582d2fab536d2e897 */
* Stub hash: 3d369227b8f6d01fffa0ffda01f379f0138fa226 */
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis___construct, 0, 0, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 0, "null")
@@ -237,6 +237,11 @@ ZEND_END_ARG_INFO()
#define arginfo_class_Redis_flushDB arginfo_class_Redis_flushAll
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_Redis_function, 0, 1, Redis, MAY_BE_BOOL|MAY_BE_STRING|MAY_BE_ARRAY)
ZEND_ARG_TYPE_INFO(0, operation, IS_STRING, 0)
ZEND_ARG_VARIADIC_TYPE_INFO(0, args, IS_MIXED, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_Redis_geoadd, 0, 4, Redis, MAY_BE_LONG|MAY_BE_FALSE)
ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, lng, IS_DOUBLE, 0)
@@ -1204,6 +1209,7 @@ ZEND_METHOD(Redis, expiretime);
ZEND_METHOD(Redis, pexpiretime);
ZEND_METHOD(Redis, flushAll);
ZEND_METHOD(Redis, flushDB);
ZEND_METHOD(Redis, function);
ZEND_METHOD(Redis, geoadd);
ZEND_METHOD(Redis, geodist);
ZEND_METHOD(Redis, geohash);
@@ -1456,6 +1462,7 @@ static const zend_function_entry class_Redis_methods[] = {
ZEND_ME(Redis, pexpiretime, arginfo_class_Redis_pexpiretime, ZEND_ACC_PUBLIC)
ZEND_ME(Redis, flushAll, arginfo_class_Redis_flushAll, ZEND_ACC_PUBLIC)
ZEND_ME(Redis, flushDB, arginfo_class_Redis_flushDB, ZEND_ACC_PUBLIC)
ZEND_ME(Redis, function, arginfo_class_Redis_function, ZEND_ACC_PUBLIC)
ZEND_ME(Redis, geoadd, arginfo_class_Redis_geoadd, ZEND_ACC_PUBLIC)
ZEND_ME(Redis, geodist, arginfo_class_Redis_geodist, ZEND_ACC_PUBLIC)
ZEND_ME(Redis, geohash, arginfo_class_Redis_geohash, ZEND_ACC_PUBLIC)
+92
View File
@@ -937,6 +937,98 @@ redis_config_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
return res;
}
int
redis_function_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
char **cmd, int *cmd_len, short *slot, void **ctx)
{
smart_string cmdstr = {0};
zend_string *op = NULL, *arg;
zval *argv = NULL;
int i, argc = 0;
ZEND_PARSE_PARAMETERS_START(1, -1)
Z_PARAM_STR(op)
Z_PARAM_OPTIONAL
Z_PARAM_VARIADIC('*', argv, argc)
ZEND_PARSE_PARAMETERS_END_EX(return FAILURE);
for (i = 0; i < argc; ++i) {
if (Z_TYPE(argv[i]) != IS_STRING) {
php_error_docref(NULL, E_WARNING, "invalid argument");
return FAILURE;
}
}
if (zend_string_equals_literal_ci(op, "DELETE")) {
if (argc < 1) {
php_error_docref(NULL, E_WARNING, "argument required");
return FAILURE;
}
} else if (zend_string_equals_literal_ci(op, "DUMP")) {
*ctx = PHPREDIS_CTX_PTR;
} else if (zend_string_equals_literal_ci(op, "FLUSH")) {
if (argc > 0 &&
!zend_string_equals_literal_ci(Z_STR(argv[0]), "SYNC") &&
!zend_string_equals_literal_ci(Z_STR(argv[0]), "ASYNC")
) {
php_error_docref(NULL, E_WARNING, "invalid argument");
return FAILURE;
}
} else if (zend_string_equals_literal_ci(op, "KILL")) {
// noop
} else if (zend_string_equals_literal_ci(op, "LIST")) {
if (argc > 0) {
if (zend_string_equals_literal_ci(Z_STR(argv[0]), "LIBRARYNAME")) {
if (argc < 2) {
php_error_docref(NULL, E_WARNING, "argument required");
return FAILURE;
}
} else if (!zend_string_equals_literal_ci(Z_STR(argv[0]), "WITHCODE")) {
php_error_docref(NULL, E_WARNING, "invalid argument");
return FAILURE;
}
}
*ctx = PHPREDIS_CTX_PTR + 1;
} else if (zend_string_equals_literal_ci(op, "LOAD")) {
if (argc < 1 || (
zend_string_equals_literal_ci(Z_STR(argv[0]), "REPLACE") && argc < 2
)) {
php_error_docref(NULL, E_WARNING, "argument required");
return FAILURE;
}
*ctx = PHPREDIS_CTX_PTR;
} else if (zend_string_equals_literal_ci(op, "RESTORE")) {
if (argc < 1 || (
argc > 1 &&
!zend_string_equals_literal_ci(Z_STR(argv[1]), "FLUSH") &&
!zend_string_equals_literal_ci(Z_STR(argv[1]), "APPEND") &&
!zend_string_equals_literal_ci(Z_STR(argv[1]), "REPLACE")
)) {
php_error_docref(NULL, E_WARNING, "invalid argument");
return FAILURE;
}
} else if (zend_string_equals_literal_ci(op, "STATS")) {
*ctx = PHPREDIS_CTX_PTR + 1;
} else {
php_error_docref(NULL, E_WARNING, "Unknown operation '%s'", ZSTR_VAL(op));
return FAILURE;
}
REDIS_CMD_INIT_SSTR_STATIC(&cmdstr, 1 + argc, "FUNCTION");
redis_cmd_append_sstr_zstr(&cmdstr, op);
for (i = 0; i < argc; i++) {
arg = zval_get_string(&argv[i]);
redis_cmd_append_sstr_zstr(&cmdstr, arg);
zend_string_release(arg);
}
*cmd = cmdstr.c;
*cmd_len = cmdstr.len;
return SUCCESS;
}
int
redis_zrandmember_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
char **cmd, int *cmd_len, short *slot, void **ctx)
+3
View File
@@ -110,6 +110,9 @@ int redis_zrange_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
int redis_config_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
char **cmd, int *cmd_len, short *slot, void **ctx);
int redis_function_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
char **cmd, int *cmd_len, short *slot, void **ctx);
int redis_zrandmember_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock,
char **cmd, int *cmd_len, short *slot, void **ctx);
+8 -1
View File
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: b0d5c56084a89230807e6ba582d2fab536d2e897 */
* Stub hash: 3d369227b8f6d01fffa0ffda01f379f0138fa226 */
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis___construct, 0, 0, 0)
ZEND_ARG_INFO(0, options)
@@ -219,6 +219,11 @@ ZEND_END_ARG_INFO()
#define arginfo_class_Redis_flushDB arginfo_class_Redis_flushAll
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis_function, 0, 0, 1)
ZEND_ARG_INFO(0, operation)
ZEND_ARG_VARIADIC_INFO(0, args)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Redis_geoadd, 0, 0, 4)
ZEND_ARG_INFO(0, key)
ZEND_ARG_INFO(0, lng)
@@ -1046,6 +1051,7 @@ ZEND_METHOD(Redis, expiretime);
ZEND_METHOD(Redis, pexpiretime);
ZEND_METHOD(Redis, flushAll);
ZEND_METHOD(Redis, flushDB);
ZEND_METHOD(Redis, function);
ZEND_METHOD(Redis, geoadd);
ZEND_METHOD(Redis, geodist);
ZEND_METHOD(Redis, geohash);
@@ -1298,6 +1304,7 @@ static const zend_function_entry class_Redis_methods[] = {
ZEND_ME(Redis, pexpiretime, arginfo_class_Redis_pexpiretime, ZEND_ACC_PUBLIC)
ZEND_ME(Redis, flushAll, arginfo_class_Redis_flushAll, ZEND_ACC_PUBLIC)
ZEND_ME(Redis, flushDB, arginfo_class_Redis_flushDB, ZEND_ACC_PUBLIC)
ZEND_ME(Redis, function, arginfo_class_Redis_function, ZEND_ACC_PUBLIC)
ZEND_ME(Redis, geoadd, arginfo_class_Redis_geoadd, ZEND_ACC_PUBLIC)
ZEND_ME(Redis, geodist, arginfo_class_Redis_geodist, ZEND_ACC_PUBLIC)
ZEND_ME(Redis, geohash, arginfo_class_Redis_geohash, ZEND_ACC_PUBLIC)
+1
View File
@@ -54,6 +54,7 @@ class Redis_Cluster_Test extends Redis_Test {
/* These 'directed node' commands work differently in RedisCluster */
public function testConfig() { return $this->markTestSkipped(); }
public function testFlushDB() { return $this->markTestSkipped(); }
public function testFunction() { return $this->markTestSkipped(); }
/* Session locking feature is currently not supported in in context of Redis Cluster.
The biggest issue for this is the distribution nature of Redis cluster */
+12
View File
@@ -7564,6 +7564,18 @@ class Redis_Test extends TestSuite
}
}
public function testFunction() {
$this->assertTrue($this->redis->function('flush', 'sync'));
$this->assertEquals('mylib', $this->redis->function('load', "#!lua name=mylib\nredis.register_function('myfunc', function(keys, args) return args[1] end)"));
$this->assertEquals('mylib', $this->redis->function('load', 'replace', "#!lua name=mylib\nredis.register_function('myfunc', function(keys, args) return args[1] end)"));
$this->assertEquals($this->redis->function('stats'), ['running_script' => false, 'engines' => ['LUA' => ['libraries_count' => 1, 'functions_count' => 1]]]);
$payload = $this->redis->function('dump');
$this->assertTrue($this->redis->function('delete', 'mylib'));
$this->assertTrue($this->redis->function('restore', $payload));
$this->assertEquals($this->redis->function('list'), [['library_name' => 'mylib', 'engine' => 'LUA', 'functions' => [['name' => 'myfunc', 'description' => false,'flags' => []]]]]);
$this->assertTrue($this->redis->function('delete', 'mylib'));
}
/* Make sure we handle a bad option value gracefully */
public function testBadOptionValue() {
$this->assertFalse(@$this->redis->setOption(pow(2, 32), false));