Stateful in-memory Repo fake (open-world).
Uses open-world semantics: the state may be incomplete. If a record is not found in state, the adapter falls through to the fallback function — it cannot assume the record doesn't exist.
For most use cases, prefer DoubleDown.Repo.InMemory (closed-world)
which is authoritative for all bare-schema reads without a
fallback. Use OpenInMemory when the state is deliberately
partial — e.g. you've inserted some records but expect the
fallback to provide others.
Implements DoubleDown.Contract.Dispatch.FakeHandler, so it can
be used by module name with Double.fake:
Usage with Double.fake
# PK reads only — no fallback needed for records in state:
DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.OpenInMemory)
# With seed data and fallback for non-PK reads:
DoubleDown.Double.fake(
DoubleDown.Repo,
DoubleDown.Repo.OpenInMemory,
[%User{id: 1, name: "Alice"}],
fallback_fn: fn
_contract, :all, [User], state ->
Map.get(state, User, %{}) |> Map.values()
_contract, :get_by, [User, [email: "alice@example.com"]], _state ->
%User{id: 1, name: "Alice"}
end
)
# Layer expects for failure simulation:
DoubleDown.Repo
|> DoubleDown.Double.fake(DoubleDown.Repo.OpenInMemory)
|> DoubleDown.Double.expect(:insert, fn [changeset] ->
{:error, Ecto.Changeset.add_error(changeset, :email, "taken")}
end)Operation dispatch (3-stage)
| Category | Operations | Behaviour |
|---|---|---|
| Writes | insert, update, delete | Always handled by state |
| PK reads | get, get! | State first, then fallback |
| get_by | get_by, get_by! | PK lookup when PK in clauses, then fallback |
| Other reads | one, all, exists?, aggregate | Always fallback |
| Bulk | insert_all, update_all, delete_all | Always fallback |
| Transactions | transact, rollback | Delegate to sub-operations |
Note: Unlike Repo.InMemory (closed-world), bulk operations
(insert_all, update_all, delete_all) in OpenInMemory always
delegate to the fallback function and do not mutate in-memory
state. This is consistent with open-world semantics — the state
is partial, so bulk mutations could produce incorrect results.
For reads, the dispatch stages are:
- State lookup — if the record is in state, return it
- Fallback function — a 4-arity
(contract, operation, args, state)function that handles operations the state can't answer - Raise — clear error suggesting a fallback clause
When to use which Repo fake
| Fake | State | Best for |
|---|---|---|
Repo.Stub | None | Fire-and-forget writes, canned reads |
Repo.InMemory | Complete store | All bare-schema reads; ExMachina factories |
Repo.OpenInMemory | Partial store | PK reads in state, fallback for rest |
See also
DoubleDown.Repo.InMemory— closed-world variant (recommended). Authoritative for all bare-schema reads without a fallback.DoubleDown.Repo.Stub— stateless stub for fire-and-forget writes.
Summary
Functions
Stateful handler function for use with DoubleDown.Testing.set_stateful_handler/3
or DoubleDown.Double.fake/2..4.
Create a new InMemory state map.
Convert a list of structs into the nested state map for seeding.
Types
Functions
Stateful handler function for use with DoubleDown.Testing.set_stateful_handler/3
or DoubleDown.Double.fake/2..4.
Handles all DoubleDown.Repo operations. The function signature is
(contract, operation, args, store) -> {result, new_store}.
Write operations are handled directly by the state. PK-based reads check the state first, then fall through to the fallback function. All other reads go directly to the fallback function. If no fallback is registered or the fallback doesn't handle the operation, an error is raised.
Create a new InMemory state map.
Arguments
seed— seed data to pre-populate the store. Accepts:- a list of structs:
[%User{id: 1, name: "Alice"}] - a pre-built store map:
%{User => %{1 => %User{id: 1}}} %{}or[]for empty (default)
- a list of structs:
opts— keyword options::fallback_fn— a 4-arity function(contract, operation, args, state) -> resultthat handles operations the state cannot answer authoritatively. Thestateargument is the clean store map (without internal keys like:__fallback_fn__), so the fallback can compose canned data with records inserted during the test. If the function raisesFunctionClauseError, dispatch falls through to an error.
Examples
# Empty state, no fallback
DoubleDown.Repo.OpenInMemory.new()
# Seeded with a list of structs
DoubleDown.Repo.OpenInMemory.new([%User{id: 1, name: "Alice"}])
# Seeded with a map
DoubleDown.Repo.OpenInMemory.new(%{User => %{1 => %User{id: 1, name: "Alice"}}})
# Seeded with fallback
DoubleDown.Repo.OpenInMemory.new(
[%User{id: 1, name: "Alice"}],
fallback_fn: fn
_contract, :all, [User], state ->
Map.get(state, User, %{}) |> Map.values()
end
)Legacy keyword-only form (still supported)
DoubleDown.Repo.OpenInMemory.new(seed: [%User{id: 1}], fallback_fn: fn ...)
Convert a list of structs into the nested state map for seeding.
Example
DoubleDown.Repo.OpenInMemory.seed([
%User{id: 1, name: "Alice"},
%User{id: 2, name: "Bob"}
])
#=> %{User => %{1 => %User{id: 1, name: "Alice"},
# 2 => %User{id: 2, name: "Bob"}}}