Lotus.Source.Adapters.Ecto (Lotus v1.2.0)

Copy Markdown View Source

Adapter wrapping any Ecto.Repo module in the Lotus.Source.Adapter behaviour.

Delegates database-specific operations to dialect modules under Lotus.Source.Adapters.Ecto.Dialects.* based on the repo's underlying Ecto adapter.

Usage

adapter = Lotus.Source.Adapters.Ecto.wrap("main", MyApp.Repo)

Lotus.Source.Adapter.execute_query(adapter, "SELECT 1", [], [])
Lotus.Source.Adapter.list_schemas(adapter)

The state field of the resulting %Adapter{} struct holds the repo module itself, since Ecto repos are statically supervised and don't require explicit connection management.

Dynamic repos

A repo started at runtime without a name (MyApp.Repo.start_link(name: nil)) is reachable only through Ecto.Repo.put_dynamic_repo/1, which applies to the calling process. Pass the state as a map to have the adapter make that call around every repo operation:

adapter =
  Lotus.Source.Adapters.Ecto.wrap("tenant", %{repo: MyApp.Repo, dynamic: pid})

:repo is the repo module the process was started from. :dynamic is what put_dynamic_repo/1 takes, a pid or an atom, or a {:via, _, _} or {:global, _} name that the adapter resolves to a pid on every call, so a repo started under Lotus.Source.Registry.via/2 can be wrapped before it is running. The previous dynamic repo of the calling process is restored after each call. with_repo/2 is the helper the callbacks use and is available to adapters that override one of them.

Extending with custom Ecto-backed adapters

External libraries can create Ecto-backed adapters by writing a dialect module and a one-liner adapter:

defmodule LotusMSSql.Adapter do
  use Lotus.Source.Adapters.Ecto, dialect: LotusMSSql.Dialect
end

The use macro injects default implementations for all Lotus.Source.Adapter callbacks, delegating shared Ecto logic to helper functions in this module and dialect-specific callbacks to the provided :dialect module. All callbacks are defoverridable.

Summary

Types

A repo module started at runtime, reached through put_dynamic_repo/1.

Where a dynamic repo runs: what Ecto.Repo.put_dynamic_repo/1 takes, or a process name that GenServer.whereis/1 resolves on every call.

The adapter state: a statically supervised repo module or a dynamic repo.

Functions

Returns the list of built-in per-dialect Ecto adapter modules.

Whether this adapter can handle the given data source entry.

Detects the source type from a repo module's underlying Ecto adapter.

Returns the repo module of an adapter state, for either shape.

Runs fun with the repo module of state, routed to the dynamic repo when state carries one.

Wraps an Ecto.Repo module in an %Adapter{} struct.

Types

dynamic_state()

@type dynamic_state() :: %{
  :repo => module(),
  :dynamic => dynamic_target(),
  optional(atom()) => term()
}

A repo module started at runtime, reached through put_dynamic_repo/1.

dynamic_target()

@type dynamic_target() ::
  pid() | atom() | {:global, term()} | {:via, module(), term()}

Where a dynamic repo runs: what Ecto.Repo.put_dynamic_repo/1 takes, or a process name that GenServer.whereis/1 resolves on every call.

state()

@type state() :: module() | dynamic_state()

The adapter state: a statically supervised repo module or a dynamic repo.

Functions

builtin_adapters()

Returns the list of built-in per-dialect Ecto adapter modules.

Used by Lotus.Source.Resolvers.Static to avoid duplicating this list.

can_handle?(repo)

@spec can_handle?(term()) :: boolean()

Whether this adapter can handle the given data source entry.

Returns true for any module that exports __adapter__/0, and for the %{repo: module, dynamic: _} form when its :repo does. This is intentionally broad — it acts as a catch-all fallback for Ecto repos that don't match a more specific per-dialect adapter. The resolver checks per-dialect adapters first (via builtin_adapters/0 and source_adapters config), so this only matches repos with unknown Ecto adapters.

detect_source_type(repo_module)

@spec detect_source_type(module()) :: Lotus.Source.Adapter.source_type()

Detects the source type from a repo module's underlying Ecto adapter.

Examples

iex> Lotus.Source.Adapters.Ecto.detect_source_type(MyApp.Repo)
:postgres

repo_module(repo)

@spec repo_module(state()) :: module()

Returns the repo module of an adapter state, for either shape.

with_repo(repo, fun)

@spec with_repo(state(), (module() -> result)) :: result when result: term()

Runs fun with the repo module of state, routed to the dynamic repo when state carries one.

For a repo module the function is called with the module as is. For a %{repo: module, dynamic: target} state, the target is resolved to a pid or an atom, Ecto.Repo.put_dynamic_repo/1 is called on the repo module, fun runs, and the dynamic repo the calling process had before is put back, whether fun returns or raises. A {:via, _, _} or {:global, _} target whose process is not running raises ArgumentError.

Examples

Lotus.Source.Adapters.Ecto.with_repo(MyApp.Repo, & &1.query!("SELECT 1"))

Lotus.Source.Adapters.Ecto.with_repo(%{repo: MyApp.Repo, dynamic: pid}, fn repo ->
  repo.query!("SELECT 1")
end)

wrap(name, state)

@spec wrap(String.t(), state()) :: Lotus.Source.Adapter.t()

Wraps an Ecto.Repo module in an %Adapter{} struct.

The resulting adapter delegates all callbacks to the appropriate source implementation based on the repo's underlying Ecto adapter.

Parameters

  • name — a human-readable identifier (e.g. "main", "warehouse")
  • state — the Ecto.Repo module (e.g. MyApp.Repo), or %{repo: MyApp.Repo, dynamic: pid_or_name} for a repo started at runtime (see the "Dynamic repos" section)

Examples

iex> adapter = Lotus.Source.Adapters.Ecto.wrap("main", MyApp.Repo)
%Lotus.Source.Adapter{name: "main", module: Lotus.Source.Adapters.Ecto, ...}

iex> Lotus.Source.Adapters.Ecto.wrap("tenant", %{repo: MyApp.Repo, dynamic: pid})
%Lotus.Source.Adapter{name: "tenant", state: %{repo: MyApp.Repo, dynamic: pid}, ...}