Packages
FerricFlow durable workflows and queues with native-protocol storage, Raft durability, and Bitcask persistence.
Current section
Files
Jump to
Current section
Files
lib/ferricstore/commands/strings.ex
# Suppress function clause grouping warnings (clauses added by different agents)
defmodule Ferricstore.Commands.Strings do
alias Ferricstore.Store.Ops
alias Ferricstore.CrossShardOp
@moduledoc """
Handles Redis string commands.
Each handler takes the uppercased command name, a list of string arguments,
and an injected store map. Returns plain Elixir terms — the connection layer
handles RESP encoding.
## Supported commands
* `GET key` — returns the value or `nil`
* `SET key value [EX secs | PX ms | EXAT unix-sec | PXAT unix-ms] [NX | XX] [GET] [KEEPTTL]` — sets a key with optional expiry/conditions
* `DEL key [key ...]` — deletes keys, returns count deleted
* `EXISTS key [key ...]` — returns count of existing keys
* `MGET key [key ...]` — returns list of values (nil for missing)
* `MSET key value [key value ...]` — sets multiple keys atomically
* `INCR key` — increment integer value by 1
* `DECR key` — decrement integer value by 1
* `INCRBY key increment` — increment integer value by given amount
* `DECRBY key decrement` — decrement integer value by given amount
* `INCRBYFLOAT key increment` — increment float value by given amount
* `APPEND key value` — append to value, return new length
* `STRLEN key` — return byte length of value
* `GETSET key value` — set key, return old value
* `GETDEL key` — get value and delete atomically
* `GETEX key [EX s | PX ms | EXAT ts | PXAT ms-ts | PERSIST]` — get and update TTL
* `SETNX key value` — set if not exists
* `SETEX key seconds value` — set with expiry in seconds
* `PSETEX key milliseconds value` — set with expiry in milliseconds
* `GETRANGE key start end` — return substring by byte range
* `SETRANGE key offset value` — overwrite part of string at offset
* `MSETNX key value [key value ...]` — set multiple only if none exist
"""
@doc """
Handles a string command.
## Parameters
- `cmd` - Uppercased command name (e.g. `"GET"`, `"SET"`)
- `args` - List of string arguments
- `store` - Injected store map with `get`, `put`, `delete`, `exists?` callbacks
and atomic operations like `incr`, `append`, etc.
## Returns
Plain Elixir term: `:ok`, `nil`, integer, string, list, or `{:error, message}`.
"""
@max_key_bytes 65_535
@spec handle(binary(), [binary()], map()) :: term()
def handle(cmd, args, store)
@wrongtype_error {:error, "WRONGTYPE Operation against a key holding the wrong kind of value"}
# ---------------------------------------------------------------------------
# TYPE -- delegated here from tests; canonical handler is Generic
# ---------------------------------------------------------------------------
def handle("TYPE", [key], store) do
{:simple, Ferricstore.Store.TypeRegistry.get_type(key, store)}
end
def handle("TYPE", _args, _store) do
{:error, "ERR wrong number of arguments for 'type' command"}
end
def handle("GET", [""], _store), do: {:error, "ERR empty key"}
def handle("GET", [key], _store) when byte_size(key) > 65_535, do: {:error, "ERR key too large"}
def handle("GET", [key], store) do
case Ops.get(store, key) do
nil ->
# Plain key is nil. Check if this is a data structure key (compound keys).
if Ops.has_compound?(store) do
type_key = Ferricstore.Store.CompoundKey.type_key(key)
case Ops.compound_get(store, key, type_key) do
nil -> nil
_type_str -> @wrongtype_error
end
else
nil
end
value when is_binary(value) -> maybe_check_type(value)
other -> other
end
end
def handle("GET", _args, _store) do
{:error, "ERR wrong number of arguments for 'get' command"}
end
def handle("SET", ["", _value | _opts], _store), do: {:error, "ERR empty key"}
def handle("SET", [key, _value | _opts], _store) when byte_size(key) > 65_535, do: {:error, "ERR key too large"}
def handle("SET", [key, value | opts], store), do: do_set(key, value, opts, store)
def handle("SET", _args, _store) do
{:error, "ERR wrong number of arguments for 'set' command"}
end
def handle("DEL", [], _store) do
{:error, "ERR wrong number of arguments for 'del' command"}
end
def handle("DEL", keys, store) do
Enum.reduce(keys, 0, fn key, acc ->
if do_del_key(key, store), do: acc + 1, else: acc
end)
end
def handle("EXISTS", [], _store) do
{:error, "ERR wrong number of arguments for 'exists' command"}
end
def handle("EXISTS", keys, store) do
Enum.reduce(keys, 0, fn key, acc ->
exists = Ops.exists?(store, key)
# Also check TypeRegistry for compound-key-based data structures
# (lists, hashes, sets, zsets) that don't use the plain key store.
exists = exists or (Ops.has_compound?(store) and
Ops.compound_get(store, key, Ferricstore.Store.CompoundKey.type_key(key)) != nil)
if exists, do: acc + 1, else: acc
end)
end
def handle("MGET", [], _store) do
{:error, "ERR wrong number of arguments for 'mget' command"}
end
def handle("MGET", keys, store), do: Enum.map(keys, &Ops.get(store, &1))
def handle("MSET", [], _store) do
{:error, "ERR wrong number of arguments for 'mset' command"}
end
def handle("MSET", args, store) do
if even_length?(args) do
# Direct recursive processing avoids chunked enumeration intermediate lists.
case mset_validate(args) do
:ok ->
mset_exec(args, store)
:ok
{:error, _} = err ->
err
end
else
{:error, "ERR wrong number of arguments for 'mset' command"}
end
end
# ---------------------------------------------------------------------------
# INCR / DECR / INCRBY / DECRBY
# ---------------------------------------------------------------------------
def handle("INCR", [key], store), do: Ops.incr(store, key, 1)
def handle("INCR", _args, _store), do: {:error, "ERR wrong number of arguments for 'incr' command"}
def handle("DECR", [key], store), do: Ops.incr(store, key, -1)
def handle("DECR", _args, _store), do: {:error, "ERR wrong number of arguments for 'decr' command"}
# Redis range: [-2^63, 2^63-1] for integer operations.
@max_int64 9_223_372_036_854_775_807
@min_int64 -9_223_372_036_854_775_808
def handle("INCRBY", [key, delta_str], store) do
case Integer.parse(delta_str) do
{delta, ""} when delta >= @min_int64 and delta <= @max_int64 ->
Ops.incr(store, key, delta)
{_delta, ""} ->
{:error, "ERR value is not an integer or out of range"}
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
def handle("INCRBY", _args, _store),
do: {:error, "ERR wrong number of arguments for 'incrby' command"}
def handle("DECRBY", [key, delta_str], store) do
case Integer.parse(delta_str) do
{delta, ""} when delta >= @min_int64 and delta <= @max_int64 ->
Ops.incr(store, key, -delta)
{_delta, ""} ->
{:error, "ERR value is not an integer or out of range"}
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
def handle("DECRBY", _args, _store),
do: {:error, "ERR wrong number of arguments for 'decrby' command"}
# ---------------------------------------------------------------------------
# INCRBYFLOAT
# ---------------------------------------------------------------------------
def handle("INCRBYFLOAT", [key, delta_str], store) do
case parse_float_arg(delta_str) do
{:ok, delta} ->
case Ops.incr_float(store, key, delta) do
{:ok, new_val} when is_float(new_val) ->
Ferricstore.Store.ValueCodec.format_float(new_val)
{:ok, new_str} when is_binary(new_str) -> new_str
{:error, _} = err -> err
end
:error ->
{:error, "ERR value is not a valid float"}
end
end
def handle("INCRBYFLOAT", _args, _store),
do: {:error, "ERR wrong number of arguments for 'incrbyfloat' command"}
# ---------------------------------------------------------------------------
# APPEND
# ---------------------------------------------------------------------------
def handle("APPEND", [key, value], store) do
{:ok, new_len} = Ops.append(store, key, value)
new_len
end
def handle("APPEND", _args, _store),
do: {:error, "ERR wrong number of arguments for 'append' command"}
# ---------------------------------------------------------------------------
# STRLEN
# ---------------------------------------------------------------------------
def handle("STRLEN", [key], store) do
case Ops.get(store, key) do
nil -> 0
v when is_integer(v) -> byte_size(Integer.to_string(v))
v when is_float(v) -> byte_size(Float.to_string(v))
v -> byte_size(v)
end
end
def handle("STRLEN", _args, _store),
do: {:error, "ERR wrong number of arguments for 'strlen' command"}
# ---------------------------------------------------------------------------
# GETSET (deprecated but supported)
# ---------------------------------------------------------------------------
def handle("GETSET", [key, value], store), do: Ops.getset(store, key, value)
def handle("GETSET", _args, _store),
do: {:error, "ERR wrong number of arguments for 'getset' command"}
# ---------------------------------------------------------------------------
# GETDEL
# ---------------------------------------------------------------------------
def handle("GETDEL", [key], store), do: Ops.getdel(store, key)
def handle("GETDEL", _args, _store),
do: {:error, "ERR wrong number of arguments for 'getdel' command"}
# ---------------------------------------------------------------------------
# GETEX
# ---------------------------------------------------------------------------
def handle("GETEX", [key], store), do: Ops.get(store, key)
def handle("GETEX", [key | opts], store), do: do_getex(key, opts, store)
def handle("GETEX", _args, _store),
do: {:error, "ERR wrong number of arguments for 'getex' command"}
# ---------------------------------------------------------------------------
# SETNX
# ---------------------------------------------------------------------------
def handle("SETNX", [key, value], store) do
if Ops.exists?(store, key) do
0
else
Ops.put(store, key, value, 0)
1
end
end
def handle("SETNX", _args, _store),
do: {:error, "ERR wrong number of arguments for 'setnx' command"}
# ---------------------------------------------------------------------------
# SETEX
# ---------------------------------------------------------------------------
def handle("SETEX", [key, secs_str, value], store) do
case Integer.parse(secs_str) do
{secs, ""} when secs > 0 ->
expire_at_ms = Ferricstore.HLC.now_ms() + secs * 1_000
Ops.put(store, key, value, expire_at_ms)
{_secs, ""} ->
{:error, "ERR invalid expire time in 'setex' command"}
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
def handle("SETEX", _args, _store),
do: {:error, "ERR wrong number of arguments for 'setex' command"}
# ---------------------------------------------------------------------------
# PSETEX
# ---------------------------------------------------------------------------
def handle("PSETEX", [key, ms_str, value], store) do
case Integer.parse(ms_str) do
{ms, ""} when ms > 0 ->
expire_at_ms = Ferricstore.HLC.now_ms() + ms
Ops.put(store, key, value, expire_at_ms)
{_ms, ""} ->
{:error, "ERR invalid expire time in 'psetex' command"}
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
def handle("PSETEX", _args, _store),
do: {:error, "ERR wrong number of arguments for 'psetex' command"}
# ---------------------------------------------------------------------------
# GETRANGE
# ---------------------------------------------------------------------------
def handle("GETRANGE", [key, start_str, end_str], store) do
with {start_idx, ""} <- Integer.parse(start_str),
{end_idx, ""} <- Integer.parse(end_str) do
case Ops.get(store, key) do
nil -> ""
v when is_integer(v) -> do_getrange(Integer.to_string(v), start_idx, end_idx)
v when is_float(v) -> do_getrange(Float.to_string(v), start_idx, end_idx)
value -> do_getrange(value, start_idx, end_idx)
end
else
_ -> {:error, "ERR value is not an integer or out of range"}
end
end
def handle("GETRANGE", _args, _store),
do: {:error, "ERR wrong number of arguments for 'getrange' command"}
# ---------------------------------------------------------------------------
# SETRANGE
# ---------------------------------------------------------------------------
# Redis caps SETRANGE offset at 512MB (536_870_911 = 2^29 - 1).
@max_setrange_offset 536_870_911
def handle("SETRANGE", [key, offset_str, value], store) do
case Integer.parse(offset_str) do
{offset, ""} when offset >= 0 and offset <= @max_setrange_offset ->
{:ok, new_len} = Ops.setrange(store, key, offset, value)
new_len
{offset, ""} when offset > @max_setrange_offset ->
{:error, "ERR string exceeds maximum allowed size (512MB)"}
{_offset, ""} ->
{:error, "ERR offset is out of range"}
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
def handle("SETRANGE", _args, _store),
do: {:error, "ERR wrong number of arguments for 'setrange' command"}
# ---------------------------------------------------------------------------
# MSETNX
# ---------------------------------------------------------------------------
def handle("MSETNX", [], _store),
do: {:error, "ERR wrong number of arguments for 'msetnx' command"}
def handle("MSETNX", args, store) do
if even_length?(args) do
keys = extract_keys(args)
CrossShardOp.execute(
Enum.map(keys, &{&1, :write}),
fn unified_store ->
if msetnx_any_exists?(args, unified_store) do
0
else
mset_exec(args, unified_store)
1
end
end,
intent: %{command: :msetnx, keys: %{targets: keys}},
store: store
)
else
{:error, "ERR wrong number of arguments for 'msetnx' command"}
end
end
# ---------------------------------------------------------------------------
# Private — GETEX option parsing and execution
# ---------------------------------------------------------------------------
defp do_getex(key, opts, store) do
case parse_getex_opts(opts) do
{:ok, expire_at_ms} ->
Ops.getex(store, key, expire_at_ms)
{:error, _} = err ->
err
end
end
defp parse_getex_opts(["PERSIST"]), do: {:ok, 0}
defp parse_getex_opts(["EX", secs_str]) do
case Integer.parse(secs_str) do
{secs, ""} when secs > 0 ->
{:ok, Ferricstore.HLC.now_ms() + secs * 1_000}
{_secs, ""} ->
{:error, "ERR invalid expire time in 'getex' command"}
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
defp parse_getex_opts(["PX", ms_str]) do
case Integer.parse(ms_str) do
{ms, ""} when ms > 0 ->
{:ok, Ferricstore.HLC.now_ms() + ms}
{_ms, ""} ->
{:error, "ERR invalid expire time in 'getex' command"}
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
defp parse_getex_opts(["EXAT", ts_str]) do
case Integer.parse(ts_str) do
{ts, ""} when ts > 0 ->
{:ok, ts * 1_000}
{_ts, ""} ->
{:error, "ERR invalid expire time in 'getex' command"}
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
defp parse_getex_opts(["PXAT", ts_str]) do
case Integer.parse(ts_str) do
{ts, ""} when ts > 0 ->
{:ok, ts}
{_ts, ""} ->
{:error, "ERR invalid expire time in 'getex' command"}
_ ->
{:error, "ERR value is not an integer or out of range"}
end
end
defp parse_getex_opts(_) do
{:error, "ERR syntax error"}
end
# ---------------------------------------------------------------------------
# Private — GETRANGE substring extraction
# ---------------------------------------------------------------------------
defp do_getrange(value, start_idx, end_idx) do
len = byte_size(value)
# Normalise negative indices
start_norm = if start_idx < 0, do: max(len + start_idx, 0), else: start_idx
end_norm = if end_idx < 0, do: len + end_idx, else: end_idx
# Clamp to bounds
start_clamped = min(start_norm, len)
end_clamped = min(end_norm, len - 1)
if start_clamped > end_clamped do
""
else
count = end_clamped - start_clamped + 1
binary_part(value, start_clamped, count)
end
end
# ---------------------------------------------------------------------------
# Private — float argument parsing
# ---------------------------------------------------------------------------
defp parse_float_arg(str) do
# Try integer first (Redis considers "10" valid for INCRBYFLOAT)
case Integer.parse(str) do
{val, ""} ->
{:ok, val * 1.0}
_ ->
case Float.parse(str) do
{val, ""} ->
# Reject inf/nan
cond do
val == :infinity -> :error
val == :neg_infinity -> :error
true -> {:ok, val}
end
_ ->
:error
end
end
end
# ---------------------------------------------------------------------------
# Private — SET option parsing and execution
# ---------------------------------------------------------------------------
defp do_set(key, value, opts, store) do
with {:ok, parsed} <- parse_set_opts(opts) do
%{expire_at_ms: expire_at_ms, nx: nx?, xx: xx?, get: get?, keepttl: keepttl?} = parsed
# Read old value/meta when GET or KEEPTTL is requested.
# We need old_meta for KEEPTTL (to preserve the existing expire_at_ms)
# and the old value for GET (to return it).
{old_value, effective_expire} =
if get? or keepttl? do
case Ops.get_meta(store, key) do
nil ->
{nil, expire_at_ms}
{old_val, old_exp} ->
# KEEPTTL: use the old expiry when no explicit expiry was set
eff_exp = if keepttl?, do: old_exp, else: expire_at_ms
{old_val, eff_exp}
end
else
{nil, expire_at_ms}
end
# Condition check: NX (only if not exists) / XX (only if exists)
skip? =
cond do
nx? and Ops.exists?(store, key) -> true
xx? and not Ops.exists?(store, key) -> true
true -> false
end
if skip? do
# When GET is set, return old value even if NX/XX prevented the write
if get?, do: old_value, else: nil
else
Ops.put(store, key, value, effective_expire)
if get?, do: old_value, else: :ok
end
end
end
# Accumulator map for SET option parsing. All fields start at their defaults.
@set_opts_default %{expire_at_ms: 0, nx: false, xx: false, get: false, keepttl: false, has_expiry: false}
defp parse_set_opts(opts), do: parse_set_opts(opts, @set_opts_default)
defp parse_set_opts([], acc) do
if acc.nx and acc.xx do
{:error, "ERR XX and NX options at the same time are not compatible"}
else
{:ok, acc}
end
end
defp parse_set_opts(["NX" | rest], acc) do
parse_set_opts(rest, %{acc | nx: true})
end
defp parse_set_opts(["XX" | rest], acc) do
parse_set_opts(rest, %{acc | xx: true})
end
defp parse_set_opts(["GET" | rest], acc) do
parse_set_opts(rest, %{acc | get: true})
end
defp parse_set_opts(["KEEPTTL" | rest], acc) do
if acc.has_expiry do
{:error, "ERR syntax error"}
else
parse_set_opts(rest, %{acc | keepttl: true, has_expiry: true})
end
end
defp parse_set_opts(["EX", secs_str | rest], acc) do
if acc.has_expiry do
{:error, "ERR syntax error"}
else
with {secs, ""} <- Integer.parse(secs_str),
true <- secs > 0 do
parse_set_opts(rest, %{acc | expire_at_ms: Ferricstore.HLC.now_ms() + secs * 1000, has_expiry: true})
else
false -> {:error, "ERR invalid expire time in 'set' command"}
_ -> {:error, "ERR value is not an integer or out of range"}
end
end
end
defp parse_set_opts(["PX", ms_str | rest], acc) do
if acc.has_expiry do
{:error, "ERR syntax error"}
else
with {ms, ""} <- Integer.parse(ms_str),
true <- ms > 0 do
parse_set_opts(rest, %{acc | expire_at_ms: Ferricstore.HLC.now_ms() + ms, has_expiry: true})
else
false -> {:error, "ERR invalid expire time in 'set' command"}
_ -> {:error, "ERR value is not an integer or out of range"}
end
end
end
defp parse_set_opts(["EXAT", ts_str | rest], acc) do
if acc.has_expiry do
{:error, "ERR syntax error"}
else
with {ts, ""} <- Integer.parse(ts_str),
true <- ts > 0 do
parse_set_opts(rest, %{acc | expire_at_ms: ts * 1000, has_expiry: true})
else
false -> {:error, "ERR invalid expire time in 'set' command"}
_ -> {:error, "ERR value is not an integer or out of range"}
end
end
end
defp parse_set_opts(["PXAT", ts_str | rest], acc) do
if acc.has_expiry do
{:error, "ERR syntax error"}
else
with {ts, ""} <- Integer.parse(ts_str),
true <- ts > 0 do
parse_set_opts(rest, %{acc | expire_at_ms: ts, has_expiry: true})
else
false -> {:error, "ERR invalid expire time in 'set' command"}
_ -> {:error, "ERR value is not an integer or out of range"}
end
end
end
defp parse_set_opts([unknown | _rest], _acc) do
{:error, "ERR syntax error, option '#{unknown}' not recognized"}
end
# ---------------------------------------------------------------------------
# Private — MSET/MSETNX helpers (direct recursion, no chunked enumeration)
# ---------------------------------------------------------------------------
# Validates all keys in a flat [k, v, k, v, ...] list without creating
# intermediate chunk lists.
defp mset_validate([]), do: :ok
defp mset_validate([k, _v | rest]) do
if k == "" or byte_size(k) > @max_key_bytes do
{:error, "ERR key too large or empty"}
else
mset_validate(rest)
end
end
# Executes MSET by walking the flat [k, v, k, v, ...] list directly.
defp mset_exec([], _store), do: :ok
defp mset_exec([k, v | rest], store) do
Ops.put(store, k, v, 0)
mset_exec(rest, store)
end
# Checks if any key in a flat [k, v, k, v, ...] list already exists.
defp msetnx_any_exists?([], _store), do: false
defp msetnx_any_exists?([k, _v | rest], store) do
if Ops.exists?(store, k), do: true, else: msetnx_any_exists?(rest, store)
end
# Extracts keys from a flat [k, v, k, v, ...] list.
defp extract_keys([]), do: []
defp extract_keys([k, _v | rest]), do: [k | extract_keys(rest)]
# O(n/2) parity check without computing full length.
defp even_length?([]), do: true
defp even_length?([_, _ | rest]), do: even_length?(rest)
defp even_length?(_), do: false
# ---------------------------------------------------------------------------
# Private — type checking for GET
# ---------------------------------------------------------------------------
# Detects if a stored binary is actually a serialized non-string type
# (list, hash, set, zset). If so, returns WRONGTYPE error instead of the
# raw binary. This matches Redis behaviour where GET on a non-string key
# returns a WRONGTYPE error.
#
# Peeks at the ETF header bytes to identify tuple tags without deserializing
# the entire payload. This avoids multi-MB heap spikes for large data
# structures (e.g., a hash with 10K fields stored as ETF).
#
# ETF format for a 2-tuple like {:list, payload}:
# 131 = ETF version tag
# 104 = SMALL_TUPLE_EXT (arity < 256)
# 2 = arity (2-tuple)
# 100 = ATOM_EXT (followed by 2-byte length + atom bytes)
# or 119 = SMALL_ATOM_UTF8_EXT (1-byte length + atom bytes)
# or 118 = ATOM_UTF8_EXT (2-byte length + atom bytes)
# or 115 = SMALL_ATOM_EXT (1-byte length + atom bytes)
defp maybe_check_type(<<131, 104, 2, rest::binary>> = value) do
case extract_etf_atom_name(rest) do
name when name in ["list", "hash", "set", "zset"] -> @wrongtype_error
_ -> value
end
end
# LARGE_TUPLE_EXT (arity 2) - same check for large tuples
defp maybe_check_type(<<131, 105, 0, 0, 0, 2, rest::binary>> = value) do
case extract_etf_atom_name(rest) do
name when name in ["list", "hash", "set", "zset"] -> @wrongtype_error
_ -> value
end
end
defp maybe_check_type(value), do: value
# Extracts the atom name from the beginning of an ETF-encoded atom.
# Returns the atom name as a string, or nil if unrecognized format.
# ATOM_EXT (tag 100): 2-byte big-endian length + atom bytes (Latin1)
defp extract_etf_atom_name(<<100, len::16, name::binary-size(len), _::binary>>), do: name
# SMALL_ATOM_UTF8_EXT (tag 119): 1-byte length + atom bytes (UTF8)
defp extract_etf_atom_name(<<119, len::8, name::binary-size(len), _::binary>>), do: name
# ATOM_UTF8_EXT (tag 118): 2-byte length + atom bytes (UTF8)
defp extract_etf_atom_name(<<118, len::16, name::binary-size(len), _::binary>>), do: name
# SMALL_ATOM_EXT (tag 115): 1-byte length + atom bytes (Latin1)
defp extract_etf_atom_name(<<115, len::8, name::binary-size(len), _::binary>>), do: name
defp extract_etf_atom_name(_), do: nil
# ---------------------------------------------------------------------------
# Private — DEL key deletion (plain + compound)
# ---------------------------------------------------------------------------
# Deletes a single key, handling both plain string keys and data structure
# keys that use compound sub-keys. Returns `true` if the key existed and
# was deleted, `false` otherwise.
defp do_del_key(key, store) do
alias Ferricstore.Store.{CompoundKey, TypeRegistry}
# Check for data structure type metadata when compound operations are
# available (the store has compound_get). When they are not available
# (e.g. raw Router-based store without data structure support), fall
# through to plain key deletion.
has_compound? = Ops.has_compound?(store)
if has_compound? do
type_key = CompoundKey.type_key(key)
case Ops.compound_get(store, key, type_key) do
nil ->
# No type metadata -- plain string key
if Ops.exists?(store, key) do
Ops.delete(store, key)
true
else
false
end
type_str ->
# Data structure key -- delete compound sub-keys, then type metadata.
# Lists store data as serialized Erlang terms in the plain key store,
# so we must also delete the plain key for list types.
prefix =
case type_str do
"hash" -> CompoundKey.hash_prefix(key)
"list" -> CompoundKey.list_prefix(key)
"set" -> CompoundKey.set_prefix(key)
"zset" -> CompoundKey.zset_prefix(key)
end
Ops.compound_delete_prefix(store, key, prefix)
if type_str == "list" do
meta_key = CompoundKey.list_meta_key(key)
Ops.compound_delete(store, key, meta_key)
end
TypeRegistry.delete_type(key, store)
true
end
else
if Ops.exists?(store, key) do
Ops.delete(store, key)
true
else
false
end
end
end
end