diff --git a/lib/excache/store/ops.ex b/lib/excache/store/ops.ex index 6bab99d..6c08888 100644 --- a/lib/excache/store/ops.ex +++ b/lib/excache/store/ops.ex @@ -208,3 +208,255 @@ defmodule Excache.Store.Ops do with_kind(table, key, now, :list, [], fn list -> at(list, index) 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