# Port contract for common Ecto Repo operations.
#
# Mirrors DoubleDown.Repo's defcallback declarations so that skuld's
# effectful Repo surface covers the same operations available through
# plain DoubleDown dispatch.
#
# ## Usage
#
# alias Skuld.Repo
#
# comp do
# {:ok, record} <- Repo.insert(changeset)
# # ...
# end
#
# ## Handler Installation
#
# # Production — delegates to your Ecto Repo
# defmodule MyApp.Repo.Port do
# use Skuld.Repo.Ecto, repo: MyApp.Repo
# end
#
# comp
# |> Port.with_handler(%{Port.Repo => MyApp.Repo.Port})
# |> Comp.run!()
#
# # Test — in-memory executor with dispatch logging
# comp
# |> Port.with_handler(
# %{Port.Repo => Port.Repo.Test},
# log: true,
# output: fn r, state -> {r, state.log} end
# )
# |> Comp.run!()
#
defmodule Skuld.Repo.Contract do
@moduledoc """
Port contract for common Ecto Repo operations.
Mirrors `DoubleDown.Repo`'s `defcallback` declarations so that skuld's
effectful Repo surface covers the same operations available through
plain DoubleDown dispatch. This enables the upgrade/downgrade path:
code using `DoubleDown.Repo` via `ContractFacade` can switch to skuld's
effectful dispatch (or back) without changing the contract.
## Write Operations
Write operations return `{:ok, struct()} | {:error, Ecto.Changeset.t()}`.
Opts-accepting variants (`insert/2`, `update/2`, `delete/2`) are provided
for use in `Ecto.Multi` callbacks and similar contexts.
Bang write variants (`insert!/1,2`, `update!/1,2`, `delete!/1,2`) are
explicit operations that raise on failure, mirroring `Ecto.Repo`.
## Upsert Operations
`insert_or_update/1,2` and their bang variants delegate to insert or
update based on whether the changeset's data has `:loaded` state.
## Bulk Operations
`insert_all/3`, `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.
Bang read variants (`get!/2`, `get_by!/2`, `one!/1`) are provided as
separate operations that mirror Ecto's raise-on-not-found semantics.
In the effectful context these dispatch `Throw` instead of raising.
## Transaction Operations
Transaction operations (`transact`, `transaction`, `rollback`,
`in_transaction?`) are deliberately omitted from the Repo contract.
In skuld, transaction coordination is handled by the `Transaction`
effect (`Skuld.Effects.Transaction`), which provides env state
rollback, nested savepoints, and optional DB transaction wrapping.
Similarly, `Ecto.Multi` is not supported — it is a limited
sequencing mechanism that is superseded by skuld's effect-based
computation composition (`comp do ... end`).
"""
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."
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."
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()],
opts :: keyword()
) :: {non_neg_integer(), nil | list()}
@doc "Update all records matching a queryable."
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(), 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 dispatch Throw if not found.
Mirrors `Ecto.Repo.get!/2` — in the effectful context this dispatches
`Throw` instead of raising.
"""
defcallback get!(queryable :: Ecto.Queryable.t(), id :: term()) :: struct()
@doc "Fetch a single record by primary key with options, or dispatch Throw."
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 dispatch Throw 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 dispatch Throw."
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 dispatch Throw 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 dispatch Throw."
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."
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."
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."
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."
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."
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. Mirrors `Ecto.Repo.load/2`."
defcallback load(
schema_or_map :: module() | map(),
data :: map() | keyword() | {list(), list()}
) :: struct() | map()
end