diff --git a/lib/excache/command/keyspace.ex b/lib/excache/command/keyspace.ex index 86f8698..1d904a6 100644 --- a/lib/excache/command/keyspace.ex +++ b/lib/excache/command/keyspace.ex @@ -156,3 +156,159 @@ defmodule Excache.Command.Keyspace do def dispatch("RENAME", [source, dest], state), do: reply(statusReply(Store.rename(source, dest)), state) + def dispatch("RENAMENX", [source, dest], state), + do: reply(integerReply(Store.renamenx(source, dest)), state) + + def dispatch("COPY", [source, dest | rest], state) do + case parseCopyOptions(rest, false) do + {:ok, _replace} when source == dest -> + reply({:error, "ERR source and destination objects are the same"}, state) + + {:ok, replace} -> + reply(integerReply(Store.copy(source, dest, replace)), state) + + {:reply, term} -> + reply(term, state) + end + end + + def dispatch("TOUCH", [_ | _] = keys, state), do: reply({:integer, Store.touch(keys)}, state) + def dispatch("UNLINK", [_ | _] = keys, state), do: reply({:integer, Store.del(keys)}, state) + + def dispatch("EXPIREAT", [key, secondsRaw | rest], state) do + case Integer.parse(secondsRaw) do + {seconds, ""} -> + case parseExpireFlags(rest) do + {:ok, flags} -> reply({:integer, expireAt(key, seconds * 1000, flags)}, state) + {:reply, term} -> reply(term, state) + end + + _ -> + reply(Reply.not_integer(), state) + end + end + + def dispatch("EXPIRETIME", [key], state), + do: reply({:integer, Store.expiretime(key, :seconds)}, state) + + def dispatch("PEXPIRETIME", [key], state), + do: reply({:integer, Store.expiretime(key, :milliseconds)}, state) + + def dispatch(_name, _args, _state), do: :unhandled + + defp setrange(key, _offset, ""), do: integerReply(Store.strlen(key)) + defp setrange(key, offset, value), do: integerReply(Store.setrange(key, offset, value)) + + defp lpos(key, element, %{count: nil} = options, state) do + case Store.lpos(key, element, options.rank, 1, options.maxlen) do + {:ok, []} -> reply({:bulk, nil}, state) + {:ok, [index]} -> reply({:integer, index}, state) + {:error, reason} -> reply(errorTerm(reason), state) + end + end + + defp lpos(key, element, options, state) do + case Store.lpos(key, element, options.rank, options.count, options.maxlen) do + {:ok, indexes} -> reply({:array, Enum.map(indexes, &{:integer, &1})}, state) + {:error, reason} -> reply(errorTerm(reason), state) + end + end + + defp parseLposOptions([], options), do: {:ok, options} + + defp parseLposOptions([token | rest], options) do + case {String.upcase(token), rest} do + {"RANK", [raw | tail]} -> + case Integer.parse(raw) do + {0, ""} -> {:reply, {:error, rankZeroMessage()}} + {rank, ""} -> parseLposOptions(tail, %{options | rank: rank}) + _ -> {:reply, Reply.not_integer()} + end + + {"COUNT", [raw | tail]} -> + case Integer.parse(raw) do + {count, ""} when count >= 0 -> parseLposOptions(tail, %{options | count: count}) + {_negative, ""} -> {:reply, {:error, "ERR COUNT can't be negative"}} + _ -> {:reply, Reply.not_integer()} + end + + {"MAXLEN", [raw | tail]} -> + case Integer.parse(raw) do + {maxlen, ""} when maxlen >= 0 -> parseLposOptions(tail, %{options | maxlen: maxlen}) + {_negative, ""} -> {:reply, {:error, "ERR MAXLEN can't be negative"}} + _ -> {:reply, Reply.not_integer()} + end + + _ -> + {:reply, Reply.syntax()} + end + end + + defp rankZeroMessage do + "ERR RANK can't be zero. Use 1 to start searching from the first matching element in the " <> + "head of the list or -1 searching backward from the tail of the list" + end + + defp object("ENCODING", [key], state) do + withEntry(key, state, fn entry -> {:bulk, encodingName(entry)} end) + end + + defp object("REFCOUNT", [key], state) do + withEntry(key, state, fn entry -> {:integer, refcount(entry)} end) + end + + defp object("IDLETIME", [key], state) do + withEntry(key, state, fn _entry -> {:integer, idleSeconds(key)} end) + end + + defp object("HELP", [], state) do + lines = [ + "OBJECT . Subcommands are:", + "ENCODING ", + " Return the kind of internal representation used to store the value of .", + "IDLETIME ", + " Return the seconds since was last accessed, or 0 when the current", + " maxmemory-policy keeps no access metadata for it.", + "REFCOUNT ", + " Return the number of references of the value associated with .", + "HELP", + " Print this help." + ] + + reply({:array, Enum.map(lines, &{:simple, &1})}, state) + end + + defp object(subcommand, _rest, state) do + message = + "ERR Unknown subcommand or wrong number of arguments for '#{subcommand}'. " <> + "Try OBJECT HELP." + + reply({:error, message}, state) + end + + defp idleSeconds(key) do + case Tracker.idleMs(Tracker, key, System.system_time(:millisecond)) do + nil -> 0 + idleMs -> div(idleMs, 1000) + end + end + + defp withEntry(key, state, fun) do + case Store.encoding(key) do + nil -> reply({:error, "ERR no such key"}, state) + {:ok, entry} -> reply(fun.(entry), state) + end + end + + defp encodingName({:string, value, _ea}) do + cond do + integerString?(value) -> "int" + byte_size(value) <= 44 -> "embstr" + true -> "raw" + end + end + + defp encodingName({:list, value, _ea}) do + if compact?(value, length(value)), do: "listpack", else: "quicklist" + end +