feat(store): add hash, set, and sorted set operations

This commit is contained in:
dualfroz committed 2026-08-23 22:40:00 +02:00
1 parent deb559023d
commit 4da7f20581
1 file changed
+252
+252
View File
@@ -208,3 +208,255 @@ defmodule Excache.Store.Ops do
with_kind(table, key, now, :list, [], fn list -> at(list, index) end) with_kind(table, key, now, :list, [], fn list -> at(list, index) end)
end end
def apply_op(table, now, {:hset, key, pairs}) do
collection(table, key, now, :hash, %{}, fn current ->
added = Enum.count(pairs, fn {field, _} -> not Map.has_key?(current, field) end)
updated =
Enum.reduce(pairs, current, fn {field, value}, acc -> Map.put(acc, field, value) end)
{updated, {:ok, added}}
end)
end
def apply_op(table, now, {:hget, key, field}) do
with_kind(table, key, now, :hash, %{}, fn map ->
case Map.fetch(map, field) do
{:ok, value} -> {:ok, value}
:error -> nil
end
end)
end
def apply_op(table, now, {:hdel, key, fields}) do
collection(table, key, now, :hash, %{}, fn current ->
removed = Enum.count(fields, &Map.has_key?(current, &1))
{Map.drop(current, fields), {:ok, removed}}
end)
end
def apply_op(table, now, {:hgetall, key}) do
with_kind(table, key, now, :hash, %{}, fn map ->
{:ok, Enum.flat_map(map, fn {field, value} -> [field, value] end)}
end)
end
def apply_op(table, now, {:hkeys, key}) do
with_kind(table, key, now, :hash, %{}, fn map -> {:ok, Map.keys(map)} end)
end
def apply_op(table, now, {:hvals, key}) do
with_kind(table, key, now, :hash, %{}, fn map -> {:ok, Map.values(map)} end)
end
def apply_op(table, now, {:hlen, key}) do
with_kind(table, key, now, :hash, %{}, fn map -> {:ok, map_size(map)} end)
end
def apply_op(table, now, {:hexists, key, field}) do
with_kind(table, key, now, :hash, %{}, fn map ->
{:ok, if(Map.has_key?(map, field), do: 1, else: 0)}
end)
end
def apply_op(table, now, {:hincrby, key, field, delta}) do
collection(table, key, now, :hash, %{}, fn current ->
case Map.fetch(current, field) do
:error ->
{Map.put(current, field, Integer.to_string(delta)), {:ok, delta}}
{:ok, value} ->
case Integer.parse(value) do
{existing, ""} ->
updated = existing + delta
{Map.put(current, field, Integer.to_string(updated)), {:ok, updated}}
_ ->
{current, {:error, :not_integer}}
end
end
end)
end
def apply_op(table, now, {:sadd, key, members}) do
collection(table, key, now, :set, MapSet.new(), fn current ->
added = Enum.count(members, &(not MapSet.member?(current, &1)))
updated = Enum.reduce(members, current, &MapSet.put(&2, &1))
{updated, {:ok, added}}
end)
end
def apply_op(table, now, {:srem, key, members}) do
collection(table, key, now, :set, MapSet.new(), fn current ->
removed = Enum.count(members, &MapSet.member?(current, &1))
updated = Enum.reduce(members, current, &MapSet.delete(&2, &1))
{updated, {:ok, removed}}
end)
end
def apply_op(table, now, {:smembers, key}) do
with_kind(table, key, now, :set, MapSet.new(), fn set -> {:ok, MapSet.to_list(set)} end)
end
def apply_op(table, now, {:sismember, key, member}) do
with_kind(table, key, now, :set, MapSet.new(), fn set ->
{:ok, if(MapSet.member?(set, member), do: 1, else: 0)}
end)
end
def apply_op(table, now, {:scard, key}) do
with_kind(table, key, now, :set, MapSet.new(), fn set -> {:ok, MapSet.size(set)} end)
end
def apply_op(table, now, {:zadd, key, entries, opts}) do
collection(table, key, now, :zset, SkipList.new(), fn current ->
{updated, added, changed} =
Enum.reduce(entries, {current, 0, 0}, fn {member, score}, {acc, added, changed} ->
zadd_entry(acc, member, score, opts, added, changed)
end)
count = if Keyword.get(opts, :ch, false), do: changed, else: added
{updated, {:ok, count}}
end)
end
def apply_op(table, now, {:zscore, key, member}) do
with_kind(table, key, now, :zset, SkipList.new(), fn zset ->
case SkipList.score(zset, member) do
{:ok, score} -> {:ok, score}
:error -> nil
end
end)
end
def apply_op(table, now, {:zrank, key, member}) do
with_kind(table, key, now, :zset, SkipList.new(), fn zset ->
case SkipList.rank(zset, member) do
{:ok, rank} -> {:ok, rank}
:error -> nil
end
end)
end
def apply_op(table, now, {:zincrby, key, delta, member}) do
collection(table, key, now, :zset, SkipList.new(), fn current ->
previous =
case SkipList.score(current, member) do
{:ok, score} -> score
:error -> 0
end
updated_score = previous + delta
{SkipList.insert(current, member, updated_score), {:ok, updated_score}}
end)
end
def apply_op(table, now, {:zrem, key, members}) do
collection(table, key, now, :zset, SkipList.new(), fn current ->
removed = Enum.count(members, &SkipList.member?(current, &1))
updated = Enum.reduce(members, current, &SkipList.delete(&2, &1))
{updated, {:ok, removed}}
end)
end
def apply_op(table, now, {:zpopmin, key, count}) do
collection(table, key, now, :zset, SkipList.new(), fn current ->
popped = SkipList.range_by_rank(current, 0, count)
{drop_zset_members(current, popped), {:ok, popped}}
end)
end
def apply_op(table, now, {:zpopmax, key, count}) do
collection(table, key, now, :zset, SkipList.new(), fn current ->
lower = max(SkipList.size(current) - count, 0)
popped = current |> SkipList.range_by_rank(lower, count) |> Enum.reverse()
{drop_zset_members(current, popped), {:ok, popped}}
end)
end
def apply_op(table, now, {:zcard, key}) do
with_kind(table, key, now, :zset, SkipList.new(), fn zset -> {:ok, SkipList.size(zset)} end)
end
def apply_op(table, now, {:zrange, key, start, stop}) do
with_kind(table, key, now, :zset, SkipList.new(), fn zset ->
{lower, count} = rank_window(SkipList.size(zset), start, stop)
{:ok, SkipList.range_by_rank(zset, lower, count)}
end)
end
def apply_op(table, now, {:zrangebyscore, key, min_bound, max_bound}) do
with_kind(table, key, now, :zset, SkipList.new(), fn zset ->
{:ok, SkipList.range_by_score(zset, min_bound, max_bound)}
end)
end
def apply_op(table, now, {:keys, pattern}) do
regex = compile_pattern(pattern)
:ets.foldl(
fn {key, _kind, _value, ea}, acc ->
if not expired?(ea, now) and Regex.match?(regex, key), do: [key | acc], else: acc
end,
[],
table
)
end
def apply_op(table, now, :dbsize) do
:ets.foldl(
fn {_key, _kind, _value, ea}, acc -> if expired?(ea, now), do: acc, else: acc + 1 end,
0,
table
)
end
def apply_op(table, _now, :flushdb) do
:ets.delete_all_objects(table)
:ok
end
def apply_op(table, now, :sweep) do
spec = [
{{:"$1", :"$2", :"$3", :"$4"}, [{:andalso, {:"/=", :"$4", nil}, {:"=<", :"$4", now}}],
[:"$1"]}
]
expired = :ets.select(table, spec)
Enum.each(expired, &:ets.delete(table, &1))
expired
end
defp zadd_entry(zset, member, score, opts, added, changed) do
exists = SkipList.member?(zset, member)
nx = Keyword.get(opts, :nx, false)
xx = Keyword.get(opts, :xx, false)
gt = Keyword.get(opts, :gt, false)
lt = Keyword.get(opts, :lt, false)
cond do
nx and exists ->
{zset, added, changed}
xx and not exists ->
{zset, added, changed}
not exists ->
{SkipList.insert(zset, member, score), added + 1, changed + 1}
true ->
{:ok, current_score} = SkipList.score(zset, member)
cond do
gt and score <= current_score -> {zset, added, changed}
lt and score >= current_score -> {zset, added, changed}
score == current_score -> {zset, added, changed}
true -> {SkipList.insert(zset, member, score), added, changed + 1}
end
end
end
defp drop_zset_members(zset, popped) do
Enum.reduce(popped, zset, fn {_score, member}, acc -> SkipList.delete(acc, member) end)
end
end