feat(command): add encodingName and compact? to keyspace

This commit is contained in:
dualfroz committed 2026-09-06 16:46:46 +02:00
1 parent 76e425bf21
commit 29b3774426
1 file changed
+190
+190
View File
@@ -312,3 +312,193 @@ defmodule Excache.Command.Keyspace do
if compact?(value, length(value)), do: "listpack", else: "quicklist"
end
defp encodingName({:hash, value, _ea}) do
flat = Enum.flat_map(value, fn {field, item} -> [field, item] end)
if compact?(flat, map_size(value)), do: "listpack", else: "hashtable"
end
defp encodingName({:set, value, _ea}) do
members = MapSet.to_list(value)
size = MapSet.size(value)
cond do
size <= @intsetEntries and Enum.all?(members, &integerString?/1) -> "intset"
compact?(members, size) -> "listpack"
true -> "hashtable"
end
end
defp encodingName({:zset, value, _ea}) do
members = value |> SkipList.to_list() |> Enum.map(fn {_score, member} -> member end)
if compact?(members, SkipList.size(value)), do: "listpack", else: "skiplist"
end
defp encodingName({:stream, _value, _ea}), do: "stream"
defp compact?(values, size) do
size <= @listpackEntries and Enum.all?(values, &(byte_size(&1) <= @listpackValue))
end
defp refcount({:string, value, _ea}) do
if integerString?(value) and String.to_integer(value) in 0..9999 do
@sharedRefcount
else
1
end
end
defp refcount(_entry), do: 1
defp integerString?(value) do
case Integer.parse(value) do
{number, ""} -> Integer.to_string(number) == value and byte_size(value) <= 20
_ -> false
end
end
defp parseIntercard(numkeysRaw, rest) do
case Integer.parse(numkeysRaw) do
{numkeys, ""} when numkeys <= 0 ->
{:reply, {:error, "ERR numkeys should be greater than 0"}}
{numkeys, ""} when numkeys > length(rest) ->
{:reply, {:error, "ERR Number of keys can't be greater than number of args"}}
{numkeys, ""} ->
{keys, tail} = Enum.split(rest, numkeys)
case parseIntercardLimit(tail) do
{:ok, limit} -> {:ok, keys, limit}
{:reply, term} -> {:reply, term}
end
_ ->
{:reply, Reply.not_integer()}
end
end
defp parseIntercardLimit([]), do: {:ok, 0}
defp parseIntercardLimit([token, raw]) do
if String.upcase(token) == "LIMIT" do
case Integer.parse(raw) do
{limit, ""} when limit >= 0 -> {:ok, limit}
{_negative, ""} -> {:reply, {:error, "ERR LIMIT can't be negative"}}
_ -> {:reply, Reply.not_integer()}
end
else
{:reply, Reply.syntax()}
end
end
defp parseIntercardLimit(_tail), do: {:reply, Reply.syntax()}
defp parseCopyOptions([], replace), do: {:ok, replace}
defp parseCopyOptions([token | rest], replace) do
case {String.upcase(token), rest} do
{"REPLACE", tail} ->
parseCopyOptions(tail, true)
{"DB", [indexRaw | tail]} ->
case Integer.parse(indexRaw) do
{0, ""} -> parseCopyOptions(tail, replace)
{_other, ""} -> {:reply, {:error, "ERR DB index is out of range"}}
_ -> {:reply, Reply.not_integer()}
end
_ ->
{:reply, Reply.syntax()}
end
end
defp parseExpireFlags(tokens) do
tokens
|> Enum.reduce_while({:ok, %{nx: false, xx: false, gt: false, lt: false}}, fn token,
{:ok, flags} ->
case String.upcase(token) do
"NX" -> {:cont, {:ok, %{flags | nx: true}}}
"XX" -> {:cont, {:ok, %{flags | xx: true}}}
"GT" -> {:cont, {:ok, %{flags | gt: true}}}
"LT" -> {:cont, {:ok, %{flags | lt: true}}}
other -> {:halt, {:reply, {:error, "ERR Unsupported option #{other}"}}}
end
end)
|> validateExpireFlags()
end
defp validateExpireFlags({:reply, term}), do: {:reply, term}
defp validateExpireFlags({:ok, flags}) do
incompatible = (flags.nx and (flags.xx or flags.gt or flags.lt)) or (flags.gt and flags.lt)
if incompatible do
{:reply, {:error, "ERR NX and XX, GT or LT options at the same time are not compatible"}}
else
{:ok, flags}
end
end
defp expireAt(key, msAbs, flags) do
case Store.expiretime(key, :milliseconds) do
-2 -> 0
current -> if expireAllowed?(flags, current, msAbs), do: Store.expireat(key, msAbs), else: 0
end
end
defp expireAllowed?(flags, current, msAbs) do
cond do
flags.nx -> current == -1
flags.xx and current == -1 -> false
flags.gt -> current != -1 and msAbs > current
flags.lt -> current == -1 or msAbs < current
true -> true
end
end
defp zrevrange(key, startRaw, stopRaw, withscores, state) do
Reply.with_two_ints(startRaw, stopRaw, state, fn start, stop ->
Reply.zrange_reply(Store.zrevrange(key, start, stop), withscores)
end)
end
defp scoreEntry(nil), do: Reply.map_score(nil)
defp scoreEntry(score), do: Reply.map_score({:ok, score})
defp withType(key, expected, state, fun) do
case Store.type(key) do
"none" -> reply(fun.(), state)
^expected -> reply(fun.(), state)
_ -> reply(Reply.wrongtype(), state)
end
end
defp singleReply({:ok, [value | _rest]}), do: {:bulk, value}
defp singleReply({:ok, []}), do: {:bulk, nil}
defp singleReply({:error, reason}), do: errorTerm(reason)
defp arrayReply({:ok, values}), do: {:array, Reply.bulks(values)}
defp arrayReply({:error, reason}), do: errorTerm(reason)
defp integerReply({:ok, value}), do: {:integer, value}
defp integerReply({:error, reason}), do: errorTerm(reason)
defp bulkReply({:ok, value}), do: {:bulk, value}
defp bulkReply(nil), do: {:bulk, nil}
defp bulkReply({:error, reason}), do: errorTerm(reason)
defp rankReply({:ok, rank}), do: {:integer, rank}
defp rankReply(nil), do: {:bulk, nil}
defp rankReply({:error, reason}), do: errorTerm(reason)
defp statusReply({:ok, "OK"}), do: {:simple, "OK"}
defp statusReply({:error, reason}), do: errorTerm(reason)
defp errorTerm(:wrongtype), do: Reply.wrongtype()
defp errorTerm(:not_integer), do: Reply.not_integer()
defp errorTerm(:not_float), do: Reply.not_float()
defp errorTerm(:nokey), do: {:error, "ERR no such key"}
defp errorTerm(:range), do: {:error, "ERR index out of range"}
defp reply(term, state), do: {[term], state}
end