Bedrock.Repo behaviour (bedrock v0.5.3)

View Source

Summary

Callbacks

Atomically adds a value to the existing integer stored at the given key.

Adds a read conflict key to the transaction.

Adds a write conflict range to the transaction.

Atomically appends the value to the existing key if the result would fit within the size limit.

Performs an atomic operation on a key with the provided value.

Atomically performs bitwise AND on the key with the provided value.

Atomically performs bitwise OR on the key with the provided value.

Atomically performs bitwise XOR on the key with the provided value.

Atomically sets the key to the lexicographically larger of the existing value and the provided value.

Atomically sets the key to the lexicographically smaller of the existing value and the provided value.

Removes a key from the database within the transaction.

Clears all keys in the specified range.

Atomically clears the key if the current value matches the expected value.

Gets a value for the given key, returning the value or nil if not found.

Lazy range query that returns an enumerable stream of key-value pairs.

Atomically sets the key to the maximum of the existing value and the provided value.

Atomically sets the key to the minimum of the existing value and the provided value.

Sets a key to a value within the transaction.

Rolls back the transaction, discarding all changes.

Selects a key-value pair using a key selector, returning {key, value} or nil if not found.

Executes a function within a database transaction.

Callbacks

add(t, value)

@callback add(Bedrock.Key.t(), value :: binary()) :: :ok

Atomically adds a value to the existing integer stored at the given key.

If the key does not exist, it is treated as having an initial value of 0. The value must be an integer (which will be encoded as little-endian binary). Existing values are interpreted as variable-length little-endian integers, with empty binaries treated as 0 for the computation.

This operation is performed atomically at the storage layer, providing strong consistency guarantees even under high contention.

Conflict Behavior

  • No Read Conflicts: Does NOT create read conflicts - enables high concurrency
  • No Write Conflicts: Does NOT create write conflicts - atomic operations never conflict
  • Blind Write: Does not read current value from storage during transaction
  • Local Computation: Subsequent reads within the same transaction return computed values

Examples

# High-concurrency counter without read conflicts
transaction fn ->
  add("global_counter", 1)  # Many can run concurrently
  {:ok, :ok}
end

# Computing values locally within transaction
transaction fn ->
  add("counter", 5)
  {:ok, value} = get("counter")  # Returns computed value (5 if key was missing)
  {:ok, value}
end

add_read_conflict_key(t)

@callback add_read_conflict_key(Bedrock.Key.t()) :: :ok

Adds a read conflict key to the transaction.

This manually adds a key to the transaction's read conflict set without actually reading the key. If any other transaction modifies this key, this transaction will conflict and retry.

This is useful for ensuring consistency when you need to conflict on a key that you don't actually read but whose modification would invalidate your transaction's assumptions.

Examples

transact(fn ->
  # Add conflict on a counter key without reading it
  add_read_conflict_key("global_counter")

  # Do other operations that depend on the counter not changing
  put("dependent_data", compute_based_on_counter())

  {:ok, :ok}
end)

add_write_conflict_range(t)

@callback add_write_conflict_range(Bedrock.ToKeyRange.t()) :: :ok

Adds a write conflict range to the transaction.

This manually adds a key range to the transaction's write conflict set without actually writing to the range. Any other transaction that writes to keys in this range will conflict with this transaction.

This is useful for reserving key ranges or ensuring exclusive access to a namespace without actually writing to it.

Examples

transact(fn ->
  # Reserve the entire user namespace
  add_write_conflict_range("user:", "user;")

  # Now we can safely assume no other transaction is modifying users
  put("user:123", user_data)

  {:ok, :ok}
end)

append_if_fits(t, value)

@callback append_if_fits(Bedrock.Key.t(), value :: binary()) :: :ok

Atomically appends the value to the existing key if the result would fit within the size limit.

The size limit is 131,072 bytes (2^17). If appending would exceed this limit, the key is left unchanged. If the key does not exist, it is set to the provided value.

Conflict Behavior

  • No Read Conflicts: Does NOT create read conflicts - enables high concurrency
  • No Write Conflicts: Does NOT create write conflicts - atomic operations never conflict
  • Blind Write: Does not read current value from storage during transaction
  • Local Computation: Subsequent reads within the same transaction return computed values

Examples

# Append to log entries without conflicts
transaction fn ->
  append_if_fits("log_buffer", "New log entry\n")
  {:ok, :ok}
end

atomic(operation, t, value)

@callback atomic(
  operation ::
    :add
    | :min
    | :max
    | :bit_and
    | :bit_or
    | :bit_xor
    | :byte_min
    | :byte_max
    | :append_if_fits
    | :compare_and_clear,
  Bedrock.Key.t(),
  value :: binary()
) :: :ok

Performs an atomic operation on a key with the provided value.

This is the low-level atomic operation interface. Most users should use the specific atomic operation functions (add, min, max, etc.) instead of calling this directly.

Examples

# These are equivalent:
add("counter", 5)
atomic(:add, "counter", <<5::64-little>>)

bit_and(t, value)

@callback bit_and(Bedrock.Key.t(), value :: binary()) :: :ok

Atomically performs bitwise AND on the key with the provided value.

Both the existing value and operand are treated as little-endian binary values. If the key does not exist, returns the operand value.

Conflict Behavior

  • No Read Conflicts: Does NOT create read conflicts - enables high concurrency
  • No Write Conflicts: Does NOT create write conflicts - atomic operations never conflict
  • Blind Write: Does not read current value from storage during transaction
  • Local Computation: Subsequent reads within the same transaction return computed values

Examples

# Clear specific bits without conflicts
transaction fn ->
  bit_and("permission_flags", <<0b11110000>>)  # Clear lower 4 bits
  {:ok, :ok}
end

# Computing values locally within transaction
transaction fn ->
  bit_and("flags", <<0b11001100>>)
  {:ok, value} = get("flags")  # Returns computed AND result
  {:ok, value}
end

bit_or(t, value)

@callback bit_or(Bedrock.Key.t(), value :: binary()) :: :ok

Atomically performs bitwise OR on the key with the provided value.

Both the existing value and operand are treated as little-endian binary values. If the key does not exist, it is treated as having an initial value of 0.

Conflict Behavior

  • No Read Conflicts: Does NOT create read conflicts - enables high concurrency
  • No Write Conflicts: Does NOT create write conflicts - atomic operations never conflict
  • Blind Write: Does not read current value from storage during transaction
  • Local Computation: Subsequent reads within the same transaction return computed values

Examples

# Set specific bits without conflicts
transaction fn ->
  bit_or("permission_flags", <<0b00001111>>)  # Set lower 4 bits
  {:ok, :ok}
end

bit_xor(t, value)

@callback bit_xor(Bedrock.Key.t(), value :: binary()) :: :ok

Atomically performs bitwise XOR on the key with the provided value.

Both the existing value and operand are treated as little-endian binary values. If the key does not exist, it is treated as having an initial value of 0.

Conflict Behavior

  • No Read Conflicts: Does NOT create read conflicts - enables high concurrency
  • No Write Conflicts: Does NOT create write conflicts - atomic operations never conflict
  • Blind Write: Does not read current value from storage during transaction
  • Local Computation: Subsequent reads within the same transaction return computed values

Examples

# Toggle specific bits without conflicts
transaction fn ->
  bit_xor("toggle_flags", <<0b10101010>>)  # Toggle alternate bits
  {:ok, :ok}
end

byte_max(t, value)

@callback byte_max(Bedrock.Key.t(), value :: binary()) :: :ok

Atomically sets the key to the lexicographically larger of the existing value and the provided value.

Uses byte-wise (lexicographic) comparison. If the key does not exist, it is set to the provided value.

Conflict Behavior

  • No Read Conflicts: Does NOT create read conflicts - enables high concurrency
  • No Write Conflicts: Does NOT create write conflicts - atomic operations never conflict
  • Blind Write: Does not read current value from storage during transaction
  • Local Computation: Subsequent reads within the same transaction return computed values

Examples

# Set maximum string value without conflicts
transaction fn ->
  byte_max("latest_version", "1.2.3")
  {:ok, :ok}
end

byte_min(t, value)

@callback byte_min(Bedrock.Key.t(), value :: binary()) :: :ok

Atomically sets the key to the lexicographically smaller of the existing value and the provided value.

Uses byte-wise (lexicographic) comparison. If the key does not exist, it is set to the provided value.

Conflict Behavior

  • No Read Conflicts: Does NOT create read conflicts - enables high concurrency
  • No Write Conflicts: Does NOT create write conflicts - atomic operations never conflict
  • Blind Write: Does not read current value from storage during transaction
  • Local Computation: Subsequent reads within the same transaction return computed values

Examples

# Set minimum string value without conflicts
transaction fn ->
  byte_min("earliest_date", "2024-01-01")
  {:ok, :ok}
end

clear(t)

@callback clear(Bedrock.Key.t()) :: :ok

Removes a key from the database within the transaction.

The key is marked for deletion and will be removed when the transaction commits. Keys must be binary and no larger than 16KiB.

Options

  • :no_write_conflict - If true, disables write conflict detection (default: false)

Examples

transact(fn ->
  clear("user:123")
  clear("index:email:" <> email)
end)

clear(t, opts)

@callback clear(Bedrock.Key.t(), opts :: [{:no_write_conflict, boolean()}]) :: :ok
@callback clear(Bedrock.Keyspace.t(), key :: binary()) :: :ok

clear(t, key, opts)

@callback clear(
  Bedrock.Keyspace.t(),
  key :: binary(),
  opts :: [{:no_write_conflict, boolean()}]
) :: :ok

clear_range(t)

@callback clear_range(Bedrock.ToKeyRange.t()) :: :ok

Clears all keys in the specified range.

Removes all key-value pairs where the key is greater than or equal to start_key and less than end_key. This operation is atomic and will be applied when the transaction commits.

Options

  • :no_write_conflict - If true, disables write conflict detection (default: false)

Examples

transact(fn ->
  # Clear all user data using key range
  clear_range("user:", "user;")

  # Clear using subspace
  user_space = Keyspace.new("users")
  clear_range(user_space)

  {:ok, :ok}
end)

clear_range(t, opts)

@callback clear_range(Bedrock.ToKeyRange.t(), opts :: [{:no_write_conflict, boolean()}]) ::
  :ok

compare_and_clear(t, expected)

@callback compare_and_clear(Bedrock.Key.t(), expected :: binary()) :: :ok

Atomically clears the key if the current value matches the expected value.

If the key does not exist or the value doesn't match, the key is left unchanged.

Conflict Behavior

  • No Read Conflicts: Does NOT create read conflicts - enables high concurrency
  • No Write Conflicts: Does NOT create write conflicts - atomic operations never conflict
  • Blind Write: Does not read current value from storage during transaction
  • Local Computation: Subsequent reads within the same transaction return computed values

Examples

# Clear a flag only if it has a specific value without conflicts
transaction fn ->
  compare_and_clear("feature_flag", "enabled")
  {:ok, :ok}
end

get(key)

@callback get(key :: binary()) :: nil | binary()

Gets a value for the given key, returning the value or nil if not found.

Keys must be binary and no larger than 16KiB.

Options

  • :snapshot - If true, bypasses conflict tracking for this read

Examples

transact(fn ->
  # Regular read with conflict tracking
  user = get("user:123")

  # Snapshot read without conflict tracking
  metadata = get("metadata", snapshot: true)

  {:ok, :ok}
end)

get(key, opts)

@callback get(key :: binary(), opts :: [{:snapshot, boolean()}]) :: nil | binary()
@callback get(Bedrock.Keyspace.t(), key :: binary()) :: nil | binary()

get(t, key, opts)

@callback get(Bedrock.Keyspace.t(), key :: binary(), opts :: [{:snapshot, boolean()}]) ::
  nil | binary()

get_range(t)

@callback get_range(Bedrock.ToKeyRange.t()) :: Enumerable.t(Bedrock.key_value())

Lazy range query that returns an enumerable stream of key-value pairs.

Like FoundationDB's get_range(), this function returns an Enumerable that lazily fetches results as needed. For eager collection, pipe to Enum.to_list/1.

Options

  • :batch_size - Number of items to fetch per batch (default: 100)
  • :timeout - Timeout per batch request (default: 5000)
  • :limit - Maximum total items to return
  • :snapshot - If true, bypasses conflict tracking (default: false)

Examples

# Lazy iteration (memory efficient)
transact(fn ->
  result =
    get_range(start_key, end_key, limit: 100)
    |> Enum.take(10)  # Only fetches what's needed

  {:ok, result}
end)

# Collect all results
transact(fn ->
  results =
    get_range(start_key, end_key)
    |> Enum.to_list()

  {:ok, results}
end)

# Snapshot read without conflicts
transact(fn ->
  metadata =
    get_range("meta/", "meta0", snapshot: true)
    |> Enum.to_list()

  {:ok, metadata}
end)

get_range(t, opts)

@callback get_range(
  Bedrock.ToKeyRange.t(),
  opts :: [
    batch_size: pos_integer(),
    limit: non_neg_integer(),
    snapshot: boolean(),
    timeout: non_neg_integer()
  ]
) :: Enumerable.t(Bedrock.key_value())

max(t, value)

@callback max(Bedrock.Key.t(), value :: binary()) :: :ok

Atomically sets the key to the maximum of the existing value and the provided value.

If the key does not exist, it is treated as having an initial value of 0, so the result will be max(0, value). The value must be an integer (which will be encoded as little-endian binary). Existing values are interpreted as variable-length little-endian integers, with empty binaries treated as 0 for the computation.

This operation is performed atomically at the storage layer, providing strong consistency guarantees even under high contention.

Conflict Behavior

  • No Read Conflicts: Does NOT create read conflicts - enables high concurrency
  • No Write Conflicts: Does NOT create write conflicts - atomic operations never conflict
  • Blind Write: Does not read current value from storage during transaction
  • Local Computation: Subsequent reads within the same transaction return computed values

Examples

# Setting maximum values without conflicts
transaction fn ->
  max("high_score", 1000)  # Many can update scores concurrently
  {:ok, :ok}
end

# Computing values locally within transaction
transaction fn ->
  max("peak_usage", 250)
  {:ok, value} = get("peak_usage")  # Returns max(current_value, 250)
  {:ok, value}
end

min(t, value)

@callback min(Bedrock.Key.t(), value :: binary()) :: :ok

Atomically sets the key to the minimum of the existing value and the provided value.

If the key does not exist, it is treated as having an initial value of 0, so the result will be min(0, value). The value must be an integer (which will be encoded as little-endian binary). Existing values are interpreted as variable-length little-endian integers, with empty binaries treated as 0 for the computation.

This operation is performed atomically at the storage layer, providing strong consistency guarantees even under high contention.

Conflict Behavior

  • No Read Conflicts: Does NOT create read conflicts - enables high concurrency
  • No Write Conflicts: Does NOT create write conflicts - atomic operations never conflict
  • Blind Write: Does not read current value from storage during transaction
  • Local Computation: Subsequent reads within the same transaction return computed values

Examples

# Setting minimum thresholds without conflicts
transaction fn ->
  min("max_connections", 100)  # Many can set limits concurrently
  {:ok, :ok}
end

# Computing values locally within transaction
transaction fn ->
  min("threshold", 50)
  {:ok, value} = get("threshold")  # Returns min(current_value, 50)
  {:ok, value}
end

put(t, value)

@callback put(Bedrock.Key.t(), value :: binary()) :: :ok

Sets a key to a value within the transaction.

The write is buffered until the transaction commits. Keys and values must be binary data. Keys must be no larger than 16KiB and values no larger than 128KiB.

Options

  • :no_write_conflict - If true, disables write conflict detection (default: false)

Examples

transact(fn ->
  put("user:123", user_data)
  put("index:email:" <> email, "user:123")
  {:ok, :ok}
end)

put(t, value, opts)

@callback put(
  Bedrock.Key.t(),
  value :: binary(),
  opts :: [{:no_write_conflict, boolean()}]
) :: :ok
@callback put(Bedrock.Keyspace.t(), key :: binary(), value :: term()) :: :ok

put(t, key, value, opts)

@callback put(
  Bedrock.Keyspace.t(),
  key :: binary(),
  value :: term(),
  opts :: [{:no_write_conflict, boolean()}]
) :: :ok

rollback(reason)

@callback rollback(reason :: term()) :: no_return()

Rolls back the transaction, discarding all changes.

This cancels the transaction and discards all buffered writes and modifications. The transaction cannot be used after rollback.

Examples

{:error, :some_reason} = transact(fn ->
  put("key", "value")

  if should_abort? do
    rollback(:some_reason)
  else
    {:ok, :ok}
  end
end)

select(t)

@callback select(Bedrock.KeySelector.t()) :: nil | {Bedrock.Key.t(), binary()}

Selects a key-value pair using a key selector, returning {key, value} or nil if not found.

Key selectors provide efficient ways to find keys based on relative positioning and prefix matching without requiring exact key knowledge.

Options

  • :snapshot - If true, bypasses conflict tracking for this read

Examples

transact(fn ->
  # Find first key after "user:"
  first_user = select(KeySelector.first_greater_than("user:"))

  # Find last key in user namespace
  last_user = select(KeySelector.last_less_or_equal("user;"))

  {:ok, :ok}
end)

select(t, opts)

@callback select(Bedrock.KeySelector.t(), opts :: [{:snapshot, boolean()}]) ::
  nil | {Bedrock.Key.t(), binary()}

transact(arg1)

@callback transact(
  (-> :ok | {:ok, result} | {:error, reason})
  | (module() -> :ok | {:ok, result} | {:error, reason})
) :: :ok | {:ok, result} | {:error, reason}
when result: term(), reason: term()

Executes a function within a database transaction.

The function is executed within a transaction context and will be retried automatically on conflicts. The transaction is committed when the function returns successfully, or rolled back if an exception is raised.

Options

  • :retry_limit - Maximum number of retries on transaction conflicts (default: nil for unlimited)
  • :timeout_in_ms - Transaction timeout in milliseconds

Examples

# Simple transaction
{:ok, result} = transact(fn ->
  put("key", "value")
  result = get("other_key")
  {:ok, result}
end)

# Transaction with retry limit
{:ok, :ok} = transact(fn ->
  put("counter", "1")
  {:ok, :ok}
end, retry_limit: 5)

transact(arg1, opts)

@callback transact(
  (-> :ok | {:ok, result} | {:error, reason})
  | (module() -> :ok | {:ok, result} | {:error, reason}),
  opts :: [
    retry_limit: non_neg_integer() | nil,
    timeout_in_ms: Bedrock.timeout_in_ms()
  ]
) :: :ok | {:ok, result} | {:error, reason}
when result: term(), reason: any()