Tx (tx v0.4.1)

A simple composable transaction library that serves as an experimental alternative solution to Ecto.Multi.

This library intend to tackle the main problem with Ecto.Multi:

  • Ecto.Multi names are global, which requires the caller of a multi-function to know about the names used in the multi.

  • Composing complex Ecto.Multi can run into name collision if not being careful enough.

Link to this section Summary

Types

t()

You can put a value of any Tx type on the right of <- notation in Tx.Macro.

Functions

Compose two transactions. Run the second one only if the first one succeeds.

Combine a list of transactions [Tx.t(a)] into a single Tx.t([a]).

Combine two transactions Tx.t(a) and Tx.t(b) into a single Tx.t({a, b}).

Avoid a transaction to rollback on exception.

Make an transaction rollback on error.

Execute the transaction Tx.t(a), producing an {:ok, a} or {:error, any}.

Make a Tx optional so if it fails, it acts as a Tx that returns {:ok, nil}.

Map over the successful result of a transaction.

Create a transaction.

Create a Tx value that always returns error.

Compose two transactions. If the first transaction fails, fallback to the second transaction.

Create a transaction that returns a pure value.

Rollback the current transaction.

The raising version of run/2.

Run a transaction to get its result.

Convert a Tx.t(a) into a Multi.t().

Link to this section Types

@type error_t() :: any()
@type fn_t(a) :: (Ecto.Repo.t() -> {:ok, a} | {:error, error_t()})
@type t() :: t(any())
@type t(a) :: fn_t(a) | Ecto.Multi.t() | {:ok, a} | {:error, error_t()}

You can put a value of any Tx type on the right of <- notation in Tx.Macro.

A Tx type is any of the following:

  • A Tx function :: any function that takes in a repo and returns a {:ok, a} | {:error, error_t()} pair. You can create a Tx function via the tx macro, or any tx combinators like Tx.new, Tx.pure, Tx.concat.

  • An Ecto.Multi.t() :: equivalent a tx function that returns {:ok, %{multi_name => value}} or {:error, multi_error}

  • A plain {:ok, a} :: acts as a fn_t(a) that constantly return {:ok, a}

  • A plain {:error, error_t} :: acts as a fn_t(a) that constantly return {:ok, a}

Link to this section Functions

@spec and_then(t(a()), (a() -> t(b()))) :: t(b())

Compose two transactions. Run the second one only if the first one succeeds.

This is the "bind" operation if you're familiar with Monad.

Example:

iex> Tx.pure(1) |> Tx.and_then(&{:error, &1}) |> Tx.execute(Repo)
{:error, 1}
@spec concat([t(a())]) :: t([a()])

Combine a list of transactions [Tx.t(a)] into a single Tx.t([a]).

iex> Tx.concat([Tx.pure(1), Tx.pure(2)]) |> Tx.execute(Repo)
{:ok, [1, 2]}
@spec concat(t(a()), t(b())) :: t({a(), b()})

Combine two transactions Tx.t(a) and Tx.t(b) into a single Tx.t({a, b}).

iex> Tx.concat(Tx.pure(1), Tx.pure(2)) |> Tx.execute(Repo)
{:ok, {1, 2}}
Link to this function

disable_rollback_on_exception(tx)

@spec disable_rollback_on_exception(t(a())) :: t(a())

Avoid a transaction to rollback on exception.

Wrap around tx with rollback_on_exception(tx, false) if you avoid tx to rollback on exception. When an exception occurs, you will get an {:error, exception} instead.

Link to this function

enable_rollback_on_error(trans)

@spec enable_rollback_on_error(t(a())) :: t(a())

Make an transaction rollback on error.

You can use this function to fine-tune the rollback behaviour on specific sub-transactions.

Link to this function

execute(tx, repo, opts \\ [])

@spec execute(t(a()), Ecto.Repo.t(), keyword()) :: {:ok, a()} | {:error, any()}

Execute the transaction Tx.t(a), producing an {:ok, a} or {:error, any}.

Options:

  • rollback_on_error (Default: true): rollback transaction if the final result is an {:error, error_t()}

  • rollback_on_exception (Default: true): rollback transaction if an uncaught exception arises within the transaction.

  • Any options Ecto.Repo.transaction/2 accepts.

Link to this function

make_optional(tx)

@spec make_optional(t(a())) :: t(a() | nil)

Make a Tx optional so if it fails, it acts as a Tx that returns {:ok, nil}.

This operation corresponds to the "optional" combinator for the Alternative instance.

Example:

iex> Tx.new_error(1) |> Tx.execute(Repo)
{:error, 1}

iex> Tx.new_error(1) |> Tx.make_optional() |> Tx.execute(Repo)
{:ok, nil}
@spec map(t(a()), (a() -> b())) :: t(b())

Map over the successful result of a transaction.

Example:

iex> Tx.pure(1) |> Tx.map(&(&1 + 1)) |> Tx.execute(Repo)
{:ok, 2}
@spec new(fn_t(a())) :: t(a())

Create a transaction.

This function creates a Tx.t() from a function of type (Ecto.Repo.t() -> {:ok, a} | {:error, error_t()}).

Internally, the new/1 constructor simply returns the function as is.

Example:

iex> t = fn _repo -> {:ok, 42} end
iex> Tx.execute(Tx.new(t), Repo)
{:ok, 42}
@spec new_error(error_t()) :: t(a())

Create a Tx value that always returns error.

This operation corresponds to the "mzero"/"empty" axiom in the MonadPlus/Alternative instance.

Example:

iex> Tx.new_error(1) |> Tx.execute(Repo)
{:error, 1}

iex> Tx.new_error(1) |> Tx.or_else(fn -> {:ok, 2} end)|> Tx.execute(Repo)
{:ok, 2}
@spec or_else(t(a()), (error_t() -> t(b())) | (() -> t(b()))) :: t(b())

Compose two transactions. If the first transaction fails, fallback to the second transaction.

This is the "mplus"/"<|>" operation if you're familiar with MonadPlus or Alternative.

Example:

iex> Tx.pure(1)
...> |> Tx.and_then(&{:error, &1})
...> |> Tx.or_else(&({:ok, &1}))
...> |> Tx.execute(Repo)
{:ok, 1}
@spec pure(a()) :: t(a())

Create a transaction that returns a pure value.

This is the "pure"/"return" operation if you're familiar with Monad.

Executing Tx with a pure value always returns the value.

pure(a) is equivalent to new(fn _ -> {:ok, a} end).

iex> Tx.execute(Tx.pure(42), Repo)
{:ok, 42}
Link to this function

rollback(repo, error)

@spec rollback(Ecto.Repo.t(), error_t()) :: no_return()

Rollback the current transaction.

Example:

iex> Tx.new(fn repo ->
...>   if 1 == 1 do
...>     Tx.rollback(repo, "One cannot be equal to one")
...>   else
...>    {:ok, :fine}
...>   end
...> end)
...> |> Tx.execute(Repo)
{:error, "One cannot be equal to one"}
@spec run!(Ecto.Repo.t(), t(a())) :: a()

The raising version of run/2.

This function should be rarely needed if you use the Tx.Macro.tx macro.

Link to this function

run(repo, multi)

@spec run(Ecto.Repo.t(), t(a()) | any()) :: {:ok, a()} | {:error, error_t()}

Run a transaction to get its result.

This is a generic adapter for extracting a {:ok, a} | {:error, error_t()} from a transaction when given a repo.

  • For normal transaction tx as a function (default), it simply call tx.(repo)
  • For Ecto.Multi, it creates a sub-transaction to execute it
  • For a non-transactional value, it simply returns the value

This function should be rarely needed if you use the Tx.Macro.tx macro. You can simply use a <- t syntax instead of a = Tx.run(repo, t) within the tx macro.

This function is meant to be used within a tx block or inside a Tx closure. If you want to get a value out of a Tx, you may want to call execute/2 instead.

Link to this function

to_multi(tx, name)

@spec to_multi(t(a()), term()) :: Ecto.Multi.t()

Convert a Tx.t(a) into a Multi.t().

You can refer to the transactin's result ({:ok, a} | {:error, any}) by name.

EctoRepo.transaction(to_multi(pure(42), :foo)) => {:ok, %{foo: {:ok, 42}}}