Ferricstore.Store.CompoundKey (ferricstore v0.3.5)

Copy Markdown View Source

Encodes and decodes compound keys for data structure storage in Bitcask.

FerricStore stores data structure sub-elements (hash fields, list positions, set members, sorted set entries) as individual Bitcask entries using compound keys. This avoids the need for per-key Bitcask instances while still allowing individual field/member access without reading entire structures.

Key Format

Each compound key has the format:

PREFIX:redis_key\0sub_key

Where:

  • PREFIX is a single uppercase letter identifying the data type:

    • H for Hash fields
    • L for List elements (sub_key is a float64 position)
    • S for Set members
    • Z for Sorted Set entries
    • T for Type metadata (no sub_key)
  • redis_key is the user-facing Redis key name

  • \0 (null byte) is the separator between the Redis key and sub-key

  • sub_key is the type-specific sub-key (field name, position, member, etc.)

The null byte separator is safe because Redis keys in normal use do not contain null bytes (and the RESP protocol does not transmit them within key strings in standard client libraries).

Type Metadata

A type index entry T:keyname stores the type as a simple string value ("hash", "list", "set", "zset"). This is used by the TYPE command and for WRONGTYPE enforcement -- attempting to use hash commands on a set key returns an error.

Summary

Functions

Decodes a position string back to a float64.

Decodes a stored type string back to the data type atom.

Encodes a float64 position as a fixed-width string that sorts lexicographically in the same order as the numeric values.

Encodes a data type atom to its stored string representation.

Extracts the Redis-level key from a compound key.

Extracts the sub-key portion from a compound key given a known prefix.

Builds a compound key for a hash field.

Returns the scan prefix for all fields of a hash.

Returns true if the given Bitcask key is an internal compound key (starts with a known type prefix like H:, L:, S:, Z:, T:).

Builds a compound key for a list element at a given float64 position.

Builds the metadata compound key for a list.

Returns the scan prefix for all elements of a list.

Builds a compound key for a set member.

Returns the scan prefix for all members of a set.

Builds the type metadata compound key for a Redis key.

Filters a list of raw ETS/store keys to only user-visible logical keys.

Builds a compound key for a sorted set member-to-score mapping.

Returns the scan prefix for all members of a sorted set.

Types

data_type()

@type data_type() :: :hash | :list | :set | :zset

Functions

decode_position(arg)

@spec decode_position(binary()) :: number()

Decodes a position string back to a float64.

Examples

iex> pos = 1000.5
iex> encoded = Ferricstore.Store.CompoundKey.encode_position(pos)
iex> Ferricstore.Store.CompoundKey.decode_position(encoded)
1000.5

decode_type(binary)

@spec decode_type(binary()) :: data_type()

Decodes a stored type string back to the data type atom.

Examples

iex> Ferricstore.Store.CompoundKey.decode_type("hash")
:hash

encode_position(position)

@spec encode_position(number()) :: binary()

Encodes a float64 position as a fixed-width string that sorts lexicographically in the same order as the numeric values.

Negative positions use a complement encoding so that lexicographic order is preserved.

Examples

iex> a = Ferricstore.Store.CompoundKey.encode_position(1.0)
iex> b = Ferricstore.Store.CompoundKey.encode_position(2.0)
iex> a < b
true

encode_type(atom)

@spec encode_type(data_type()) :: binary()

Encodes a data type atom to its stored string representation.

Examples

iex> Ferricstore.Store.CompoundKey.encode_type(:hash)
"hash"

extract_redis_key(key)

@spec extract_redis_key(binary()) :: binary()

Extracts the Redis-level key from a compound key.

For compound keys like S:myset\0member, returns "myset". For type metadata keys like T:myset, returns "myset". For plain (non-compound) keys, returns the key as-is.

Used by the cross-shard lock checking logic to determine which Redis key a compound Raft write affects.

Examples

iex> Ferricstore.Store.CompoundKey.extract_redis_key("S:myset" <> <<0>> <> "member")
"myset"

iex> Ferricstore.Store.CompoundKey.extract_redis_key("H:hash" <> <<0>> <> "field")
"hash"

iex> Ferricstore.Store.CompoundKey.extract_redis_key("T:mykey")
"mykey"

iex> Ferricstore.Store.CompoundKey.extract_redis_key("plain_key")
"plain_key"

extract_subkey(compound_key, prefix)

@spec extract_subkey(binary(), binary()) :: binary()

Extracts the sub-key portion from a compound key given a known prefix.

Examples

iex> prefix = Ferricstore.Store.CompoundKey.hash_prefix("user:123")
iex> compound = Ferricstore.Store.CompoundKey.hash_field("user:123", "name")
iex> Ferricstore.Store.CompoundKey.extract_subkey(compound, prefix)
"name"

hash_field(redis_key, field)

@spec hash_field(binary(), binary()) :: binary()

Builds a compound key for a hash field.

Examples

iex> Ferricstore.Store.CompoundKey.hash_field("user:123", "name")
<<"H:user:123", 0, "name">>

hash_prefix(redis_key)

@spec hash_prefix(binary()) :: binary()

Returns the scan prefix for all fields of a hash.

Examples

iex> Ferricstore.Store.CompoundKey.hash_prefix("user:123")
<<"H:user:123", 0>>

internal_key?(arg1)

@spec internal_key?(binary()) :: boolean()

Returns true if the given Bitcask key is an internal compound key (starts with a known type prefix like H:, L:, S:, Z:, T:).

Used to filter internal keys out of user-facing KEYS and DBSIZE results.

Examples

iex> Ferricstore.Store.CompoundKey.internal_key?("H:user:123" <> <<0>> <> "name")
true

iex> Ferricstore.Store.CompoundKey.internal_key?("my_plain_key")
false

list_element(redis_key, position)

@spec list_element(binary(), float()) :: binary()

Builds a compound key for a list element at a given float64 position.

The position is encoded as a fixed-width, zero-padded float string to ensure lexicographic ordering matches numeric ordering. Format is {sign}{integer_part}.{fractional_part} with 20 digits before and 17 digits after the decimal point.

Examples

iex> key = Ferricstore.Store.CompoundKey.list_element("mylist", 1000.0)
iex> String.starts_with?(key, "L:mylist" <> <<0>>)
true

list_meta_key(redis_key)

@spec list_meta_key(binary()) :: binary()

Builds the metadata compound key for a list.

list_prefix(redis_key)

@spec list_prefix(binary()) :: binary()

Returns the scan prefix for all elements of a list.

Examples

iex> Ferricstore.Store.CompoundKey.list_prefix("mylist")
<<"L:mylist", 0>>

set_member(redis_key, member)

@spec set_member(binary(), binary()) :: binary()

Builds a compound key for a set member.

The member name IS the sub-key; the value stored is "1" (presence marker).

Examples

iex> Ferricstore.Store.CompoundKey.set_member("tags:post:789", "elixir")
<<"S:tags:post:789", 0, "elixir">>

set_prefix(redis_key)

@spec set_prefix(binary()) :: binary()

Returns the scan prefix for all members of a set.

Examples

iex> Ferricstore.Store.CompoundKey.set_prefix("tags:post:789")
<<"S:tags:post:789", 0>>

type_key(redis_key)

@spec type_key(binary()) :: binary()

Builds the type metadata compound key for a Redis key.

Examples

iex> Ferricstore.Store.CompoundKey.type_key("user:123")
"T:user:123"

user_visible_keys(raw_keys)

@spec user_visible_keys([binary()]) :: [binary()]

Filters a list of raw ETS/store keys to only user-visible logical keys.

  • Rejects all internal compound keys (H:, L:, S:, Z:, V:, VM:, PM:, LM:)
  • Extracts logical keys from type registry entries (T:key -> key)
  • Keeps plain string keys as-is
  • Deduplicates (a key may exist both as a plain entry and a T: entry)

zset_member(redis_key, member)

@spec zset_member(binary(), binary()) :: binary()

Builds a compound key for a sorted set member-to-score mapping.

Stores member -> score for O(1) score lookups by member.

Examples

iex> Ferricstore.Store.CompoundKey.zset_member("leaderboard", "alice")
<<"Z:leaderboard", 0, "alice">>

zset_prefix(redis_key)

@spec zset_prefix(binary()) :: binary()

Returns the scan prefix for all members of a sorted set.

Examples

iex> Ferricstore.Store.CompoundKey.zset_prefix("leaderboard")
<<"Z:leaderboard", 0>>