Packages
double_down
0.63.2
0.69.0
0.68.0
0.66.0
0.65.0
0.64.1
0.64.0
0.63.3
0.63.2
0.63.1
0.63.0
0.62.1
0.61.0
0.60.4
0.60.3
0.60.2
0.60.1
0.60.0
0.59.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.0
0.54.0
0.53.0
0.52.3
0.52.2
0.52.1
0.52.0
0.51.0
0.50.1
0.50.0
0.49.0
0.48.1
0.48.0
0.47.2
0.47.1
0.47.0
0.46.3
0.46.2
0.46.1
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.1
0.41.0
0.40.0
0.39.0
0.38.0
0.37.2
0.37.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.31.0
0.30.1
0.30.0
0.29.0
0.28.1
0.28.0
0.27.0
0.26.0
0.24.0
Builds on the Mox pattern — generates behaviours and dispatch facades from `defcallback` declarations — and adds stateful test doubles powerful enough to test Ecto.Repo operations without a database.
Current section
Files
Jump to
Current section
Files
lib/double_down/repo.ex
# Repo contract for common Ecto Repo operations.
#
# Provides a built-in set of defcallback declarations so that every domain
# using DoubleDown for DB operations doesn't need to redeclare insert/update/
# delete/get/all etc. with identical boilerplate.
#
# ## Usage
#
# # Define a facade in your app:
# defmodule MyApp.Repo do
# use DoubleDown.ContractFacade, contract: DoubleDown.Repo, otp_app: :my_app
# end
#
# {:ok, user} = MyApp.Repo.insert(changeset)
#
# ## Configuration
#
# config :my_app, DoubleDown.Repo, impl: MyApp.EctoRepo
#
if Code.ensure_loaded?(Ecto) do
defmodule DoubleDown.Repo do
@moduledoc """
Repo contract for common Ecto Repo operations.
Provides `defcallback` declarations for the standard write and read operations
from `Ecto.Repo`, so that code using `DoubleDown` for database access doesn't
need to redeclare these with identical boilerplate.
## Usage
# Define a facade in your app:
defmodule MyApp.Repo do
use DoubleDown.ContractFacade, contract: DoubleDown.Repo, otp_app: :my_app
end
changeset = User.changeset(%User{}, attrs)
{:ok, user} = MyApp.Repo.insert(changeset)
user = MyApp.Repo.get!(User, user_id)
## Write Operations
Write operations return `{:ok, struct()} | {:error, Ecto.Changeset.t()}`.
## Bulk Operations
`update_all/3` and `delete_all/2` follow Ecto's return convention of
`{count, nil | list}`.
## Read Operations
Read operations follow Ecto's conventions: `get/2`, `get_by/2`, `one/1`
return `nil` on not-found; `all/1` returns a list; `exists?/1` returns
a boolean; `aggregate/3` returns a term.
Raise-on-not-found variants (`get!/2`, `get_by!/2`, `one!/1`) mirror
Ecto's semantics.
## Relationship to Ecto.Repo
This module is a `DoubleDown.Contract`, not an `Ecto.Repo` behaviour
implementation. It mirrors most of the `Ecto.Repo` API but differs in
two ways:
- **`transact` is the preferred name for `transaction`** — both are
supported. `transact` is recommended for new code; `transaction` is
provided for compatibility with existing `Ecto.Repo` call sites so
that production code works unchanged through both ContractFacade and
DynamicFacade routes.
- **Some rarely-used callbacks omitted** — `checkout/1`, `checked_out?/0`,
`put_dynamic_repo/1`, `get_dynamic_repo/0`, `to_sql/2` are not included.
These can be added incrementally if needed.
"""
use DoubleDown.Contract
# -----------------------------------------------------------------
# Write Operations
# -----------------------------------------------------------------
@doc "Insert a new record from a changeset or struct."
defcallback insert(struct_or_changeset :: Ecto.Changeset.t() | struct()) ::
{:ok, struct()} | {:error, Ecto.Changeset.t()}
@doc "Insert a new record from a changeset or struct with options."
defcallback insert(struct_or_changeset :: Ecto.Changeset.t() | struct(), opts :: keyword()) ::
{:ok, struct()} | {:error, Ecto.Changeset.t()}
@doc "Update an existing record from a changeset."
defcallback update(changeset :: Ecto.Changeset.t()) ::
{:ok, struct()} | {:error, Ecto.Changeset.t()}
@doc "Update an existing record from a changeset with options."
defcallback update(changeset :: Ecto.Changeset.t(), opts :: keyword()) ::
{:ok, struct()} | {:error, Ecto.Changeset.t()}
@doc "Delete a record or changeset."
defcallback delete(struct_or_changeset :: struct() | Ecto.Changeset.t()) ::
{:ok, struct()} | {:error, Ecto.Changeset.t()}
@doc "Delete a record or changeset with options."
defcallback delete(struct_or_changeset :: struct() | Ecto.Changeset.t(), opts :: keyword()) ::
{:ok, struct()} | {:error, Ecto.Changeset.t()}
# -----------------------------------------------------------------
# Bang Write Operations
# -----------------------------------------------------------------
@doc "Insert a new record, raising on failure. Mirrors `Ecto.Repo.insert!/2`."
defcallback insert!(struct_or_changeset :: Ecto.Changeset.t() | struct()) :: struct()
@doc "Insert a new record with options, raising on failure."
defcallback insert!(struct_or_changeset :: Ecto.Changeset.t() | struct(), opts :: keyword()) ::
struct()
@doc "Update an existing record, raising on failure. Mirrors `Ecto.Repo.update!/2`."
defcallback update!(changeset :: Ecto.Changeset.t()) :: struct()
@doc "Update an existing record with options, raising on failure."
defcallback update!(changeset :: Ecto.Changeset.t(), opts :: keyword()) :: struct()
@doc "Delete a record or changeset, raising on failure. Mirrors `Ecto.Repo.delete!/2`."
defcallback delete!(struct_or_changeset :: struct() | Ecto.Changeset.t()) :: struct()
@doc "Delete a record or changeset with options, raising on failure."
defcallback delete!(struct_or_changeset :: struct() | Ecto.Changeset.t(), opts :: keyword()) ::
struct()
# -----------------------------------------------------------------
# Upsert Operations
# -----------------------------------------------------------------
@doc """
Insert or update a record depending on whether it has been loaded.
If the changeset's data has `:loaded` state, delegates to `update`;
otherwise delegates to `insert`. Mirrors `Ecto.Repo.insert_or_update/2`.
"""
defcallback insert_or_update(changeset :: Ecto.Changeset.t()) ::
{:ok, struct()} | {:error, Ecto.Changeset.t()}
@doc "Insert or update a record with options."
defcallback insert_or_update(changeset :: Ecto.Changeset.t(), opts :: keyword()) ::
{:ok, struct()} | {:error, Ecto.Changeset.t()}
@doc """
Insert or update a record, raising on failure.
Mirrors `Ecto.Repo.insert_or_update!/2`.
"""
defcallback insert_or_update!(changeset :: Ecto.Changeset.t()) :: struct()
@doc "Insert or update a record with options, raising on failure."
defcallback insert_or_update!(changeset :: Ecto.Changeset.t(), opts :: keyword()) :: struct()
# -----------------------------------------------------------------
# Raw SQL Operations
# -----------------------------------------------------------------
@doc "Execute a raw SQL query. Returns `{:ok, result} | {:error, term()}`."
defcallback query(sql :: String.t()) :: {:ok, term()} | {:error, term()}
@doc "Execute a raw SQL query with parameters."
defcallback query(sql :: String.t(), params :: list()) :: {:ok, term()} | {:error, term()}
@doc "Execute a raw SQL query with parameters and options."
defcallback query(sql :: String.t(), params :: list(), opts :: keyword()) ::
{:ok, term()} | {:error, term()}
@doc "Execute a raw SQL query, raising on error."
defcallback query!(sql :: String.t()) :: term()
@doc "Execute a raw SQL query with parameters, raising on error."
defcallback query!(sql :: String.t(), params :: list()) :: term()
@doc "Execute a raw SQL query with parameters and options, raising on error."
defcallback query!(sql :: String.t(), params :: list(), opts :: keyword()) :: term()
# -----------------------------------------------------------------
# Bulk Operations
# -----------------------------------------------------------------
@doc "Insert all entries into a schema or source at once."
defcallback insert_all(
source :: Ecto.Queryable.t() | binary(),
entries :: [map() | keyword()]
) :: {non_neg_integer(), nil | list()}
@doc "Insert all entries into a schema or source at once with options."
defcallback insert_all(
source :: Ecto.Queryable.t() | binary(),
entries :: [map() | keyword()],
opts :: keyword()
) :: {non_neg_integer(), nil | list()}
@doc "Update all records matching a queryable."
defcallback update_all(
queryable :: Ecto.Queryable.t(),
updates :: keyword()
) :: {non_neg_integer(), nil | list()}
@doc "Update all records matching a queryable with options."
defcallback update_all(
queryable :: Ecto.Queryable.t(),
updates :: keyword(),
opts :: keyword()
) :: {non_neg_integer(), nil | list()}
@doc "Delete all records matching a queryable."
defcallback delete_all(queryable :: Ecto.Queryable.t()) ::
{non_neg_integer(), nil | list()}
@doc "Delete all records matching a queryable with options."
defcallback delete_all(queryable :: Ecto.Queryable.t(), opts :: keyword()) ::
{non_neg_integer(), nil | list()}
# -----------------------------------------------------------------
# Read Operations
# -----------------------------------------------------------------
@doc "Fetch a single record by primary key. Returns `nil` if not found."
defcallback get(queryable :: Ecto.Queryable.t(), id :: term()) :: struct() | nil
@doc "Fetch a single record by primary key with options."
defcallback get(queryable :: Ecto.Queryable.t(), id :: term(), opts :: keyword()) ::
struct() | nil
@doc """
Fetch a single record by primary key, or raise if not found.
Mirrors `Ecto.Repo.get!/2`.
"""
defcallback get!(queryable :: Ecto.Queryable.t(), id :: term()) :: struct()
@doc "Fetch a single record by primary key with options, or raise if not found."
defcallback get!(queryable :: Ecto.Queryable.t(), id :: term(), opts :: keyword()) :: struct()
@doc "Fetch a single record by the given clauses. Returns `nil` if not found."
defcallback get_by(queryable :: Ecto.Queryable.t(), clauses :: keyword() | map()) ::
struct() | nil
@doc "Fetch a single record by the given clauses with options."
defcallback get_by(
queryable :: Ecto.Queryable.t(),
clauses :: keyword() | map(),
opts :: keyword()
) :: struct() | nil
@doc """
Fetch a single record by the given clauses, or raise if not found.
Mirrors `Ecto.Repo.get_by!/2`.
"""
defcallback get_by!(queryable :: Ecto.Queryable.t(), clauses :: keyword() | map()) :: struct()
@doc "Fetch a single record by the given clauses with options, or raise if not found."
defcallback get_by!(
queryable :: Ecto.Queryable.t(),
clauses :: keyword() | map(),
opts :: keyword()
) :: struct()
@doc "Fetch a single result from a query. Returns `nil` if no result."
defcallback one(queryable :: Ecto.Queryable.t()) :: struct() | nil
@doc "Fetch a single result from a query with options."
defcallback one(queryable :: Ecto.Queryable.t(), opts :: keyword()) :: struct() | nil
@doc """
Fetch a single result from a query, or raise if no result.
Mirrors `Ecto.Repo.one!/1`.
"""
defcallback one!(queryable :: Ecto.Queryable.t()) :: struct()
@doc "Fetch a single result from a query with options, or raise if not found."
defcallback one!(queryable :: Ecto.Queryable.t(), opts :: keyword()) :: struct()
@doc "Fetch all records matching a queryable."
defcallback all(queryable :: Ecto.Queryable.t()) :: list(struct())
@doc "Fetch all records matching a queryable with options."
defcallback all(queryable :: Ecto.Queryable.t(), opts :: keyword()) :: list(struct())
@doc "Check whether any record matching the queryable exists."
defcallback exists?(queryable :: Ecto.Queryable.t()) :: boolean()
@doc "Check whether any record matching the queryable exists, with options."
defcallback exists?(queryable :: Ecto.Queryable.t(), opts :: keyword()) :: boolean()
@doc "Calculate an aggregate over the given field."
defcallback aggregate(queryable :: Ecto.Queryable.t(), aggregate :: atom(), field :: atom()) ::
term()
@doc "Calculate an aggregate over the given field with options."
defcallback aggregate(
queryable :: Ecto.Queryable.t(),
aggregate :: atom(),
field :: atom(),
opts :: keyword()
) :: term()
@doc """
Fetch all records matching the given clauses.
Similar to `get_by`, but returns all matching records as a list
instead of just the first. New in Ecto 3.13. Mirrors `Ecto.Repo.all_by/3`.
"""
defcallback all_by(queryable :: Ecto.Queryable.t(), clauses :: keyword() | map()) ::
list(struct())
@doc "Fetch all records matching the given clauses with options."
defcallback all_by(
queryable :: Ecto.Queryable.t(),
clauses :: keyword() | map(),
opts :: keyword()
) :: list(struct())
# -----------------------------------------------------------------
# Stream Operations
# -----------------------------------------------------------------
@doc """
Return a lazy enumerable that emits all records matching a queryable.
Mirrors `Ecto.Repo.stream/2`. Must be called inside a transaction
in real Ecto; test adapters delegate to fallback.
"""
defcallback stream(queryable :: Ecto.Queryable.t()) :: Enum.t()
@doc "Return a lazy enumerable with options."
defcallback stream(queryable :: Ecto.Queryable.t(), opts :: keyword()) :: Enum.t()
# -----------------------------------------------------------------
# Reload Operations
# -----------------------------------------------------------------
@doc """
Reload a struct or list of structs from the data store.
Re-fetches the record by primary key. Returns `nil` if not found
(for a single struct) or `nil` in the corresponding list position
(for a list). Mirrors `Ecto.Repo.reload/2`.
"""
defcallback reload(struct_or_structs :: struct() | list(struct())) ::
struct() | nil | list(struct() | nil)
@doc "Reload a struct or list of structs with options."
defcallback reload(struct_or_structs :: struct() | list(struct()), opts :: keyword()) ::
struct() | nil | list(struct() | nil)
@doc """
Reload a struct or list of structs, raising if any are not found.
Mirrors `Ecto.Repo.reload!/2`.
"""
defcallback reload!(struct_or_structs :: struct() | list(struct())) ::
struct() | list(struct())
@doc "Reload a struct or list of structs with options, raising if not found."
defcallback reload!(struct_or_structs :: struct() | list(struct()), opts :: keyword()) ::
struct() | list(struct())
# -----------------------------------------------------------------
# Preload Operations
# -----------------------------------------------------------------
@doc """
Preload associations on a struct, list of structs, or nil.
Uses schema reflection to resolve associations from the in-memory
store (when using InMemory fakes) or delegates to the real Repo.
Mirrors `Ecto.Repo.preload/3`.
"""
defcallback preload(
structs_or_struct_or_nil :: list(struct()) | struct() | nil,
preloads :: term()
) :: list(struct()) | struct() | nil
@doc "Preload associations with options."
defcallback preload(
structs_or_struct_or_nil :: list(struct()) | struct() | nil,
preloads :: term(),
opts :: keyword()
) :: list(struct()) | struct() | nil
# -----------------------------------------------------------------
# Load Operations
# -----------------------------------------------------------------
@doc """
Load a schema struct or map from raw data.
Coerces raw data (map, keyword list, or `{columns, values}` tuple)
into a schema struct using Ecto's type system. Stateless — does not
touch the data store. Mirrors `Ecto.Repo.load/2`.
"""
defcallback load(
schema_or_map :: module() | map(),
data :: map() | keyword() | {list(), list()}
) :: struct() | map()
# -----------------------------------------------------------------
# Transaction Operations
# -----------------------------------------------------------------
@doc """
Run a function or `Ecto.Multi` inside a database transaction.
Mirrors `Ecto.Repo.transact/2`. Accepts either a function or an
`Ecto.Multi` struct as the first argument.
## Use with function
The function may be 0-arity or 1-arity:
- **0-arity:** `fn -> {:ok, result} | {:error, reason} end`
- **1-arity:** `fn repo -> {:ok, result} | {:error, reason} end` — where
`repo` is the facade module. Calls to `repo.insert/1`, `repo.get/2`,
etc. go through the facade dispatch chain, ensuring logging, telemetry,
and other facade-level concerns are applied.
The function **must** return `{:ok, result}` or `{:error, reason}`.
On `{:ok, result}`, the transaction is committed and `{:ok, result}` is returned.
On `{:error, reason}`, the transaction is rolled back and `{:error, reason}` is returned.
1-arity functions are wrapped into 0-arity thunks at the facade boundary,
binding the facade module. Implementations always receive a 0-arity function.
## Use with Ecto.Multi
When given an `Ecto.Multi`, all operations are executed in order.
On success, returns `{:ok, changes}` where `changes` is a map of
operation names to their results. On failure, returns
`{:error, failed_operation, failed_value, changes_so_far}`.
The facade module is injected into opts under the `DoubleDown.Repo.Facade`
key so that test adapters can pass it to `DoubleDown.Repo.Impl.MultiStepper`
for `:run` callbacks.
"""
defcallback transact(fun_or_multi :: term()) ::
{:ok, term()} | {:error, term()} | {:error, term(), term(), term()},
pre_dispatch: fn args, facade_mod ->
case args do
[fun] when is_function(fun, 1) ->
[fn -> fun.(facade_mod) end, []]
[fun] when is_function(fun, 0) ->
[fun, []]
[%Ecto.Multi{} = multi] ->
[multi, Keyword.put([], DoubleDown.Repo.Facade, facade_mod)]
end
end
@doc """
Run a function or `Ecto.Multi` inside a database transaction with options.
See `transact/1` for full documentation.
"""
defcallback transact(fun_or_multi :: term(), opts :: keyword()) ::
{:ok, term()} | {:error, term()} | {:error, term(), term(), term()},
pre_dispatch: fn args, facade_mod ->
case args do
[fun, opts] when is_function(fun, 1) ->
[fn -> fun.(facade_mod) end, opts]
[%Ecto.Multi{} = _multi, opts] ->
[Enum.at(args, 0), Keyword.put(opts, DoubleDown.Repo.Facade, facade_mod)]
[fun, _opts] when is_function(fun, 0) ->
args
end
end
@doc """
Run a function or `Ecto.Multi` inside a database transaction.
Mirrors `Ecto.Repo.transaction/2`. Accepts either a function or an
`Ecto.Multi` struct as the first argument.
Unlike `transact/2`, `transaction/2` wraps the function's return
value in `{:ok, _}` and does **not** auto-rollback on `{:error, _}`.
Rollback is only triggered by `Repo.rollback/1`. This matches
`Ecto.Repo.transaction/2` semantics.
`transact/2` is the preferred name for new code; `transaction/2`
is provided for compatibility with existing `Ecto.Repo` call sites.
See `transact/1` for full documentation on function and Multi usage.
"""
defcallback transaction(fun_or_multi :: term()) ::
{:ok, term()} | {:error, term()} | {:error, term(), term(), term()},
pre_dispatch: fn args, facade_mod ->
case args do
[fun] when is_function(fun, 1) ->
[fn -> fun.(facade_mod) end, []]
[fun] when is_function(fun, 0) ->
[fun, []]
[%Ecto.Multi{} = multi] ->
[multi, Keyword.put([], DoubleDown.Repo.Facade, facade_mod)]
end
end
@doc """
Run a function or `Ecto.Multi` inside a database transaction with options.
See `transaction/1` for full documentation.
"""
defcallback transaction(fun_or_multi :: term(), opts :: keyword()) ::
{:ok, term()} | {:error, term()} | {:error, term(), term(), term()},
pre_dispatch: fn args, facade_mod ->
case args do
[fun, opts] when is_function(fun, 1) ->
[fn -> fun.(facade_mod) end, opts]
[%Ecto.Multi{} = _multi, opts] ->
[Enum.at(args, 0), Keyword.put(opts, DoubleDown.Repo.Facade, facade_mod)]
[fun, _opts] when is_function(fun, 0) ->
args
end
end
@doc """
Roll back the current transaction.
Throws `{:rollback, value}`, which is caught by `transact` or
`transaction` and returned as `{:error, value}`.
Mirrors `Ecto.Repo.rollback/1`.
Must be called from within a `transact`/`transaction` callback.
Calling outside a transaction raises.
"""
defcallback rollback(value :: term()) :: no_return()
@doc """
Check whether the current process is inside a transaction.
Returns `true` if called from within a `transact` or `transaction`
callback, `false` otherwise. Mirrors `Ecto.Repo.in_transaction?/0`.
"""
defcallback in_transaction?() :: boolean()
end
end