feat(command): add dispatch and setrange to keyspace

This commit is contained in:
dualfroz committed 2026-09-06 15:33:06 +02:00
1 parent d3a1c170ac
commit 76e425bf21
1 file changed
+156
+156
View File
@@ -156,3 +156,159 @@ defmodule Excache.Command.Keyspace do
def dispatch("RENAME", [source, dest], state), def dispatch("RENAME", [source, dest], state),
do: reply(statusReply(Store.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 <subcommand> <key>. Subcommands are:",
"ENCODING <key>",
" Return the kind of internal representation used to store the value of <key>.",
"IDLETIME <key>",
" Return the seconds since <key> was last accessed, or 0 when the current",
" maxmemory-policy keeps no access metadata for it.",
"REFCOUNT <key>",
" Return the number of references of the value associated with <key>.",
"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