DoubleDown.Repo.OpenInMemory (double_down v0.48.1)

Copy Markdown View Source

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)

CategoryOperationsBehaviour
Writesinsert, update, deleteAlways handled by state
PK readsget, get!State first, then fallback
get_byget_by, get_by!PK lookup when PK in clauses, then fallback
Other readsone, all, exists?, aggregateAlways fallback
Bulkinsert_all, update_all, delete_allAlways fallback
Transactionstransact, rollbackDelegate 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:

  1. State lookup — if the record is in state, return it
  2. Fallback function — a 4-arity (contract, operation, args, state) function that handles operations the state can't answer
  3. Raise — clear error suggesting a fallback clause

When to use which Repo fake

FakeStateBest for
Repo.StubNoneFire-and-forget writes, canned reads
Repo.InMemoryComplete storeAll bare-schema reads; ExMachina factories
Repo.OpenInMemoryPartial storePK reads in state, fallback for rest

See also

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

store()

@type store() :: DoubleDown.Repo.Impl.InMemoryShared.store()

Functions

dispatch(contract, operation, args, store)

@spec dispatch(module(), atom(), list(), store()) :: {term(), store()}

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.

new(seed \\ %{}, opts \\ [])

@spec new(
  term(),
  keyword()
) :: store()

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)
  • opts — keyword options:
    • :fallback_fn — a 4-arity function (contract, operation, args, state) -> result that handles operations the state cannot answer authoritatively. The state argument 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 raises FunctionClauseError, 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 ...)

seed(records)

@spec seed([struct()]) :: store()

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"}}}