Could recycle the effort that went into Ecto.NoResultsError
and Ecto.MultipleResultsError
.
defp multiple_results_error(queryable, count) do
info = Ecto.MultipleResultsError.exception(queryable: queryable, count: count)
{:error, {info.__struct__, info.message}}
end
defp no_results_error(queryable) do
info = Ecto.NoResultsError.exception(queryable: queryable)
{:error, {info.__struct__, info.message}}
end
defmodule RepoX do
import Ecto.Query
alias MusicDB.Repo
# Separate module to avoid clash with future Ecto.Repo functions
# shameless ripoff
# https://github.com/elixir-ecto/ecto/blob/master/lib/ecto/repo/queryable.ex
defp assert_schema!(%{from: %{source: {_source, schema}}}) when schema != nil, do: schema
defp assert_schema!(query) do
raise Ecto.QueryError,
query: query,
message: "expected a from expression with a schema"
end
defp query_for_fetch(_queryable, nil) do
raise ArgumentError, "cannot perform RepoX.fetch/3 because the given value is nil"
end
defp query_for_fetch(queryable, id) do
query = Ecto.Queryable.to_query(queryable)
schema = assert_schema!(query)
case schema.__schema__(:primary_key) do
[pk] ->
from(x in query, where: field(x, ^pk) == ^id)
pks ->
raise ArgumentError,
"RepoX.fetch/3 requires the schema #{inspect(schema)} " <>
"to have exactly one primary key, got: #{inspect(pks)}"
end
end
defp query_for_fetch_by(queryable, clauses),
do: where(queryable, [], ^Enum.to_list(clauses))
defp multiple_results_error(queryable, count) do
info = Ecto.MultipleResultsError.exception(queryable: queryable, count: count)
{:error, {info.__struct__, info.message}}
end
defp no_results_error(queryable) do
info = Ecto.NoResultsError.exception(queryable: queryable)
{:error, {info.__struct__, info.message}}
end
def fetch_one(queryable, opts \\ []) do
case Repo.all(queryable, opts) do
[one] -> {:ok, one}
[] -> no_results_error(queryable)
other -> multiple_results_error(queryable, length(other))
end
end
def fetch(queryable, id, opts \\ []),
do: fetch_one(query_for_fetch(queryable, id), opts)
def fetch_by(queryable, clauses, opts \\ []),
do: fetch_one(query_for_fetch_by(queryable, clauses), opts)
end
defmodule Playground do
import Ecto.Query
alias MusicDB.Repo
alias MusicDB.{Album, Artist}
defp by_pk(%{from: %{source: {_source, schema}}} = queryable, id) do
query = Ecto.Queryable.to_query(queryable)
[pk] = schema.__schema__(:primary_key)
from(x in query, where: field(x, ^pk) == ^id)
end
defp by_clauses(queryable, clauses),
do: where(queryable, [], ^Enum.to_list(clauses))
defp one(queryable) do
with {:all, [one]} <- {:all, Repo.all(queryable)} do
{:ok, one}
else
{:all, [_ | _] = results} -> {:error, {:many, results}}
{:all, []} -> {:error, :none}
end
end
def play do
noResultId = 0
someResultId = 1
queryOne = from(a in Album, where: a.id == 1)
queryOneNoResults = from(a in Album, where: a.id == 0)
queryOneMultipleResults = from(a in Album, where: a.id > 0)
IO.puts(inspect(RepoX.fetch_one(queryOne)))
IO.puts(inspect(RepoX.fetch_one(queryOneNoResults)))
IO.puts(inspect(RepoX.fetch_one(queryOneMultipleResults)))
# {:error, {Ecto.MultipleResultsError,
# "expected at most one result but got 5 in query:
#
# from a0 in MusicDB.Album,
# where: a0.id > 0
# "}}
IO.puts(inspect(RepoX.fetch(Album, noResultId)))
IO.puts(inspect(RepoX.fetch(Album, someResultId)))
IO.puts(inspect(RepoX.fetch_by(Album, id: noResultId)))
IO.puts(inspect(RepoX.fetch_by(Album, id: someResultId)))
queryNotNilData = from(r in Artist, select: r.inserted_at)
queryNilData = from(r in Artist, select: r.death_date)
IO.puts(inspect(RepoX.fetch(queryNotNilData, someResultId)))
# {:ok, ~N[2019-05-03 00:16:32]}
IO.puts(inspect(RepoX.fetch(queryNilData, someResultId)))
# {:ok, nil}
IO.puts(inspect(RepoX.fetch(queryNotNilData, noResultId)))
# {:error, {Ecto.NoResultsError,
# "expected at least one result but got none in query:
#
# from a0 in MusicDB.Artist,
# where: a0.id == ^0,
# select: a0.inserted_at
# "}}
queryOneMultipleResults
|> one()
|> inspect
|> IO.puts()
# {:error, {:many, [...]}}
queryNotNilData
|> by_pk(someResultId)
|> one()
|> inspect
|> IO.puts()
# {:ok, ~N[2019-05-03 00:16:32]}
queryNilData
|> by_clauses(id: someResultId)
|> one()
|> inspect
|> IO.puts()
# {:ok, nil}
queryNotNilData
|> by_pk(noResultId)
|> one()
|> inspect
|> IO.puts()
# {:error, :none}
:ok
end
end