-
-
Save pkulchenko/b226540580d98a59cced6e21b832c867 to your computer and use it in GitHub Desktop.
Redbean Redis Client
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local redis = { | |
_VERSION = 'redis-lua 2.0.5-dev', | |
_DESCRIPTION = 'A Lua client library for the redis key value storage system.', | |
_COPYRIGHT = 'Copyright (C) 2009-2012 Daniele Alessandri', | |
} | |
-- The following line is used for backwards compatibility in order to keep the `Redis` | |
-- global module name. Using `Redis` is now deprecated so you should explicitly assign | |
-- the module to a local variable when requiring it: `local redis = require('redis')`. | |
Redis = redis | |
local unpack = _G.unpack or table.unpack | |
local network, request, response = {}, {}, {} | |
local defaults = { | |
host = '127.0.0.1', | |
port = 6379, | |
tcp_nodelay = true, | |
path = nil, | |
} | |
local function merge_defaults(parameters) | |
if parameters == nil then | |
parameters = {} | |
end | |
for k, v in pairs(defaults) do | |
if parameters[k] == nil then | |
parameters[k] = defaults[k] | |
end | |
end | |
return parameters | |
end | |
local function parse_boolean(v) | |
if v == '1' or v == 'true' or v == 'TRUE' then | |
return true | |
elseif v == '0' or v == 'false' or v == 'FALSE' then | |
return false | |
else | |
return nil | |
end | |
end | |
local function toboolean(value) return value == 1 end | |
local function sort_request(client, command, key, params) | |
--[[ params = { | |
by = 'weight_*', | |
get = 'object_*', | |
limit = { 0, 10 }, | |
sort = 'desc', | |
alpha = true, | |
} ]] | |
local query = { key } | |
if params then | |
if params.by then | |
table.insert(query, 'BY') | |
table.insert(query, params.by) | |
end | |
if type(params.limit) == 'table' then | |
-- TODO: check for lower and upper limits | |
table.insert(query, 'LIMIT') | |
table.insert(query, params.limit[1]) | |
table.insert(query, params.limit[2]) | |
end | |
if params.get then | |
if (type(params.get) == 'table') then | |
for _, getarg in pairs(params.get) do | |
table.insert(query, 'GET') | |
table.insert(query, getarg) | |
end | |
else | |
table.insert(query, 'GET') | |
table.insert(query, params.get) | |
end | |
end | |
if params.sort then | |
table.insert(query, params.sort) | |
end | |
if params.alpha == true then | |
table.insert(query, 'ALPHA') | |
end | |
if params.store then | |
table.insert(query, 'STORE') | |
table.insert(query, params.store) | |
end | |
end | |
request.multibulk(client, command, query) | |
end | |
local function zset_range_request(client, command, ...) | |
local args, opts = {...}, { } | |
if #args >= 1 and type(args[#args]) == 'table' then | |
local options = table.remove(args, #args) | |
if options.withscores then | |
table.insert(opts, 'WITHSCORES') | |
end | |
end | |
for _, v in pairs(opts) do table.insert(args, v) end | |
request.multibulk(client, command, args) | |
end | |
local function zset_range_byscore_request(client, command, ...) | |
local args, opts = {...}, { } | |
if #args >= 1 and type(args[#args]) == 'table' then | |
local options = table.remove(args, #args) | |
if options.limit then | |
table.insert(opts, 'LIMIT') | |
table.insert(opts, options.limit.offset or options.limit[1]) | |
table.insert(opts, options.limit.count or options.limit[2]) | |
end | |
if options.withscores then | |
table.insert(opts, 'WITHSCORES') | |
end | |
end | |
for _, v in pairs(opts) do table.insert(args, v) end | |
request.multibulk(client, command, args) | |
end | |
local function zset_range_reply(reply, command, ...) | |
local args = {...} | |
local opts = args[4] | |
if opts and (opts.withscores or string.lower(tostring(opts)) == 'withscores') then | |
local new_reply = { } | |
for i = 1, #reply, 2 do | |
table.insert(new_reply, { reply[i], reply[i + 1] }) | |
end | |
return new_reply | |
else | |
return reply | |
end | |
end | |
local function zset_store_request(client, command, ...) | |
local args, opts = {...}, { } | |
if #args >= 1 and type(args[#args]) == 'table' then | |
local options = table.remove(args, #args) | |
if options.weights and type(options.weights) == 'table' then | |
table.insert(opts, 'WEIGHTS') | |
for _, weight in ipairs(options.weights) do | |
table.insert(opts, weight) | |
end | |
end | |
if options.aggregate then | |
table.insert(opts, 'AGGREGATE') | |
table.insert(opts, options.aggregate) | |
end | |
end | |
for _, v in pairs(opts) do table.insert(args, v) end | |
request.multibulk(client, command, args) | |
end | |
local function mset_filter_args(client, command, ...) | |
local args, arguments = {...}, {} | |
if (#args == 1 and type(args[1]) == 'table') then | |
for k,v in pairs(args[1]) do | |
table.insert(arguments, k) | |
table.insert(arguments, v) | |
end | |
else | |
arguments = args | |
end | |
request.multibulk(client, command, arguments) | |
end | |
local function hash_multi_request_builder(builder_callback) | |
return function(client, command, ...) | |
local args, arguments = {...}, { } | |
if #args == 2 then | |
table.insert(arguments, args[1]) | |
for k, v in pairs(args[2]) do | |
builder_callback(arguments, k, v) | |
end | |
else | |
arguments = args | |
end | |
request.multibulk(client, command, arguments) | |
end | |
end | |
local function parse_info(response) | |
local info = {} | |
local current = info | |
response:gsub('([^\r\n]*)\r\n', function(kv) | |
if kv == '' then return end | |
local section = kv:match('^# (%w+)$') | |
if section then | |
current = {} | |
info[section:lower()] = current | |
return | |
end | |
local k,v = kv:match(('([^:]*):([^:]*)'):rep(1)) | |
if k:match('db%d+') then | |
current[k] = {} | |
v:gsub(',', function(dbkv) | |
local dbk,dbv = kv:match('([^:]*)=([^:]*)') | |
current[k][dbk] = dbv | |
end) | |
else | |
current[k] = v | |
end | |
end) | |
return info | |
end | |
local function scan_request(client, command, ...) | |
local args, req, params = {...}, { }, nil | |
if command == 'SCAN' then | |
table.insert(req, args[1]) | |
params = args[2] | |
else | |
table.insert(req, args[1]) | |
table.insert(req, args[2]) | |
params = args[3] | |
end | |
if params and params.match then | |
table.insert(req, 'MATCH') | |
table.insert(req, params.match) | |
end | |
if params and params.count then | |
table.insert(req, 'COUNT') | |
table.insert(req, params.count) | |
end | |
request.multibulk(client, command, req) | |
end | |
local zscan_response = function(reply, command, ...) | |
local original, new = reply[2], { } | |
for i = 1, #original, 2 do | |
table.insert(new, { original[i], tonumber(original[i + 1]) }) | |
end | |
reply[2] = new | |
return reply | |
end | |
local hscan_response = function(reply, command, ...) | |
local original, new = reply[2], { } | |
for i = 1, #original, 2 do | |
new[original[i]] = original[i + 1] | |
end | |
reply[2] = new | |
return reply | |
end | |
local function load_methods(proto, commands) | |
local client = setmetatable ({}, getmetatable(proto)) | |
for cmd, fn in pairs(commands) do | |
if type(fn) ~= 'function' then | |
redis.error('invalid type for command ' .. cmd .. '(must be a function)') | |
end | |
client[cmd] = fn | |
end | |
for i, v in pairs(proto) do | |
client[i] = v | |
end | |
return client | |
end | |
local function create_client(proto, client_socket, commands) | |
local client = load_methods(proto, commands) | |
client.error = redis.error | |
client.network = { | |
socket = client_socket, | |
read = network.read, | |
write = network.write, | |
} | |
client.requests = { | |
multibulk = request.multibulk, | |
} | |
return client | |
end | |
-- ############################################################################ | |
function network.write(client, buffer) | |
local _, err = unix.send(client.network.socket, buffer) | |
if err then client.error(err) end | |
end | |
function network.read(client, len) | |
if len == nil then len = '*l' end | |
local line, err = unix.recv(client.network.socket, tonumber(len)) | |
if not err then return line else client.error('connection error: ' .. err) end | |
end | |
-- ############################################################################ | |
function response.read(client) | |
local payload = client.network.read(client) | |
local prefix, data = payload:sub(1, -#payload), payload:sub(2) | |
-- status reply | |
if prefix == '+' then | |
if data == 'OK' then | |
return true | |
elseif data == 'QUEUED' then | |
return { queued = true } | |
else | |
return data | |
end | |
-- error reply | |
elseif prefix == '-' then | |
return client.error('redis error: ' .. data) | |
-- integer reply | |
elseif prefix == ':' then | |
local number = tonumber(data) | |
if not number then | |
if data == 'nil' then | |
return nil | |
end | |
client.error('cannot parse ' .. data .. ' as a numeric response.') | |
end | |
return number | |
-- bulk reply | |
elseif prefix == '$' then | |
local length = tonumber(data) | |
if not length then | |
client.error('cannot parse ' .. length .. ' as data length') | |
end | |
if length == -1 then | |
return nil | |
end | |
local nextchunk = client.network.read(client, length + 2) | |
return nextchunk:sub(1, -3) | |
-- multibulk reply | |
elseif prefix == '*' then | |
local count = tonumber(data) | |
if count == -1 then | |
return nil | |
end | |
local list = {} | |
if count > 0 then | |
local reader = response.read | |
for i = 1, count do | |
list[i] = reader(client) | |
end | |
end | |
return list | |
-- unknown type of reply | |
else | |
return client.error('unknown response prefix: ' .. prefix) | |
end | |
end | |
-- ############################################################################ | |
function request.raw(client, buffer) | |
local bufferType = type(buffer) | |
if bufferType == 'table' then | |
client.network.write(client, table.concat(buffer)) | |
elseif bufferType == 'string' then | |
client.network.write(client, buffer) | |
else | |
client.error('argument error: ' .. bufferType) | |
end | |
end | |
function request.multibulk(client, command, ...) | |
local args = {...} | |
local argsn = #args | |
local buffer = { true, true } | |
if argsn == 1 and type(args[1]) == 'table' then | |
argsn, args = #args[1], args[1] | |
end | |
buffer[1] = '*' .. tostring(argsn + 1) .. "\r\n" | |
buffer[2] = '$' .. #command .. "\r\n" .. command .. "\r\n" | |
local table_insert = table.insert | |
for i = 1, argsn do | |
local s_argument = tostring(args[i] or '') | |
table_insert(buffer, '$' .. #s_argument .. "\r\n" .. s_argument .. "\r\n") | |
end | |
client.network.write(client, table.concat(buffer)) | |
end | |
-- ############################################################################ | |
local function custom(command, send, parse) | |
command = string.upper(command) | |
return function(client, ...) | |
send(client, command, ...) | |
local reply = response.read(client) | |
if type(reply) == 'table' and reply.queued then | |
reply.parser = parse | |
return reply | |
else | |
if parse then | |
return parse(reply, command, ...) | |
end | |
return reply | |
end | |
end | |
end | |
local function command(command, opts) | |
if opts == nil or type(opts) == 'function' then | |
return custom(command, request.multibulk, opts) | |
else | |
return custom(command, opts.request or request.multibulk, opts.response) | |
end | |
end | |
local define_command_impl = function(target, name, opts) | |
local opts = opts or {} | |
target[string.lower(name)] = custom( | |
opts.command or string.upper(name), | |
opts.request or request.multibulk, | |
opts.response or nil | |
) | |
end | |
local undefine_command_impl = function(target, name) | |
target[string.lower(name)] = nil | |
end | |
-- ############################################################################ | |
local client_prototype = {} | |
client_prototype.raw_cmd = function(client, buffer) | |
request.raw(client, buffer .. "\r\n") | |
return response.read(client) | |
end | |
-- obsolete | |
client_prototype.define_command = function(client, name, opts) | |
define_command_impl(client, name, opts) | |
end | |
-- obsolete | |
client_prototype.undefine_command = function(client, name) | |
undefine_command_impl(client, name) | |
end | |
client_prototype.quit = function(client) | |
request.multibulk(client, 'QUIT') | |
unix.close(client.network.socket) | |
return true | |
end | |
client_prototype.shutdown = function(client) | |
request.multibulk(client, 'SHUTDOWN') | |
unix.close(client.network.socket) | |
end | |
-- Command pipelining | |
client_prototype.pipeline = function(client, block) | |
local requests, replies, parsers = {}, {}, {} | |
local table_insert = table.insert | |
local socket_write, socket_read = client.network.write, client.network.read | |
client.network.write = function(_, buffer) | |
table_insert(requests, buffer) | |
end | |
-- TODO: this hack is necessary to temporarily reuse the current | |
-- request -> response handling implementation of redis-lua | |
-- without further changes in the code, but it will surely | |
-- disappear when the new command-definition infrastructure | |
-- will finally be in place. | |
client.network.read = function() return '+QUEUED' end | |
local pipeline = setmetatable({}, { | |
__index = function(env, name) | |
local cmd = client[name] | |
if not cmd then | |
client.error('unknown redis command: ' .. name, 2) | |
end | |
return function(self, ...) | |
local reply = cmd(client, ...) | |
table_insert(parsers, #requests, reply.parser) | |
return reply | |
end | |
end | |
}) | |
local success, retval = pcall(block, pipeline) | |
client.network.write, client.network.read = socket_write, socket_read | |
if not success then client.error(retval, 0) end | |
client.network.write(client, table.concat(requests, '')) | |
for i = 1, #requests do | |
local reply, parser = response.read(client), parsers[i] | |
if parser then | |
reply = parser(reply) | |
end | |
table_insert(replies, i, reply) | |
end | |
return replies, #requests | |
end | |
-- Publish/Subscribe | |
do | |
local channels = function(channels) | |
if type(channels) == 'string' then | |
channels = { channels } | |
end | |
return channels | |
end | |
local subscribe = function(client, ...) | |
request.multibulk(client, 'subscribe', ...) | |
end | |
local psubscribe = function(client, ...) | |
request.multibulk(client, 'psubscribe', ...) | |
end | |
local unsubscribe = function(client, ...) | |
request.multibulk(client, 'unsubscribe') | |
end | |
local punsubscribe = function(client, ...) | |
request.multibulk(client, 'punsubscribe') | |
end | |
local consumer_loop = function(client) | |
local aborting, subscriptions = false, 0 | |
local abort = function() | |
if not aborting then | |
unsubscribe(client) | |
punsubscribe(client) | |
aborting = true | |
end | |
end | |
return coroutine.wrap(function() | |
while true do | |
local message | |
local response = response.read(client) | |
if response[1] == 'pmessage' then | |
message = { | |
kind = response[1], | |
pattern = response[2], | |
channel = response[3], | |
payload = response[4], | |
} | |
else | |
message = { | |
kind = response[1], | |
channel = response[2], | |
payload = response[3], | |
} | |
end | |
if string.match(message.kind, '^p?subscribe$') then | |
subscriptions = subscriptions + 1 | |
end | |
if string.match(message.kind, '^p?unsubscribe$') then | |
subscriptions = subscriptions - 1 | |
end | |
if aborting and subscriptions == 0 then | |
break | |
end | |
coroutine.yield(message, abort) | |
end | |
end) | |
end | |
client_prototype.pubsub = function(client, subscriptions) | |
if type(subscriptions) == 'table' then | |
if subscriptions.subscribe then | |
subscribe(client, channels(subscriptions.subscribe)) | |
end | |
if subscriptions.psubscribe then | |
psubscribe(client, channels(subscriptions.psubscribe)) | |
end | |
end | |
return consumer_loop(client) | |
end | |
end | |
-- Redis transactions (MULTI/EXEC) | |
do | |
local function identity(...) return ... end | |
local emptytable = {} | |
local function initialize_transaction(client, options, block, queued_parsers) | |
local table_insert = table.insert | |
local coro = coroutine.create(block) | |
if options.watch then | |
local watch_keys = {} | |
for _, key in pairs(options.watch) do | |
table_insert(watch_keys, key) | |
end | |
if #watch_keys > 0 then | |
client:watch(unpack(watch_keys)) | |
end | |
end | |
local transaction_client = setmetatable({}, {__index=client}) | |
transaction_client.exec = function(...) | |
client.error('cannot use EXEC inside a transaction block') | |
end | |
transaction_client.multi = function(...) | |
coroutine.yield() | |
end | |
transaction_client.commands_queued = function() | |
return #queued_parsers | |
end | |
assert(coroutine.resume(coro, transaction_client)) | |
transaction_client.multi = nil | |
transaction_client.discard = function(...) | |
local reply = client:discard() | |
for i, v in pairs(queued_parsers) do | |
queued_parsers[i]=nil | |
end | |
coro = initialize_transaction(client, options, block, queued_parsers) | |
return reply | |
end | |
transaction_client.watch = function(...) | |
client.error('WATCH inside MULTI is not allowed') | |
end | |
setmetatable(transaction_client, { __index = function(t, k) | |
local cmd = client[k] | |
if type(cmd) == "function" then | |
local function queuey(self, ...) | |
local reply = cmd(client, ...) | |
assert((reply or emptytable).queued == true, 'a QUEUED reply was expected') | |
table_insert(queued_parsers, reply.parser or identity) | |
return reply | |
end | |
t[k]=queuey | |
return queuey | |
else | |
return cmd | |
end | |
end | |
}) | |
client:multi() | |
return coro | |
end | |
local function transaction(client, options, coroutine_block, attempts) | |
local queued_parsers, replies = {}, {} | |
local retry = tonumber(attempts) or tonumber(options.retry) or 2 | |
local coro = initialize_transaction(client, options, coroutine_block, queued_parsers) | |
local success, retval | |
if coroutine.status(coro) == 'suspended' then | |
success, retval = coroutine.resume(coro) | |
else | |
-- do not fail if the coroutine has not been resumed (missing t:multi() with CAS) | |
success, retval = true, 'empty transaction' | |
end | |
if #queued_parsers == 0 or not success then | |
client:discard() | |
assert(success, retval) | |
return replies, 0 | |
end | |
local raw_replies = client:exec() | |
if not raw_replies then | |
if (retry or 0) <= 0 then | |
client.error("MULTI/EXEC transaction aborted by the server") | |
else | |
--we're not quite done yet | |
return transaction(client, options, coroutine_block, retry - 1) | |
end | |
end | |
local table_insert = table.insert | |
for i, parser in pairs(queued_parsers) do | |
table_insert(replies, i, parser(raw_replies[i])) | |
end | |
return replies, #queued_parsers | |
end | |
client_prototype.transaction = function(client, arg1, arg2) | |
local options, block | |
if not arg2 then | |
options, block = {}, arg1 | |
elseif arg1 then --and arg2, implicitly | |
options, block = type(arg1)=="table" and arg1 or { arg1 }, arg2 | |
else | |
client.error("Invalid parameters for redis transaction.") | |
end | |
if not options.watch then | |
local watch_keys = { } | |
for i, v in pairs(options) do | |
if tonumber(i) then | |
table.insert(watch_keys, v) | |
options[i] = nil | |
end | |
end | |
options.watch = watch_keys | |
elseif not (type(options.watch) == 'table') then | |
options.watch = { options.watch } | |
end | |
if not options.cas then | |
local tx_block = block | |
block = function(client, ...) | |
client:multi() | |
return tx_block(client, ...) --can't wrap this in pcall because we're in a coroutine. | |
end | |
end | |
return transaction(client, options, block) | |
end | |
end | |
-- MONITOR context | |
do | |
local monitor_loop = function(client) | |
local monitoring = true | |
-- Tricky since the payload format changed starting from Redis 2.6. | |
local pattern = '^(%d+%.%d+)( ?.- ?) ?"(%a+)" ?(.-)$' | |
local abort = function() | |
monitoring = false | |
end | |
return coroutine.wrap(function() | |
client:monitor() | |
while monitoring do | |
local message, matched | |
local response = response.read(client) | |
local ok = response:gsub(pattern, function(time, info, cmd, args) | |
message = { | |
timestamp = tonumber(time), | |
client = info:match('%d+.%d+.%d+.%d+:%d+'), | |
database = tonumber(info:match('%d+')) or 0, | |
command = cmd, | |
arguments = args:match('.+'), | |
} | |
matched = true | |
end) | |
if not matched then | |
client.error('Unable to match MONITOR payload: '..response) | |
end | |
coroutine.yield(message, abort) | |
end | |
end) | |
end | |
client_prototype.monitor_messages = function(client) | |
return monitor_loop(client) | |
end | |
end | |
-- ############################################################################ | |
local function connect_tcp(socket, parameters) | |
local host, port = parameters.host, tonumber(parameters.port) | |
if parameters.timeout then | |
-- socket:settimeout(parameters.timeout, 't') | |
end | |
local ok, err unix.connect(socket, ResolveIp(host), port) | |
if not ok then | |
redis.error('could not connect to '..host..':'..port..' ['..err..']') | |
end | |
-- socket:setoption('tcp-nodelay', parameters.tcp_nodelay) | |
return socket | |
end | |
local function connect_unix(socket, parameters) | |
local ok, err = socket:connect(parameters.path) | |
if not ok then | |
redis.error('could not connect to '..parameters.path..' ['..err..']') | |
end | |
return socket | |
end | |
local function create_connection(parameters) | |
if parameters.socket then | |
return parameters.socket | |
end | |
local perform_connection, socket | |
if parameters.scheme == 'unix' then | |
perform_connection, socket = connect_unix, unix.socket() | |
assert(socket, 'your build of LuaSocket does not support UNIX domain sockets') | |
else | |
if parameters.scheme then | |
local scheme = parameters.scheme | |
assert(scheme == 'redis' or scheme == 'tcp', 'invalid scheme: '..scheme) | |
end | |
perform_connection, socket = connect_tcp, unix.socket() | |
end | |
return perform_connection(socket, parameters) | |
end | |
-- ############################################################################ | |
function redis.error(message, level) | |
error(message, (level or 1) + 1) | |
end | |
function redis.connect(...) | |
local args, parameters = {...}, nil | |
if #args == 1 then | |
if type(args[1]) == 'table' then | |
parameters = args[1] | |
else | |
local uri = require('socket.url') | |
parameters = uri.parse(select(1, ...)) | |
if parameters.scheme then | |
if parameters.query then | |
for k, v in parameters.query:gmatch('([-_%w]+)=([-_%w]+)') do | |
if k == 'tcp_nodelay' or k == 'tcp-nodelay' then | |
parameters.tcp_nodelay = parse_boolean(v) | |
elseif k == 'timeout' then | |
parameters.timeout = tonumber(v) | |
end | |
end | |
end | |
else | |
parameters.host = parameters.path | |
end | |
end | |
elseif #args > 1 then | |
local host, port, timeout = unpack(args) | |
parameters = { host = host, port = port, timeout = tonumber(timeout) } | |
end | |
local commands = redis.commands or {} | |
if type(commands) ~= 'table' then | |
redis.error('invalid type for the commands table') | |
end | |
local socket = create_connection(merge_defaults(parameters)) | |
local client = create_client(client_prototype, socket, commands) | |
return client | |
end | |
function redis.command(cmd, opts) | |
return command(cmd, opts) | |
end | |
-- obsolete | |
function redis.define_command(name, opts) | |
define_command_impl(redis.commands, name, opts) | |
end | |
-- obsolete | |
function redis.undefine_command(name) | |
undefine_command_impl(redis.commands, name) | |
end | |
-- ############################################################################ | |
-- Commands defined in this table do not take the precedence over | |
-- methods defined in the client prototype table. | |
redis.commands = { | |
-- commands operating on the key space | |
exists = command('EXISTS', { | |
response = toboolean | |
}), | |
del = command('DEL'), | |
type = command('TYPE'), | |
rename = command('RENAME'), | |
renamenx = command('RENAMENX', { | |
response = toboolean | |
}), | |
expire = command('EXPIRE', { | |
response = toboolean | |
}), | |
pexpire = command('PEXPIRE', { -- >= 2.6 | |
response = toboolean | |
}), | |
expireat = command('EXPIREAT', { | |
response = toboolean | |
}), | |
pexpireat = command('PEXPIREAT', { -- >= 2.6 | |
response = toboolean | |
}), | |
ttl = command('TTL'), | |
pttl = command('PTTL'), -- >= 2.6 | |
move = command('MOVE', { | |
response = toboolean | |
}), | |
dbsize = command('DBSIZE'), | |
persist = command('PERSIST', { -- >= 2.2 | |
response = toboolean | |
}), | |
keys = command('KEYS', { | |
response = function(response) | |
if type(response) == 'string' then | |
-- backwards compatibility path for Redis < 2.0 | |
local keys = {} | |
response:gsub('[^%s]+', function(key) | |
table.insert(keys, key) | |
end) | |
response = keys | |
end | |
return response | |
end | |
}), | |
randomkey = command('RANDOMKEY'), | |
sort = command('SORT', { | |
request = sort_request, | |
}), | |
scan = command('SCAN', { -- >= 2.8 | |
request = scan_request, | |
}), | |
-- commands operating on string values | |
set = command('SET'), | |
setnx = command('SETNX', { | |
response = toboolean | |
}), | |
setex = command('SETEX'), -- >= 2.0 | |
psetex = command('PSETEX'), -- >= 2.6 | |
mset = command('MSET', { | |
request = mset_filter_args | |
}), | |
msetnx = command('MSETNX', { | |
request = mset_filter_args, | |
response = toboolean | |
}), | |
get = command('GET'), | |
mget = command('MGET'), | |
getset = command('GETSET'), | |
incr = command('INCR'), | |
incrby = command('INCRBY'), | |
incrbyfloat = command('INCRBYFLOAT', { -- >= 2.6 | |
response = function(reply, command, ...) | |
return tonumber(reply) | |
end, | |
}), | |
decr = command('DECR'), | |
decrby = command('DECRBY'), | |
append = command('APPEND'), -- >= 2.0 | |
substr = command('SUBSTR'), -- >= 2.0 | |
strlen = command('STRLEN'), -- >= 2.2 | |
setrange = command('SETRANGE'), -- >= 2.2 | |
getrange = command('GETRANGE'), -- >= 2.2 | |
setbit = command('SETBIT'), -- >= 2.2 | |
getbit = command('GETBIT'), -- >= 2.2 | |
bitop = command('BITOP'), -- >= 2.6 | |
bitcount = command('BITCOUNT'), -- >= 2.6 | |
-- commands operating on lists | |
rpush = command('RPUSH'), | |
lpush = command('LPUSH'), | |
llen = command('LLEN'), | |
lrange = command('LRANGE'), | |
ltrim = command('LTRIM'), | |
lindex = command('LINDEX'), | |
lset = command('LSET'), | |
lrem = command('LREM'), | |
lpop = command('LPOP'), | |
rpop = command('RPOP'), | |
rpoplpush = command('RPOPLPUSH'), | |
blpop = command('BLPOP'), -- >= 2.0 | |
brpop = command('BRPOP'), -- >= 2.0 | |
rpushx = command('RPUSHX'), -- >= 2.2 | |
lpushx = command('LPUSHX'), -- >= 2.2 | |
linsert = command('LINSERT'), -- >= 2.2 | |
brpoplpush = command('BRPOPLPUSH'), -- >= 2.2 | |
-- commands operating on sets | |
sadd = command('SADD'), | |
srem = command('SREM'), | |
spop = command('SPOP'), | |
smove = command('SMOVE', { | |
response = toboolean | |
}), | |
scard = command('SCARD'), | |
sismember = command('SISMEMBER', { | |
response = toboolean | |
}), | |
sinter = command('SINTER'), | |
sinterstore = command('SINTERSTORE'), | |
sunion = command('SUNION'), | |
sunionstore = command('SUNIONSTORE'), | |
sdiff = command('SDIFF'), | |
sdiffstore = command('SDIFFSTORE'), | |
smembers = command('SMEMBERS'), | |
srandmember = command('SRANDMEMBER'), | |
sscan = command('SSCAN', { -- >= 2.8 | |
request = scan_request, | |
}), | |
-- commands operating on sorted sets | |
zadd = command('ZADD'), | |
zincrby = command('ZINCRBY', { | |
response = function(reply, command, ...) | |
return tonumber(reply) | |
end, | |
}), | |
zrem = command('ZREM'), | |
zrange = command('ZRANGE', { | |
request = zset_range_request, | |
response = zset_range_reply, | |
}), | |
zrevrange = command('ZREVRANGE', { | |
request = zset_range_request, | |
response = zset_range_reply, | |
}), | |
zrangebyscore = command('ZRANGEBYSCORE', { | |
request = zset_range_byscore_request, | |
response = zset_range_reply, | |
}), | |
zrevrangebyscore = command('ZREVRANGEBYSCORE', { -- >= 2.2 | |
request = zset_range_byscore_request, | |
response = zset_range_reply, | |
}), | |
zunionstore = command('ZUNIONSTORE', { -- >= 2.0 | |
request = zset_store_request | |
}), | |
zinterstore = command('ZINTERSTORE', { -- >= 2.0 | |
request = zset_store_request | |
}), | |
zcount = command('ZCOUNT'), | |
zcard = command('ZCARD'), | |
zscore = command('ZSCORE'), | |
zremrangebyscore = command('ZREMRANGEBYSCORE'), | |
zrank = command('ZRANK'), -- >= 2.0 | |
zrevrank = command('ZREVRANK'), -- >= 2.0 | |
zremrangebyrank = command('ZREMRANGEBYRANK'), -- >= 2.0 | |
zscan = command('ZSCAN', { -- >= 2.8 | |
request = scan_request, | |
response = zscan_response, | |
}), | |
-- commands operating on hashes | |
hset = command('HSET', { -- >= 2.0 | |
response = toboolean | |
}), | |
hsetnx = command('HSETNX', { -- >= 2.0 | |
response = toboolean | |
}), | |
hmset = command('HMSET', { -- >= 2.0 | |
request = hash_multi_request_builder(function(args, k, v) | |
table.insert(args, k) | |
table.insert(args, v) | |
end), | |
}), | |
hincrby = command('HINCRBY'), -- >= 2.0 | |
hincrbyfloat = command('HINCRBYFLOAT', {-- >= 2.6 | |
response = function(reply, command, ...) | |
return tonumber(reply) | |
end, | |
}), | |
hget = command('HGET'), -- >= 2.0 | |
hmget = command('HMGET', { -- >= 2.0 | |
request = hash_multi_request_builder(function(args, k, v) | |
table.insert(args, v) | |
end), | |
}), | |
hdel = command('HDEL'), -- >= 2.0 | |
hexists = command('HEXISTS', { -- >= 2.0 | |
response = toboolean | |
}), | |
hlen = command('HLEN'), -- >= 2.0 | |
hkeys = command('HKEYS'), -- >= 2.0 | |
hvals = command('HVALS'), -- >= 2.0 | |
hgetall = command('HGETALL', { -- >= 2.0 | |
response = function(reply, command, ...) | |
local new_reply = { } | |
for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end | |
return new_reply | |
end | |
}), | |
hscan = command('HSCAN', { -- >= 2.8 | |
request = scan_request, | |
response = hscan_response, | |
}), | |
-- connection related commands | |
ping = command('PING', { | |
response = function(response) return response == 'PONG' end | |
}), | |
echo = command('ECHO'), | |
auth = command('AUTH'), | |
select = command('SELECT'), | |
-- transactions | |
multi = command('MULTI'), -- >= 2.0 | |
exec = command('EXEC'), -- >= 2.0 | |
discard = command('DISCARD'), -- >= 2.0 | |
watch = command('WATCH'), -- >= 2.2 | |
unwatch = command('UNWATCH'), -- >= 2.2 | |
-- publish - subscribe | |
subscribe = command('SUBSCRIBE'), -- >= 2.0 | |
unsubscribe = command('UNSUBSCRIBE'), -- >= 2.0 | |
psubscribe = command('PSUBSCRIBE'), -- >= 2.0 | |
punsubscribe = command('PUNSUBSCRIBE'), -- >= 2.0 | |
publish = command('PUBLISH'), -- >= 2.0 | |
-- redis scripting | |
eval = command('EVAL'), -- >= 2.6 | |
evalsha = command('EVALSHA'), -- >= 2.6 | |
script = command('SCRIPT'), -- >= 2.6 | |
-- remote server control commands | |
bgrewriteaof = command('BGREWRITEAOF'), | |
config = command('CONFIG', { -- >= 2.0 | |
response = function(reply, command, ...) | |
if (type(reply) == 'table') then | |
local new_reply = { } | |
for i = 1, #reply, 2 do new_reply[reply[i]] = reply[i + 1] end | |
return new_reply | |
end | |
return reply | |
end | |
}), | |
client = command('CLIENT'), -- >= 2.4 | |
slaveof = command('SLAVEOF'), | |
save = command('SAVE'), | |
bgsave = command('BGSAVE'), | |
lastsave = command('LASTSAVE'), | |
flushdb = command('FLUSHDB'), | |
flushall = command('FLUSHALL'), | |
monitor = command('MONITOR'), | |
time = command('TIME'), -- >= 2.6 | |
slowlog = command('SLOWLOG', { -- >= 2.2.13 | |
response = function(reply, command, ...) | |
if (type(reply) == 'table') then | |
local structured = { } | |
for index, entry in ipairs(reply) do | |
structured[index] = { | |
id = tonumber(entry[1]), | |
timestamp = tonumber(entry[2]), | |
duration = tonumber(entry[3]), | |
command = entry[4], | |
} | |
end | |
return structured | |
end | |
return reply | |
end | |
}), | |
info = command('INFO', { | |
response = parse_info, | |
}), | |
} | |
-- ############################################################################ | |
return redis |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment