From 29b377442675fe341376c08a64c84ba8b41080e0 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 6 Sep 2026 16:46:46 +0200 Subject: [PATCH] feat(command): add encodingName and compact? to keyspace --- lib/excache/command/keyspace.ex | 190 ++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/lib/excache/command/keyspace.ex b/lib/excache/command/keyspace.ex index 1d904a6..1430748 100644 --- a/lib/excache/command/keyspace.ex +++ b/lib/excache/command/keyspace.ex @@ -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