DoubleDown.Repo.InMemory (double_down v0.48.1)

Copy Markdown View Source

Stateful in-memory Repo fake (closed-world). Recommended default.

The state is the complete truth — if a record isn't in the store, it doesn't exist. This makes the adapter authoritative for all bare schema operations without needing a fallback function.

Implements DoubleDown.Contract.Dispatch.FakeHandler, so it can be used by module name with Double.fake:

Usage with Double.fake

# Basic — all bare-schema reads work without fallback:
DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.InMemory)

# With seed data:
DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.InMemory,
  [%User{id: 1, name: "Alice"}, %Post{id: 1, title: "Hello"}])

# Layer expects for failure simulation:
DoubleDown.Repo
|> DoubleDown.Double.fake(DoubleDown.Repo.InMemory)
|> DoubleDown.Double.expect(:insert, fn [changeset] ->
  {:error, Ecto.Changeset.add_error(changeset, :email, "taken")}
end)

ExMachina integration

setup do
  DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.InMemory)
  insert(:user, name: "Alice", email: "alice@example.com")
  insert(:user, name: "Bob", email: "bob@example.com")
  :ok
end

test "lists all users" do
  assert [_, _] = MyApp.Repo.all(User)
end

test "finds user by email" do
  assert %User{name: "Alice"} =
    MyApp.Repo.get_by(User, email: "alice@example.com")
end

test "count users" do
  assert 2 = MyApp.Repo.aggregate(User, :count, :id)
end

Authoritative operations (bare schema queryables)

CategoryOperationsBehaviour
Writesinsert, update, deleteStore in state
PK readsget, get!nil/raise on miss (no fallback)
Clause readsget_by, get_by!Scan and filter
Collectionall, one/one!, exists?Scan state
AggregatesaggregateCompute from state
Bulk writesinsert_all, delete_all, update_all (set:)Modify state
Transactionstransact, rollbackDelegate to sub-operations

Ecto.Query fallback

Operations with Ecto.Query queryables (containing where, join, select etc.) cannot be evaluated in-memory. These fall through to the fallback function, or raise with a clear error:

DoubleDown.Double.fake(DoubleDown.Repo, DoubleDown.Repo.InMemory, [],
  fallback_fn: fn
    _contract, :all, [%Ecto.Query{}], _state -> []
  end
)

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

Limitations

The following insert_all options are silently ignored because they depend on database constraints that don't exist in memory:

  • on_conflict — upsert behaviour (:nothing, {:replace, fields}, etc.)
  • conflict_target — which fields/index define the conflict

This is intentional: test code should match production code without modification. Testing constraint behaviour requires a real database via DataCase.

When returning: is a list of fields, insert_all returns maps containing only those fields (matching Ecto adapter behaviour). returning: true returns full structs.

Binary table name sources (e.g. "users") are not supported by insert_all — use a fallback function or Double.expect for these.

See also

Summary

Functions

Stateful handler function with closed-world semantics.

Create a new InMemory state map. Same API as Repo.OpenInMemory.new/2.

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 with closed-world semantics.

Bare schema reads are authoritative — the state is the full truth. Ecto.Query reads fall through to the fallback function.

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

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

Create a new InMemory state map. Same API as Repo.OpenInMemory.new/2.

seed(records)

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

Convert a list of structs into the nested state map for seeding.