Current section

Files

Jump to
fdb lib fdb transaction.ex
Raw

lib/fdb/transaction.ex

defmodule FDB.Transaction do
alias FDB.Native
alias FDB.Future
alias FDB.Utils
alias FDB.KeySelector
alias FDB.KeyRange
alias FDB.KeySelectorRange
alias FDB.Transaction
alias FDB.Database
alias FDB.Transaction.Coder
alias FDB.Option
alias FDB.RangeResult
defstruct resource: nil, coder: nil, snapshot: 0
@type t :: %__MODULE__{resource: identifier, coder: Transaction.Coder.t(), snapshot: integer}
@doc """
Creates a new transaction.
"""
@spec create(Database.t(), map) :: t
def create(%Database{} = database, defaults \\ %{}) when is_map(defaults) do
resource =
Native.database_create_transaction(database.resource)
|> Utils.verify_result()
defaults = Utils.normalize_bool_values(defaults, [:snapshot])
struct!(__MODULE__, Map.take(database, [:coder]))
|> struct!(defaults)
|> struct!(%{resource: resource})
end
@doc """
Changes the default options associated with the transaction.
"""
@spec set_defaults(t, map) :: t
def set_defaults(%Transaction{} = transaction, defaults) when is_map(defaults) do
defaults = Utils.normalize_bool_values(defaults, [:snapshot])
struct!(transaction, defaults)
end
@doc """
Refer `FDB.Option` for the list of options. Any option that starts with `transaction_option_` is allowed.
"""
@spec set_option(t, Option.key()) :: :ok
def set_option(%Transaction{} = transaction, option) do
Option.verify_transaction_option(option)
Native.transaction_set_option(transaction.resource, option)
|> Utils.verify_ok()
end
@doc """
Refer `FDB.Option` for the list of options. Any option that starts with `transaction_option_` is allowed.
"""
@spec set_option(t, Option.key(), Option.value()) :: :ok
def set_option(%Transaction{} = transaction, option, value) do
Option.verify_transaction_option(option, value)
Native.transaction_set_option(transaction.resource, option, Option.normalize_value(value))
|> Utils.verify_ok()
end
@doc """
Reads a value from the database snapshot represented by transaction.
If *key* is not present in the database, `nil` is returned as the result.
## Options
* `:snapshot` - (boolean) Defaults to `false`.
"""
@spec get(t, any, map) :: any
def get(%Transaction{} = transaction, key, options \\ %{}) when is_map(options) do
get_q(transaction, key, options)
|> Future.await()
end
@doc """
Async version of `get/3`
"""
@spec get_q(t, any, map) :: Future.t()
def get_q(%Transaction{} = transaction, key, options \\ %{}) when is_map(options) do
options = Utils.normalize_bool_values(options, [:snapshot])
coder = Map.get(options, :coder, transaction.coder)
Native.transaction_get(
transaction.resource,
Coder.encode_key(coder, key),
Map.get(options, :snapshot, transaction.snapshot)
)
|> Future.create()
|> Future.map(&Coder.decode_value(coder, &1))
end
defp do_get_range(%Transaction{} = transaction, begin_key_selector, end_key_selector, options) do
Native.transaction_get_range(
transaction.resource,
begin_key_selector.key,
begin_key_selector.or_equal,
begin_key_selector.offset,
end_key_selector.key,
end_key_selector.or_equal,
end_key_selector.offset,
Map.get(options, :limit, 0),
Map.get(options, :target_bytes, 0),
Map.get(options, :mode, FDB.Option.streaming_mode_iterator()),
Map.get(options, :iteration, 1),
Map.get(options, :snapshot, transaction.snapshot),
Map.get(options, :reverse, 0)
)
|> Future.create()
|> Future.await()
end
defp decode_range_items(coder, items) do
Enum.map(items, fn {key, value} ->
key = Coder.decode_key(coder, key)
value = Coder.decode_value(coder, value)
{key, value}
end)
end
defp do_get_range_with_continuation(
%Transaction{} = transaction,
state
) do
{has_more, list} =
do_get_range(
transaction,
state.begin_key_selector,
state.end_key_selector,
state
)
limit =
if state.has_limit do
state.limit - length(list)
else
0
end
has_more =
if (state.has_limit && limit <= 0) || Enum.empty?(list) do
0
else
has_more
end
key_values = decode_range_items(state.coder, list)
if has_more == 0 do
%RangeResult{has_more: false, key_values: key_values}
else
{key, _value} = List.last(list)
{begin_key_selector, end_key_selector} =
if state.reverse == 0 do
{KeySelector.first_greater_than(key), state.end_key_selector}
else
{state.begin_key_selector, KeySelector.first_greater_or_equal(key)}
end
state = %{
state
| has_more: has_more,
limit: limit,
iteration: state.iteration + 1,
begin_key_selector: begin_key_selector,
end_key_selector: end_key_selector
}
%RangeResult{
has_more: true,
key_values: key_values,
next: fn %Transaction{} = transaction ->
do_get_range_with_continuation(transaction, state)
end
}
end
end
@doc """
Reads all key-value pairs in the database snapshot represented by
transaction which have a key lexicographically greater than or equal
to the key resolved by the begin key selector and lexicographically
less than the key resolved by the end key selector.
Makes a single call to fetch data. The returned struct
`t:FDB.RangeResult.t/0` contains `key_values`, `has_more` and `next`
function. If `has_more` is true, then `next` function can be called
to fetch the next batch of `t:FDB.RangeResult.t/0`.
The `next` function expects `transaction` as the first argument and
returns `t:FDB.RangeResult.t/0`
The amount of data returned on each call is determined by the
options like `target_bytes` and `mode`.
## Options
* `:snapshot` - (boolean) Defaults to `false`.
* `:reverse` - (boolean) Defaults to `false`.
* `:target_bytes` - (boolean) If non-zero, indicates a (soft) cap on
the combined number of bytes of keys and values to return per
call. Defaults to `0`.
* `:mode` - (`t:FDB.Option.key/0`) Refer `FDB.Option` for the list
of options. Any option that starts with `streaming_mode_` is
allowed. Defaults to `FDB.Option.streaming_mode_iterator/0`.
* `:limit` - (number) If non-zero, indicates the maximum number of
key-value pairs to return. Defaults to `0`.
"""
@spec get_range(t, KeySelectorRange.t(), map) :: RangeResult.t()
def get_range(
%Transaction{} = transaction,
%KeySelectorRange{} = key_selector_range,
options \\ %{}
)
when is_map(options) do
coder = Map.get(options, :coder, transaction.coder)
options =
Utils.normalize_bool_values(options, [:reverse, :snapshot])
|> Utils.verify_value(:limit, :positive_integer)
|> Utils.verify_value(:target_bytes, :positive_integer)
|> Utils.verify_value(:mode, &Option.verify_streaming_mode/1)
begin_key_selector = %{
key_selector_range.begin
| key:
Coder.encode_range(
coder,
key_selector_range.begin.key,
key_selector_range.begin.prefix
)
}
end_key_selector = %{
key_selector_range.end
| key:
Coder.encode_range(
coder,
key_selector_range.end.key,
key_selector_range.end.prefix
)
}
has_limit = Map.has_key?(options, :limit) && options.limit > 0
state =
Map.merge(
options,
%{
has_limit: has_limit,
coder: coder,
limit: Map.get(options, :limit, 0),
reverse: Map.get(options, :reverse, 0),
has_more: 1,
iteration: 1,
mode: Map.get(options, :mode, FDB.Option.streaming_mode_iterator()),
begin_key_selector: begin_key_selector,
end_key_selector: end_key_selector
}
)
do_get_range_with_continuation(transaction, state)
end
@doc """
See `get_range/3` for options
A `Stream` is returned which fetches all the key value pairs lazily
using `get_range/3` function. This is suitable for iterating over
large list of key value pair.
"""
@spec get_range_stream(t | Database.t(), KeySelectorRange.t(), map) :: Enumerable.t()
def get_range_stream(
%{__struct__: struct} = transaction,
%KeySelectorRange{} = key_selector_range,
options \\ %{}
)
when is_map(options) and struct in [Transaction, Database] do
database_or_transaction = transaction
with_transaction = fn cb ->
fn ->
case database_or_transaction do
%Database{} ->
Database.transact(database_or_transaction, fn t ->
cb.(t)
end)
%Transaction{} ->
cb.(transaction)
end
end
end
Stream.unfold(
with_transaction.(fn t ->
get_range(t, key_selector_range, options)
end),
fn
:halt ->
nil
next ->
range_result = next.()
case range_result do
%RangeResult{has_more: false, key_values: key_values} ->
{key_values, :halt}
%RangeResult{has_more: true, key_values: key_values, next: next} ->
{key_values, with_transaction.(next)}
end
end
)
|> Stream.flat_map(& &1)
end
@doc """
Returns the transaction snapshot read version.
The transaction obtains a snapshot read version automatically at the
time of the first call to `get_*` (including this one) and (unless
causal consistency has been deliberately compromised by transaction
options) is guaranteed to represent all transactions which were
reported committed before that call.
"""
@spec get_read_version(t) :: integer()
def get_read_version(%Transaction{} = transaction) do
get_read_version_q(transaction)
|> Future.await()
end
@doc """
Async version of `get_read_version/1`
"""
@spec get_read_version_q(t) :: Future.t()
def get_read_version_q(%Transaction{} = transaction) do
Native.transaction_get_read_version(transaction.resource)
|> Future.create()
end
@doc """
Returns the approximate transaction size so far in the returned future,
which is the summation of the estimated size of mutations,
read conflict ranges, and write conflict ranges.
This can be called multiple times before the transaction is committed.
"""
@spec get_approximate_size(t) :: integer()
def get_approximate_size(%Transaction{} = transaction) do
get_approximate_size_q(transaction)
|> Future.await()
end
@doc """
Async version of `get_approximate_size/1`
"""
@spec get_approximate_size_q(t) :: Future.t()
def get_approximate_size_q(%Transaction{} = transaction) do
Native.transaction_get_approximate_size(transaction.resource)
|> Future.create()
end
@doc """
Retrieves the database version number at which a given transaction
was committed.
`commit/1` must have been called on transaction and not an error
before this function is called, or the behavior is
undefined. Read-only transactions do not modify the database when
committed and will have a committed version of -1. Keep in mind that
a transaction which reads keys and then sets them to their current
values may be optimized to a read-only transaction.
Note that database versions are not necessarily unique to a given
transaction and so cannot be used to determine in what order two
transactions completed. The only use for this function is to
manually enforce causal consistency when calling
`set_read_version/2` on another subsequent transaction.
Most applications will not call this function.
"""
@spec get_committed_version(t) :: integer()
def get_committed_version(%Transaction{} = transaction) do
Native.transaction_get_committed_version(transaction.resource)
|> Utils.verify_result()
end
@doc """
Returns an `t:FDB.Future.t/0` which will be set to the `t:FDB.Versionstamp.t/0` which was used by any versionstamp operations in this transaction.
The future will be ready only after the successful completion of a
call to `commit/1` on this transaction. Read-only transactions do
not modify the database when committed and will result in the future
completing with an error. Keep in mind that a transaction which
reads keys and then sets them to their current values may be
optimized to a read-only transaction.
"""
@spec get_versionstamp_q(t) :: Future.t()
def get_versionstamp_q(%Transaction{} = transaction) do
Native.transaction_get_versionstamp(transaction.resource)
|> Future.create()
|> Future.map(&FDB.Versionstamp.new(&1, 0))
end
@doc """
watch’s behavior is relative to the transaction that created it. A
watch will report a change in relation to the key’s value as
readable by that transaction. The initial value used for comparison
is either that of the transaction’s read version or the value as
modified by the transaction itself prior to the creation of the
watch. If the value changes and then changes back to its initial
value, the watch might not report the change.
Until the transaction that created it has been committed, a watch
will not report changes made by other transactions. In contrast, a
watch will immediately report changes made by the transaction
itself. Watches cannot be created if the transaction has set the
`FDB.Option.transaction_option_read_your_writes_disable/0`
transaction option, and an attempt to do so will return an
watches_disabled error.
If the transaction used to create a watch encounters an error during
commit, then the watch will be set with that error. A transaction
whose commit result is unknown will set all of its watches with the
commit_unknown_result error. If an uncommitted transaction is reset
or destroyed, then any watches it created will be set with the
transaction_cancelled error.
Returns an `t:FDB.Future.t/0` representing an empty value that will
be set once the watch has detected a change to the value at the
specified key.
By default, each database connection can have no more than 10,000
watches that have not yet reported a change. When this number is
exceeded, an attempt to create a watch will return a
too_many_watches error. This limit can be changed using the
`FDB.Option.database_option_max_watches/0` database option.
"""
@spec watch_q(t, any) :: Future.t()
def watch_q(%Transaction{} = transaction, key, options \\ %{}) do
coder = Map.get(options, :coder, transaction.coder)
Native.transaction_watch(transaction.resource, Coder.encode_key(coder, key))
|> Future.create()
end
@doc """
Resolves a key selector against the keys in the database snapshot
represented by transaction.
Returns the key in the database matching the key selector.
"""
@spec get_key(t, KeySelector.t()) :: any
def get_key(%Transaction{} = transaction, %KeySelector{} = key_selector, options \\ %{})
when is_map(options) do
get_key_q(transaction, key_selector, options)
|> Future.await()
end
@doc """
Async version of `get_key/2`
"""
@spec get_key_q(t, KeySelector.t()) :: Future.t()
def get_key_q(%Transaction{} = transaction, %KeySelector{} = key_selector, options \\ %{})
when is_map(options) do
options = Utils.normalize_bool_values(options, [:snapshot])
coder = Map.get(options, :coder, transaction.coder)
key = Coder.encode_range(coder, key_selector.key, key_selector.prefix)
Native.transaction_get_key(
transaction.resource,
key,
key_selector.or_equal,
key_selector.offset,
Map.get(options, :snapshot, transaction.snapshot)
)
|> Future.create()
|> Future.map(&Coder.decode_key(coder, &1))
end
@doc """
Returns a list of public network addresses as strings, one for each
of the storage servers responsible for storing key and its
associated value.
"""
@spec get_addresses_for_key(t, any) :: [String.t()]
def get_addresses_for_key(%Transaction{} = transaction, key) do
get_addresses_for_key_q(transaction, key)
|> Future.await()
end
@doc """
Async version of `get_addresses_for_key/2`
"""
@spec get_addresses_for_key_q(t, any) :: Future.t()
def get_addresses_for_key_q(%Transaction{} = transaction, key, options \\ %{}) do
coder = Map.get(options, :coder, transaction.coder)
Native.transaction_get_addresses_for_key(
transaction.resource,
Coder.encode_key(coder, key)
)
|> Future.create()
end
@doc """
Modify the database snapshot represented by transaction to change
the given key to have the given value.
If the given key was not previously present in the database it is
inserted. The modification affects the actual database only if
transaction is later committed with `commit/1`.
"""
@spec set(t, any, any, map) :: :ok
def set(%Transaction{} = transaction, key, value, options \\ %{}) do
coder = Map.get(options, :coder, transaction.coder)
Native.transaction_set(
transaction.resource,
Coder.encode_key(coder, key),
Coder.encode_value(coder, value)
)
|> Utils.verify_ok()
end
@doc """
Sets the snapshot read version used by a transaction.
This is not needed in simple cases. If the given version is too old,
subsequent reads will fail with error_code_past_version; if it is
too new, subsequent reads may be delayed indefinitely and/or fail
with error_code_future_version. If any of `get*` have been called on
this transaction already, the result is undefined.
"""
@spec set_read_version(t, integer) :: :ok
def set_read_version(%Transaction{} = transaction, version) when is_integer(version) do
Native.transaction_set_read_version(transaction.resource, version)
|> Utils.verify_ok()
end
@doc """
Same as set, but replaces the placeholder versionstamp in the key
The semantics are same as `set/4` except the key should have one
incomplete `t:FDB.Versionstamp.t/0`. The versionstamp will get
replaced on commit of the transaction.
A transaction is not permitted to read any transformed key or value
previously set within that transaction, and an attempt to do so will
result in an error.
This operation is not compatible with the READ_YOUR_WRITES_DISABLE
transaction option and will generate an error if used with it.
> The database version should be atleast 5.2.
### Example
coder =
FDB.Transaction.Coder.new(
Coder.Tuple.new({Coder.ByteString.new(), Coder.Versionstamp.new()})
)
future =
Database.transact(db, fn t ->
:ok =
Transaction.set_versionstamped_key(
t,
{"stamped", FDB.Versionstamp.incomplete()},
random_value()
)
Transaction.get_versionstamp_q(t)
end)
stamp = Future.await(future)
[{{"stamped", key_stamp}, _}] =
Database.get_range_stream(db, KeySelectorRange.starts_with({"stamped"}))
|> Enum.to_list()
assert stamp == key_stamp
"""
@spec set_versionstamped_key(t, any, any, map) :: :ok
def set_versionstamped_key(%Transaction{} = transaction, key, value, options \\ %{}) do
coder = Map.get(options, :coder, transaction.coder)
case Coder.encode_key_versionstamped(coder, key) do
{:ok, encoded} ->
operation_type = FDB.Option.mutation_type_set_versionstamped_key()
param = Coder.encode_value(coder, value)
Option.verify_mutation_type(operation_type, param)
Native.transaction_atomic_op(
transaction.resource,
encoded,
param,
operation_type
)
|> Utils.verify_ok()
{:error, 0} ->
raise ArgumentError, "No incomplete versionstamp found in the key"
{:error, n} when n > 1 ->
raise ArgumentError, "More than 1 incomplete versionstamps found in the key"
end
end
@doc """
Same as set, but replaces the placeholder versionstamp in the value
The semantics are same as `set/4` except the value should have one
incomplete `t:FDB.Versionstamp.t/0`. The versionstamp will get
replaced on commit of the transaction.
A transaction is not permitted to read any transformed key or value
previously set within that transaction, and an attempt to do so will
result in an error.
This operation is not compatible with the READ_YOUR_WRITES_DISABLE
transaction option and will generate an error if used with it.
> The database version should be atleast 5.2.
### Example
coder =
FDB.Transaction.Coder.new(
Coder.ByteString.new(),
Coder.Tuple.new({Coder.ByteString.new(), Coder.Versionstamp.new()})
)
future =
Database.transact(db, fn t ->
:ok =
Transaction.set_versionstamped_value(
t,
key,
{"stamped", FDB.Versionstamp.incomplete()}
)
Transaction.get_versionstamp_q(t)
end)
stamp = Future.await(future)
value =
Database.transact(db, fn t ->
Transaction.get(t, key)
end)
assert {"stamped", stamp} == value
"""
@spec set_versionstamped_value(t, any, any, map) :: :ok
def set_versionstamped_value(%Transaction{} = transaction, key, value, options \\ %{}) do
coder = Map.get(options, :coder, transaction.coder)
case Coder.encode_value_versionstamped(coder, value) do
{:ok, param} ->
operation_type = FDB.Option.mutation_type_set_versionstamped_value()
Option.verify_mutation_type(operation_type, param)
Native.transaction_atomic_op(
transaction.resource,
Coder.encode_key(coder, key),
param,
operation_type
)
|> Utils.verify_ok()
{:error, 0} ->
raise ArgumentError, "No incomplete versionstamp found in the value"
{:error, n} when n > 1 ->
raise ArgumentError, "More than 1 incomplete versionstamps found in the value"
end
end
@doc """
Modify the database snapshot represented by transaction to perform
the operation indicated by operation_type with operand param to the
value stored by the given key.
An atomic operation is a single database command that carries out
several logical steps: reading the value of a key, performing a
transformation on that value, and writing the result. Different
atomic operations perform different transformations. Like other
database operations, an atomic operation is used within a
transaction; however, its use within a transaction will not cause
the transaction to conflict.
Atomic operations do not expose the current value of the key to the
client but simply send the database the transformation to apply. In
regard to conflict checking, an atomic operation is equivalent to a
write without a read. It can only cause other transactions
performing reads of the key to conflict.
By combining these logical steps into a single, read-free operation,
FoundationDB can guarantee that the transaction will not conflict
due to the operation. This makes atomic operations ideal for
operating on keys that are frequently modified. A common example is
the use of a key-value pair as a counter.
> If a transaction uses both an atomic operation and a serializable
read on the same key, the benefits of using the atomic operation
(for both conflict checking and performance) are lost.
The modification affects the actual database only if transaction is
later committed with `commit/1`.
Refer `FDB.Option` for the list of operation_type. Any option that
starts with `mutation_type_` is allowed
"""
@spec atomic_op(t, any, Option.key(), Option.value()) :: :ok
def atomic_op(%Transaction{} = transaction, key, operation_type, param, options \\ %{}) do
coder = Map.get(options, :coder, transaction.coder)
param = Coder.encode_value(coder, param)
Option.verify_mutation_type(operation_type, param)
Native.transaction_atomic_op(
transaction.resource,
Coder.encode_key(coder, key),
param,
operation_type
)
|> Utils.verify_ok()
end
@doc """
Modifies the database snapshot represented by transaction to remove
the given key from the database. If the key was not previously
present in the database, there is no effect.
The modification affects the actual database only if transaction is
later committed with `commit/1`.
"""
@spec clear(t, any) :: :ok
def clear(%Transaction{} = transaction, key, options \\ %{}) do
coder = Map.get(options, :coder, transaction.coder)
Native.transaction_clear(transaction.resource, Coder.encode_key(coder, key))
|> Utils.verify_ok()
end
@doc """
Modify the database snapshot represented by transaction to remove
all keys (if any) which are lexicographically greater than or equal
to the given begin key and lexicographically less than the given end
key.
The modification affects the actual database only if transaction is
later committed with `commit/1`.
"""
@spec clear_range(t, KeyRange.t()) :: :ok
def clear_range(%Transaction{} = transaction, %KeyRange{} = key_range, options \\ %{}) do
coder = Map.get(options, :coder, transaction.coder)
begin_key = Coder.encode_range(coder, key_range.begin.key, key_range.begin.prefix)
end_key = Coder.encode_range(coder, key_range.end.key, key_range.end.prefix)
Native.transaction_clear_range(transaction.resource, begin_key, end_key)
|> Utils.verify_ok()
end
@doc """
Attempts to commit the sets and clears previously applied to the
database snapshot represented by transaction to the actual
database. The commit may or may not succeed – in particular, if a
conflicting transaction previously committed, then the commit must
fail in order to preserve transactional isolation. If the commit
does succeed, the transaction is durably committed to the database
and all subsequently started transactions will observe its effects.
It is not necessary to commit a read-only transaction.
As with other client/server databases, in some failure scenarios a
client may be unable to determine whether a transaction
succeeded. In these cases, `commit/1` will return a
commit_unknown_result error. The `FDB.Database.transact/2` function
treats this error as retryable, so this could execute the
transaction twice. In these cases, you must consider the idempotence
of the transaction.
Normally, commit will wait for outstanding reads to return. However,
if those reads were snapshot reads or the transaction option for
`FDB.Option.transaction_option_read_your_writes_disable/0` has been
invoked, any outstanding reads will immediately return errors.
"""
@spec commit(t) :: :ok
def commit(%Transaction{} = transaction) do
commit_q(transaction)
|> Future.await()
end
@doc """
Async version of `commit/1`
"""
@spec commit_q(t) :: Future.t()
def commit_q(%Transaction{} = transaction) do
Native.transaction_commit(transaction.resource)
|> Future.create()
end
@doc """
Cancels the transaction. All pending or future uses of the
transaction will return a transaction_cancelled error.
> If your program attempts to cancel a transaction after `commit/1`
has been called but before it returns, unpredictable behavior will
result. While it is guaranteed that the transaction will
eventually end up in a cancelled state, the commit may or may not
occur. Moreover, even if the call to `commit/1` appears to return
a transaction_cancelled error, the commit may have occurred or may
occur in the future. This can make it more difficult to reason
about the order in which transactions occur.
"""
@spec cancel(t) :: :ok
def cancel(%Transaction{} = transaction) do
Native.transaction_cancel(transaction.resource)
|> Utils.verify_ok()
end
@spec on_error(t, integer) :: :ok
def on_error(%Transaction{} = transaction, code) when is_integer(code) do
on_error_q(transaction, code)
|> Future.await()
end
@doc """
Async version of `on_error/2`
"""
@spec on_error_q(t, integer) :: Future.t()
def on_error_q(%Transaction{} = transaction, code) when is_integer(code) do
Native.transaction_on_error(transaction.resource, code)
|> Future.create()
end
@doc """
Adds a conflict range to a transaction without performing the
associated read or write.
> Most applications will use the serializable isolation that
transactions provide by default and will not need to manipulate
conflict ranges.
"""
@spec add_conflict_range(t, KeyRange.t(), Option.key()) :: :ok
def add_conflict_range(
%Transaction{} = transaction,
%KeyRange{} = key_range,
type,
options \\ %{}
) do
Option.verify_conflict_range_type(type)
coder = Map.get(options, :coder, transaction.coder)
begin_key = Coder.encode_range(coder, key_range.begin.key, key_range.begin.prefix)
end_key = Coder.encode_range(coder, key_range.end.key, key_range.end.prefix)
Native.transaction_add_conflict_range(transaction.resource, begin_key, end_key, type)
|> Utils.verify_ok()
end
@doc """
Returns the estimated size of the key range given.
"""
@spec get_estimated_range_size_bytes(t, KeyRange.t(), map) :: integer()
def get_estimated_range_size_bytes(
%Transaction{} = transaction,
%KeyRange{} = key_range,
options \\ %{}
) do
get_estimated_range_size_bytes_q(transaction, key_range, options)
|> Future.await()
end
@doc """
Async version of `get_estimated_range_size_bytes/3`
"""
@spec get_estimated_range_size_bytes_q(t, KeyRange.t(), map) :: Future.t()
def get_estimated_range_size_bytes_q(
%Transaction{} = transaction,
%KeyRange{} = key_range,
options \\ %{}
) do
coder = Map.get(options, :coder, transaction.coder)
begin_key = Coder.encode_range(coder, key_range.begin.key, key_range.begin.prefix)
end_key = Coder.encode_range(coder, key_range.end.key, key_range.end.prefix)
Native.transaction_get_estimated_range_size_bytes(transaction.resource, begin_key, end_key)
|> Future.create()
end
@doc """
Returns a list of keys that can split the given key range into
similar sized chunks based on chunk_size.
"""
@spec get_range_split_points(t, KeyRange.t(), integer(), map) :: integer()
def get_range_split_points(
%Transaction{} = transaction,
%KeyRange{} = key_range,
chunk_size,
options \\ %{}
) do
get_range_split_points_q(transaction, key_range, chunk_size, options)
|> Future.await()
end
@doc """
Async version of `get_range_split_points/4`
"""
@spec get_range_split_points_q(t, KeyRange.t(), integer(), map) :: Future.t()
def get_range_split_points_q(
%Transaction{} = transaction,
%KeyRange{} = key_range,
chunk_size,
options \\ %{}
) do
coder = Map.get(options, :coder, transaction.coder)
begin_key = Coder.encode_range(coder, key_range.begin.key, key_range.begin.prefix)
end_key = Coder.encode_range(coder, key_range.end.key, key_range.end.prefix)
Native.transaction_get_range_split_points(
transaction.resource,
begin_key,
end_key,
chunk_size
)
|> Future.create()
end
def add_conflict_key(%Transaction{} = transaction, key, type, options \\ %{}) do
Option.verify_conflict_range_type(type)
coder = Map.get(options, :coder, transaction.coder)
begin_key = Coder.encode_key(coder, key)
end_key = begin_key <> <<0x00>>
Native.transaction_add_conflict_range(transaction.resource, begin_key, end_key, type)
|> Utils.verify_ok()
end
@metadata_version_key "\xff/metadataVersion"
@metadata_version_required_value "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
@doc """
Get metadata version.
Note: This might return nil if the metadata version was never set
earlier.
"""
@spec get_metadata_version(t) :: FDB.Versionstamp.t() | nil
def get_metadata_version(%Transaction{} = transaction) do
case get(transaction, @metadata_version_key, %{coder: Transaction.Coder.new()}) do
nil -> nil
version -> FDB.Versionstamp.new(version, 0)
end
end
@doc """
Update metadata version.
"""
@spec update_metadata_version(t) :: :ok
def update_metadata_version(%Transaction{} = transaction) do
atomic_op(
transaction,
@metadata_version_key,
FDB.Option.mutation_type_set_versionstamped_value(),
@metadata_version_required_value,
%{coder: Transaction.Coder.new()}
)
end
end