├── .gitignore ├── CHANGELOG ├── LICENSE ├── README.markdown ├── VERSION ├── examples ├── define_commands.lua ├── monitor.lua ├── pipeline.lua ├── pubsub.lua ├── simple.lua └── transaction.lua ├── rockspec ├── redis-lua-1.0.1-0.rockspec ├── redis-lua-2.0.0-0.rockspec ├── redis-lua-2.0.1-0.rockspec ├── redis-lua-2.0.2-0.rockspec ├── redis-lua-2.0.3-0.rockspec ├── redis-lua-2.0.4-0.rockspec └── redis-lua-scm-1.rockspec ├── src └── redis.lua └── test └── test_client.lua /.gitignore: -------------------------------------------------------------------------------- 1 | experiments/* 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v2.0.5 (2013-xx-xx) 2 | * New commands added: `BITOP`, `BITCOUNT`, `SCAN`, `SSCAN`, `ZSCAN`, `HSCAN`. 3 | 4 | * Added the ability to specify a socket timeout in the connection parameters. 5 | 6 | * Fixed returned value of `ZINCRBY` (float value instead of string). 7 | 8 | * Fixed command serialization when command arguments contain nil values. 9 | 10 | v2.0.4 (2012-07-15) 11 | * The library is now fully compatible with Lua 5.2. 12 | 13 | * Implemented some small optimizations in the handling of the Redis protocol. 14 | 15 | * Client instances can be initialized with an already connected socket using 16 | the `socket` field in the parameters table (it takes the precedence over 17 | other fields such as `host`, `port` or `path`). 18 | 19 | * The module now exposes its default command table in the `commands` field. 20 | 21 | * It is available a new way to define custom Redis commands on a module or 22 | client-level instance. The old way is still in place but it is considered 23 | deprecated and will be removed in the next major release. For more details 24 | see `examples/define_commands.lua` and the test suite. 25 | 26 | v2.0.3 (2012-04-01) 27 | * It is now possible to require redis-lua by assigning the returned module 28 | to a local variable like `local redis = require 'redis'`. Starting with 29 | this release this is the preferred way to require redis-lua. The `Redis` 30 | global name is still available for backwards compatibility but it will 31 | be removed in the next major version. 32 | 33 | * Changed to using an error function per client instead of a global one. 34 | 35 | * Added an abstraction for MONITOR that makes possible to consume messages 36 | the messages returned by Redis using a coroutine-based iterator. 37 | 38 | * Implemented all the new commands of Redis 2.6. 39 | 40 | * Implemented `CONFIG GET`, `CONFIG SET`, `CONFIG RESETSTAT` and `SLOWLOG`. 41 | 42 | * In order to support the variadic flavor of certain commands implemented 43 | in Redis >= 2.4, now `SADD`, `SREM`, `ZADD`, `ZREM` and `HDEL` now return 44 | the number of elements involved by the respective operation instead of a 45 | boolean value. 46 | 47 | * Fixed the parsing of INFO replies returned by Redis 2.6. 48 | 49 | v2.0.2 (2011-06-20) 50 | * Added an abstraction for PUB/SUB that makes possible to consume messages 51 | pushed to channels using a coroutine-based iterator. 52 | 53 | * Added support for connecting to Redis using UNIX domain sockets when they 54 | are available in LuaSocket. 55 | 56 | v2.0.1 (2011-01-24) 57 | * Vastly improved abstraction for Redis transactions (MULTI/EXEC) supporting 58 | check-and-set (CAS), automatic retries upon transaction failures and a few 59 | optional arguments for initialization (enable CAS support, list of keys to 60 | watch automatically and number of attempts upon failed transactions). The 61 | public interface is completely backwards compatible with previous versions. 62 | 63 | * Pipelines and transactions return the number of commands processed as the 64 | second return value, useful for iterating over a returned list of replies 65 | that contains holes (nil values). 66 | 67 | * Since SORT can accept multiple GET parameters, redis:sort() has been 68 | modified accordingly to accept either a string or a table for the 'get' 69 | parameter. 70 | 71 | v2.0.0 (2010-11-27) 72 | * The client library is no longer compatible with Redis 1.0. 73 | 74 | * Support for long names of Redis commands has been dropped, the client now 75 | uses the same command names as defined by Redis. 76 | 77 | * Inline and bulk requests are not supported anymore and the related code 78 | has been removed from the library. Commands are defined as multibulk 79 | requests by default. 80 | 81 | * The public interface for pipelining has been slightly changed (see the 82 | examples/pipeline.lua file for more details). 83 | 84 | * The public interface for Redis transactions (MULTI/EXEC) basically works 85 | in the same way of pipelining. 86 | 87 | * Developers can now define their own commands at module level and not only 88 | on client instances. 89 | 90 | v1.0.1 (2010-07-30) 91 | * Providing a more generalized version of the multibulk request serializer. 92 | 93 | * _G is now passed as the argument of a pipeline block. This will change in 94 | a future major release of redis-lua, but for now it is useful to enable 95 | the usage of global functions inside of a pipeline block. 96 | 97 | * Fix: user-added commands were not available when pipelining commands. 98 | 99 | v1.0.0 (2010-06-02) 100 | * First versioned release of redis-lua 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2013 Daniele Alessandri 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # redis-lua # 2 | 3 | ## About ## 4 | 5 | redis-lua is a pure Lua client library for the Redis advanced key-value database. 6 | 7 | ## Main features ## 8 | 9 | - Support for Redis >= 1.2 10 | - Command pipelining 11 | - Redis transactions (MULTI/EXEC) with CAS 12 | - User-definable commands 13 | - UNIX domain sockets (when available in LuaSocket) 14 | 15 | ## Compatibility ## 16 | 17 | This library is tested and works with __Lua 5.1__, __Lua 5.2__ (using a compatible 18 | version of LuaSocket) and __LuaJit 2.0__. 19 | 20 | ## Examples of usage ## 21 | 22 | ### Include redis-lua in your script ### 23 | 24 | Just require the `redis` module assigning it to a variable: 25 | 26 | ``` lua 27 | local redis = require 'redis' 28 | ``` 29 | 30 | Previous versions of the library defined a global `Redis` alias as soon as the module was 31 | imported by the user. This global alias is still defined but it is considered deprecated 32 | and it will be removed in the next major version. 33 | 34 | ### Connect to a redis-server instance and send a PING command ### 35 | 36 | ``` lua 37 | local redis = require 'redis' 38 | local client = redis.connect('127.0.0.1', 6379) 39 | local response = client:ping() -- true 40 | ``` 41 | 42 | It is also possible to connect to a local redis instance using __UNIX domain sockets__ 43 | if LuaSocket has been compiled with them enabled (unfortunately it is not the default): 44 | 45 | ``` lua 46 | local redis = require 'redis' 47 | local client = redis.connect('unix:///tmp/redis.sock') 48 | ``` 49 | 50 | ### Set keys and get their values ### 51 | 52 | ``` lua 53 | client:set('usr:nrk', 10) 54 | client:set('usr:nobody', 5) 55 | local value = client:get('usr:nrk') -- 10 56 | ``` 57 | 58 | ### Sort list values by using various parameters supported by the server ### 59 | 60 | ``` lua 61 | for _,v in ipairs({ 10,3,2,6,1,4,23 }) do 62 | client:rpush('usr:nrk:ids',v) 63 | end 64 | 65 | local sorted = client:sort('usr:nrk:ids', { 66 | sort = 'asc', alpha = true, limit = { 1, 5 } 67 | }) -- {1=10,2=2,3=23,4=3,5=4} 68 | ``` 69 | 70 | ### Pipeline commands 71 | 72 | ``` lua 73 | local replies = client:pipeline(function(p) 74 | p:incrby('counter', 10) 75 | p:incrby('counter', 30) 76 | p:get('counter') 77 | end) 78 | ``` 79 | 80 | ### Variadic commands 81 | 82 | Some commands such as RPUSH, SADD, SINTER and others have been improved in Redis 2.4 83 | to accept a list of values or keys depending on the nature of the command. Sometimes 84 | it can be useful to pass these arguments as a list in a table, but since redis-lua does 85 | not currently do anything to handle such a case you can use `unpack()` albeit with a 86 | limitation on the maximum number of items which is defined in Lua by LUAI_MAXCSTACK 87 | (the default on Lua 5.1 is set to `8000`, see `luaconf.h`): 88 | 89 | ```lua 90 | local values = { 'value1', 'value2', 'value3' } 91 | client:rpush('list', unpack(values)) 92 | 93 | -- the previous line has the same effect of the following one: 94 | client:rpush('list', 'value1', 'value2', 'value3') 95 | ``` 96 | 97 | ### Leverage Redis MULTI / EXEC transaction (Redis > 2.0) 98 | 99 | ``` lua 100 | local replies = client:transaction(function(t) 101 | t:incrby('counter', 10) 102 | t:incrby('counter', 30) 103 | t:get('counter') 104 | end) 105 | ``` 106 | 107 | ### Leverage WATCH / MULTI / EXEC for check-and-set (CAS) operations (Redis > 2.2) 108 | 109 | ``` lua 110 | local options = { watch = "key_to_watch", cas = true, retry = 2 } 111 | local replies = client:transaction(options, function(t) 112 | local val = t:get("key_to_watch") 113 | t:multi() 114 | t:set("akey", val) 115 | t:set("anotherkey", val) 116 | end) 117 | ``` 118 | 119 | ### Get useful information from the server ### 120 | 121 | ``` lua 122 | for k,v in pairs(client:info()) do 123 | print(k .. ' => ' .. tostring(v)) 124 | end 125 | --[[ 126 | redis_git_dirty => 0 127 | redis_git_sha1 => aaed0894 128 | process_id => 23115 129 | vm_enabled => 0 130 | hash_max_zipmap_entries => 64 131 | expired_keys => 9 132 | changes_since_last_save => 2 133 | role => master 134 | last_save_time => 1283621624 135 | used_memory => 537204 136 | bgsave_in_progress => 0 137 | redis_version => 2.0.0 138 | multiplexing_api => epoll 139 | total_connections_received => 314 140 | db0 => {keys=3,expires=0} 141 | pubsub_patterns => 0 142 | used_memory_human => 524.61K 143 | pubsub_channels => 0 144 | uptime_in_seconds => 1033 145 | connected_slaves => 0 146 | connected_clients => 1 147 | bgrewriteaof_in_progress => 0 148 | blocked_clients => 0 149 | arch_bits => 32 150 | total_commands_processed => 3982 151 | hash_max_zipmap_value => 512 152 | db15 => {keys=1,expires=0} 153 | uptime_in_days => 0 154 | ]] 155 | ``` 156 | 157 | ## Dependencies ## 158 | 159 | - [Lua 5.1 and 5.2](http://www.lua.org/) or [LuaJIT 2.0](http://luajit.org/) 160 | - [LuaSocket 2.0](http://www.tecgraf.puc-rio.br/~diego/professional/luasocket/) 161 | - [Telescope](http://telescope.luaforge.net/) (required to run the test suite) 162 | 163 | ## Links ## 164 | 165 | ### Project ### 166 | - [Source code](http://github.com/nrk/redis-lua/) 167 | - [Issue tracker](http://github.com/nrk/redis-lua/issues) 168 | 169 | ### Related ### 170 | - [Redis](http://redis.io/) 171 | - [Git](http://git-scm.com/) 172 | 173 | ## Authors ## 174 | 175 | [Daniele Alessandri](mailto:suppakilla@gmail.com) 176 | 177 | ### Contributors ### 178 | 179 | [Leo Ponomarev](http://github.com/slact/) 180 | 181 | ## License ## 182 | 183 | The code for redis-lua is distributed under the terms of the MIT/X11 license (see LICENSE). 184 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.0.5-dev 2 | -------------------------------------------------------------------------------- /examples/define_commands.lua: -------------------------------------------------------------------------------- 1 | package.path = "../src/?.lua;src/?.lua;" .. package.path 2 | pcall(require, "luarocks.require") 3 | 4 | local redis = require 'redis' 5 | 6 | local params = { 7 | host = '127.0.0.1', 8 | port = 6379, 9 | } 10 | 11 | -- commands defined in the redis.commands table are available at module 12 | -- level and are used to populate each new client instance. 13 | redis.commands.hset = redis.command('hset') 14 | 15 | -- you can also specify a response callback to parse raw replies 16 | redis.commands.hgetall = redis.command('hgetall', { 17 | response = function(reply, command, ...) 18 | local new_reply = { } 19 | for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end 20 | return new_reply 21 | end 22 | }) 23 | 24 | local client = redis.connect(params) 25 | client:select(15) -- for testing purposes 26 | 27 | client:hset('user:1000', 'name', 'John Doe') 28 | client:hset('user:1000', 'nickname', 'anonymous') 29 | client:hset('user:1000', 'email', 'anything@anywhere.tld') 30 | 31 | local user = client:hgetall('user:1000') 32 | print(string.format('%s is also known as %s and his email address is %s.', 33 | user.name, user.nickname, user.email 34 | )) 35 | 36 | --[[ 37 | OUTPUT: 38 | John Doe is also known as anonymous and his email address is anything@anywhere.tld. 39 | ]] 40 | -------------------------------------------------------------------------------- /examples/monitor.lua: -------------------------------------------------------------------------------- 1 | package.path = "../src/?.lua;src/?.lua;" .. package.path 2 | pcall(require, "luarocks.require") 3 | 4 | local redis = require 'redis' 5 | 6 | local params = { 7 | host = '127.0.0.1', 8 | port = 6379, 9 | } 10 | 11 | local client = redis.connect(params) 12 | client:select(15) -- for testing purposes 13 | 14 | -- Start processing the monitor messages. Open a terminal and use redis-cli to 15 | -- send some commands to the server that will make MONITOR return some entries. 16 | 17 | local counter = 0 18 | for msg, abort in client:monitor_messages() do 19 | counter = counter + 1 20 | 21 | local feedback = string.format("[%d] Received %s on database %d", msg.timestamp, msg.command, msg.database) 22 | if msg.arguments then 23 | feedback = string.format('%s with arguments %s', feedback, msg.arguments) 24 | end 25 | 26 | print(feedback) 27 | 28 | if counter == 5 then 29 | abort() 30 | end 31 | end 32 | 33 | print(string.format("Closed the MONITOR context after receiving %d commands.", counter)) 34 | -------------------------------------------------------------------------------- /examples/pipeline.lua: -------------------------------------------------------------------------------- 1 | package.path = "../src/?.lua;src/?.lua;" .. package.path 2 | pcall(require, "luarocks.require") 3 | 4 | local redis = require 'redis' 5 | 6 | local params = { 7 | host = '127.0.0.1', 8 | port = 6379, 9 | } 10 | 11 | local client = redis.connect(params) 12 | client:select(15) -- for testing purposes 13 | 14 | local replies = client:pipeline(function(p) 15 | p:ping() 16 | p:flushdb() 17 | p:exists('counter') 18 | p:incrby('counter', 10) 19 | p:incrby('counter', 30) 20 | p:exists('counter') 21 | p:get('counter') 22 | p:mset({ foo = 'bar', hoge = 'piyo'}) 23 | p:del('foo', 'hoge') 24 | p:mget('does_not_exist', 'counter') 25 | p:info() 26 | end) 27 | 28 | for _, reply in pairs(replies) do 29 | print('*', reply) 30 | end 31 | -------------------------------------------------------------------------------- /examples/pubsub.lua: -------------------------------------------------------------------------------- 1 | package.path = "../src/?.lua;src/?.lua;" .. package.path 2 | pcall(require, "luarocks.require") 3 | 4 | local redis = require 'redis' 5 | 6 | local params = { 7 | host = '127.0.0.1', 8 | port = 6379, 9 | } 10 | 11 | local client = redis.connect(params) 12 | client:select(15) -- for testing purposes 13 | 14 | local channels = { 'control_channel', 'notifications' } 15 | 16 | -- Start processing the pubsup messages. Open a terminal and use redis-cli 17 | -- to push messages to the channels. Examples: 18 | -- ./redis-cli PUBLISH notifications "this is a test" 19 | -- ./redis-cli PUBLISH control_channel quit_loop 20 | 21 | for msg, abort in client:pubsub({ subscribe = channels }) do 22 | if msg.kind == 'subscribe' then 23 | print('Subscribed to channel '..msg.channel) 24 | elseif msg.kind == 'message' then 25 | if msg.channel == 'control_channel' then 26 | if msg.payload == 'quit_loop' then 27 | print('Aborting pubsub loop...') 28 | abort() 29 | else 30 | print('Received an unrecognized command: '..msg.payload) 31 | end 32 | else 33 | print('Received the following message from '..msg.channel.."\n "..msg.payload.."\n") 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /examples/simple.lua: -------------------------------------------------------------------------------- 1 | package.path = "../src/?.lua;src/?.lua;" .. package.path 2 | pcall(require, "luarocks.require") 3 | 4 | local redis = require 'redis' 5 | 6 | local params = { 7 | host = '127.0.0.1', 8 | port = 6379, 9 | } 10 | 11 | local client = redis.connect(params) 12 | client:select(15) -- for testing purposes 13 | 14 | client:set('foo', 'bar') 15 | local value = client:get('foo') 16 | 17 | print(value) 18 | -------------------------------------------------------------------------------- /examples/transaction.lua: -------------------------------------------------------------------------------- 1 | package.path = "../src/?.lua;src/?.lua;" .. package.path 2 | pcall(require, "luarocks.require") 3 | 4 | local redis = require 'redis' 5 | 6 | local params = { 7 | host = '127.0.0.1', 8 | port = 6379, 9 | } 10 | 11 | local client = redis.connect(params) 12 | client:select(15) -- for testing purposes 13 | 14 | local replies = client:transaction(function(t) 15 | t:incrby('counter', 10) 16 | t:incrby('counter', 30) 17 | t:decrby('counter', 15) 18 | end) 19 | 20 | -- check-and-set (CAS) 21 | client:set('foo', 'bar') 22 | local replies = client:transaction({ watch = 'foo', cas = true }, function(t) 23 | --executed after WATCH but before MULTI 24 | local val = t:get('foo') 25 | t:multi() 26 | --executing during MULTI block 27 | t:set('foo', 'foo' .. val) 28 | t:get('foo') 29 | end) 30 | 31 | for _, reply in pairs(replies) do 32 | print('*', reply) 33 | end 34 | -------------------------------------------------------------------------------- /rockspec/redis-lua-1.0.1-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "redis-lua" 2 | version = "1.0.1-0" 3 | 4 | source = { 5 | url = "http://cloud.github.com/downloads/nrk/redis-lua/redis-lua-1.0.1-0.tar.gz", 6 | md5 = "0e00178a8bc7d68d463007eec49117d5" 7 | } 8 | 9 | description = { 10 | summary = "A Lua client library for the redis key value storage system.", 11 | detailed = [[ 12 | A Lua client library for the redis key value storage system. 13 | ]], 14 | homepage = "http://github.com/nrk/redis-lua", 15 | license = "MIT/X11" 16 | } 17 | 18 | dependencies = { 19 | "lua >= 5.1", 20 | "luasocket" 21 | } 22 | 23 | build = { 24 | type = "none", 25 | install = { 26 | lua = { 27 | "redis.lua" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rockspec/redis-lua-2.0.0-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "redis-lua" 2 | version = "2.0.0-0" 3 | 4 | source = { 5 | url = "http://cloud.github.com/downloads/nrk/redis-lua/redis-lua-2.0.0-0.tar.gz", 6 | md5 = "db1f9a74d13158c1b551a4fa054a92ba" 7 | } 8 | 9 | description = { 10 | summary = "A Lua client library for the redis key value storage system.", 11 | detailed = [[ 12 | A Lua client library for the redis key value storage system. 13 | ]], 14 | homepage = "http://github.com/nrk/redis-lua", 15 | license = "MIT/X11" 16 | } 17 | 18 | dependencies = { 19 | "lua >= 5.1", 20 | "luasocket" 21 | } 22 | 23 | build = { 24 | type = "none", 25 | install = { 26 | lua = { 27 | redis = "src/redis.lua" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rockspec/redis-lua-2.0.1-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "redis-lua" 2 | version = "2.0.1-0" 3 | 4 | source = { 5 | url = "http://cloud.github.com/downloads/nrk/redis-lua/redis-lua-2.0.1-0.tar.gz", 6 | md5 = "824c9bd4e98b919747c6f1f3be322196" 7 | } 8 | 9 | description = { 10 | summary = "A Lua client library for the redis key value storage system.", 11 | detailed = [[ 12 | A Lua client library for the redis key value storage system. 13 | ]], 14 | homepage = "http://github.com/nrk/redis-lua", 15 | license = "MIT/X11" 16 | } 17 | 18 | dependencies = { 19 | "lua >= 5.1", 20 | "luasocket" 21 | } 22 | 23 | build = { 24 | type = "none", 25 | install = { 26 | lua = { 27 | redis = "src/redis.lua" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rockspec/redis-lua-2.0.2-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "redis-lua" 2 | version = "2.0.2-0" 3 | 4 | source = { 5 | url = "http://cloud.github.com/downloads/nrk/redis-lua/redis-lua-2.0.2-0.tar.gz", 6 | md5 = "4fcfd73761f47470c59a30c3818bee97" 7 | } 8 | 9 | description = { 10 | summary = "A Lua client library for the redis key value storage system.", 11 | detailed = [[ 12 | A Lua client library for the redis key value storage system. 13 | ]], 14 | homepage = "http://github.com/nrk/redis-lua", 15 | license = "MIT/X11" 16 | } 17 | 18 | dependencies = { 19 | "lua >= 5.1", 20 | "luasocket" 21 | } 22 | 23 | build = { 24 | type = "none", 25 | install = { 26 | lua = { 27 | redis = "src/redis.lua" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rockspec/redis-lua-2.0.3-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "redis-lua" 2 | version = "2.0.3-0" 3 | 4 | source = { 5 | url = "http://cloud.github.com/downloads/nrk/redis-lua/redis-lua-2.0.3-0.tar.gz", 6 | md5 = "28b370247c63a9cfd9e346e57c1a7f7a" 7 | } 8 | 9 | description = { 10 | summary = "A Lua client library for the redis key value storage system.", 11 | detailed = [[ 12 | A Lua client library for the redis key value storage system. 13 | ]], 14 | homepage = "http://github.com/nrk/redis-lua", 15 | license = "MIT/X11" 16 | } 17 | 18 | dependencies = { 19 | "lua >= 5.1", 20 | "luasocket" 21 | } 22 | 23 | build = { 24 | type = "none", 25 | install = { 26 | lua = { 27 | redis = "src/redis.lua" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rockspec/redis-lua-2.0.4-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "redis-lua" 2 | version = "2.0.4-0" 3 | 4 | source = { 5 | url = "http://cloud.github.com/downloads/nrk/redis-lua/redis-lua-2.0.4-0.tar.gz", 6 | md5 = "46e962a4f5361c82473ccd33d4b18003" 7 | } 8 | 9 | description = { 10 | summary = "A Lua client library for the redis key value storage system.", 11 | detailed = [[ 12 | A Lua client library for the redis key value storage system. 13 | ]], 14 | homepage = "http://github.com/nrk/redis-lua", 15 | license = "MIT/X11" 16 | } 17 | 18 | dependencies = { 19 | "lua >= 5.1", 20 | "luasocket" 21 | } 22 | 23 | build = { 24 | type = "none", 25 | install = { 26 | lua = { 27 | redis = "src/redis.lua" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rockspec/redis-lua-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "redis-lua" 2 | version = "scm-1" 3 | 4 | source = { 5 | url = "git://github.com/nrk/redis-lua.git" 6 | } 7 | 8 | description = { 9 | summary = "A Lua client library for the redis key value storage system.", 10 | detailed = [[ 11 | A Lua client library for the redis key value storage system. 12 | ]], 13 | homepage = "http://github.com/nrk/redis-lua", 14 | license = "MIT/X11" 15 | } 16 | 17 | dependencies = { 18 | "lua >= 5.1", 19 | "luasocket" 20 | } 21 | 22 | build = { 23 | type = "none", 24 | install = { 25 | lua = { 26 | redis = "src/redis.lua" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/redis.lua: -------------------------------------------------------------------------------- 1 | local redis = { 2 | _VERSION = 'redis-lua 2.0.5-dev', 3 | _DESCRIPTION = 'A Lua client library for the redis key value storage system.', 4 | _COPYRIGHT = 'Copyright (C) 2009-2012 Daniele Alessandri', 5 | } 6 | 7 | -- The following line is used for backwards compatibility in order to keep the `Redis` 8 | -- global module name. Using `Redis` is now deprecated so you should explicitly assign 9 | -- the module to a local variable when requiring it: `local redis = require('redis')`. 10 | Redis = redis 11 | 12 | local unpack = _G.unpack or table.unpack 13 | local network, request, response = {}, {}, {} 14 | 15 | local defaults = { 16 | host = '127.0.0.1', 17 | port = 6379, 18 | tcp_nodelay = true, 19 | path = nil, 20 | } 21 | 22 | local function merge_defaults(parameters) 23 | if parameters == nil then 24 | parameters = {} 25 | end 26 | for k, v in pairs(defaults) do 27 | if parameters[k] == nil then 28 | parameters[k] = defaults[k] 29 | end 30 | end 31 | return parameters 32 | end 33 | 34 | local function parse_boolean(v) 35 | if v == '1' or v == 'true' or v == 'TRUE' then 36 | return true 37 | elseif v == '0' or v == 'false' or v == 'FALSE' then 38 | return false 39 | else 40 | return nil 41 | end 42 | end 43 | 44 | local function toboolean(value) return value == 1 end 45 | 46 | local function sort_request(client, command, key, params) 47 | --[[ params = { 48 | by = 'weight_*', 49 | get = 'object_*', 50 | limit = { 0, 10 }, 51 | sort = 'desc', 52 | alpha = true, 53 | } ]] 54 | local query = { key } 55 | 56 | if params then 57 | if params.by then 58 | table.insert(query, 'BY') 59 | table.insert(query, params.by) 60 | end 61 | 62 | if type(params.limit) == 'table' then 63 | -- TODO: check for lower and upper limits 64 | table.insert(query, 'LIMIT') 65 | table.insert(query, params.limit[1]) 66 | table.insert(query, params.limit[2]) 67 | end 68 | 69 | if params.get then 70 | if (type(params.get) == 'table') then 71 | for _, getarg in pairs(params.get) do 72 | table.insert(query, 'GET') 73 | table.insert(query, getarg) 74 | end 75 | else 76 | table.insert(query, 'GET') 77 | table.insert(query, params.get) 78 | end 79 | end 80 | 81 | if params.sort then 82 | table.insert(query, params.sort) 83 | end 84 | 85 | if params.alpha == true then 86 | table.insert(query, 'ALPHA') 87 | end 88 | 89 | if params.store then 90 | table.insert(query, 'STORE') 91 | table.insert(query, params.store) 92 | end 93 | end 94 | 95 | request.multibulk(client, command, query) 96 | end 97 | 98 | local function zset_range_request(client, command, ...) 99 | local args, opts = {...}, { } 100 | 101 | if #args >= 1 and type(args[#args]) == 'table' then 102 | local options = table.remove(args, #args) 103 | if options.withscores then 104 | table.insert(opts, 'WITHSCORES') 105 | end 106 | end 107 | 108 | for _, v in pairs(opts) do table.insert(args, v) end 109 | request.multibulk(client, command, args) 110 | end 111 | 112 | local function zset_range_byscore_request(client, command, ...) 113 | local args, opts = {...}, { } 114 | 115 | if #args >= 1 and type(args[#args]) == 'table' then 116 | local options = table.remove(args, #args) 117 | if options.limit then 118 | table.insert(opts, 'LIMIT') 119 | table.insert(opts, options.limit.offset or options.limit[1]) 120 | table.insert(opts, options.limit.count or options.limit[2]) 121 | end 122 | if options.withscores then 123 | table.insert(opts, 'WITHSCORES') 124 | end 125 | end 126 | 127 | for _, v in pairs(opts) do table.insert(args, v) end 128 | request.multibulk(client, command, args) 129 | end 130 | 131 | local function zset_range_reply(reply, command, ...) 132 | local args = {...} 133 | local opts = args[4] 134 | if opts and (opts.withscores or string.lower(tostring(opts)) == 'withscores') then 135 | local new_reply = { } 136 | for i = 1, #reply, 2 do 137 | table.insert(new_reply, { reply[i], reply[i + 1] }) 138 | end 139 | return new_reply 140 | else 141 | return reply 142 | end 143 | end 144 | 145 | local function zset_store_request(client, command, ...) 146 | local args, opts = {...}, { } 147 | 148 | if #args >= 1 and type(args[#args]) == 'table' then 149 | local options = table.remove(args, #args) 150 | if options.weights and type(options.weights) == 'table' then 151 | table.insert(opts, 'WEIGHTS') 152 | for _, weight in ipairs(options.weights) do 153 | table.insert(opts, weight) 154 | end 155 | end 156 | if options.aggregate then 157 | table.insert(opts, 'AGGREGATE') 158 | table.insert(opts, options.aggregate) 159 | end 160 | end 161 | 162 | for _, v in pairs(opts) do table.insert(args, v) end 163 | request.multibulk(client, command, args) 164 | end 165 | 166 | local function mset_filter_args(client, command, ...) 167 | local args, arguments = {...}, {} 168 | if (#args == 1 and type(args[1]) == 'table') then 169 | for k,v in pairs(args[1]) do 170 | table.insert(arguments, k) 171 | table.insert(arguments, v) 172 | end 173 | else 174 | arguments = args 175 | end 176 | request.multibulk(client, command, arguments) 177 | end 178 | 179 | local function hash_multi_request_builder(builder_callback) 180 | return function(client, command, ...) 181 | local args, arguments = {...}, { } 182 | if #args == 2 then 183 | table.insert(arguments, args[1]) 184 | for k, v in pairs(args[2]) do 185 | builder_callback(arguments, k, v) 186 | end 187 | else 188 | arguments = args 189 | end 190 | request.multibulk(client, command, arguments) 191 | end 192 | end 193 | 194 | local function parse_info(response) 195 | local info = {} 196 | local current = info 197 | 198 | response:gsub('([^\r\n]*)\r\n', function(kv) 199 | if kv == '' then return end 200 | 201 | local section = kv:match('^# (%w+)$') 202 | if section then 203 | current = {} 204 | info[section:lower()] = current 205 | return 206 | end 207 | 208 | local k,v = kv:match(('([^:]*):([^:]*)'):rep(1)) 209 | if k:match('db%d+') then 210 | current[k] = {} 211 | v:gsub(',', function(dbkv) 212 | local dbk,dbv = kv:match('([^:]*)=([^:]*)') 213 | current[k][dbk] = dbv 214 | end) 215 | else 216 | current[k] = v 217 | end 218 | end) 219 | 220 | return info 221 | end 222 | 223 | local function scan_request(client, command, ...) 224 | local args, req, params = {...}, { }, nil 225 | 226 | if command == 'SCAN' then 227 | table.insert(req, args[1]) 228 | params = args[2] 229 | else 230 | table.insert(req, args[1]) 231 | table.insert(req, args[2]) 232 | params = args[3] 233 | end 234 | 235 | if params and params.match then 236 | table.insert(req, 'MATCH') 237 | table.insert(req, params.match) 238 | end 239 | 240 | if params and params.count then 241 | table.insert(req, 'COUNT') 242 | table.insert(req, params.count) 243 | end 244 | 245 | request.multibulk(client, command, req) 246 | end 247 | 248 | local zscan_response = function(reply, command, ...) 249 | local original, new = reply[2], { } 250 | for i = 1, #original, 2 do 251 | table.insert(new, { original[i], tonumber(original[i + 1]) }) 252 | end 253 | reply[2] = new 254 | 255 | return reply 256 | end 257 | 258 | local hscan_response = function(reply, command, ...) 259 | local original, new = reply[2], { } 260 | for i = 1, #original, 2 do 261 | new[original[i]] = original[i + 1] 262 | end 263 | reply[2] = new 264 | 265 | return reply 266 | end 267 | 268 | local function load_methods(proto, commands) 269 | local client = setmetatable ({}, getmetatable(proto)) 270 | 271 | for cmd, fn in pairs(commands) do 272 | if type(fn) ~= 'function' then 273 | redis.error('invalid type for command ' .. cmd .. '(must be a function)') 274 | end 275 | client[cmd] = fn 276 | end 277 | 278 | for i, v in pairs(proto) do 279 | client[i] = v 280 | end 281 | 282 | return client 283 | end 284 | 285 | local function create_client(proto, client_socket, commands) 286 | local client = load_methods(proto, commands) 287 | client.error = redis.error 288 | client.network = { 289 | socket = client_socket, 290 | read = network.read, 291 | write = network.write, 292 | } 293 | client.requests = { 294 | multibulk = request.multibulk, 295 | } 296 | return client 297 | end 298 | 299 | -- ############################################################################ 300 | 301 | function network.write(client, buffer) 302 | local _, err = client.network.socket:send(buffer) 303 | if err then client.error(err) end 304 | end 305 | 306 | function network.read(client, len) 307 | if len == nil then len = '*l' end 308 | local line, err = client.network.socket:receive(len) 309 | if not err then return line else client.error('connection error: ' .. err) end 310 | end 311 | 312 | -- ############################################################################ 313 | 314 | function response.read(client) 315 | local payload = client.network.read(client) 316 | local prefix, data = payload:sub(1, -#payload), payload:sub(2) 317 | 318 | -- status reply 319 | if prefix == '+' then 320 | if data == 'OK' then 321 | return true 322 | elseif data == 'QUEUED' then 323 | return { queued = true } 324 | else 325 | return data 326 | end 327 | 328 | -- error reply 329 | elseif prefix == '-' then 330 | return client.error('redis error: ' .. data) 331 | 332 | -- integer reply 333 | elseif prefix == ':' then 334 | local number = tonumber(data) 335 | 336 | if not number then 337 | if data == 'nil' then 338 | return nil 339 | end 340 | client.error('cannot parse ' .. data .. ' as a numeric response.') 341 | end 342 | 343 | return number 344 | 345 | -- bulk reply 346 | elseif prefix == '$' then 347 | local length = tonumber(data) 348 | 349 | if not length then 350 | client.error('cannot parse ' .. length .. ' as data length') 351 | end 352 | 353 | if length == -1 then 354 | return nil 355 | end 356 | 357 | local nextchunk = client.network.read(client, length + 2) 358 | 359 | return nextchunk:sub(1, -3) 360 | 361 | -- multibulk reply 362 | elseif prefix == '*' then 363 | local count = tonumber(data) 364 | 365 | if count == -1 then 366 | return nil 367 | end 368 | 369 | local list = {} 370 | if count > 0 then 371 | local reader = response.read 372 | for i = 1, count do 373 | list[i] = reader(client) 374 | end 375 | end 376 | return list 377 | 378 | -- unknown type of reply 379 | else 380 | return client.error('unknown response prefix: ' .. prefix) 381 | end 382 | end 383 | 384 | -- ############################################################################ 385 | 386 | function request.raw(client, buffer) 387 | local bufferType = type(buffer) 388 | 389 | if bufferType == 'table' then 390 | client.network.write(client, table.concat(buffer)) 391 | elseif bufferType == 'string' then 392 | client.network.write(client, buffer) 393 | else 394 | client.error('argument error: ' .. bufferType) 395 | end 396 | end 397 | 398 | function request.multibulk(client, command, ...) 399 | local args = {...} 400 | local argsn = #args 401 | local buffer = { true, true } 402 | 403 | if argsn == 1 and type(args[1]) == 'table' then 404 | argsn, args = #args[1], args[1] 405 | end 406 | 407 | buffer[1] = '*' .. tostring(argsn + 1) .. "\r\n" 408 | buffer[2] = '$' .. #command .. "\r\n" .. command .. "\r\n" 409 | 410 | local table_insert = table.insert 411 | for i = 1, argsn do 412 | local s_argument = tostring(args[i] or '') 413 | table_insert(buffer, '$' .. #s_argument .. "\r\n" .. s_argument .. "\r\n") 414 | end 415 | 416 | client.network.write(client, table.concat(buffer)) 417 | end 418 | 419 | -- ############################################################################ 420 | 421 | local function custom(command, send, parse) 422 | command = string.upper(command) 423 | return function(client, ...) 424 | send(client, command, ...) 425 | local reply = response.read(client) 426 | 427 | if type(reply) == 'table' and reply.queued then 428 | reply.parser = parse 429 | return reply 430 | else 431 | if parse then 432 | return parse(reply, command, ...) 433 | end 434 | return reply 435 | end 436 | end 437 | end 438 | 439 | local function command(command, opts) 440 | if opts == nil or type(opts) == 'function' then 441 | return custom(command, request.multibulk, opts) 442 | else 443 | return custom(command, opts.request or request.multibulk, opts.response) 444 | end 445 | end 446 | 447 | local define_command_impl = function(target, name, opts) 448 | local opts = opts or {} 449 | target[string.lower(name)] = custom( 450 | opts.command or string.upper(name), 451 | opts.request or request.multibulk, 452 | opts.response or nil 453 | ) 454 | end 455 | 456 | local undefine_command_impl = function(target, name) 457 | target[string.lower(name)] = nil 458 | end 459 | 460 | -- ############################################################################ 461 | 462 | local client_prototype = {} 463 | 464 | client_prototype.raw_cmd = function(client, buffer) 465 | request.raw(client, buffer .. "\r\n") 466 | return response.read(client) 467 | end 468 | 469 | -- obsolete 470 | client_prototype.define_command = function(client, name, opts) 471 | define_command_impl(client, name, opts) 472 | end 473 | 474 | -- obsolete 475 | client_prototype.undefine_command = function(client, name) 476 | undefine_command_impl(client, name) 477 | end 478 | 479 | client_prototype.quit = function(client) 480 | request.multibulk(client, 'QUIT') 481 | client.network.socket:shutdown() 482 | return true 483 | end 484 | 485 | client_prototype.shutdown = function(client) 486 | request.multibulk(client, 'SHUTDOWN') 487 | client.network.socket:shutdown() 488 | end 489 | 490 | -- Command pipelining 491 | 492 | client_prototype.pipeline = function(client, block) 493 | local requests, replies, parsers = {}, {}, {} 494 | local table_insert = table.insert 495 | local socket_write, socket_read = client.network.write, client.network.read 496 | 497 | client.network.write = function(_, buffer) 498 | table_insert(requests, buffer) 499 | end 500 | 501 | -- TODO: this hack is necessary to temporarily reuse the current 502 | -- request -> response handling implementation of redis-lua 503 | -- without further changes in the code, but it will surely 504 | -- disappear when the new command-definition infrastructure 505 | -- will finally be in place. 506 | client.network.read = function() return '+QUEUED' end 507 | 508 | local pipeline = setmetatable({}, { 509 | __index = function(env, name) 510 | local cmd = client[name] 511 | if not cmd then 512 | client.error('unknown redis command: ' .. name, 2) 513 | end 514 | return function(self, ...) 515 | local reply = cmd(client, ...) 516 | table_insert(parsers, #requests, reply.parser) 517 | return reply 518 | end 519 | end 520 | }) 521 | 522 | local success, retval = pcall(block, pipeline) 523 | 524 | client.network.write, client.network.read = socket_write, socket_read 525 | if not success then client.error(retval, 0) end 526 | 527 | client.network.write(client, table.concat(requests, '')) 528 | 529 | for i = 1, #requests do 530 | local reply, parser = response.read(client), parsers[i] 531 | if parser then 532 | reply = parser(reply) 533 | end 534 | table_insert(replies, i, reply) 535 | end 536 | 537 | return replies, #requests 538 | end 539 | 540 | -- Publish/Subscribe 541 | 542 | do 543 | local channels = function(channels) 544 | if type(channels) == 'string' then 545 | channels = { channels } 546 | end 547 | return channels 548 | end 549 | 550 | local subscribe = function(client, ...) 551 | request.multibulk(client, 'subscribe', ...) 552 | end 553 | local psubscribe = function(client, ...) 554 | request.multibulk(client, 'psubscribe', ...) 555 | end 556 | local unsubscribe = function(client, ...) 557 | request.multibulk(client, 'unsubscribe') 558 | end 559 | local punsubscribe = function(client, ...) 560 | request.multibulk(client, 'punsubscribe') 561 | end 562 | 563 | local consumer_loop = function(client) 564 | local aborting, subscriptions = false, 0 565 | 566 | local abort = function() 567 | if not aborting then 568 | unsubscribe(client) 569 | punsubscribe(client) 570 | aborting = true 571 | end 572 | end 573 | 574 | return coroutine.wrap(function() 575 | while true do 576 | local message 577 | local response = response.read(client) 578 | 579 | if response[1] == 'pmessage' then 580 | message = { 581 | kind = response[1], 582 | pattern = response[2], 583 | channel = response[3], 584 | payload = response[4], 585 | } 586 | else 587 | message = { 588 | kind = response[1], 589 | channel = response[2], 590 | payload = response[3], 591 | } 592 | end 593 | 594 | if string.match(message.kind, '^p?subscribe$') then 595 | subscriptions = subscriptions + 1 596 | end 597 | if string.match(message.kind, '^p?unsubscribe$') then 598 | subscriptions = subscriptions - 1 599 | end 600 | 601 | if aborting and subscriptions == 0 then 602 | break 603 | end 604 | coroutine.yield(message, abort) 605 | end 606 | end) 607 | end 608 | 609 | client_prototype.pubsub = function(client, subscriptions) 610 | if type(subscriptions) == 'table' then 611 | if subscriptions.subscribe then 612 | subscribe(client, channels(subscriptions.subscribe)) 613 | end 614 | if subscriptions.psubscribe then 615 | psubscribe(client, channels(subscriptions.psubscribe)) 616 | end 617 | end 618 | return consumer_loop(client) 619 | end 620 | end 621 | 622 | -- Redis transactions (MULTI/EXEC) 623 | 624 | do 625 | local function identity(...) return ... end 626 | local emptytable = {} 627 | 628 | local function initialize_transaction(client, options, block, queued_parsers) 629 | local table_insert = table.insert 630 | local coro = coroutine.create(block) 631 | 632 | if options.watch then 633 | local watch_keys = {} 634 | for _, key in pairs(options.watch) do 635 | table_insert(watch_keys, key) 636 | end 637 | if #watch_keys > 0 then 638 | client:watch(unpack(watch_keys)) 639 | end 640 | end 641 | 642 | local transaction_client = setmetatable({}, {__index=client}) 643 | transaction_client.exec = function(...) 644 | client.error('cannot use EXEC inside a transaction block') 645 | end 646 | transaction_client.multi = function(...) 647 | coroutine.yield() 648 | end 649 | transaction_client.commands_queued = function() 650 | return #queued_parsers 651 | end 652 | 653 | assert(coroutine.resume(coro, transaction_client)) 654 | 655 | transaction_client.multi = nil 656 | transaction_client.discard = function(...) 657 | local reply = client:discard() 658 | for i, v in pairs(queued_parsers) do 659 | queued_parsers[i]=nil 660 | end 661 | coro = initialize_transaction(client, options, block, queued_parsers) 662 | return reply 663 | end 664 | transaction_client.watch = function(...) 665 | client.error('WATCH inside MULTI is not allowed') 666 | end 667 | setmetatable(transaction_client, { __index = function(t, k) 668 | local cmd = client[k] 669 | if type(cmd) == "function" then 670 | local function queuey(self, ...) 671 | local reply = cmd(client, ...) 672 | assert((reply or emptytable).queued == true, 'a QUEUED reply was expected') 673 | table_insert(queued_parsers, reply.parser or identity) 674 | return reply 675 | end 676 | t[k]=queuey 677 | return queuey 678 | else 679 | return cmd 680 | end 681 | end 682 | }) 683 | client:multi() 684 | return coro 685 | end 686 | 687 | local function transaction(client, options, coroutine_block, attempts) 688 | local queued_parsers, replies = {}, {} 689 | local retry = tonumber(attempts) or tonumber(options.retry) or 2 690 | local coro = initialize_transaction(client, options, coroutine_block, queued_parsers) 691 | 692 | local success, retval 693 | if coroutine.status(coro) == 'suspended' then 694 | success, retval = coroutine.resume(coro) 695 | else 696 | -- do not fail if the coroutine has not been resumed (missing t:multi() with CAS) 697 | success, retval = true, 'empty transaction' 698 | end 699 | if #queued_parsers == 0 or not success then 700 | client:discard() 701 | assert(success, retval) 702 | return replies, 0 703 | end 704 | 705 | local raw_replies = client:exec() 706 | if not raw_replies then 707 | if (retry or 0) <= 0 then 708 | client.error("MULTI/EXEC transaction aborted by the server") 709 | else 710 | --we're not quite done yet 711 | return transaction(client, options, coroutine_block, retry - 1) 712 | end 713 | end 714 | 715 | local table_insert = table.insert 716 | for i, parser in pairs(queued_parsers) do 717 | table_insert(replies, i, parser(raw_replies[i])) 718 | end 719 | 720 | return replies, #queued_parsers 721 | end 722 | 723 | client_prototype.transaction = function(client, arg1, arg2) 724 | local options, block 725 | if not arg2 then 726 | options, block = {}, arg1 727 | elseif arg1 then --and arg2, implicitly 728 | options, block = type(arg1)=="table" and arg1 or { arg1 }, arg2 729 | else 730 | client.error("Invalid parameters for redis transaction.") 731 | end 732 | 733 | if not options.watch then 734 | local watch_keys = { } 735 | for i, v in pairs(options) do 736 | if tonumber(i) then 737 | table.insert(watch_keys, v) 738 | options[i] = nil 739 | end 740 | end 741 | options.watch = watch_keys 742 | elseif not (type(options.watch) == 'table') then 743 | options.watch = { options.watch } 744 | end 745 | 746 | if not options.cas then 747 | local tx_block = block 748 | block = function(client, ...) 749 | client:multi() 750 | return tx_block(client, ...) --can't wrap this in pcall because we're in a coroutine. 751 | end 752 | end 753 | 754 | return transaction(client, options, block) 755 | end 756 | end 757 | 758 | -- MONITOR context 759 | 760 | do 761 | local monitor_loop = function(client) 762 | local monitoring = true 763 | 764 | -- Tricky since the payload format changed starting from Redis 2.6. 765 | local pattern = '^(%d+%.%d+)( ?.- ?) ?"(%a+)" ?(.-)$' 766 | 767 | local abort = function() 768 | monitoring = false 769 | end 770 | 771 | return coroutine.wrap(function() 772 | client:monitor() 773 | 774 | while monitoring do 775 | local message, matched 776 | local response = response.read(client) 777 | 778 | local ok = response:gsub(pattern, function(time, info, cmd, args) 779 | message = { 780 | timestamp = tonumber(time), 781 | client = info:match('%d+.%d+.%d+.%d+:%d+'), 782 | database = tonumber(info:match('%d+')) or 0, 783 | command = cmd, 784 | arguments = args:match('.+'), 785 | } 786 | matched = true 787 | end) 788 | 789 | if not matched then 790 | client.error('Unable to match MONITOR payload: '..response) 791 | end 792 | 793 | coroutine.yield(message, abort) 794 | end 795 | end) 796 | end 797 | 798 | client_prototype.monitor_messages = function(client) 799 | return monitor_loop(client) 800 | end 801 | end 802 | 803 | -- ############################################################################ 804 | 805 | local function connect_tcp(socket, parameters) 806 | local host, port = parameters.host, tonumber(parameters.port) 807 | if parameters.timeout then 808 | socket:settimeout(parameters.timeout, 't') 809 | end 810 | 811 | local ok, err = socket:connect(host, port) 812 | if not ok then 813 | redis.error('could not connect to '..host..':'..port..' ['..err..']') 814 | end 815 | socket:setoption('tcp-nodelay', parameters.tcp_nodelay) 816 | return socket 817 | end 818 | 819 | local function connect_unix(socket, parameters) 820 | local ok, err = socket:connect(parameters.path) 821 | if not ok then 822 | redis.error('could not connect to '..parameters.path..' ['..err..']') 823 | end 824 | return socket 825 | end 826 | 827 | local function create_connection(parameters) 828 | if parameters.socket then 829 | return parameters.socket 830 | end 831 | 832 | local perform_connection, socket 833 | 834 | if parameters.scheme == 'unix' then 835 | perform_connection, socket = connect_unix, require('socket.unix') 836 | assert(socket, 'your build of LuaSocket does not support UNIX domain sockets') 837 | else 838 | if parameters.scheme then 839 | local scheme = parameters.scheme 840 | assert(scheme == 'redis' or scheme == 'tcp', 'invalid scheme: '..scheme) 841 | end 842 | perform_connection, socket = connect_tcp, require('socket').tcp 843 | end 844 | 845 | return perform_connection(socket(), parameters) 846 | end 847 | 848 | -- ############################################################################ 849 | 850 | function redis.error(message, level) 851 | error(message, (level or 1) + 1) 852 | end 853 | 854 | function redis.connect(...) 855 | local args, parameters = {...}, nil 856 | 857 | if #args == 1 then 858 | if type(args[1]) == 'table' then 859 | parameters = args[1] 860 | else 861 | local uri = require('socket.url') 862 | parameters = uri.parse(select(1, ...)) 863 | if parameters.scheme then 864 | if parameters.query then 865 | for k, v in parameters.query:gmatch('([-_%w]+)=([-_%w]+)') do 866 | if k == 'tcp_nodelay' or k == 'tcp-nodelay' then 867 | parameters.tcp_nodelay = parse_boolean(v) 868 | elseif k == 'timeout' then 869 | parameters.timeout = tonumber(v) 870 | end 871 | end 872 | end 873 | else 874 | parameters.host = parameters.path 875 | end 876 | end 877 | elseif #args > 1 then 878 | local host, port, timeout = unpack(args) 879 | parameters = { host = host, port = port, timeout = tonumber(timeout) } 880 | end 881 | 882 | local commands = redis.commands or {} 883 | if type(commands) ~= 'table' then 884 | redis.error('invalid type for the commands table') 885 | end 886 | 887 | local socket = create_connection(merge_defaults(parameters)) 888 | local client = create_client(client_prototype, socket, commands) 889 | 890 | return client 891 | end 892 | 893 | function redis.command(cmd, opts) 894 | return command(cmd, opts) 895 | end 896 | 897 | -- obsolete 898 | function redis.define_command(name, opts) 899 | define_command_impl(redis.commands, name, opts) 900 | end 901 | 902 | -- obsolete 903 | function redis.undefine_command(name) 904 | undefine_command_impl(redis.commands, name) 905 | end 906 | 907 | -- ############################################################################ 908 | 909 | -- Commands defined in this table do not take the precedence over 910 | -- methods defined in the client prototype table. 911 | 912 | redis.commands = { 913 | -- commands operating on the key space 914 | exists = command('EXISTS', { 915 | response = toboolean 916 | }), 917 | del = command('DEL'), 918 | type = command('TYPE'), 919 | rename = command('RENAME'), 920 | renamenx = command('RENAMENX', { 921 | response = toboolean 922 | }), 923 | expire = command('EXPIRE', { 924 | response = toboolean 925 | }), 926 | pexpire = command('PEXPIRE', { -- >= 2.6 927 | response = toboolean 928 | }), 929 | expireat = command('EXPIREAT', { 930 | response = toboolean 931 | }), 932 | pexpireat = command('PEXPIREAT', { -- >= 2.6 933 | response = toboolean 934 | }), 935 | ttl = command('TTL'), 936 | pttl = command('PTTL'), -- >= 2.6 937 | move = command('MOVE', { 938 | response = toboolean 939 | }), 940 | dbsize = command('DBSIZE'), 941 | persist = command('PERSIST', { -- >= 2.2 942 | response = toboolean 943 | }), 944 | keys = command('KEYS', { 945 | response = function(response) 946 | if type(response) == 'string' then 947 | -- backwards compatibility path for Redis < 2.0 948 | local keys = {} 949 | response:gsub('[^%s]+', function(key) 950 | table.insert(keys, key) 951 | end) 952 | response = keys 953 | end 954 | return response 955 | end 956 | }), 957 | randomkey = command('RANDOMKEY'), 958 | sort = command('SORT', { 959 | request = sort_request, 960 | }), 961 | scan = command('SCAN', { -- >= 2.8 962 | request = scan_request, 963 | }), 964 | 965 | -- commands operating on string values 966 | set = command('SET'), 967 | setnx = command('SETNX', { 968 | response = toboolean 969 | }), 970 | setex = command('SETEX'), -- >= 2.0 971 | psetex = command('PSETEX'), -- >= 2.6 972 | mset = command('MSET', { 973 | request = mset_filter_args 974 | }), 975 | msetnx = command('MSETNX', { 976 | request = mset_filter_args, 977 | response = toboolean 978 | }), 979 | get = command('GET'), 980 | mget = command('MGET'), 981 | getset = command('GETSET'), 982 | incr = command('INCR'), 983 | incrby = command('INCRBY'), 984 | incrbyfloat = command('INCRBYFLOAT', { -- >= 2.6 985 | response = function(reply, command, ...) 986 | return tonumber(reply) 987 | end, 988 | }), 989 | decr = command('DECR'), 990 | decrby = command('DECRBY'), 991 | append = command('APPEND'), -- >= 2.0 992 | substr = command('SUBSTR'), -- >= 2.0 993 | strlen = command('STRLEN'), -- >= 2.2 994 | setrange = command('SETRANGE'), -- >= 2.2 995 | getrange = command('GETRANGE'), -- >= 2.2 996 | setbit = command('SETBIT'), -- >= 2.2 997 | getbit = command('GETBIT'), -- >= 2.2 998 | bitop = command('BITOP'), -- >= 2.6 999 | bitcount = command('BITCOUNT'), -- >= 2.6 1000 | 1001 | -- commands operating on lists 1002 | rpush = command('RPUSH'), 1003 | lpush = command('LPUSH'), 1004 | llen = command('LLEN'), 1005 | lrange = command('LRANGE'), 1006 | ltrim = command('LTRIM'), 1007 | lindex = command('LINDEX'), 1008 | lset = command('LSET'), 1009 | lrem = command('LREM'), 1010 | lpop = command('LPOP'), 1011 | rpop = command('RPOP'), 1012 | rpoplpush = command('RPOPLPUSH'), 1013 | blpop = command('BLPOP'), -- >= 2.0 1014 | brpop = command('BRPOP'), -- >= 2.0 1015 | rpushx = command('RPUSHX'), -- >= 2.2 1016 | lpushx = command('LPUSHX'), -- >= 2.2 1017 | linsert = command('LINSERT'), -- >= 2.2 1018 | brpoplpush = command('BRPOPLPUSH'), -- >= 2.2 1019 | 1020 | -- commands operating on sets 1021 | sadd = command('SADD'), 1022 | srem = command('SREM'), 1023 | spop = command('SPOP'), 1024 | smove = command('SMOVE', { 1025 | response = toboolean 1026 | }), 1027 | scard = command('SCARD'), 1028 | sismember = command('SISMEMBER', { 1029 | response = toboolean 1030 | }), 1031 | sinter = command('SINTER'), 1032 | sinterstore = command('SINTERSTORE'), 1033 | sunion = command('SUNION'), 1034 | sunionstore = command('SUNIONSTORE'), 1035 | sdiff = command('SDIFF'), 1036 | sdiffstore = command('SDIFFSTORE'), 1037 | smembers = command('SMEMBERS'), 1038 | srandmember = command('SRANDMEMBER'), 1039 | sscan = command('SSCAN', { -- >= 2.8 1040 | request = scan_request, 1041 | }), 1042 | 1043 | -- commands operating on sorted sets 1044 | zadd = command('ZADD'), 1045 | zincrby = command('ZINCRBY', { 1046 | response = function(reply, command, ...) 1047 | return tonumber(reply) 1048 | end, 1049 | }), 1050 | zrem = command('ZREM'), 1051 | zrange = command('ZRANGE', { 1052 | request = zset_range_request, 1053 | response = zset_range_reply, 1054 | }), 1055 | zrevrange = command('ZREVRANGE', { 1056 | request = zset_range_request, 1057 | response = zset_range_reply, 1058 | }), 1059 | zrangebyscore = command('ZRANGEBYSCORE', { 1060 | request = zset_range_byscore_request, 1061 | response = zset_range_reply, 1062 | }), 1063 | zrevrangebyscore = command('ZREVRANGEBYSCORE', { -- >= 2.2 1064 | request = zset_range_byscore_request, 1065 | response = zset_range_reply, 1066 | }), 1067 | zunionstore = command('ZUNIONSTORE', { -- >= 2.0 1068 | request = zset_store_request 1069 | }), 1070 | zinterstore = command('ZINTERSTORE', { -- >= 2.0 1071 | request = zset_store_request 1072 | }), 1073 | zcount = command('ZCOUNT'), 1074 | zcard = command('ZCARD'), 1075 | zscore = command('ZSCORE'), 1076 | zremrangebyscore = command('ZREMRANGEBYSCORE'), 1077 | zrank = command('ZRANK'), -- >= 2.0 1078 | zrevrank = command('ZREVRANK'), -- >= 2.0 1079 | zremrangebyrank = command('ZREMRANGEBYRANK'), -- >= 2.0 1080 | zscan = command('ZSCAN', { -- >= 2.8 1081 | request = scan_request, 1082 | response = zscan_response, 1083 | }), 1084 | 1085 | -- commands operating on hashes 1086 | hset = command('HSET', { -- >= 2.0 1087 | response = toboolean 1088 | }), 1089 | hsetnx = command('HSETNX', { -- >= 2.0 1090 | response = toboolean 1091 | }), 1092 | hmset = command('HMSET', { -- >= 2.0 1093 | request = hash_multi_request_builder(function(args, k, v) 1094 | table.insert(args, k) 1095 | table.insert(args, v) 1096 | end), 1097 | }), 1098 | hincrby = command('HINCRBY'), -- >= 2.0 1099 | hincrbyfloat = command('HINCRBYFLOAT', {-- >= 2.6 1100 | response = function(reply, command, ...) 1101 | return tonumber(reply) 1102 | end, 1103 | }), 1104 | hget = command('HGET'), -- >= 2.0 1105 | hmget = command('HMGET', { -- >= 2.0 1106 | request = hash_multi_request_builder(function(args, k, v) 1107 | table.insert(args, v) 1108 | end), 1109 | }), 1110 | hdel = command('HDEL'), -- >= 2.0 1111 | hexists = command('HEXISTS', { -- >= 2.0 1112 | response = toboolean 1113 | }), 1114 | hlen = command('HLEN'), -- >= 2.0 1115 | hkeys = command('HKEYS'), -- >= 2.0 1116 | hvals = command('HVALS'), -- >= 2.0 1117 | hgetall = command('HGETALL', { -- >= 2.0 1118 | response = function(reply, command, ...) 1119 | local new_reply = { } 1120 | for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end 1121 | return new_reply 1122 | end 1123 | }), 1124 | hscan = command('HSCAN', { -- >= 2.8 1125 | request = scan_request, 1126 | response = hscan_response, 1127 | }), 1128 | 1129 | -- connection related commands 1130 | ping = command('PING', { 1131 | response = function(response) return response == 'PONG' end 1132 | }), 1133 | echo = command('ECHO'), 1134 | auth = command('AUTH'), 1135 | select = command('SELECT'), 1136 | 1137 | -- transactions 1138 | multi = command('MULTI'), -- >= 2.0 1139 | exec = command('EXEC'), -- >= 2.0 1140 | discard = command('DISCARD'), -- >= 2.0 1141 | watch = command('WATCH'), -- >= 2.2 1142 | unwatch = command('UNWATCH'), -- >= 2.2 1143 | 1144 | -- publish - subscribe 1145 | subscribe = command('SUBSCRIBE'), -- >= 2.0 1146 | unsubscribe = command('UNSUBSCRIBE'), -- >= 2.0 1147 | psubscribe = command('PSUBSCRIBE'), -- >= 2.0 1148 | punsubscribe = command('PUNSUBSCRIBE'), -- >= 2.0 1149 | publish = command('PUBLISH'), -- >= 2.0 1150 | 1151 | -- redis scripting 1152 | eval = command('EVAL'), -- >= 2.6 1153 | evalsha = command('EVALSHA'), -- >= 2.6 1154 | script = command('SCRIPT'), -- >= 2.6 1155 | 1156 | -- remote server control commands 1157 | bgrewriteaof = command('BGREWRITEAOF'), 1158 | config = command('CONFIG', { -- >= 2.0 1159 | response = function(reply, command, ...) 1160 | if (type(reply) == 'table') then 1161 | local new_reply = { } 1162 | for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end 1163 | return new_reply 1164 | end 1165 | 1166 | return reply 1167 | end 1168 | }), 1169 | client = command('CLIENT'), -- >= 2.4 1170 | slaveof = command('SLAVEOF'), 1171 | save = command('SAVE'), 1172 | bgsave = command('BGSAVE'), 1173 | lastsave = command('LASTSAVE'), 1174 | flushdb = command('FLUSHDB'), 1175 | flushall = command('FLUSHALL'), 1176 | monitor = command('MONITOR'), 1177 | time = command('TIME'), -- >= 2.6 1178 | slowlog = command('SLOWLOG', { -- >= 2.2.13 1179 | response = function(reply, command, ...) 1180 | if (type(reply) == 'table') then 1181 | local structured = { } 1182 | for index, entry in ipairs(reply) do 1183 | structured[index] = { 1184 | id = tonumber(entry[1]), 1185 | timestamp = tonumber(entry[2]), 1186 | duration = tonumber(entry[3]), 1187 | command = entry[4], 1188 | } 1189 | end 1190 | return structured 1191 | end 1192 | 1193 | return reply 1194 | end 1195 | }), 1196 | info = command('INFO', { 1197 | response = parse_info, 1198 | }), 1199 | } 1200 | 1201 | -- ############################################################################ 1202 | 1203 | return redis 1204 | -------------------------------------------------------------------------------- /test/test_client.lua: -------------------------------------------------------------------------------- 1 | package.path = "../src/?.lua;src/?.lua;" .. package.path 2 | 3 | pcall(require, "luarocks.require") 4 | 5 | local unpack = _G.unpack or table.unpack 6 | 7 | local tsc = require "telescope" 8 | local redis = require "redis" 9 | 10 | local settings = { 11 | host = '127.0.0.1', 12 | port = 6379, 13 | database = 14, 14 | password = nil, 15 | } 16 | 17 | function table.merge(self, tbl2) 18 | local new_table = {} 19 | for k,v in pairs(self) do new_table[k] = v end 20 | for k,v in pairs(tbl2) do new_table[k] = v end 21 | return new_table 22 | end 23 | 24 | function table.keys(self) 25 | local keys = {} 26 | for k, _ in pairs(self) do table.insert(keys, k) end 27 | return keys 28 | end 29 | 30 | function table.values(self) 31 | local values = {} 32 | for _, v in pairs(self) do table.insert(values, v) end 33 | return values 34 | end 35 | 36 | function table.contains(self, value) 37 | for _, v in pairs(self) do 38 | if v == value then return true end 39 | end 40 | return false 41 | end 42 | 43 | function table.slice(self, first, length) 44 | -- TODO: must be improved 45 | local new_table = {} 46 | for i = first, first + length - 1 do 47 | table.insert(new_table, self[i]) 48 | end 49 | return new_table 50 | end 51 | 52 | function table.compare(self, other) 53 | -- NOTE: the body of this function was taken and slightly adapted from 54 | -- Penlight (http://github.com/stevedonovan/Penlight) 55 | if #self ~= #other then return false end 56 | local visited = {} 57 | for i = 1, #self do 58 | local val, gotcha = self[i], nil 59 | for j = 1, #other do 60 | if not visited[j] then 61 | if (type(val) == 'table') then 62 | if (table.compare(val, other[j])) then 63 | gotcha = j 64 | break 65 | end 66 | else 67 | if val == other[j] then 68 | gotcha = j 69 | break 70 | end 71 | end 72 | end 73 | end 74 | if not gotcha then return false end 75 | visited[gotcha] = true 76 | end 77 | return true 78 | end 79 | 80 | function parse_version(version_str) 81 | local major, minor, patch, status = version_str:match('^(%d+)%.(%d+)%.(%d+)%-?(%w-)$') 82 | 83 | local info = { 84 | string = version_str, 85 | compare = function(self, other) 86 | if type(other) == 'string' then 87 | other = parse_version(other) 88 | end 89 | if self.unrecognized or other.unrecognized then 90 | error('Cannot compare versions') 91 | end 92 | 93 | for _, part in ipairs({ 'major', 'minor', 'patch' }) do 94 | if self[part] < other[part] then 95 | return -1 96 | end 97 | if self[part] > other[part] then 98 | return 1 99 | end 100 | end 101 | 102 | return 0 103 | end, 104 | is = function(self, op, other) 105 | local comparation = self:compare(other); 106 | if op == '<' then return comparation < 0 end 107 | if op == '<=' then return comparation <= 0 end 108 | if op == '=' then return comparation == 0 end 109 | if op == '>=' then return comparation >= 0 end 110 | if op == '>' then return comparation > 0 end 111 | 112 | error('Invalid comparison operator: '..op) 113 | end, 114 | } 115 | 116 | if major and minor and patch then 117 | info.major = tonumber(major) 118 | info.minor = tonumber(minor) 119 | info.patch = tonumber(patch) 120 | if status then 121 | info.status = status 122 | end 123 | else 124 | info.unrecognized = true 125 | end 126 | 127 | return info 128 | end 129 | 130 | local utils = { 131 | create_client = function(parameters) 132 | if parameters == nil then 133 | parameters = settings 134 | end 135 | 136 | local client = redis.connect(parameters.host, parameters.port) 137 | if parameters.password then client:auth(parameters.password) end 138 | if parameters.database then client:select(parameters.database) end 139 | client:flushdb() 140 | 141 | local info = client:info() 142 | local version = parse_version(info.redis_version or info.server.redis_version) 143 | 144 | if version:is('<', '1.2.0') then 145 | error("redis-lua does not support Redis < 1.2.0 (current: "..version.string..")") 146 | end 147 | 148 | return client, version 149 | end, 150 | rpush_return = function(client, key, values, wipe) 151 | if wipe then client:del(key) end 152 | for _, v in ipairs(values) do 153 | client:rpush(key, v) 154 | end 155 | return values 156 | end, 157 | sadd_return = function(client, key, values, wipe) 158 | if wipe then client:del(key) end 159 | for _, v in ipairs(values) do 160 | client:sadd(key, v) 161 | end 162 | return values 163 | end, 164 | zadd_return = function(client, key, values, wipe) 165 | if wipe then client:del(key) end 166 | for k, v in pairs(values) do 167 | client:zadd(key, v, k) 168 | end 169 | return values 170 | end, 171 | sleep = function(sec) 172 | socket.select(nil, nil, sec) 173 | end, 174 | } 175 | 176 | local shared = { 177 | kvs_table = function() 178 | return { 179 | foo = 'bar', 180 | hoge = 'piyo', 181 | foofoo = 'barbar', 182 | } 183 | end, 184 | kvs_ns_table = function() 185 | return { 186 | ['metavars:foo'] = 'bar', 187 | ['metavars:hoge'] = 'piyo', 188 | ['metavars:foofoo'] = 'barbar', 189 | } 190 | end, 191 | lang_table = function() 192 | return { 193 | italian = "ciao", 194 | english = "hello", 195 | japanese = "こんいちは!", 196 | } 197 | end, 198 | numbers = function() 199 | return { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' } 200 | end, 201 | zset_sample = function() 202 | return { a = -10, b = 0, c = 10, d = 20, e = 20, f = 30 } 203 | end, 204 | } 205 | 206 | tsc.make_assertion("table_values", "'%s' to have the same values as '%s'", table.compare) 207 | tsc.make_assertion("response_queued", "to be queued", function(response) 208 | if type(response) == 'table' and response.queued == true then 209 | return true 210 | else 211 | return false 212 | end 213 | end) 214 | tsc.make_assertion("error_message", "result to be an error with the expected message", function(msg, f) 215 | local ok, err = pcall(f) 216 | return not ok and err:match(msg) 217 | end) 218 | 219 | -- ------------------------------------------------------------------------- -- 220 | 221 | context("Client initialization", function() 222 | test("Can connect successfully", function() 223 | local client = redis.connect(settings.host, settings.port) 224 | assert_type(client, 'table') 225 | assert_true(table.contains(table.keys(client.network), 'socket')) 226 | 227 | client.network.socket:send("PING\r\n") 228 | assert_equal(client.network.socket:receive('*l'), '+PONG') 229 | end) 230 | 231 | test("Can handle connection failures", function() 232 | assert_error_message("could not connect to .*:%d+ %[connection refused%]", function() 233 | redis.connect(settings.host, settings.port + 100) 234 | end) 235 | end) 236 | 237 | test("Accepts an URI for connection parameters", function() 238 | local uri = 'redis://'..settings.host..':'..settings.port 239 | local client = redis.connect(uri) 240 | assert_type(client, 'table') 241 | end) 242 | 243 | test("Accepts a table for connection parameters", function() 244 | local client = redis.connect(settings) 245 | assert_type(client, 'table') 246 | end) 247 | 248 | test("Can use an already connected socket", function() 249 | local connection = require('socket').tcp() 250 | connection:connect(settings.host, settings.port) 251 | 252 | local client = redis.connect({ socket = connection }) 253 | assert_type(client, 'table') 254 | assert_true(client:ping()) 255 | end) 256 | 257 | test("Can specify a timeout for connecting", function() 258 | local time, timeout = os.time(), 2; 259 | 260 | assert_error_message("could not connect to .*:%d+ %[timeout%]", function() 261 | redis.connect({ host = '169.254.255.255', timeout = timeout }) 262 | end) 263 | 264 | assert_equal(time + timeout, os.time()) 265 | end) 266 | end) 267 | 268 | context("Client features", function() 269 | before(function() 270 | client = utils.create_client(settings) 271 | end) 272 | 273 | test("Send raw commands", function() 274 | assert_equal(client:raw_cmd("PING\r\n"), 'PONG') 275 | assert_true(client:raw_cmd("*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n")) 276 | assert_equal(client:raw_cmd("GET foo\r\n"), 'bar') 277 | end) 278 | 279 | test("Tansform nil command arguments into empty strings", function() 280 | assert_true(client:set(nil, 'bar')) 281 | assert_equal(client:get(''), 'bar') 282 | end) 283 | 284 | test("Create a new unbound command object", function() 285 | local cmd = redis.command('doesnotexist') 286 | assert_nil(client.doesnotexist) 287 | assert_error(function() cmd(client) end) 288 | 289 | local cmd = redis.command('ping', { 290 | response = function(response) return response == 'PONG' end 291 | }) 292 | assert_equal(cmd(client), true) 293 | end) 294 | 295 | test("Define commands at module level", function() 296 | redis.commands.doesnotexist = redis.command('doesnotexist') 297 | local client2 = utils.create_client(settings) 298 | 299 | redis.commands.doesnotexist = nil 300 | local client3 = utils.create_client(settings) 301 | 302 | assert_nil(client.doesnotexist) 303 | assert_not_nil(client2.doesnotexist) 304 | assert_nil(client3.doesnotexist) 305 | end) 306 | 307 | test("Define commands at module level (OLD)", function() 308 | redis.define_command('doesnotexist') 309 | local client2 = utils.create_client(settings) 310 | 311 | redis.undefine_command('doesnotexist') 312 | local client3 = utils.create_client(settings) 313 | 314 | assert_nil(client.doesnotexist) 315 | assert_not_nil(client2.doesnotexist) 316 | assert_nil(client3.doesnotexist) 317 | end) 318 | 319 | test("Define new commands at client instance level", function() 320 | client.doesnotexist = redis.command('doesnotexist') 321 | assert_not_nil(client.doesnotexist) 322 | assert_error(function() client:doesnotexist() end) 323 | 324 | client.doesnotexist = nil 325 | assert_nil(client.doesnotexist) 326 | 327 | client.ping = redis.command('ping') 328 | assert_not_nil(client.ping) 329 | assert_equal(client:ping(), 'PONG') 330 | 331 | client.ping = redis.command('ping', { 332 | request = client.requests.multibulk 333 | }) 334 | assert_not_nil(client.ping) 335 | assert_equal(client:ping(), 'PONG') 336 | 337 | client.ping = redis.command('ping', { 338 | request = client.requests.multibulk, 339 | response = function(reply) return reply == 'PONG' end 340 | }) 341 | assert_not_nil(client.ping) 342 | assert_true(client:ping()) 343 | end) 344 | 345 | test("Define new commands at client instance level (OLD)", function() 346 | client:define_command('doesnotexist') 347 | assert_not_nil(client.doesnotexist) 348 | assert_error(function() client:doesnotexist() end) 349 | 350 | client:undefine_command('doesnotexist') 351 | assert_nil(client.doesnotexist) 352 | 353 | client:define_command('ping') 354 | assert_not_nil(client.ping) 355 | assert_equal(client:ping(), 'PONG') 356 | 357 | client:define_command('ping', { 358 | request = client.requests.multibulk 359 | }) 360 | assert_not_nil(client.ping) 361 | assert_equal(client:ping(), 'PONG') 362 | 363 | client:define_command('ping', { 364 | request = client.requests.multibulk, 365 | response = function(reply) return reply == 'PONG' end 366 | }) 367 | assert_not_nil(client.ping) 368 | assert_true(client:ping()) 369 | end) 370 | 371 | test("Pipelining commands", function() 372 | local replies, count = client:pipeline(function(p) 373 | p:ping() 374 | p:exists('counter') 375 | p:incrby('counter', 10) 376 | p:incrby('counter', 30) 377 | p:exists('counter') 378 | p:get('counter') 379 | p:mset({ foo = 'bar', hoge = 'piyo'}) 380 | p:del('foo', 'hoge') 381 | p:mget('does_not_exist', 'counter') 382 | p:info() 383 | p:get('nilkey') 384 | end) 385 | 386 | assert_type(replies, 'table') 387 | assert_equal(count, 11) 388 | assert_equal(#replies, 10) 389 | assert_true(replies[1]) 390 | assert_type(replies[9], 'table') 391 | assert_equal(replies[9][2], '40') 392 | assert_type(replies[10], 'table') 393 | end) 394 | 395 | after(function() 396 | client:quit() 397 | end) 398 | end) 399 | 400 | context("Redis commands", function() 401 | before(function() 402 | client, version = utils.create_client(settings) 403 | end) 404 | 405 | after(function() 406 | client:quit() 407 | end) 408 | 409 | context("Connection related commands", function() 410 | test("PING (client:ping)", function() 411 | assert_true(client:ping()) 412 | end) 413 | 414 | test("ECHO (client:echo)", function() 415 | local str_ascii, str_utf8 = "Can you hear me?", "聞こえますか?" 416 | 417 | assert_equal(client:echo(str_ascii), str_ascii) 418 | assert_equal(client:echo(str_utf8), str_utf8) 419 | end) 420 | 421 | test("SELECT (client:select)", function() 422 | if not settings.database then return end 423 | 424 | assert_true(client:select(0)) 425 | assert_true(client:select(settings.database)) 426 | assert_error(function() client:select(100) end) 427 | assert_error(function() client:select(-1) end) 428 | end) 429 | end) 430 | 431 | context("Commands operating on the key space", function() 432 | test("KEYS (client:keys)", function() 433 | local kvs_prefixed = shared.kvs_ns_table() 434 | local kvs_unprefixed = { aaa = 1, aba = 2, aca = 3 } 435 | local kvs_all = table.merge(kvs_prefixed, kvs_unprefixed) 436 | 437 | client:mset(kvs_all) 438 | 439 | assert_empty(client:keys('nokeys:*')) 440 | assert_table_values( 441 | table.values(client:keys('*')), 442 | table.keys(kvs_all) 443 | ) 444 | assert_table_values( 445 | table.values(client:keys('metavars:*')), 446 | table.keys(kvs_prefixed) 447 | ) 448 | assert_table_values( 449 | table.values(client:keys('a?a')), 450 | table.keys(kvs_unprefixed) 451 | ) 452 | end) 453 | 454 | test("EXISTS (client:exists)", function() 455 | client:set('foo', 'bar') 456 | 457 | assert_true(client:exists('foo')) 458 | assert_false(client:exists('hoge')) 459 | end) 460 | 461 | test("DEL (client:del)", function() 462 | client:mset(shared.kvs_table()) 463 | 464 | assert_equal(client:del('doesnotexist'), 0) 465 | assert_equal(client:del('foofoo'), 1) 466 | assert_equal(client:del('foo', 'hoge', 'doesnotexist'), 2) 467 | end) 468 | 469 | test("TYPE (client:type)", function() 470 | assert_equal(client:type('doesnotexist'), 'none') 471 | 472 | client:set('fooString', 'bar') 473 | assert_equal(client:type('fooString'), 'string') 474 | 475 | client:rpush('fooList', 'bar') 476 | assert_equal(client:type('fooList'), 'list') 477 | 478 | client:sadd('fooSet', 'bar') 479 | assert_equal(client:type('fooSet'), 'set') 480 | 481 | client:zadd('fooZSet', 0, 'bar') 482 | assert_equal(client:type('fooZSet'), 'zset') 483 | 484 | if version:is('>=', '2.0.0') then 485 | client:hset('fooHash', 'value', 'bar') 486 | assert_equal('hash', client:type('fooHash')) 487 | end 488 | end) 489 | 490 | test("RANDOMKEY (client:randomkey)", function() 491 | local kvs = shared.kvs_table() 492 | 493 | assert_nil(client:randomkey()) 494 | client:mset(kvs) 495 | assert_true(table.contains(table.keys(kvs), client:randomkey())) 496 | end) 497 | 498 | test("RENAME (client:rename)", function() 499 | local kvs = shared.kvs_table() 500 | client:mset(kvs) 501 | 502 | assert_true(client:rename('hoge', 'hogehoge')) 503 | assert_false(client:exists('hoge')) 504 | assert_equal(client:get('hogehoge'), 'piyo') 505 | 506 | -- rename overwrites existing keys 507 | assert_true(client:rename('foo', 'foofoo')) 508 | assert_false(client:exists('foo')) 509 | assert_equal(client:get('foofoo'), 'bar') 510 | 511 | -- rename fails when the key does not exist 512 | assert_error(function() 513 | client:rename('doesnotexist', 'fuga') 514 | end) 515 | end) 516 | 517 | test("RENAMENX (client:renamenx)", function() 518 | local kvs = shared.kvs_table() 519 | client:mset(kvs) 520 | 521 | assert_true(client:renamenx('hoge', 'hogehoge')) 522 | assert_false(client:exists('hoge')) 523 | assert_equal(client:get('hogehoge'), 'piyo') 524 | 525 | -- rename overwrites existing keys 526 | assert_false(client:renamenx('foo', 'foofoo')) 527 | assert_true(client:exists('foo')) 528 | 529 | -- rename fails when the key does not exist 530 | assert_error(function() 531 | client:renamenx('doesnotexist', 'fuga') 532 | end) 533 | end) 534 | 535 | test("TTL (client:ttl)", function() 536 | client:set('foo', 'bar') 537 | assert_equal(client:ttl('foo'), -1) 538 | 539 | assert_true(client:expire('foo', 5)) 540 | assert_lte(client:ttl('foo'), 5) 541 | end) 542 | 543 | test("PTTL (client:pttl)", function() 544 | if version:is('<', '2.5.0') then return end 545 | 546 | client:set('foo', 'bar') 547 | assert_equal(client:pttl('foo'), -1) 548 | 549 | local ttl = 5 550 | assert_true(client:expire('foo', ttl)) 551 | assert_lte(client:pttl('foo'), 5 * 1000) 552 | assert_gte(client:pttl('foo'), 5 * 1000 - 500) 553 | end) 554 | 555 | test("EXPIRE (client:expire)", function() 556 | client:set('foo', 'bar') 557 | assert_true(client:expire('foo', 2)) 558 | assert_true(client:exists('foo')) 559 | assert_lte(client:ttl('foo'), 2) 560 | utils.sleep(3) 561 | assert_false(client:exists('foo')) 562 | 563 | client:set('foo', 'bar') 564 | assert_true(client:expire('foo', 100)) 565 | utils.sleep(3) 566 | assert_lte(client:ttl('foo'), 97) 567 | 568 | assert_true(client:expire('foo', -100)) 569 | assert_false(client:exists('foo')) 570 | end) 571 | 572 | test("PEXPIRE (client:pexpire)", function() 573 | if version:is('<', '2.5.0') then return end 574 | 575 | local ttl = 1 576 | client:set('foo', 'bar') 577 | assert_true(client:pexpire('foo', ttl * 1000)) 578 | assert_true(client:exists('foo')) 579 | assert_lte(client:pttl('foo'), ttl * 1000) 580 | assert_gte(client:pttl('foo'), ttl * 1000 - 500) 581 | utils.sleep(ttl) 582 | assert_false(client:exists('foo')) 583 | end) 584 | 585 | test("EXPIREAT (client:expireat)", function() 586 | client:set('foo', 'bar') 587 | assert_true(client:expireat('foo', os.time() + 2)) 588 | assert_lte(client:ttl('foo'), 2) 589 | utils.sleep(3) 590 | assert_false(client:exists('foo')) 591 | 592 | client:set('foo', 'bar') 593 | assert_true(client:expireat('foo', os.time() - 100)) 594 | assert_false(client:exists('foo')) 595 | end) 596 | 597 | test("PEXPIREAT (client:pexpireat)", function() 598 | if version:is('<', '2.5.0') then return end 599 | 600 | local ttl = 2 601 | client:set('foo', 'bar') 602 | assert_true(client:pexpireat('foo', os.time() + ttl * 1000)) 603 | assert_lte(client:pttl('foo'), ttl * 1000) 604 | utils.sleep(ttl + 1) 605 | assert_false(client:exists('foo')) 606 | 607 | client:set('foo', 'bar') 608 | assert_true(client:pexpireat('foo', os.time() - 100 * 1000)) 609 | assert_false(client:exists('foo')) 610 | end) 611 | 612 | test("MOVE (client:move)", function() 613 | if not settings.database then return end 614 | 615 | local other_db = settings.database + 1 616 | client:set('foo', 'bar') 617 | client:select(other_db) 618 | client:flushdb() 619 | client:select(settings.database) 620 | 621 | assert_true(client:move('foo', other_db)) 622 | assert_false(client:move('foo', other_db)) 623 | assert_false(client:move('doesnotexist', other_db)) 624 | 625 | client:set('hoge', 'piyo') 626 | assert_error(function() client:move('hoge', 100) end) 627 | end) 628 | 629 | test("DBSIZE (client:dbsize)", function() 630 | assert_equal(client:dbsize(), 0) 631 | client:mset(shared.kvs_table()) 632 | assert_greater_than(client:dbsize(), 0) 633 | end) 634 | 635 | test("PERSIST (client:persist)", function() 636 | if version:is('<', '2.1.0') then return end 637 | 638 | client:set('foo', 'bar') 639 | 640 | assert_true(client:expire('foo', 1)) 641 | assert_equal(client:ttl('foo'), 1) 642 | assert_true(client:persist('foo')) 643 | assert_equal(client:ttl('foo'), -1) 644 | 645 | assert_false(client:persist('foo')) 646 | assert_false(client:persist('foobar')) 647 | end) 648 | 649 | test("SCAN (client:scan)", function() 650 | if version:is('<', '2.8.0') then return end 651 | 652 | client:mset( 653 | 'scan:1', '1', 654 | 'scan:2', '2', 655 | 'scan:3', '3', 656 | 'scan:4', '4' 657 | ) 658 | 659 | local cursor, keys = unpack(client:scan(0, { 660 | match = 'scan:*', count = 10 661 | })) 662 | 663 | assert_type(cursor, 'string') 664 | assert_type(keys, 'table') 665 | 666 | assert_table_values(keys, {'scan:1','scan:2','scan:3','scan:4'}) 667 | end) 668 | end) 669 | 670 | context("Commands operating on the key space - SORT", function() 671 | -- TODO: missing tests for params GET and BY 672 | 673 | before(function() 674 | -- TODO: code duplication! 675 | list01, list01_values = "list01", { "4","2","3","5","1" } 676 | for _,v in ipairs(list01_values) do client:rpush(list01,v) end 677 | 678 | list02, list02_values = "list02", { "1","10","2","20","3","30" } 679 | for _,v in ipairs(list02_values) do client:rpush(list02,v) end 680 | end) 681 | 682 | test("SORT (client:sort)", function() 683 | local sorted = client:sort(list01) 684 | assert_table_values(sorted, { "1","2","3","4","5" }) 685 | end) 686 | 687 | test("SORT (client:sort) with parameter ASC/DESC", function() 688 | assert_table_values(client:sort(list01, { sort = 'asc'}), { "1","2","3","4","5" }) 689 | assert_table_values(client:sort(list01, { sort = 'desc'}), { "5","4","3","2","1" }) 690 | end) 691 | 692 | test("SORT (client:sort) with parameter LIMIT", function() 693 | assert_table_values(client:sort(list01, { limit = { 0,3 } }), { "1","2", "3" }) 694 | assert_table_values(client:sort(list01, { limit = { 3,2 } }), { "4","5" }) 695 | end) 696 | 697 | test("SORT (client:sort) with parameter ALPHA", function() 698 | assert_table_values(client:sort(list02, { alpha = false }), { "1","2","3","10","20","30" }) 699 | assert_table_values(client:sort(list02, { alpha = true }), { "1","10","2","20","3","30" }) 700 | end) 701 | 702 | test("SORT (client:sort) with parameter GET", function() 703 | client:rpush('uids', 1003) 704 | client:rpush('uids', 1001) 705 | client:rpush('uids', 1002) 706 | client:rpush('uids', 1000) 707 | local sortget = { 708 | ['uid:1000'] = 'foo', ['uid:1001'] = 'bar', 709 | ['uid:1002'] = 'hoge', ['uid:1003'] = 'piyo', 710 | } 711 | client:mset(sortget) 712 | 713 | assert_table_values(client:sort('uids', { get = 'uid:*' }), table.values(sortget)) 714 | assert_table_values(client:sort('uids', { get = { 'uid:*' } }), table.values(sortget)) 715 | end) 716 | 717 | test("SORT (client:sort) with multiple parameters", function() 718 | assert_table_values(client:sort(list02, { 719 | alpha = false, 720 | sort = 'desc', 721 | limit = { 1, 4 } 722 | }), { "20","10","3","2" }) 723 | end) 724 | 725 | test("SORT (client:sort) with parameter STORE", function() 726 | assert_equal(client:sort(list01, { store = 'list01_ordered' }), 5) 727 | assert_true(client:exists('list01_ordered')) 728 | end) 729 | end) 730 | 731 | context("Commands operating on string values", function() 732 | test("SET (client:set)", function() 733 | assert_true(client:set('foo', 'bar')) 734 | assert_equal(client:get('foo'), 'bar') 735 | end) 736 | 737 | test("GET (client:get)", function() 738 | client:set('foo', 'bar') 739 | 740 | assert_equal(client:get('foo'), 'bar') 741 | assert_nil(client:get('hoge')) 742 | 743 | assert_error(function() 744 | client:rpush('metavars', 'foo') 745 | client:get('metavars') 746 | end) 747 | end) 748 | 749 | test("SETNX (client:setnx)", function() 750 | assert_true(client:setnx('foo', 'bar')) 751 | assert_false(client:setnx('foo', 'baz')) 752 | assert_equal(client:get('foo'), 'bar') 753 | end) 754 | 755 | test("SETEX (client:setex)", function() 756 | if version:is('<', '2.0.0') then return end 757 | 758 | assert_true(client:setex('foo', 10, 'bar')) 759 | assert_true(client:exists('foo')) 760 | assert_lte(client:ttl('foo'), 10) 761 | 762 | assert_true(client:setex('hoge', 1, 'piyo')) 763 | utils.sleep(2) 764 | assert_false(client:exists('hoge')) 765 | 766 | assert_error(function() client:setex('hoge', 2.5, 'piyo') end) 767 | assert_error(function() client:setex('hoge', 0, 'piyo') end) 768 | assert_error(function() client:setex('hoge', -10, 'piyo') end) 769 | end) 770 | 771 | test("PSETEX (client:psetex)", function() 772 | if version:is('<', '2.5.0') then return end 773 | 774 | local ttl = 10 * 1000 775 | assert_true(client:psetex('foo', ttl, 'bar')) 776 | assert_true(client:exists('foo')) 777 | assert_lte(client:pttl('foo'), ttl) 778 | assert_gte(client:pttl('foo'), ttl - 500) 779 | 780 | assert_true(client:psetex('hoge', 1 * 1000, 'piyo')) 781 | utils.sleep(2) 782 | assert_false(client:exists('hoge')) 783 | 784 | assert_error(function() client:psetex('hoge', 2.5, 'piyo') end) 785 | assert_error(function() client:psetex('hoge', 0, 'piyo') end) 786 | assert_error(function() client:psetex('hoge', -10, 'piyo') end) 787 | end) 788 | 789 | test("MSET (client:mset)", function() 790 | local kvs = shared.kvs_table() 791 | 792 | assert_true(client:mset(kvs)) 793 | for k,v in pairs(kvs) do 794 | assert_equal(client:get(k), v) 795 | end 796 | 797 | assert_true(client:mset('a', '1', 'b', '2', 'c', '3')) 798 | assert_equal(client:get('a'), '1') 799 | assert_equal(client:get('b'), '2') 800 | assert_equal(client:get('c'), '3') 801 | end) 802 | 803 | test("MSETNX (client:msetnx)", function() 804 | assert_true(client:msetnx({ a = '1', b = '2' })) 805 | assert_false(client:msetnx({ c = '3', a = '100'})) 806 | assert_equal(client:get('a'), '1') 807 | assert_equal(client:get('b'), '2') 808 | end) 809 | 810 | test("MGET (client:mget)", function() 811 | local kvs = shared.kvs_table() 812 | local keys, values = table.keys(kvs), table.values(kvs) 813 | 814 | assert_true(client:mset(kvs)) 815 | assert_table_values(client:mget(unpack(keys)), values) 816 | end) 817 | 818 | test("GETSET (client:getset)", function() 819 | assert_nil(client:getset('foo', 'bar')) 820 | assert_equal(client:getset('foo', 'barbar'), 'bar') 821 | assert_equal(client:getset('foo', 'baz'), 'barbar') 822 | end) 823 | 824 | test("INCR (client:incr)", function() 825 | assert_equal(client:incr('foo'), 1) 826 | assert_equal(client:incr('foo'), 2) 827 | 828 | assert_true(client:set('hoge', 'piyo')) 829 | 830 | if version:is('<', '2.0.0') then 831 | assert_equal(client:incr('hoge'), 1) 832 | else 833 | assert_error(function() 834 | client:incr('hoge') 835 | end) 836 | end 837 | end) 838 | 839 | test("INCRBY (client:incrby)", function() 840 | client:set('foo', 2) 841 | assert_equal(client:incrby('foo', 20), 22) 842 | assert_equal(client:incrby('foo', -12), 10) 843 | assert_equal(client:incrby('foo', -110), -100) 844 | end) 845 | 846 | test("INCRBYFLOAT (client:incrbyfloat)", function() 847 | if version:is('<', '2.5.0') then return end 848 | 849 | client:set('foo', 2) 850 | assert_equal(client:incrbyfloat('foo', 20.123), 22.123) 851 | assert_equal(client:incrbyfloat('foo', -12.123), 10) 852 | assert_equal(client:incrbyfloat('foo', -110.01), -100.01) 853 | end) 854 | 855 | test("DECR (client:decr)", function() 856 | assert_equal(client:decr('foo'), -1) 857 | assert_equal(client:decr('foo'), -2) 858 | 859 | assert_true(client:set('hoge', 'piyo')) 860 | if version:is('<', '2.0.0') then 861 | assert_equal(client:decr('hoge'), -1) 862 | else 863 | assert_error(function() 864 | client:decr('hoge') 865 | end) 866 | end 867 | end) 868 | 869 | test("DECRBY (client:decrby)", function() 870 | client:set('foo', -2) 871 | assert_equal(client:decrby('foo', 20), -22) 872 | assert_equal(client:decrby('foo', -12), -10) 873 | assert_equal(client:decrby('foo', -110), 100) 874 | end) 875 | 876 | test("APPEND (client:append)", function() 877 | if version:is('<', '2.0.0') then return end 878 | 879 | client:set('foo', 'bar') 880 | assert_equal(client:append('foo', '__'), 5) 881 | assert_equal(client:append('foo', 'bar'), 8) 882 | assert_equal(client:get('foo'), 'bar__bar') 883 | 884 | assert_equal(client:append('hoge', 'piyo'), 4) 885 | assert_equal(client:get('hoge'), 'piyo') 886 | 887 | assert_error(function() 888 | client:rpush('metavars', 'foo') 889 | client:append('metavars', 'bar') 890 | end) 891 | end) 892 | 893 | test("SUBSTR (client:substr)", function() 894 | if version:is('<', '2.0.0') then return end 895 | 896 | client:set('var', 'foobar') 897 | assert_equal(client:substr('var', 0, 2), 'foo') 898 | assert_equal(client:substr('var', 3, 5), 'bar') 899 | assert_equal(client:substr('var', -3, -1), 'bar') 900 | 901 | assert_equal(client:substr('var', 5, 0), '') 902 | 903 | client:set('numeric', 123456789) 904 | assert_equal(client:substr('numeric', 0, 4), '12345') 905 | 906 | assert_error(function() 907 | client:rpush('metavars', 'foo') 908 | client:substr('metavars', 0, 3) 909 | end) 910 | end) 911 | 912 | test("STRLEN (client:strlen)", function() 913 | if version:is('<', '2.1.0') then return end 914 | 915 | client:set('var', 'foobar') 916 | assert_equal(client:strlen('var'), 6) 917 | assert_equal(client:append('var', '___'), 9) 918 | assert_equal(client:strlen('var'), 9) 919 | 920 | assert_error(function() 921 | client:rpush('metavars', 'foo') 922 | qclient:strlen('metavars') 923 | end) 924 | end) 925 | 926 | test("SETRANGE (client:setrange)", function() 927 | if version:is('<', '2.1.0') then return end 928 | 929 | assert_equal(client:setrange('var', 0, 'foobar'), 6) 930 | assert_equal(client:get('var'), 'foobar') 931 | assert_equal(client:setrange('var', 3, 'foo'), 6) 932 | assert_equal(client:get('var'), 'foofoo') 933 | assert_equal(client:setrange('var', 10, 'barbar'), 16) 934 | assert_equal(client:get('var'), "foofoo\0\0\0\0barbar") 935 | 936 | assert_error(function() 937 | client:setrange('var', -1, 'bogus') 938 | end) 939 | 940 | assert_error(function() 941 | client:rpush('metavars', 'foo') 942 | client:setrange('metavars', 0, 'hoge') 943 | end) 944 | end) 945 | 946 | test("GETRANGE (client:getrange)", function() 947 | if version:is('<', '2.1.0') then return end 948 | 949 | client:set('var', 'foobar') 950 | assert_equal(client:getrange('var', 0, 2), 'foo') 951 | assert_equal(client:getrange('var', 3, 5), 'bar') 952 | assert_equal(client:getrange('var', -3, -1), 'bar') 953 | 954 | assert_equal(client:substr('var', 5, 0), '') 955 | 956 | client:set('numeric', 123456789) 957 | assert_equal(client:getrange('numeric', 0, 4), '12345') 958 | 959 | assert_error(function() 960 | client:rpush('metavars', 'foo') 961 | client:getrange('metavars', 0, 3) 962 | end) 963 | end) 964 | 965 | test("SETBIT (client:setbit)", function() 966 | if version:is('<', '2.1.0') then return end 967 | 968 | assert_equal(client:setbit('binary', 31, 1), 0) 969 | assert_equal(client:setbit('binary', 0, 1), 0) 970 | assert_equal(client:strlen('binary'), 4) 971 | assert_equal(client:get('binary'), "\128\0\0\1") 972 | 973 | assert_equal(client:setbit('binary', 0, 0), 1) 974 | assert_equal(client:setbit('binary', 0, 0), 0) 975 | assert_equal(client:get('binary'), "\0\0\0\1") 976 | 977 | assert_error(function() 978 | client:setbit('binary', -1, 1) 979 | end) 980 | 981 | assert_error(function() 982 | client:setbit('binary', 'invalid', 1) 983 | end) 984 | 985 | assert_error(function() 986 | client:setbit('binary', 'invalid', 1) 987 | end) 988 | 989 | assert_error(function() 990 | client:setbit('binary', 15, 255) 991 | end) 992 | 993 | assert_error(function() 994 | client:setbit('binary', 15, 'invalid') 995 | end) 996 | 997 | assert_error(function() 998 | client:rpush('metavars', 'foo') 999 | client:setbit('metavars', 0, 1) 1000 | end) 1001 | end) 1002 | 1003 | test("GETBIT (client:getbit)", function() 1004 | if version:is('<', '2.1.0') then return end 1005 | 1006 | client:set('binary', "\128\0\0\1") 1007 | 1008 | assert_equal(client:getbit('binary', 0), 1) 1009 | assert_equal(client:getbit('binary', 15), 0) 1010 | assert_equal(client:getbit('binary', 31), 1) 1011 | assert_equal(client:getbit('binary', 63), 0) 1012 | 1013 | assert_error(function() 1014 | client:getbit('binary', -1) 1015 | end) 1016 | 1017 | assert_error(function() 1018 | client:getbit('binary', 'invalid') 1019 | end) 1020 | 1021 | assert_error(function() 1022 | client:rpush('metavars', 'foo') 1023 | client:getbit('metavars', 0) 1024 | end) 1025 | end) 1026 | 1027 | test("BITOP (client:bitop)", function() 1028 | if version:is('<', '2.5.10') then return end 1029 | 1030 | client:set('foo', 'a') 1031 | client:set('bar', 'b') 1032 | 1033 | client:bitop('AND', 'foo&bar', 'foo', 'bar') 1034 | client:bitop('OR', 'foo|bar', 'foo', 'bar') 1035 | client:bitop('XOR', 'foo^bar', 'foo', 'bar') 1036 | client:bitop('NOT', '-foo', 'foo') 1037 | 1038 | assert_equal(client:get('foo&bar'), '\96') 1039 | assert_equal(client:get('foo|bar'), '\99') 1040 | assert_equal(client:get('foo^bar'), '\3') 1041 | assert_equal(client:get('-foo'), '\158') 1042 | end) 1043 | 1044 | test("BITCOUNT (client:bitcount)", function() 1045 | if version:is('<', '2.5.10') then return end 1046 | 1047 | client:set('foo', 'abcde') 1048 | 1049 | assert_equal(client:bitcount('foo', 1, 3), 10) 1050 | assert_equal(client:bitcount('foo', 0, -1), 17) 1051 | end) 1052 | end) 1053 | 1054 | context("Commands operating on lists", function() 1055 | test("RPUSH (client:rpush)", function() 1056 | if version:is('<', '2.0.0') then 1057 | assert_true(client:rpush('metavars', 'foo')) 1058 | assert_true(client:rpush('metavars', 'hoge')) 1059 | else 1060 | assert_equal(client:rpush('metavars', 'foo'), 1) 1061 | assert_equal(client:rpush('metavars', 'hoge'), 2) 1062 | end 1063 | assert_error(function() 1064 | client:set('foo', 'bar') 1065 | client:rpush('foo', 'baz') 1066 | end) 1067 | end) 1068 | 1069 | test("RPUSHX (client:rpushx)", function() 1070 | if version:is('<', '2.1.0') then return end 1071 | 1072 | assert_equal(client:rpushx('numbers', 1), 0) 1073 | assert_equal(client:rpush('numbers', 2), 1) 1074 | assert_equal(client:rpushx('numbers', 3), 2) 1075 | assert_equal(client:llen('numbers'), 2) 1076 | assert_table_values(client:lrange('numbers', 0, -1), { '2', '3' }) 1077 | 1078 | assert_error(function() 1079 | client:set('foo', 'bar') 1080 | client:rpushx('foo', 'baz') 1081 | end) 1082 | end) 1083 | 1084 | test("LPUSH (client:lpush)", function() 1085 | if version:is('<', '2.0.0') then 1086 | assert_true(client:lpush('metavars', 'foo')) 1087 | assert_true(client:lpush('metavars', 'hoge')) 1088 | else 1089 | assert_equal(client:lpush('metavars', 'foo'), 1) 1090 | assert_equal(client:lpush('metavars', 'hoge'), 2) 1091 | end 1092 | assert_error(function() 1093 | client:set('foo', 'bar') 1094 | client:lpush('foo', 'baz') 1095 | end) 1096 | end) 1097 | 1098 | test("LPUSHX (client:lpushx)", function() 1099 | if version:is('<', '2.1.0') then return end 1100 | 1101 | assert_equal(client:lpushx('numbers', 1), 0) 1102 | assert_equal(client:lpush('numbers', 2), 1) 1103 | assert_equal(client:lpushx('numbers', 3), 2) 1104 | 1105 | assert_equal(client:llen('numbers'), 2) 1106 | assert_table_values(client:lrange('numbers', 0, -1), { '3', '2' }) 1107 | 1108 | assert_error(function() 1109 | client:set('foo', 'bar') 1110 | client:lpushx('foo', 'baz') 1111 | end) 1112 | end) 1113 | 1114 | test("LLEN (client:llen)", function() 1115 | local kvs = shared.kvs_table() 1116 | for _, v in pairs(kvs) do 1117 | client:rpush('metavars', v) 1118 | end 1119 | 1120 | assert_equal(client:llen('metavars'), 3) 1121 | assert_equal(client:llen('doesnotexist'), 0) 1122 | assert_error(function() 1123 | client:set('foo', 'bar') 1124 | client:llen('foo') 1125 | end) 1126 | end) 1127 | 1128 | test("LRANGE (client:lrange)", function() 1129 | local numbers = utils.rpush_return(client, 'numbers', shared.numbers()) 1130 | 1131 | assert_table_values(client:lrange('numbers', 0, 3), table.slice(numbers, 1, 4)) 1132 | assert_table_values(client:lrange('numbers', 4, 8), table.slice(numbers, 5, 5)) 1133 | assert_table_values(client:lrange('numbers', 0, 0), table.slice(numbers, 1, 1)) 1134 | assert_empty(client:lrange('numbers', 1, 0)) 1135 | assert_table_values(client:lrange('numbers', 0, -1), numbers) 1136 | assert_table_values(client:lrange('numbers', 5, -5), { '5' }) 1137 | assert_empty(client:lrange('numbers', 7, -5)) 1138 | assert_table_values(client:lrange('numbers', -5, -2), table.slice(numbers, 6, 4)) 1139 | assert_table_values(client:lrange('numbers', -100, 100), numbers) 1140 | end) 1141 | 1142 | test("LTRIM (client:ltrim)", function() 1143 | local numbers = utils.rpush_return(client, 'numbers', shared.numbers(), true) 1144 | assert_true(client:ltrim('numbers', 0, 2)) 1145 | assert_table_values(client:lrange('numbers', 0, -1), table.slice(numbers, 1, 3)) 1146 | 1147 | local numbers = utils.rpush_return(client, 'numbers', shared.numbers(), true) 1148 | assert_true(client:ltrim('numbers', 5, 9)) 1149 | assert_table_values(client:lrange('numbers', 0, -1), table.slice(numbers, 6, 5)) 1150 | 1151 | local numbers = utils.rpush_return(client, 'numbers', shared.numbers(), true) 1152 | assert_true(client:ltrim('numbers', 0, -6)) 1153 | assert_table_values(client:lrange('numbers', 0, -1), table.slice(numbers, 1, 5)) 1154 | 1155 | local numbers = utils.rpush_return(client, 'numbers', shared.numbers(), true) 1156 | assert_true(client:ltrim('numbers', -5, -3)) 1157 | assert_table_values(client:lrange('numbers', 0, -1), table.slice(numbers, 6, 3)) 1158 | 1159 | local numbers = utils.rpush_return(client, 'numbers', shared.numbers(), true) 1160 | assert_true(client:ltrim('numbers', -100, 100)) 1161 | assert_table_values(client:lrange('numbers', 0, -1), numbers) 1162 | 1163 | assert_error(function() 1164 | client:set('foo', 'bar') 1165 | client:ltrim('foo', 0, 1) 1166 | end) 1167 | end) 1168 | 1169 | test("LINDEX (client:lindex)", function() 1170 | local numbers = utils.rpush_return(client, 'numbers', shared.numbers()) 1171 | 1172 | assert_equal(client:lindex('numbers', 0), numbers[1]) 1173 | assert_equal(client:lindex('numbers', 5), numbers[6]) 1174 | assert_equal(client:lindex('numbers', 9), numbers[10]) 1175 | assert_nil(client:lindex('numbers', 100)) 1176 | 1177 | assert_equal(client:lindex('numbers', -0), numbers[1]) 1178 | assert_equal(client:lindex('numbers', -1), numbers[10]) 1179 | assert_equal(client:lindex('numbers', -3), numbers[8]) 1180 | assert_nil(client:lindex('numbers', -100)) 1181 | 1182 | assert_error(function() 1183 | client:set('foo', 'bar') 1184 | client:lindex('foo', 0) 1185 | end) 1186 | end) 1187 | 1188 | test("LSET (client:lset)", function() 1189 | utils.rpush_return(client, 'numbers', shared.numbers()) 1190 | 1191 | assert_true(client:lset('numbers', 5, -5)) 1192 | assert_equal(client:lindex('numbers', 5), '-5') 1193 | 1194 | assert_error(function() 1195 | client:lset('numbers', 99, 99) 1196 | end) 1197 | 1198 | assert_error(function() 1199 | client:set('foo', 'bar') 1200 | client:lset('foo', 0, 0) 1201 | end) 1202 | end) 1203 | 1204 | test("LREM (client:lrem)", function() 1205 | local mixed = { '0', '_', '2', '_', '4', '_', '6', '_' } 1206 | 1207 | utils.rpush_return(client, 'mixed', mixed, true) 1208 | assert_equal(client:lrem('mixed', 2, '_'), 2) 1209 | assert_table_values(client:lrange('mixed', 0, -1), { '0', '2', '4', '_', '6', '_' }) 1210 | 1211 | utils.rpush_return(client, 'mixed', mixed, true) 1212 | assert_equal(client:lrem('mixed', 0, '_'), 4) 1213 | assert_table_values(client:lrange('mixed', 0, -1), { '0', '2', '4', '6' }) 1214 | 1215 | utils.rpush_return(client, 'mixed', mixed, true) 1216 | assert_equal(client:lrem('mixed', -2, '_'), 2) 1217 | assert_table_values(client:lrange('mixed', 0, -1), { '0', '_', '2', '_', '4', '6' }) 1218 | 1219 | utils.rpush_return(client, 'mixed', mixed, true) 1220 | assert_equal(client:lrem('mixed', 2, '|'), 0) 1221 | assert_table_values(client:lrange('mixed', 0, -1), mixed) 1222 | 1223 | assert_equal(client:lrem('doesnotexist', 2, '_'), 0) 1224 | 1225 | assert_error(function() 1226 | client:set('foo', 'bar') 1227 | client:lrem('foo', 0, 0) 1228 | end) 1229 | end) 1230 | 1231 | test("LPOP (client:lpop)", function() 1232 | local numbers = utils.rpush_return(client, 'numbers', { '0', '1', '2', '3', '4' }) 1233 | 1234 | assert_equal(client:lpop('numbers'), numbers[1]) 1235 | assert_equal(client:lpop('numbers'), numbers[2]) 1236 | assert_equal(client:lpop('numbers'), numbers[3]) 1237 | 1238 | assert_table_values(client:lrange('numbers', 0, -1), { '3', '4' }) 1239 | 1240 | client:lpop('numbers') 1241 | client:lpop('numbers') 1242 | assert_nil(client:lpop('numbers')) 1243 | 1244 | assert_nil(client:lpop('doesnotexist')) 1245 | 1246 | assert_error(function() 1247 | client:set('foo', 'bar') 1248 | client:lpop('foo') 1249 | end) 1250 | end) 1251 | 1252 | test("RPOP (client:rpop)", function() 1253 | local numbers = utils.rpush_return(client, 'numbers', { '0', '1', '2', '3', '4' }) 1254 | 1255 | assert_equal(client:rpop('numbers'), numbers[5]) 1256 | assert_equal(client:rpop('numbers'), numbers[4]) 1257 | assert_equal(client:rpop('numbers'), numbers[3]) 1258 | 1259 | assert_table_values(client:lrange('numbers', 0, -1), { '0', '1' }) 1260 | 1261 | client:rpop('numbers') 1262 | client:rpop('numbers') 1263 | assert_nil(client:rpop('numbers')) 1264 | 1265 | assert_nil(client:rpop('doesnotexist')) 1266 | 1267 | assert_error(function() 1268 | client:set('foo', 'bar') 1269 | client:rpop('foo') 1270 | end) 1271 | end) 1272 | 1273 | test("RPOPLPUSH (client:rpoplpush)", function() 1274 | local numbers = utils.rpush_return(client, 'numbers', { '0', '1', '2' }, true) 1275 | assert_equal(client:llen('temporary'), 0) 1276 | assert_equal(client:rpoplpush('numbers', 'temporary'), '2') 1277 | assert_equal(client:rpoplpush('numbers', 'temporary'), '1') 1278 | assert_equal(client:rpoplpush('numbers', 'temporary'), '0') 1279 | assert_equal(client:llen('numbers'), 0) 1280 | assert_equal(client:llen('temporary'), 3) 1281 | 1282 | local numbers = utils.rpush_return(client, 'numbers', { '0', '1', '2' }, true) 1283 | client:rpoplpush('numbers', 'numbers') 1284 | client:rpoplpush('numbers', 'numbers') 1285 | client:rpoplpush('numbers', 'numbers') 1286 | assert_table_values(client:lrange('numbers', 0, -1), numbers) 1287 | 1288 | assert_nil(client:rpoplpush('doesnotexist1', 'doesnotexist2')) 1289 | 1290 | assert_error(function() 1291 | client:set('foo', 'bar') 1292 | client:rpoplpush('foo', 'hoge') 1293 | end) 1294 | 1295 | assert_error(function() 1296 | client:set('foo', 'bar') 1297 | client:rpoplpush('temporary', 'foo') 1298 | end) 1299 | end) 1300 | 1301 | test("BLPOP (client:blpop)", function() 1302 | if version:is('<', '2.0.0') then return end 1303 | -- TODO: implement tests 1304 | end) 1305 | 1306 | test("BRPOP (client:brpop)", function() 1307 | if version:is('<', '2.0.0') then return end 1308 | -- TODO: implement tests 1309 | end) 1310 | 1311 | test("BRPOPLPUSH (client:brpoplpush)", function() 1312 | if version:is('<', '2.1.0') then return end 1313 | -- TODO: implement tests 1314 | end) 1315 | 1316 | test("LINSERT (client:linsert)", function() 1317 | if version:is('<', '2.1.0') then return end 1318 | 1319 | utils.rpush_return(client, 'numbers', shared.numbers(), true) 1320 | 1321 | assert_equal(client:linsert('numbers', 'before', 0, -2), 11) 1322 | assert_equal(client:linsert('numbers', 'after', -2, -1), 12) 1323 | assert_table_values(client:lrange('numbers', 0, 3), { '-2', '-1', '0', '1' }); 1324 | 1325 | assert_equal(client:linsert('numbers', 'before', 100, 200), -1) 1326 | assert_equal(client:linsert('numbers', 'after', 100, 50), -1) 1327 | 1328 | assert_error(function() 1329 | client:set('foo', 'bar') 1330 | client:linsert('foo', 0, 0) 1331 | end) 1332 | end) 1333 | end) 1334 | 1335 | context("Commands operating on sets", function() 1336 | test("SADD (client:sadd)", function() 1337 | assert_equal(client:sadd('set', 0), 1) 1338 | assert_equal(client:sadd('set', 1), 1) 1339 | assert_equal(client:sadd('set', 0), 0) 1340 | 1341 | assert_error(function() 1342 | client:set('foo', 'bar') 1343 | client:sadd('foo', 0) 1344 | end) 1345 | end) 1346 | 1347 | test("SREM (client:srem)", function() 1348 | utils.sadd_return(client, 'set', { '0', '1', '2', '3', '4' }) 1349 | 1350 | assert_equal(client:srem('set', 0), 1) 1351 | assert_equal(client:srem('set', 4), 1) 1352 | assert_equal(client:srem('set', 10), 0) 1353 | 1354 | assert_error(function() 1355 | client:set('foo', 'bar') 1356 | client:srem('foo', 0) 1357 | end) 1358 | end) 1359 | 1360 | test("SPOP (client:spop)", function() 1361 | local set = utils.sadd_return(client, 'set', { '0', '1', '2', '3', '4' }) 1362 | 1363 | assert_true(table.contains(set, client:spop('set'))) 1364 | assert_nil(client:spop('doesnotexist')) 1365 | 1366 | assert_error(function() 1367 | client:set('foo', 'bar') 1368 | client:spop('foo') 1369 | end) 1370 | end) 1371 | 1372 | test("SMOVE (client:smove)", function() 1373 | utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5' }) 1374 | utils.sadd_return(client, 'setB', { '5', '6', '7', '8', '9', '10' }) 1375 | 1376 | assert_true(client:smove('setA', 'setB', 0)) 1377 | assert_equal(client:srem('setA', 0), 0) 1378 | assert_equal(client:srem('setB', 0), 1) 1379 | 1380 | assert_true(client:smove('setA', 'setB', 5)) 1381 | assert_false(client:smove('setA', 'setB', 100)) 1382 | 1383 | assert_error(function() 1384 | client:set('foo', 'bar') 1385 | client:smove('foo', 'setB', 5) 1386 | end) 1387 | 1388 | assert_error(function() 1389 | client:set('foo', 'bar') 1390 | client:smove('setA', 'foo', 5) 1391 | end) 1392 | end) 1393 | 1394 | test("SCARD (client:scard)", function() 1395 | utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5' }) 1396 | 1397 | assert_equal(client:scard('setA'), 6) 1398 | 1399 | -- empty set 1400 | client:sadd('setB', 0) 1401 | client:spop('setB') 1402 | assert_equal(client:scard('doesnotexist'), 0) 1403 | 1404 | -- non-existent set 1405 | assert_equal(client:scard('doesnotexist'), 0) 1406 | 1407 | assert_error(function() 1408 | client:set('foo', 'bar') 1409 | client:scard('foo') 1410 | end) 1411 | end) 1412 | 1413 | test("SISMEMBER (client:sismember)", function() 1414 | utils.sadd_return(client, 'set', { '0', '1', '2', '3', '4', '5' }) 1415 | 1416 | assert_true(client:sismember('set', 3)) 1417 | assert_false(client:sismember('set', 100)) 1418 | assert_false(client:sismember('doesnotexist', 0)) 1419 | 1420 | assert_error(function() 1421 | client:set('foo', 'bar') 1422 | client:sismember('foo', 0) 1423 | end) 1424 | end) 1425 | 1426 | test("SMEMBERS (client:smembers)", function() 1427 | local set = utils.sadd_return(client, 'set', { '0', '1', '2', '3', '4', '5' }) 1428 | 1429 | assert_table_values(client:smembers('set'), set) 1430 | 1431 | if version:is('<', '2.0.0') then 1432 | assert_nil(client:smembers('doesnotexist')) 1433 | else 1434 | assert_table_values(client:smembers('doesnotexist'), {}) 1435 | end 1436 | 1437 | assert_error(function() 1438 | client:set('foo', 'bar') 1439 | client:smembers('foo') 1440 | end) 1441 | end) 1442 | 1443 | test("SINTER (client:sinter)", function() 1444 | local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' }) 1445 | local setB = utils.sadd_return(client, 'setB', { '1', '3', '4', '6', '9', '10' }) 1446 | 1447 | assert_table_values(client:sinter('setA'), setA) 1448 | assert_table_values(client:sinter('setA', 'setB'), { '3', '4', '6', '1' }) 1449 | 1450 | if version:is('<', '2.0.0') then 1451 | assert_nil(client:sinter('setA', 'doesnotexist')) 1452 | else 1453 | assert_table_values(client:sinter('setA', 'doesnotexist'), {}) 1454 | end 1455 | 1456 | assert_error(function() 1457 | client:set('foo', 'bar') 1458 | client:sinter('foo') 1459 | end) 1460 | 1461 | assert_error(function() 1462 | client:set('foo', 'bar') 1463 | client:sinter('setA', 'foo') 1464 | end) 1465 | end) 1466 | 1467 | test("SINTERSTORE (client:sinterstore)", function() 1468 | local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' }) 1469 | local setB = utils.sadd_return(client, 'setB', { '1', '3', '4', '6', '9', '10' }) 1470 | 1471 | assert_equal(client:sinterstore('setC', 'setA'), #setA) 1472 | assert_table_values(client:smembers('setC'), setA) 1473 | 1474 | client:del('setC') 1475 | -- this behaviour has changed in redis 2.0 1476 | assert_equal(client:sinterstore('setC', 'setA', 'setB'), 4) 1477 | assert_table_values(client:smembers('setC'), { '1', '3', '4', '6' }) 1478 | 1479 | client:del('setC') 1480 | assert_equal(client:sinterstore('setC', 'doesnotexist'), 0) 1481 | assert_false(client:exists('setC')) 1482 | 1483 | -- existing keys are replaced by SINTERSTORE 1484 | client:set('foo', 'bar') 1485 | assert_equal(client:sinterstore('foo', 'setA'), #setA) 1486 | 1487 | assert_error(function() 1488 | client:set('foo', 'bar') 1489 | client:sinterstore('setA', 'foo') 1490 | end) 1491 | end) 1492 | 1493 | test("SUNION (client:sunion)", function() 1494 | local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' }) 1495 | local setB = utils.sadd_return(client, 'setB', { '1', '3', '4', '6', '9', '10' }) 1496 | 1497 | assert_table_values(client:sunion('setA'), setA) 1498 | assert_table_values( 1499 | client:sunion('setA', 'setB'), 1500 | { '0', '1', '10', '2', '3', '4', '5', '6', '9' } 1501 | ) 1502 | 1503 | -- this behaviour has changed in redis 2.0 1504 | assert_table_values(client:sunion('setA', 'doesnotexist'), setA) 1505 | 1506 | assert_error(function() 1507 | client:set('foo', 'bar') 1508 | client:sunion('foo') 1509 | end) 1510 | 1511 | assert_error(function() 1512 | client:set('foo', 'bar') 1513 | client:sunion('setA', 'foo') 1514 | end) 1515 | end) 1516 | 1517 | test("SUNIONSTORE (client:sunionstore)", function() 1518 | local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' }) 1519 | local setB = utils.sadd_return(client, 'setB', { '1', '3', '4', '6', '9', '10' }) 1520 | 1521 | assert_equal(client:sunionstore('setC', 'setA'), #setA) 1522 | assert_table_values(client:smembers('setC'), setA) 1523 | 1524 | client:del('setC') 1525 | assert_equal(client:sunionstore('setC', 'setA', 'setB'), 9) 1526 | assert_table_values( 1527 | client:smembers('setC'), 1528 | { '0' ,'1' , '10', '2', '3', '4', '5', '6', '9' } 1529 | ) 1530 | 1531 | client:del('setC') 1532 | assert_equal(client:sunionstore('setC', 'doesnotexist'), 0) 1533 | if version:is('<', '2.0.0') then 1534 | assert_true(client:exists('setC')) 1535 | else 1536 | assert_false(client:exists('setC')) 1537 | end 1538 | assert_equal(client:scard('setC'), 0) 1539 | 1540 | -- existing keys are replaced by SUNIONSTORE 1541 | client:set('foo', 'bar') 1542 | assert_equal(client:sunionstore('foo', 'setA'), #setA) 1543 | 1544 | assert_error(function() 1545 | client:set('foo', 'bar') 1546 | client:sunionstore('setA', 'foo') 1547 | end) 1548 | end) 1549 | 1550 | test("SDIFF (client:sdiff)", function() 1551 | local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' }) 1552 | local setB = utils.sadd_return(client, 'setB', { '1', '3', '4', '6', '9', '10' }) 1553 | 1554 | assert_table_values(client:sdiff('setA'), setA) 1555 | assert_table_values(client:sdiff('setA', 'setB'), { '5', '0', '2' }) 1556 | assert_table_values(client:sdiff('setA', 'doesnotexist'), setA) 1557 | 1558 | assert_error(function() 1559 | client:set('foo', 'bar') 1560 | client:sdiff('foo') 1561 | end) 1562 | 1563 | assert_error(function() 1564 | client:set('foo', 'bar') 1565 | client:sdiff('setA', 'foo') 1566 | end) 1567 | end) 1568 | 1569 | test("SDIFFSTORE (client:sdiffstore)", function() 1570 | local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' }) 1571 | local setB = utils.sadd_return(client, 'setB', { '1', '3', '4', '6', '9', '10' }) 1572 | 1573 | assert_equal(client:sdiffstore('setC', 'setA'), #setA) 1574 | assert_table_values(client:smembers('setC'), setA) 1575 | 1576 | client:del('setC') 1577 | assert_equal(client:sdiffstore('setC', 'setA', 'setB'), 3) 1578 | assert_table_values(client:smembers('setC'), { '5', '0', '2' }) 1579 | 1580 | client:del('setC') 1581 | assert_equal(client:sdiffstore('setC', 'doesnotexist'), 0) 1582 | if version:is('<', '2.0.0') then 1583 | assert_true(client:exists('setC')) 1584 | else 1585 | assert_false(client:exists('setC')) 1586 | end 1587 | assert_equal(client:scard('setC'), 0) 1588 | 1589 | -- existing keys are replaced by SDIFFSTORE 1590 | client:set('foo', 'bar') 1591 | assert_equal(client:sdiffstore('foo', 'setA'), #setA) 1592 | 1593 | assert_error(function() 1594 | client:set('foo', 'bar') 1595 | client:sdiffstore('setA', 'foo') 1596 | end) 1597 | end) 1598 | 1599 | test("SRANDMEMBER (client:srandmember)", function() 1600 | local setA = utils.sadd_return(client, 'setA', { '0', '1', '2', '3', '4', '5', '6' }) 1601 | 1602 | assert_true(table.contains(setA, client:srandmember('setA'))) 1603 | assert_nil(client:srandmember('doesnotexist')) 1604 | 1605 | assert_error(function() 1606 | client:set('foo', 'bar') 1607 | client:srandmember('foo') 1608 | end) 1609 | end) 1610 | 1611 | test("SSCAN (client:sscan)", function() 1612 | if version:is('<', '2.8.0') then return end 1613 | 1614 | local args = { 'member:1st', 'member:2nd', 'member:3rd', 'member:4th' } 1615 | client:sadd('key:set', unpack(args)) 1616 | 1617 | local cursor, members = unpack(client:sscan('key:set', 0)) 1618 | assert_type(cursor, 'string') 1619 | assert_type(members, 'table') 1620 | assert_table_values(members, args) 1621 | 1622 | local cursor, members = unpack(client:sscan('key:set', 0, { 1623 | match = 'member:*d', count = 10 1624 | })) 1625 | assert_type(cursor, 'string') 1626 | assert_type(members, 'table') 1627 | assert_table_values(members, { 'member:2nd', 'member:3rd' }) 1628 | end) 1629 | end) 1630 | 1631 | context("Commands operating on zsets", function() 1632 | test("ZADD (client:zadd)", function() 1633 | assert_equal(client:zadd('zset', 0, 'a'), 1) 1634 | assert_equal(client:zadd('zset', 1, 'b'), 1) 1635 | assert_equal(client:zadd('zset', -1, 'c'), 1) 1636 | 1637 | assert_equal(client:zadd('zset', 2, 'b'), 0) 1638 | assert_equal(client:zadd('zset', -22, 'b'), 0) 1639 | 1640 | assert_error(function() 1641 | client:set('foo', 'bar') 1642 | client:zadd('foo', 0, 'a') 1643 | end) 1644 | end) 1645 | 1646 | test("ZINCRBY (client:zincrby)", function() 1647 | assert_equal(client:zincrby('doesnotexist', 1, 'foo'), 1) 1648 | assert_equal(client:type('doesnotexist'), 'zset') 1649 | 1650 | utils.zadd_return(client, 'zset', shared.zset_sample()) 1651 | assert_equal(client:zincrby('zset', 5, 'a'), -5) 1652 | assert_equal(client:zincrby('zset', 1, 'b'), 1) 1653 | assert_equal(client:zincrby('zset', 0, 'c'), 10) 1654 | assert_equal(client:zincrby('zset', -20, 'd'), 0) 1655 | assert_equal(client:zincrby('zset', 2, 'd'), 2) 1656 | assert_equal(client:zincrby('zset', -30, 'e'), -10) 1657 | assert_equal(client:zincrby('zset', 1, 'x'), 1) 1658 | 1659 | assert_error(function() 1660 | client:set('foo', 'bar') 1661 | client:zincrby('foo', 1, 'a') 1662 | end) 1663 | end) 1664 | 1665 | test("ZREM (client:zrem)", function() 1666 | utils.zadd_return(client, 'zset', shared.zset_sample()) 1667 | 1668 | assert_equal(client:zrem('zset', 'a'), 1) 1669 | assert_equal(client:zrem('zset', 'x'), 0) 1670 | 1671 | assert_error(function() 1672 | client:set('foo', 'bar') 1673 | client:zrem('foo', 'bar') 1674 | end) 1675 | end) 1676 | 1677 | test("ZRANGE (client:zrange)", function() 1678 | local zset = utils.zadd_return(client, 'zset', shared.zset_sample()) 1679 | 1680 | assert_table_values(client:zrange('zset', 0, 3), { 'a', 'b', 'c', 'd' }) 1681 | assert_table_values(client:zrange('zset', 0, 0), { 'a' }) 1682 | assert_empty(client:zrange('zset', 1, 0)) 1683 | assert_table_values(client:zrange('zset', 0, -1), table.keys(zset)) 1684 | assert_table_values(client:zrange('zset', 3, -3), { 'd' }) 1685 | assert_empty(client:zrange('zset', 5, -3)) 1686 | assert_table_values(client:zrange('zset', -100, 100), table.keys(zset)) 1687 | 1688 | assert_table_values( 1689 | client:zrange('zset', 0, 2, 'withscores'), 1690 | { { 'a', '-10' }, { 'b', '0' }, { 'c', '10' } } 1691 | ) 1692 | 1693 | assert_table_values( 1694 | client:zrange('zset', 0, 2, { withscores = true }), 1695 | { { 'a', '-10' }, { 'b', '0' }, { 'c', '10' } } 1696 | ) 1697 | 1698 | assert_error(function() 1699 | client:set('foo', 'bar') 1700 | client:zrange('foo', 0, -1) 1701 | end) 1702 | end) 1703 | 1704 | test("ZREVRANGE (client:zrevrange)", function() 1705 | local zset = utils.zadd_return(client, 'zset', shared.zset_sample()) 1706 | 1707 | assert_table_values(client:zrevrange('zset', 0, 3), { 'f', 'e', 'd', 'c' }) 1708 | assert_table_values(client:zrevrange('zset', 0, 0), { 'f' }) 1709 | assert_empty(client:zrevrange('zset', 1, 0)) 1710 | assert_table_values(client:zrevrange('zset', 0, -1), table.keys(zset)) 1711 | assert_table_values(client:zrevrange('zset', 3, -3), { 'c' }) 1712 | assert_empty(client:zrevrange('zset', 5, -3)) 1713 | assert_table_values(client:zrevrange('zset', -100, 100), table.keys(zset)) 1714 | 1715 | assert_table_values( 1716 | client:zrevrange('zset', 0, 2, 'withscores'), 1717 | { { 'f', '30' }, { 'e', '20' }, { 'd', '20' } } 1718 | ) 1719 | 1720 | assert_table_values( 1721 | client:zrevrange('zset', 0, 2, { withscores = true }), 1722 | { { 'f', '30' }, { 'e', '20' }, { 'd', '20' } } 1723 | ) 1724 | 1725 | assert_error(function() 1726 | client:set('foo', 'bar') 1727 | client:zrevrange('foo', 0, -1) 1728 | end) 1729 | end) 1730 | 1731 | test("ZRANGEBYSCORE (client:zrangebyscore)", function() 1732 | local zset = utils.zadd_return(client, 'zset', shared.zset_sample()) 1733 | 1734 | assert_table_values(client:zrangebyscore('zset', -10, -10), { 'a' }) 1735 | assert_table_values(client:zrangebyscore('zset', 10, 30), { 'c', 'd', 'e', 'f' }) 1736 | assert_table_values(client:zrangebyscore('zset', 20, 20), { 'd', 'e' }) 1737 | assert_empty(client:zrangebyscore('zset', 30, 0)) 1738 | 1739 | assert_table_values( 1740 | client:zrangebyscore('zset', 10, 20, 'withscores'), 1741 | { { 'c', '10' }, { 'd', '20' }, { 'e', '20' } } 1742 | ) 1743 | 1744 | assert_table_values( 1745 | client:zrangebyscore('zset', 10, 20, { withscores = true }), 1746 | { { 'c', '10' }, { 'd', '20' }, { 'e', '20' } } 1747 | ) 1748 | 1749 | assert_table_values( 1750 | client:zrangebyscore('zset', 10, 20, { limit = { 1, 2 } }), 1751 | { 'd', 'e' } 1752 | ) 1753 | 1754 | assert_table_values( 1755 | client:zrangebyscore('zset', 10, 20, { 1756 | limit = { offset = 1, count = 2 } 1757 | }), 1758 | { 'd', 'e' } 1759 | ) 1760 | 1761 | assert_table_values( 1762 | client:zrangebyscore('zset', 10, 20, { 1763 | limit = { offset = 1, count = 2 }, 1764 | withscores = true 1765 | }), 1766 | { { 'd', '20' }, { 'e', '20' } } 1767 | ) 1768 | 1769 | assert_error(function() 1770 | client:set('foo', 'bar') 1771 | client:zrangebyscore('foo', 0, -1) 1772 | end) 1773 | end) 1774 | 1775 | test("ZREVRANGEBYSCORE (client:zrevrangebyscore)", function() 1776 | local zset = utils.zadd_return(client, 'zset', shared.zset_sample()) 1777 | 1778 | assert_table_values(client:zrevrangebyscore('zset', -10, -10), { 'a' }) 1779 | assert_table_values(client:zrevrangebyscore('zset', 0, -10), { 'b', 'a' }) 1780 | assert_table_values(client:zrevrangebyscore('zset', 20, 20), { 'e', 'd' }) 1781 | assert_table_values(client:zrevrangebyscore('zset', 30, 0), { 'f', 'e', 'd', 'c', 'b' }) 1782 | 1783 | assert_table_values( 1784 | client:zrevrangebyscore('zset', 20, 10, 'withscores'), 1785 | { { 'e', '20' }, { 'd', '20' }, { 'c', '10' } } 1786 | ) 1787 | 1788 | assert_table_values( 1789 | client:zrevrangebyscore('zset', 20, 10, { limit = { 1, 2 } }), 1790 | { 'd', 'c' } 1791 | ) 1792 | 1793 | assert_table_values( 1794 | client:zrevrangebyscore('zset', 20, 10, { 1795 | limit = { offset = 1, count = 2 } 1796 | }), 1797 | { 'd', 'c' } 1798 | ) 1799 | 1800 | assert_table_values( 1801 | client:zrevrangebyscore('zset', 20, 10, { 1802 | limit = { offset = 1, count = 2 }, 1803 | withscores = true 1804 | }), 1805 | { { 'd', '20' }, { 'c', '10' } } 1806 | ) 1807 | 1808 | assert_error(function() 1809 | client:set('foo', 'bar') 1810 | client:zrevrangebyscore('foo', 0, -1) 1811 | end) 1812 | end) 1813 | 1814 | 1815 | test("ZUNIONSTORE (client:zunionstore)", function() 1816 | if version:is('<', '2.0.0') then return end 1817 | 1818 | utils.zadd_return(client, 'zseta', { a = 1, b = 2, c = 3 }) 1819 | utils.zadd_return(client, 'zsetb', { b = 1, c = 2, d = 3 }) 1820 | 1821 | -- basic ZUNIONSTORE 1822 | assert_equal(client:zunionstore('zsetc', 2, 'zseta', 'zsetb'), 4) 1823 | assert_table_values( 1824 | client:zrange('zsetc', 0, -1, 'withscores'), 1825 | { { 'a', '1' }, { 'b', '3' }, { 'd', '3' }, { 'c', '5' } } 1826 | ) 1827 | 1828 | assert_equal(client:zunionstore('zsetc', 2, 'zseta', 'zsetbNull'), 3) 1829 | assert_table_values( 1830 | client:zrange('zsetc', 0, -1, 'withscores'), 1831 | { { 'a', '1' }, { 'b', '2' }, { 'c', '3' }} 1832 | ) 1833 | 1834 | assert_equal(client:zunionstore('zsetc', 2, 'zsetaNull', 'zsetb'), 3) 1835 | assert_table_values( 1836 | client:zrange('zsetc', 0, -1, 'withscores'), 1837 | { { 'b', '1' }, { 'c', '2' }, { 'd', '3' }} 1838 | ) 1839 | 1840 | assert_equal(client:zunionstore('zsetc', 2, 'zsetaNull', 'zsetbNull'), 0) 1841 | 1842 | -- with WEIGHTS 1843 | local opts = { weights = { 2, 3 } } 1844 | assert_equal(client:zunionstore('zsetc', 2, 'zseta', 'zsetb', opts), 4) 1845 | assert_table_values( 1846 | client:zrange('zsetc', 0, -1, 'withscores'), 1847 | { { 'a', '2' }, { 'b', '7' }, { 'd', '9' }, { 'c', '12' } } 1848 | ) 1849 | 1850 | -- with AGGREGATE (min) 1851 | local opts = { aggregate = 'min' } 1852 | assert_equal(client:zunionstore('zsetc', 2, 'zseta', 'zsetb', opts), 4) 1853 | assert_table_values( 1854 | client:zrange('zsetc', 0, -1, 'withscores'), 1855 | { { 'a', '1' }, { 'b', '1' }, { 'c', '2' }, { 'd', '3' } } 1856 | ) 1857 | 1858 | -- with AGGREGATE (max) 1859 | local opts = { aggregate = 'max' } 1860 | assert_equal(client:zunionstore('zsetc', 2, 'zseta', 'zsetb', opts), 4) 1861 | assert_table_values( 1862 | client:zrange('zsetc', 0, -1, 'withscores'), 1863 | { { 'a', '1' }, { 'b', '2' }, { 'c', '3' }, { 'd', '3' } } 1864 | ) 1865 | 1866 | assert_error(function() 1867 | client:set('zsetFake', 'fake') 1868 | client:zunionstore('zsetc', 2, 'zseta', 'zsetFake') 1869 | end) 1870 | end) 1871 | 1872 | test("ZINTERSTORE (client:zinterstore)", function() 1873 | if version:is('<', '2.0.0') then return end 1874 | 1875 | utils.zadd_return(client, 'zseta', { a = 1, b = 2, c = 3 }) 1876 | utils.zadd_return(client, 'zsetb', { b = 1, c = 2, d = 3 }) 1877 | 1878 | -- basic ZUNIONSTORE 1879 | assert_equal(client:zinterstore('zsetc', 2, 'zseta', 'zsetb'), 2) 1880 | assert_table_values( 1881 | client:zrange('zsetc', 0, -1, 'withscores'), 1882 | { { 'b', '3' }, { 'c', '5' } } 1883 | ) 1884 | 1885 | assert_equal(client:zinterstore('zsetc', 2, 'zseta', 'zsetbNull'), 0) 1886 | assert_equal(client:zinterstore('zsetc', 2, 'zsetaNull', 'zsetb'), 0) 1887 | assert_equal(client:zinterstore('zsetc', 2, 'zsetaNull', 'zsetbNull'), 0) 1888 | 1889 | -- with WEIGHTS 1890 | local opts = { weights = { 2, 3 } } 1891 | assert_equal(client:zinterstore('zsetc', 2, 'zseta', 'zsetb', opts), 2) 1892 | assert_table_values( 1893 | client:zrange('zsetc', 0, -1, 'withscores'), 1894 | { { 'b', '7' }, { 'c', '12' } } 1895 | ) 1896 | 1897 | -- with AGGREGATE (min) 1898 | local opts = { aggregate = 'min' } 1899 | assert_equal(client:zinterstore('zsetc', 2, 'zseta', 'zsetb', opts), 2) 1900 | assert_table_values( 1901 | client:zrange('zsetc', 0, -1, 'withscores'), 1902 | { { 'b', '1' }, { 'c', '2' } } 1903 | ) 1904 | 1905 | -- with AGGREGATE (max) 1906 | local opts = { aggregate = 'max' } 1907 | assert_equal(client:zinterstore('zsetc', 2, 'zseta', 'zsetb', opts), 2) 1908 | assert_table_values( 1909 | client:zrange('zsetc', 0, -1, 'withscores'), 1910 | { { 'b', '2' }, { 'c', '3' } } 1911 | ) 1912 | 1913 | assert_error(function() 1914 | client:set('zsetFake', 'fake') 1915 | client:zinterstore('zsetc', 2, 'zseta', 'zsetFake') 1916 | end) 1917 | end) 1918 | 1919 | test("ZCOUNT (client:zcount)", function() 1920 | if version:is('<', '2.0.0') then return end 1921 | 1922 | utils.zadd_return(client, 'zset', shared.zset_sample()) 1923 | 1924 | assert_equal(client:zcount('zset', 50, 100), 0) 1925 | assert_equal(client:zcount('zset', -100, 100), 6) 1926 | assert_equal(client:zcount('zset', 10, 20), 3) 1927 | assert_equal(client:zcount('zset', '(10', 20), 2) 1928 | assert_equal(client:zcount('zset', 10, '(20'), 1) 1929 | assert_equal(client:zcount('zset', '(10', '(20'), 0) 1930 | assert_equal(client:zcount('zset', '(0', '(30'), 3) 1931 | 1932 | assert_error(function() 1933 | client:set('foo', 'bar') 1934 | client:zcount('foo', 0, 0) 1935 | end) 1936 | end) 1937 | 1938 | test("ZCARD (client:zcard)", function() 1939 | local zset = utils.zadd_return(client, 'zset', shared.zset_sample()) 1940 | 1941 | assert_equal(client:zcard('zset'), #table.keys(zset)) 1942 | 1943 | client:zrem('zset', 'a') 1944 | assert_equal(client:zcard('zset'), #table.keys(zset) - 1) 1945 | 1946 | client:zadd('zsetB', 0, 'a') 1947 | client:zrem('zsetB', 'a') 1948 | assert_equal(client:zcard('zsetB'), 0) 1949 | 1950 | assert_equal(client:zcard('doesnotexist'), 0) 1951 | 1952 | assert_error(function() 1953 | client:set('foo', 'bar') 1954 | client:zcard('foo') 1955 | end) 1956 | end) 1957 | 1958 | test("ZSCORE (client:zscore)", function() 1959 | utils.zadd_return(client, 'zset', shared.zset_sample()) 1960 | 1961 | assert_equal(client:zscore('zset', 'a'), '-10') 1962 | assert_equal(client:zscore('zset', 'c'), '10') 1963 | assert_equal(client:zscore('zset', 'e'), '20') 1964 | 1965 | assert_nil(client:zscore('zset', 'x')) 1966 | assert_nil(client:zscore('doesnotexist', 'a')) 1967 | 1968 | assert_error(function() 1969 | client:set('foo', 'bar') 1970 | client:zscore('foo', 'a') 1971 | end) 1972 | end) 1973 | 1974 | test("ZREMRANGEBYSCORE (client:zremrangebyscore)", function() 1975 | utils.zadd_return(client, 'zset', shared.zset_sample()) 1976 | 1977 | assert_equal(client:zremrangebyscore('zset', -10, 0), 2) 1978 | assert_table_values(client:zrange('zset', 0, -1), { 'c', 'd', 'e', 'f' }) 1979 | 1980 | assert_equal(client:zremrangebyscore('zset', 10, 10), 1) 1981 | assert_table_values(client:zrange('zset', 0, -1), { 'd', 'e', 'f' }) 1982 | 1983 | assert_equal(client:zremrangebyscore('zset', 100, 100), 0) 1984 | 1985 | assert_equal(client:zremrangebyscore('zset', 0, 100), 3) 1986 | assert_equal(client:zremrangebyscore('zset', 0, 100), 0) 1987 | 1988 | assert_error(function() 1989 | client:set('foo', 'bar') 1990 | client:zremrangebyscore('foo', 0, 0) 1991 | end) 1992 | end) 1993 | 1994 | test("ZRANK (client:zrank)", function() 1995 | if version:is('<', '2.0.0') then return end 1996 | 1997 | utils.zadd_return(client, 'zset', shared.zset_sample()) 1998 | 1999 | assert_equal(client:zrank('zset', 'a'), 0) 2000 | assert_equal(client:zrank('zset', 'b'), 1) 2001 | assert_equal(client:zrank('zset', 'e'), 4) 2002 | 2003 | client:zrem('zset', 'd') 2004 | assert_equal(client:zrank('zset', 'e'), 3) 2005 | 2006 | assert_nil(client:zrank('zset', 'x')) 2007 | 2008 | assert_error(function() 2009 | client:set('foo', 'bar') 2010 | client:zrank('foo', 'a') 2011 | end) 2012 | end) 2013 | 2014 | test("ZREVRANK (client:zrevrank)", function() 2015 | if version:is('<', '2.0.0') then return end 2016 | 2017 | utils.zadd_return(client, 'zset', shared.zset_sample()) 2018 | 2019 | assert_equal(client:zrevrank('zset', 'a'), 5) 2020 | assert_equal(client:zrevrank('zset', 'b'), 4) 2021 | assert_equal(client:zrevrank('zset', 'e'), 1) 2022 | 2023 | client:zrem('zset', 'e') 2024 | assert_equal(client:zrevrank('zset', 'd'), 1) 2025 | 2026 | assert_nil(client:zrevrank('zset', 'x')) 2027 | 2028 | assert_error(function() 2029 | client:set('foo', 'bar') 2030 | client:zrevrank('foo', 'a') 2031 | end) 2032 | end) 2033 | 2034 | test("ZREMRANGEBYRANK (client:zremrangebyrank)", function() 2035 | if version:is('<', '2.0.0') then return end 2036 | 2037 | utils.zadd_return(client, 'zseta', shared.zset_sample()) 2038 | assert_equal(client:zremrangebyrank('zseta', 0, 2), 3) 2039 | assert_table_values(client:zrange('zseta', 0, -1), { 'd', 'e', 'f' }) 2040 | assert_equal(client:zremrangebyrank('zseta', 0, 0), 1) 2041 | assert_table_values(client:zrange('zseta', 0, -1), { 'e', 'f' }) 2042 | 2043 | utils.zadd_return(client, 'zsetb', shared.zset_sample()) 2044 | assert_equal(client:zremrangebyrank('zsetb', -3, -1), 3) 2045 | assert_table_values(client:zrange('zsetb', 0, -1), { 'a', 'b', 'c' }) 2046 | assert_equal(client:zremrangebyrank('zsetb', -1, -1), 1) 2047 | assert_table_values(client:zrange('zsetb', 0, -1), { 'a', 'b' }) 2048 | assert_equal(client:zremrangebyrank('zsetb', -2, -1), 2) 2049 | assert_table_values(client:zrange('zsetb', 0, -1), { }) 2050 | assert_false(client:exists('zsetb')) 2051 | 2052 | assert_equal(client:zremrangebyrank('zsetc', 0, 0), 0) 2053 | 2054 | assert_error(function() 2055 | client:set('foo', 'bar') 2056 | client:zremrangebyrank('foo', 0, 1) 2057 | end) 2058 | end) 2059 | 2060 | test("ZSCAN (client:zscan)", function() 2061 | if version:is('<', '2.8.0') then return end 2062 | 2063 | client:zadd('key:zset', 2064 | 1, 'member:1st', 2, 'member:2nd', 3, 'member:3rd', 4, 'member:4th' 2065 | ) 2066 | 2067 | local cursor, members = unpack(client:zscan('key:zset', 0)) 2068 | assert_type(cursor, 'string') 2069 | assert_type(members, 'table') 2070 | assert_table_values(members, { 2071 | { 'member:1st', 1 }, { 'member:2nd', 2 }, 2072 | { 'member:3rd', 3 }, { 'member:4th', 4 }, 2073 | }) 2074 | 2075 | local cursor, members = unpack(client:zscan('key:zset', 0, { 2076 | match = 'member:*d', count = 10 2077 | })) 2078 | assert_type(cursor, 'string') 2079 | assert_type(members, 'table') 2080 | assert_table_values(members, { { 'member:2nd', 2 }, { 'member:3rd', 3 } }) 2081 | end) 2082 | end) 2083 | 2084 | context("Commands operating on hashes", function() 2085 | test("HSET (client:hset)", function() 2086 | if version:is('<', '2.0.0') then return end 2087 | 2088 | assert_true(client:hset('metavars', 'foo', 'bar')) 2089 | assert_true(client:hset('metavars', 'hoge', 'piyo')) 2090 | assert_equal(client:hget('metavars', 'foo'), 'bar') 2091 | assert_equal(client:hget('metavars', 'hoge'), 'piyo') 2092 | 2093 | assert_error(function() 2094 | client:set('test', 'foobar') 2095 | client:hset('test', 'hoge', 'piyo') 2096 | end) 2097 | end) 2098 | 2099 | test("HGET (client:hget)", function() 2100 | if version:is('<', '2.0.0') then return end 2101 | 2102 | assert_true(client:hset('metavars', 'foo', 'bar')) 2103 | assert_equal(client:hget('metavars', 'foo'), 'bar') 2104 | assert_nil(client:hget('metavars', 'hoge')) 2105 | assert_nil(client:hget('hashDoesNotExist', 'field')) 2106 | 2107 | assert_error(function() 2108 | client:rpush('metavars', 'foo') 2109 | client:hget('metavars', 'foo') 2110 | end) 2111 | end) 2112 | 2113 | test("HEXISTS (client:hexists)", function() 2114 | if version:is('<', '2.0.0') then return end 2115 | 2116 | assert_true(client:hset('metavars', 'foo', 'bar')) 2117 | assert_true(client:hexists('metavars', 'foo')) 2118 | assert_false(client:hexists('metavars', 'hoge')) 2119 | assert_false(client:hexists('hashDoesNotExist', 'field')) 2120 | 2121 | assert_error(function() 2122 | client:set('foo', 'bar') 2123 | client:hexists('foo') 2124 | end) 2125 | end) 2126 | 2127 | test("HDEL (client:hdel)", function() 2128 | if version:is('<', '2.0.0') then return end 2129 | 2130 | assert_true(client:hset('metavars', 'foo', 'bar')) 2131 | assert_true(client:hexists('metavars', 'foo')) 2132 | assert_equal(client:hdel('metavars', 'foo'), 1) 2133 | assert_false(client:hexists('metavars', 'foo')) 2134 | 2135 | assert_equal(client:hdel('metavars', 'hoge'), 0) 2136 | assert_equal(client:hdel('hashDoesNotExist', 'field'), 0) 2137 | 2138 | assert_error(function() 2139 | client:set('foo', 'bar') 2140 | client:hdel('foo', 'field') 2141 | end) 2142 | end) 2143 | 2144 | test("HLEN (client:hlen)", function() 2145 | if version:is('<', '2.0.0') then return end 2146 | 2147 | assert_true(client:hset('metavars', 'foo', 'bar')) 2148 | assert_true(client:hset('metavars', 'hoge', 'piyo')) 2149 | assert_true(client:hset('metavars', 'foofoo', 'barbar')) 2150 | assert_true(client:hset('metavars', 'hogehoge', 'piyopiyo')) 2151 | 2152 | assert_equal(client:hlen('metavars'), 4) 2153 | client:hdel('metavars', 'foo') 2154 | assert_equal(client:hlen('metavars'), 3) 2155 | assert_equal(client:hlen('hashDoesNotExist'), 0) 2156 | 2157 | assert_error(function() 2158 | client:set('foo', 'bar') 2159 | client:hlen('foo') 2160 | end) 2161 | end) 2162 | 2163 | test("HSETNX (client:hsetnx)", function() 2164 | if version:is('<', '2.0.0') then return end 2165 | 2166 | assert_true(client:hsetnx('metavars', 'foo', 'bar')) 2167 | assert_false(client:hsetnx('metavars', 'foo', 'barbar')) 2168 | assert_equal(client:hget('metavars', 'foo'), 'bar') 2169 | 2170 | assert_error(function() 2171 | client:set('test', 'foobar') 2172 | client:hsetnx('test', 'hoge', 'piyo') 2173 | end) 2174 | end) 2175 | 2176 | test("HMSET / HMGET (client:hmset, client:hmget)", function() 2177 | if version:is('<', '2.0.0') then return end 2178 | 2179 | local hashKVs = { foo = 'bar', hoge = 'piyo' } 2180 | 2181 | -- key => value pairs via table 2182 | assert_true(client:hmset('metavars', hashKVs)) 2183 | local retval = client:hmget('metavars', table.keys(hashKVs)) 2184 | assert_table_values(retval, table.values(hashKVs)) 2185 | 2186 | -- key => value pairs via function arguments 2187 | client:del('metavars') 2188 | assert_true(client:hmset('metavars', 'foo', 'bar', 'hoge', 'piyo')) 2189 | assert_table_values(retval, table.values(hashKVs)) 2190 | end) 2191 | 2192 | test("HINCRBY (client:hincrby)", function() 2193 | if version:is('<', '2.0.0') then return end 2194 | 2195 | assert_equal(client:hincrby('hash', 'counter', 10), 10) 2196 | assert_equal(client:hincrby('hash', 'counter', 10), 20) 2197 | assert_equal(client:hincrby('hash', 'counter', -20), 0) 2198 | 2199 | assert_error(function() 2200 | client:hset('hash', 'field', 'string_value') 2201 | client:hincrby('hash', 'field', 10) 2202 | end) 2203 | 2204 | assert_error(function() 2205 | client:set('foo', 'bar') 2206 | client:hincrby('foo', 'bar', 1) 2207 | end) 2208 | end) 2209 | 2210 | test("HINCRBYFLOAT (client:hincrbyfloat)", function() 2211 | if version:is('<', '2.5.0') then return end 2212 | 2213 | assert_equal(client:hincrbyfloat('hash', 'counter', 10.1), 10.1) 2214 | assert_equal(client:hincrbyfloat('hash', 'counter', 10.4), 20.5) 2215 | assert_equal(client:hincrbyfloat('hash', 'counter', -20.000), 0.5) 2216 | 2217 | assert_error(function() 2218 | client:hset('hash', 'field', 'string_value') 2219 | client:hincrbyfloat('hash', 'field', 10.10) 2220 | end) 2221 | 2222 | assert_error(function() 2223 | client:set('foo', 'bar') 2224 | client:hincrbyfloat('foo', 'bar', 1.10) 2225 | end) 2226 | end) 2227 | 2228 | test("HKEYS (client:hkeys)", function() 2229 | if version:is('<', '2.0.0') then return end 2230 | 2231 | local hashKVs = { foo = 'bar', hoge = 'piyo' } 2232 | assert_true(client:hmset('metavars', hashKVs)) 2233 | 2234 | assert_table_values(client:hkeys('metavars'), table.keys(hashKVs)) 2235 | assert_table_values(client:hkeys('hashDoesNotExist'), { }) 2236 | 2237 | assert_error(function() 2238 | client:set('foo', 'bar') 2239 | client:hkeys('foo') 2240 | end) 2241 | end) 2242 | 2243 | test("HVALS (client:hvals)", function() 2244 | if version:is('<', '2.0.0') then return end 2245 | 2246 | local hashKVs = { foo = 'bar', hoge = 'piyo' } 2247 | assert_true(client:hmset('metavars', hashKVs)) 2248 | 2249 | assert_table_values(client:hvals('metavars'), table.values(hashKVs)) 2250 | assert_table_values(client:hvals('hashDoesNotExist'), { }) 2251 | 2252 | assert_error(function() 2253 | client:set('foo', 'bar') 2254 | client:hvals('foo') 2255 | end) 2256 | end) 2257 | 2258 | test("HGETALL (client:hgetall)", function() 2259 | if version:is('<', '2.0.0') then return end 2260 | 2261 | local hashKVs = { foo = 'bar', hoge = 'piyo' } 2262 | assert_true(client:hmset('metavars', hashKVs)) 2263 | 2264 | assert_true(table.compare(client:hgetall('metavars'), hashKVs)) 2265 | assert_true(table.compare(client:hgetall('hashDoesNotExist'), { })) 2266 | 2267 | assert_error(function() 2268 | client:set('foo', 'bar') 2269 | client:hgetall('foo') 2270 | end) 2271 | end) 2272 | 2273 | test("HSCAN (client:hscan)", function() 2274 | if version:is('<', '2.8.0') then return end 2275 | 2276 | local args = { 2277 | ['field:1st'] = 2, ['field:2nd'] = 2, 2278 | ['field:3rd'] = 3, ['field:4th'] = 4, 2279 | } 2280 | 2281 | client:hmset('key:hash', args) 2282 | 2283 | local cursor, kvs = unpack(client:hscan('key:hash', 0)) 2284 | assert_type(cursor, 'string') 2285 | assert_type(kvs, 'table') 2286 | assert_true(table.compare(kvs, args)) 2287 | 2288 | local cursor, kvs = unpack(client:hscan('key:hash', 0, { 2289 | match = 'field:*d', count = 10 2290 | })) 2291 | assert_type(cursor, 'string') 2292 | assert_type(kvs, 'table') 2293 | assert_table_values(kvs, { ['field:2nd'] = 2, ['field:3rd'] = 3 }) 2294 | end) 2295 | end) 2296 | 2297 | context("Remote server control commands", function() 2298 | test("INFO (client:info)", function() 2299 | local info = client:info() 2300 | assert_type(info, 'table') 2301 | assert_not_nil(info.redis_version or info.server.redis_version) 2302 | end) 2303 | 2304 | test("CONFIG GET (client:config)", function() 2305 | if version:is('<', '2.0.0') then return end 2306 | 2307 | local config = client:config('get', '*') 2308 | assert_type(config, 'table') 2309 | assert_not_nil(config['list-max-ziplist-entries']) 2310 | if version:is('>=', '2.4.0') then 2311 | assert_not_nil(config.loglevel) 2312 | end 2313 | 2314 | local config = client:config('get', '*max-*-entries*') 2315 | assert_type(config, 'table') 2316 | assert_not_nil(config['list-max-ziplist-entries']) 2317 | if version:is('>=', '2.4.0') then 2318 | assert_nil(config.loglevel) 2319 | end 2320 | end) 2321 | 2322 | test("CONFIG SET (client:config)", function() 2323 | if version:is('<', '2.4.0') then return end 2324 | 2325 | local new, previous = 'notice', client:config('get', 'loglevel').loglevel 2326 | 2327 | assert_type(previous, 'string') 2328 | 2329 | assert_true(client:config('set', 'loglevel', new)) 2330 | assert_equal(client:config('get', 'loglevel').loglevel, new) 2331 | 2332 | assert_true(client:config('set', 'loglevel', previous)) 2333 | end) 2334 | 2335 | test("CONFIG RESETSTAT (client:config)", function() 2336 | assert_true(client:config('resetstat')) 2337 | end) 2338 | 2339 | test("SLOWLOG RESET (client:slowlog)", function() 2340 | if version:is('<', '2.2.12') then return end 2341 | 2342 | assert_true(client:slowlog('reset')) 2343 | end) 2344 | 2345 | test("SLOWLOG GET (client:slowlog)", function() 2346 | if version:is('<', '2.2.12') then return end 2347 | 2348 | local previous = client:config('get', 'slowlog-log-slower-than')['slowlog-log-slower-than'] 2349 | 2350 | client:config('set', 'slowlog-log-slower-than', 0) 2351 | client:set('foo', 'bar') 2352 | client:del('foo') 2353 | 2354 | local log = client:slowlog('get') 2355 | assert_type(log, 'table') 2356 | assert_greater_than(#log, 0) 2357 | 2358 | assert_type(log[1], 'table') 2359 | assert_greater_than(log[1].id, 0) 2360 | assert_greater_than(log[1].timestamp, 0) 2361 | assert_greater_than(log[1].duration, 0) 2362 | assert_type(log[1].command, 'table') 2363 | 2364 | local log = client:slowlog('get', 1) 2365 | assert_type(log, 'table') 2366 | assert_equal(#log, 1) 2367 | 2368 | client:config('set', 'slowlog-log-slower-than', previous or 10000) 2369 | end) 2370 | 2371 | test("TIME (client:time)", function() 2372 | if version:is('<', '2.5.0') then return end 2373 | 2374 | local redis_time = client:time() 2375 | assert_type(redis_time, 'table') 2376 | assert_not_nil(redis_time[1]) 2377 | assert_not_nil(redis_time[2]) 2378 | end) 2379 | 2380 | test("CLIENT (client:client)", function() 2381 | if version:is('<', '2.4.0') then return end 2382 | -- TODO: implement tests 2383 | end) 2384 | 2385 | test("LASTSAVE (client:lastsave)", function() 2386 | assert_not_nil(client:lastsave()) 2387 | end) 2388 | 2389 | test("FLUSHDB (client:flushdb)", function() 2390 | assert_true(client:flushdb()) 2391 | end) 2392 | end) 2393 | 2394 | context("Transactions", function() 2395 | test("MULTI / EXEC (client:multi, client:exec)", function() 2396 | if version:is('<', '2.0.0') then return end 2397 | 2398 | assert_true(client:multi()) 2399 | assert_response_queued(client:ping()) 2400 | assert_response_queued(client:echo('hello')) 2401 | assert_response_queued(client:echo('redis')) 2402 | assert_table_values(client:exec(), { 'PONG', 'hello', 'redis' }) 2403 | 2404 | assert_true(client:multi()) 2405 | assert_table_values(client:exec(), {}) 2406 | 2407 | -- should raise an error when trying to EXEC without having previously issued MULTI 2408 | assert_error(function() client:exec() end) 2409 | end) 2410 | test("DISCARD (client:discard)", function() 2411 | if version:is('<', '2.0.0') then return end 2412 | 2413 | assert_true(client:multi()) 2414 | assert_response_queued(client:set('foo', 'bar')) 2415 | assert_response_queued(client:set('hoge', 'piyo')) 2416 | assert_true(client:discard()) 2417 | 2418 | -- should raise an error when trying to EXEC after a DISCARD 2419 | assert_error(function() client:exec() end) 2420 | 2421 | assert_false(client:exists('foo')) 2422 | assert_false(client:exists('hoge')) 2423 | end) 2424 | 2425 | test("WATCH", function() 2426 | if version:is('<', '2.1.0') then return end 2427 | 2428 | local client2 = utils.create_client(settings) 2429 | assert_true(client:set('foo', 'bar')) 2430 | assert_true(client:watch('foo')) 2431 | assert_true(client:multi()) 2432 | assert_response_queued(client:get('foo')) 2433 | assert_true(client2:set('foo', 'hijacked')) 2434 | assert_nil(client:exec()) 2435 | end) 2436 | 2437 | test("UNWATCH", function() 2438 | if version:is('<', '2.1.0') then return end 2439 | 2440 | local client2 = utils.create_client(settings) 2441 | assert_true(client:set('foo', 'bar')) 2442 | assert_true(client:watch('foo')) 2443 | assert_true(client:unwatch()) 2444 | assert_true(client:multi()) 2445 | assert_response_queued(client:get('foo')) 2446 | assert_true(client2:set('foo', 'hijacked')) 2447 | assert_table_values(client:exec(), { 'hijacked' }) 2448 | end) 2449 | 2450 | test("MULTI / EXEC / DISCARD abstraction", function() 2451 | if version:is('<', '2.0.0') then return end 2452 | 2453 | local replies, processed 2454 | 2455 | replies, processed = client:transaction(function(t) 2456 | -- empty transaction 2457 | end) 2458 | assert_table_values(replies, { }) 2459 | assert_equal(processed, 0) 2460 | 2461 | replies, processed = client:transaction(function(t) 2462 | t:discard() 2463 | end) 2464 | assert_table_values(replies, { }) 2465 | assert_equal(processed, 0) 2466 | 2467 | replies, processed = client:transaction(function(t) 2468 | assert_response_queued(t:set('foo', 'bar')) 2469 | assert_true(t:discard()) 2470 | assert_response_queued(t:ping()) 2471 | assert_response_queued(t:echo('hello')) 2472 | assert_response_queued(t:echo('redis')) 2473 | assert_response_queued(t:exists('foo')) 2474 | end) 2475 | assert_table_values(replies, { true, 'hello', 'redis', false }) 2476 | assert_equal(processed, 4) 2477 | 2478 | -- clean up transaction after client-side errors 2479 | assert_error(function() 2480 | client:transaction(function(t) 2481 | t:lpush('metavars', 'foo') 2482 | error('whoops!') 2483 | t:lpush('metavars', 'hoge') 2484 | end) 2485 | end) 2486 | assert_false(client:exists('metavars')) 2487 | end) 2488 | 2489 | test("WATCH / MULTI / EXEC abstraction", function() 2490 | if version:is('<', '2.1.0') then return end 2491 | 2492 | local redis2 = utils.create_client(settings) 2493 | local watch_keys = { 'foo' } 2494 | 2495 | local replies, processed = client:transaction(watch_keys, function(t) 2496 | -- empty transaction 2497 | end) 2498 | assert_table_values(replies, { }) 2499 | assert_equal(processed, 0) 2500 | 2501 | assert_error(function() 2502 | client:transaction(watch_keys, function(t) 2503 | t:set('foo', 'bar') 2504 | redis2:set('foo', 'hijacked') 2505 | t:get('foo') 2506 | end) 2507 | end) 2508 | end) 2509 | 2510 | test("WATCH / MULTI / EXEC with check-and-set (CAS) abstraction", function() 2511 | if version:is('<', '2.1.0') then return end 2512 | 2513 | local opts, replies, processed 2514 | 2515 | opts = { cas = 'foo' } 2516 | replies, processed = client:transaction(opts, function(t) 2517 | -- empty transaction (with missing call to t:multi()) 2518 | end) 2519 | assert_table_values(replies, { }) 2520 | assert_equal(processed, 0) 2521 | 2522 | opts = { watch = 'foo', cas = true } 2523 | replies, processed = client:transaction(opts, function(t) 2524 | t:multi() 2525 | -- empty transaction 2526 | end) 2527 | assert_table_values(replies, { }) 2528 | assert_equal(processed, 0) 2529 | 2530 | local redis2 = utils.create_client(settings) 2531 | local n = 5 2532 | opts = { watch = 'foobarr', cas = true, retry = 5 } 2533 | replies, processed = client:transaction(opts, function(t) 2534 | t:set('foobar', 'bazaar') 2535 | local val = t:get('foobar') 2536 | t:multi() 2537 | assert_response_queued(t:set('discardable', 'bar')) 2538 | assert_equal(t:commands_queued(), 1) 2539 | assert_true(t:discard()) 2540 | assert_response_queued(t:ping()) 2541 | assert_equal(t:commands_queued(), 1) 2542 | assert_response_queued(t:echo('hello')) 2543 | assert_response_queued(t:echo('redis')) 2544 | assert_equal(t:commands_queued(), 3) 2545 | if n>0 then 2546 | n = n-1 2547 | redis2:set("foobarr", n) 2548 | end 2549 | assert_response_queued(t:exists('foo')) 2550 | assert_response_queued(t:get('foobar')) 2551 | assert_response_queued(t:get('foobarr')) 2552 | end) 2553 | assert_table_values(replies, { true, 'hello', 'redis', false, "bazaar", '0' }) 2554 | assert_equal(processed, 6) 2555 | end) 2556 | 2557 | test("Abstraction options", function() 2558 | -- TODO: more in-depth tests (proxy calls to WATCH) 2559 | local opts, replies, processed 2560 | local tx_empty = function(t) end 2561 | local tx_cas_empty = function(t) t:multi() end 2562 | 2563 | replies, processed = client:transaction(tx_empty) 2564 | assert_table_values(replies, { }) 2565 | 2566 | assert_error(function() 2567 | client:transaction(opts, tx_empty) 2568 | end) 2569 | 2570 | opts = 'foo' 2571 | replies, processed = client:transaction(opts, tx_empty) 2572 | assert_table_values(replies, { }) 2573 | assert_equal(processed, 0) 2574 | 2575 | opts = { 'foo', 'bar' } 2576 | replies, processed = client:transaction(opts, tx_empty) 2577 | assert_equal(processed, 0) 2578 | 2579 | opts = { watch = 'foo' } 2580 | replies, processed = client:transaction(opts, tx_empty) 2581 | assert_equal(processed, 0) 2582 | 2583 | opts = { watch = { 'foo', 'bar' } } 2584 | replies, processed = client:transaction(opts, tx_empty) 2585 | assert_equal(processed, 0) 2586 | 2587 | opts = { cas = true } 2588 | replies, processed = client:transaction(opts, tx_cas_empty) 2589 | assert_equal(processed, 0) 2590 | 2591 | opts = { 'foo', 'bar', cas = true } 2592 | replies, processed = client:transaction(opts, tx_cas_empty) 2593 | assert_equal(processed, 0) 2594 | 2595 | opts = { 'foo', nil, 'bar', cas = true } 2596 | replies, processed = client:transaction(opts, tx_cas_empty) 2597 | assert_equal(processed, 0) 2598 | 2599 | opts = { watch = { 'foo', 'bar' }, cas = true } 2600 | replies, processed = client:transaction(opts, tx_cas_empty) 2601 | assert_equal(processed, 0) 2602 | 2603 | opts = { nil, cas = true } 2604 | replies, processed = client:transaction(opts, tx_cas_empty) 2605 | assert_equal(processed, 0) 2606 | end) 2607 | end) 2608 | 2609 | context("Pub/Sub", function() 2610 | test('PUBLISH (client:publish)', function() 2611 | assert_equal(client:publish('redis-lua-publish', 'test'), 0) 2612 | end) 2613 | 2614 | test('SUBSCRIBE (client:subscribe)', function() 2615 | client:subscribe('redis-lua-publish') 2616 | 2617 | -- we have one subscriber 2618 | data = 'data' .. tostring(math.random(1000)) 2619 | publisher = utils.create_client(settings) 2620 | assert_equal(publisher:publish('redis-lua-publish', data), 1) 2621 | -- we have data 2622 | response = client:subscribe('redis-lua-publish') 2623 | -- {"message","redis-lua-publish","testXXX"} 2624 | assert_true(table.contains(response, 'message')) 2625 | assert_true(table.contains(response, 'redis-lua-publish')) 2626 | assert_true(table.contains(response, data)) 2627 | 2628 | client:unsubscribe('redis-lua-publish') 2629 | end) 2630 | end) 2631 | 2632 | context("Scripting", function() 2633 | test('EVAL (client:eval)', function() 2634 | if version:is('<', '2.5.0') then return end 2635 | end) 2636 | 2637 | test('EVALSHA (client:evalsha)', function() 2638 | if version:is('<', '2.5.0') then return end 2639 | end) 2640 | 2641 | test('SCRIPT (client:script)', function() 2642 | if version:is('<', '2.5.0') then return end 2643 | end) 2644 | end) 2645 | end) 2646 | --------------------------------------------------------------------------------