1 module('Redis', package.seeall)
3 require('socket') -- requires LuaSocket as a dependency
5 -- ############################################################################
8 newline = '\r\n', ok = 'OK', err = 'ERR', null = 'nil',
11 -- ############################################################################
13 local function toboolean(value)
17 local function _write(self, buffer)
18 local _, err = self.socket:send(buffer)
19 if err then error(err) end
22 local function _read(self, len)
23 if len == nil then len = '*l' end
24 local line, err = self.socket:receive(len)
25 if not err then return line else error('Connection error: ' .. err) end
28 -- ############################################################################
30 local function _read_response(self)
31 if options and options.close == true then return end
33 local res = _read(self)
34 local prefix = res:sub(1, -#res)
35 local response_handler = protocol.prefixes[prefix]
37 if not response_handler then
38 error("Unknown response prefix: " .. prefix)
40 return response_handler(self, res)
45 local function _send_raw(self, buffer)
47 local bufferType = type(buffer)
49 if bufferType == 'string' then
51 elseif bufferType == 'table' then
52 _write(self, table.concat(buffer))
54 error('Argument error: ' .. bufferType)
57 return _read_response(self)
60 local function _send_inline(self, command, ...)
62 _write(self, command .. protocol.newline)
67 if #arguments > 0 then
68 arguments = table.concat(arguments, ' ')
73 _write(self, command .. ' ' .. arguments .. protocol.newline)
76 return _read_response(self)
79 local function _send_bulk(self, command, ...)
81 local data = tostring(table.remove(arguments))
85 if #arguments > 0 then
86 arguments = table.concat(arguments, ' ')
91 return _send_raw(self, {
92 command, ' ', arguments, ' ', #data, protocol.newline, data, protocol.newline
97 local function _read_line(self, response)
98 return response:sub(2)
101 local function _read_error(self, response)
102 local err_line = response:sub(2)
104 if err_line:sub(1, 3) == protocol.err then
105 error("Redis error: " .. err_line:sub(5))
107 error("Redis error: " .. err_line)
111 local function _read_bulk(self, response)
112 local str = response:sub(2)
113 local len = tonumber(str)
116 error('Cannot parse ' .. str .. ' as data length.')
118 if len == -1 then return nil end
119 local data = _read(self, len + 2)
120 return data:sub(1, -3);
124 local function _read_multibulk(self, response)
125 local str = response:sub(2)
127 -- TODO: add a check if the returned value is indeed a number
128 local list_count = tonumber(str)
130 if list_count == -1 then
135 if list_count > 0 then
136 for i = 1, list_count do
137 table.insert(list, i, _read_bulk(self, _read(self)))
145 local function _read_integer(self, response)
146 local res = response:sub(2)
147 local number = tonumber(res)
150 if res == protocol.null then
153 error('Cannot parse ' .. res .. ' as numeric response.')
160 -- ############################################################################
162 protocol.prefixes = {
166 ['*'] = _read_multibulk,
167 [':'] = _read_integer,
170 -- ############################################################################
173 -- miscellaneous commands
175 'PING', _send_inline, function(response)
176 if response == 'PONG' then return true else return false end
179 echo = { 'ECHO', _send_bulk },
180 -- TODO: the server returns an empty -ERR on authentication failure
183 -- connection handling
184 quit = { 'QUIT', function(self, command)
185 _write(self, command .. protocol.newline)
189 -- commands operating on string values
190 set = { 'SET', _send_bulk },
191 set_preserve = { 'SETNX', _send_bulk, toboolean },
193 get_multiple = { 'MGET' },
194 increment = { 'INCR' },
195 increment_by = { 'INCRBY' },
196 decrement = { 'DECR' },
197 decrement_by = { 'DECRBY' },
198 exists = { 'EXISTS', _send_inline, toboolean },
199 delete = { 'DEL', _send_inline, toboolean },
202 -- commands operating on the key space
204 'KEYS', _send_inline, function(response)
206 response:gsub('%w+', function(key)
207 table.insert(keys, key)
212 random_key = { 'RANDOMKEY' },
213 rename = { 'RENAME' },
214 rename_preserve = { 'RENAMENX' },
215 database_size = { 'DBSIZE' },
217 -- commands operating on lists
218 push_tail = { 'RPUSH', _send_bulk },
219 push_head = { 'LPUSH', _send_bulk },
220 list_length = { 'LLEN', _send_inline, function(response, key)
221 --[[ TODO: redis seems to return a -ERR when the specified key does
222 not hold a list value, but this behaviour is not
223 consistent with the specs docs. This might be due to the
224 -ERR response paradigm being new, which supersedes the
225 check for negative numbers to identify errors. ]]
226 if response == -2 then
227 error('Key ' .. key .. ' does not hold a list value')
232 list_range = { 'LRANGE' },
233 list_trim = { 'LTRIM' },
234 list_index = { 'LINDEX' },
235 list_set = { 'LSET', _send_bulk },
236 list_remove = { 'LREM', _send_bulk },
237 pop_first = { 'LPOP' },
238 pop_last = { 'RPOP' },
240 -- commands operating on sets
241 set_add = { 'SADD' },
242 set_remove = { 'SREM' },
243 set_cardinality = { 'SCARD' },
244 set_is_member = { 'SISMEMBER' },
245 set_intersection = { 'SINTER' },
246 set_intersection_store = { 'SINTERSTORE' },
247 set_members = { 'SMEMBERS' },
249 -- multiple databases handling commands
250 select_database = { 'SELECT' },
251 move_key = { 'MOVE' },
252 flush_database = { 'FLUSHDB' },
253 flush_databases = { 'FLUSHALL' },
257 TODO: should we pass sort parameters as a table? e.g:
262 sort = { 'desc', 'alpha' }
267 -- persistence control commands
269 background_save = { 'BGSAVE' },
270 last_save = { 'LASTSAVE' },
271 shutdown = { 'SHUTDOWN', function(self, command)
272 _write(self, command .. protocol.newline)
276 -- remote server control commands
278 'INFO', _send_inline, function(response)
280 response:gsub('([^\r\n]*)\r\n', function(kv)
281 local k,v = kv:match(('([^:]*):([^:]*)'):rep(1))
289 function connect(host, port)
290 local client_socket = socket.connect(host, port)
292 if not client_socket then
293 error('Could not connect to ' .. host .. ':' .. port)
296 local redis_client = {
297 socket = client_socket,
298 raw_cmd = function(self, buffer)
299 return _send_raw(self, buffer .. protocol.newline)
303 return setmetatable(redis_client, {
304 __index = function(self, method)
305 local redis_meth = methods[method]
307 return function(self, ...)
308 if not redis_meth[2] then
309 table.insert(redis_meth, 2, _send_inline)
312 local response = redis_meth[2](self, redis_meth[1], ...)
313 if redis_meth[3] then
314 return redis_meth[3](response, ...)