]> git.saurik.com Git - redis.git/blame - client-libraries/lua/redis.lua
ZLEN renamed ZCARD for consistency with SCARD
[redis.git] / client-libraries / lua / redis.lua
CommitLineData
928394cd 1local _G = _G
2local require, error, type, print = require, error, type, print
3local table, pairs, tostring, tonumber = table, pairs, tostring, tonumber
f2aa84bd 4
928394cd 5module('Redis')
f2aa84bd 6
928394cd 7local socket = require('socket') -- requires LuaSocket as a dependency
f2aa84bd 8
928394cd 9local redis_commands = {}
10local network, request, response, utils = {}, {}, {}, {}, {}
f2aa84bd 11
928394cd 12local protocol = { newline = '\r\n', ok = 'OK', err = 'ERR', null = 'nil' }
13
14local function toboolean(value) return value == 1 end
15
16local function load_methods(proto, methods)
17 local redis = _G.setmetatable ({}, _G.getmetatable(proto))
18 for i, v in pairs(proto) do redis[i] = v end
f2aa84bd 19
928394cd 20 for i, v in pairs(methods) do redis[i] = v end
21 return redis
f2aa84bd 22end
23
928394cd 24-- ############################################################################
25
26function network.write(client, buffer)
27 local _, err = client.socket:send(buffer)
f2aa84bd 28 if err then error(err) end
29end
30
928394cd 31function network.read(client, len)
f2aa84bd 32 if len == nil then len = '*l' end
928394cd 33 local line, err = client.socket:receive(len)
f2aa84bd 34 if not err then return line else error('Connection error: ' .. err) end
35end
36
37-- ############################################################################
38
928394cd 39function response.read(client)
40 local res = network.read(client)
f2aa84bd 41 local prefix = res:sub(1, -#res)
42 local response_handler = protocol.prefixes[prefix]
43
44 if not response_handler then
45 error("Unknown response prefix: " .. prefix)
46 else
928394cd 47 return response_handler(client, res)
48 end
49end
50
51function response.status(client, data)
52 local sub = data:sub(2)
53 if sub == protocol.ok then return true else return sub end
54end
55
56function response.error(client, data)
57 local err_line = data:sub(2)
58
59 if err_line:sub(1, 3) == protocol.err then
60 error("Redis error: " .. err_line:sub(5))
61 else
62 error("Redis error: " .. err_line)
63 end
64end
65
66function response.bulk(client, data)
67 local str = data:sub(2)
68 local len = tonumber(str)
69
70 if not len then
71 error('Cannot parse ' .. str .. ' as data length.')
72 else
73 if len == -1 then return nil end
74 local next_chunk = network.read(client, len + 2)
75 return next_chunk:sub(1, -3);
76 end
77end
78
79function response.multibulk(client, data)
80 local str = data:sub(2)
81
82 -- TODO: add a check if the returned value is indeed a number
83 local list_count = tonumber(str)
84
85 if list_count == -1 then
86 return nil
87 else
88 local list = {}
89
90 if list_count > 0 then
91 for i = 1, list_count do
92 table.insert(list, i, response.bulk(client, network.read(client)))
93 end
94 end
95
96 return list
f2aa84bd 97 end
98end
99
928394cd 100function response.integer(client, data)
101 local res = data:sub(2)
102 local number = tonumber(res)
f2aa84bd 103
928394cd 104 if not number then
105 if res == protocol.null then
106 return nil
107 else
108 error('Cannot parse ' .. res .. ' as numeric response.')
109 end
110 end
111
112 return number
113end
114
115protocol.prefixes = {
116 ['+'] = response.status,
117 ['-'] = response.error,
118 ['$'] = response.bulk,
119 ['*'] = response.multibulk,
120 [':'] = response.integer,
121}
122
123-- ############################################################################
124
125function request.raw(client, buffer)
f2aa84bd 126 -- TODO: optimize
127 local bufferType = type(buffer)
128
129 if bufferType == 'string' then
928394cd 130 network.write(client, buffer)
f2aa84bd 131 elseif bufferType == 'table' then
928394cd 132 network.write(client, table.concat(buffer))
f2aa84bd 133 else
134 error('Argument error: ' .. bufferType)
135 end
136
928394cd 137 return response.read(client)
f2aa84bd 138end
139
928394cd 140function request.inline(client, command, ...)
f2aa84bd 141 if arg.n == 0 then
928394cd 142 network.write(client, command .. protocol.newline)
f2aa84bd 143 else
144 local arguments = arg
145 arguments.n = nil
146
147 if #arguments > 0 then
148 arguments = table.concat(arguments, ' ')
149 else
150 arguments = ''
151 end
152
928394cd 153 network.write(client, command .. ' ' .. arguments .. protocol.newline)
f2aa84bd 154 end
155
928394cd 156 return response.read(client)
f2aa84bd 157end
158
928394cd 159function request.bulk(client, command, ...)
f2aa84bd 160 local arguments = arg
161 local data = tostring(table.remove(arguments))
162 arguments.n = nil
163
164 -- TODO: optimize
165 if #arguments > 0 then
166 arguments = table.concat(arguments, ' ')
167 else
168 arguments = ''
169 end
170
928394cd 171 return request.raw(client, {
f2aa84bd 172 command, ' ', arguments, ' ', #data, protocol.newline, data, protocol.newline
173 })
174end
175
928394cd 176-- ############################################################################
f2aa84bd 177
928394cd 178local function custom(command, send, parse)
179 return function(self, ...)
180 local reply = send(self, command, ...)
181 if parse then
182 return parse(reply, command, ...)
183 else
184 return reply
185 end
f2aa84bd 186 end
187end
188
928394cd 189local function bulk(command, reader)
190 return custom(command, request.bulk, reader)
f2aa84bd 191end
192
928394cd 193local function inline(command, reader)
194 return custom(command, request.inline, reader)
f2aa84bd 195end
196
928394cd 197-- ############################################################################
f2aa84bd 198
928394cd 199function connect(host, port)
200 local client_socket = socket.connect(host, port)
201 if not client_socket then
202 error('Could not connect to ' .. host .. ':' .. port)
f2aa84bd 203 end
204
928394cd 205 local redis_client = {
206 socket = client_socket,
207 raw_cmd = function(self, buffer)
208 return request.raw(self, buffer .. protocol.newline)
209 end,
210 }
f2aa84bd 211
928394cd 212 return load_methods(redis_client, redis_commands)
213end
f2aa84bd 214
215-- ############################################################################
216
928394cd 217redis_commands = {
f2aa84bd 218 -- miscellaneous commands
928394cd 219 ping = inline('PING',
220 function(response)
f2aa84bd 221 if response == 'PONG' then return true else return false end
222 end
928394cd 223 ),
224 echo = bulk('ECHO'),
f2aa84bd 225 -- TODO: the server returns an empty -ERR on authentication failure
928394cd 226 auth = inline('AUTH'),
f2aa84bd 227
228 -- connection handling
928394cd 229 quit = custom('QUIT',
230 function(client, command)
231 -- let's fire and forget! the connection is closed as soon
232 -- as the QUIT command is received by the server.
233 network.write(client, command .. protocol.newline)
f2aa84bd 234 end
928394cd 235 ),
f2aa84bd 236
237 -- commands operating on string values
928394cd 238 set = bulk('SET'),
239 set_preserve = bulk('SETNX', toboolean),
240 get = inline('GET'),
241 get_multiple = inline('MGET'),
111d9959 242 get_set = bulk('GETSET'),
928394cd 243 increment = inline('INCR'),
244 increment_by = inline('INCRBY'),
245 decrement = inline('DECR'),
246 decrement_by = inline('DECRBY'),
247 exists = inline('EXISTS', toboolean),
248 delete = inline('DEL', toboolean),
249 type = inline('TYPE'),
f2aa84bd 250
251 -- commands operating on the key space
928394cd 252 keys = inline('KEYS',
253 function(response)
f2aa84bd 254 local keys = {}
255 response:gsub('%w+', function(key)
256 table.insert(keys, key)
257 end)
258 return keys
259 end
928394cd 260 ),
261 random_key = inline('RANDOMKEY'),
262 rename = inline('RENAME'),
263 rename_preserve = inline('RENAMENX'),
264 expire = inline('EXPIRE', toboolean),
265 database_size = inline('DBSIZE'),
111d9959 266 time_to_live = inline('TTL'),
f2aa84bd 267
268 -- commands operating on lists
928394cd 269 push_tail = bulk('RPUSH'),
270 push_head = bulk('LPUSH'),
271 list_length = inline('LLEN'),
272 list_range = inline('LRANGE'),
273 list_trim = inline('LTRIM'),
274 list_index = inline('LINDEX'),
275 list_set = bulk('LSET'),
276 list_remove = bulk('LREM'),
277 pop_first = inline('LPOP'),
278 pop_last = inline('RPOP'),
f2aa84bd 279
280 -- commands operating on sets
111d9959 281 set_add = bulk('SADD'),
282 set_remove = bulk('SREM'),
283 set_move = bulk('SMOVE'),
928394cd 284 set_cardinality = inline('SCARD'),
285 set_is_member = inline('SISMEMBER'),
286 set_intersection = inline('SINTER'),
287 set_intersection_store = inline('SINTERSTORE'),
111d9959 288 set_union = inline('SUNION'),
289 set_union_store = inline('SUNIONSTORE'),
290 set_diff = inline('SDIFF'),
291 set_diff_store = inline('SDIFFSTORE'),
928394cd 292 set_members = inline('SMEMBERS'),
f2aa84bd 293
294 -- multiple databases handling commands
928394cd 295 select_database = inline('SELECT'),
296 move_key = inline('MOVE'),
297 flush_database = inline('FLUSHDB'),
298 flush_databases = inline('FLUSHALL'),
f2aa84bd 299
300 -- sorting
301 --[[
302 TODO: should we pass sort parameters as a table? e.g:
303 params = {
304 by = 'weight_*',
305 get = 'object_*',
306 limit = { 0, 10 },
307 sort = { 'desc', 'alpha' }
308 }
309 --]]
928394cd 310 sort = custom('SORT',
311 function(client, command, params)
312 -- TODO: here we will put the logic needed to serialize the params
313 -- table to be sent as the argument of the SORT command.
314 return request.inline(client, command, params)
315 end
316 ),
f2aa84bd 317
318 -- persistence control commands
928394cd 319 save = inline('SAVE'),
320 background_save = inline('BGSAVE'),
321 last_save = inline('LASTSAVE'),
322 shutdown = custom('SHUTDOWN',
323 function(client, command)
324 -- let's fire and forget! the connection is closed as soon
325 -- as the SHUTDOWN command is received by the server.
d7fc9edb 326 network.write(client, command .. protocol.newline)
f2aa84bd 327 end
928394cd 328 ),
f2aa84bd 329
330 -- remote server control commands
111d9959 331 info = inline('INFO',
928394cd 332 function(response)
f2aa84bd 333 local info = {}
334 response:gsub('([^\r\n]*)\r\n', function(kv)
335 local k,v = kv:match(('([^:]*):([^:]*)'):rep(1))
336 info[k] = v
337 end)
338 return info
339 end
928394cd 340 ),
111d9959 341 slave_of = inline('SLAVEOF'),
342 slave_of_no_one = custom('SLAVEOF',
343 function(client, command)
344 return request.inline(client, command, 'NO ONE')
345 end
346 ),
f2aa84bd 347}