Current section
Files
Jump to
Current section
Files
lib/dataloader/ecto.ex
if Code.ensure_loaded?(Ecto) do
defmodule Dataloader.Ecto do
@moduledoc """
Ecto source for Dataloader
This defines a schema and an implementation of the `Dataloader.Source` protocol
for handling Ecto related batching.
A simple Ecto source only needs to know about your application's Repo.
## Basic Usage
Querying by primary key (analogous to Ecto.Repo.get/3):
```elixir
source = Dataloader.Ecto.new(MyApp.Repo)
loader =
Dataloader.new
|> Dataloader.add_source(Accounts, source)
|> Dataloader.load(Accounts, User, 1)
|> Dataloader.load_many(Accounts, Organization, [4, 9])
|> Dataloader.run
organizations = Dataloader.get_many(loader, Accounts, Organization, [4,9])
```
Querying for associations. Here we look up the `:users` association on all
the organizations, and the `:organization` for a single user.
```elixir
loader =
loader
|> Dataloader.load(Accounts, :organization, user)
|> Dataloader.load_many(Accounts, :users, organizations)
|> Dataloader.run
```
Querying by a column other than the primary key:
```elixir
loader =
loader
|> Dataloader.load(Accounts, {:one, User}, name: "admin")
|> Dataloader.run
```
Here we pass a keyword list of length one. It is only possible to
query by one column here; for more complex queries, see "filtering" below.
Notice here that we need to also specify the cardinality in the batch_key
(`:many` or `:one`), which will decide whether to return a list or a single
value (or nil). This is because the column may not be a key and there may be
multiple matching records. Note also that even if we are returning `:many` values
here from multiple matching records, this is still a call to `Dataloader.load/4`
rather than `Dataloader.load_many/4` because there is only one val specified.
## Filtering / Ordering
`Dataloader.Ecto.new/2` can receive a 2 arity function that can be used to apply
broad ordering and filtering rules, as well as handle parameters
```elixir
source = Dataloader.Ecto.new(MyApp.Repo, query: &Accounts.query/2)
loader =
Dataloader.new
|> Dataloader.add_source(Accounts, source)
```
When we call `Dataloader.load/4` we can pass in a tuple as the batch key with a keyword list
of parameters in addition to the queryable or assoc_field
```elixir
# with a queryable
loader
|> Dataloader.load(Accounts, {User, order: :name}, 1)
# or an association
loader
|> Dataloader.load_many(Accounts, {:users, order: :name}, organizations)
# this is still supported
loader
|> Dataloader.load(Accounts, User, 1)
# as is this
loader
|> Dataloader.load(:accounts, :user, organization)
```
In all cases the `Accounts.query` function would be:
```elixir
def query(User, params) do
field = params[:order] || :id
from u in User, order_by: [asc: field(u, ^field)]
end
def query(queryable, _) do
queryable
end
```
If we query something that ends up using the `User` schema, whether directly
or via association, the `query/2` function will match on the first clause and
we can handle the params. If no params are supplied, the params arg defaults
to `source.default_params` which itself defaults to `%{}`.
`default_params` is an extremely useful place to store values like the current user:
```elixir
source = Dataloader.Ecto.new(MyApp.Repo, [
query: &Accounts.query/2,
default_params: %{current_user: current_user},
])
loader =
Dataloader.new
|> Dataloader.add_source(Accounts, source)
|> Dataloader.load_many(Accounts, Organization, ids)
|> Dataloader.run
# the query function
def query(Organization, %{current_user: user}) do
from o in Organization,
join: m in assoc(o, :memberships),
where: m.user_id == ^user.id
end
def query(queryable, _) do
queryable
end
```
In our query function we are pattern matching on the current user to make sure
that we are only able to lookup data in organizations that the user actually
has a membership in. Additional options you specify IE `{Organization, %{order: :asc}}`
are merged into the default.
## Custom batch queries
There are cases where you want to run the batch function yourself. To do this
we can add a custom `run_batch/5` callback to our source.
The `run_batch/5` function is executed with the query returned from the `query/2`
function.
For example, we want to get the post count for a set of users.
First we add a custom `run_batch/5` function.
```
def run_batch(_, query, :post_count, users, repo_opts) do
user_ids = Enum.map(users, & &1.id)
default_count = 0
result =
query
|> where([p], p.user_id in ^user_ids)
|> group_by([p], p.user_id)
|> select([p], {p.user_id, count("*")})
|> Repo.all(repo_opts)
|> Map.new()
for %{id: id} <- users do
[Map.get(result, id, default_count)]
end
end
# Fallback to original run_batch
def run_batch(queryable, query, col, inputs, repo_opts) do
Dataloader.Ecto.run_batch(Repo, queryable, query, col, inputs, repo_opts)
end
```
This function is supplied with a list of users, does a query and will return
the post count for each of user. If the user id is not found in the resultset,
because the user has no posts, we return a post count of 0.
Now we need to call `run_batch/5` from dataloader. First we add a few posts
to the database.
After that, the custom `run_batch/5` function is provided to the Dataloader
source. Now, we can load the post count for several users. When the dataloader
runs it will call the custom `run_batch/5` and we can retrieve the posts counts
for each individual user.
```
[user1, user2] = [%User{id: 1}, %User{id: 2}]
rows = [
%{user_id: user1.id, title: "foo", published: true},
%{user_id: user1.id, title: "baz", published: false}
]
_ = Repo.insert_all(Post, rows)
source =
Dataloader.Ecto.new(
Repo,
query: &query/2,
run_batch: &run_batch/5
)
loader =
Dataloader.new()
|> Dataloader.add_source(Posts, source)
loader =
loader
|> Dataloader.load(Posts, {:one, Post}, post_count: user1)
|> Dataloader.load(Posts, {:one, Post}, post_count: user2)
|> Dataloader.run()
# Returns 2
Dataloader.get(loader, Posts, {:one, Post}, post_count: user1)
# Returns 0
Dataloader.get(loader, Posts, {:one, Post}, post_count: user2)
```
Additional params for the `query/2` function can be passed to the load functions
with a 3-tuple.
For example, to limit the above example to only return published we can add a query
function to filter the published posts:
```
def query(Post, %{published: published}) do
from p in Post,
where: p.published == ^published
end
def query(queryable, _) do
queryable
end
```
And we can return the published posts with a 3-tuple on the loader:
```
loader =
loader
|> Dataloader.load(Posts, {:one, Post}, post_count: user1)
|> Dataloader.load(Posts, {:one, Post, %{published: true}}, post_count: user1)
|> Dataloader.run()
# Returns 2
Dataloader.get(loader, Posts, {:one, Post}, post_count: user1)
# Returns 1
Dataloader.get(loader, Posts, {:one, Post, %{published: true}}, post_count: user1)
```
"""
defstruct [
:repo,
:query,
:run_batch,
repo_opts: [],
batches: %{},
results: %{},
default_params: %{},
options: []
]
@type t :: %__MODULE__{
repo: Ecto.Repo.t(),
query: query_fun,
repo_opts: repo_opts,
batches: map,
results: map,
default_params: map,
run_batch: batch_fun,
options: Keyword.t()
}
@type query_fun :: (Ecto.Queryable.t(), any -> Ecto.Queryable.t())
@type repo_opts :: Keyword.t()
@type batch_fun :: (Ecto.Queryable.t(), Ecto.Query.t(), any, [any], repo_opts -> [any])
@type opt ::
{:query, query_fun}
| {:default_params, map()}
| {:repo_opts, repo_opts}
| {:timeout, pos_integer}
| {:run_batch, batch_fun()}
import Ecto.Query
@doc """
Create an Ecto Dataloader source.
This module handles retrieving data from Ecto for dataloader. It requires a
valid Ecto Repo. It also accepts a `repo_opts:` option which is handy for
applying options to any calls to Repo functions that this module makes.
For example, you can use this module in a multi-tenant context by using
the `prefix` option:
```
Dataloader.Ecto.new(MyApp.Repo, repo_opts: [prefix: "tenant"])
```
"""
@spec new(Ecto.Repo.t(), [opt]) :: t
def new(repo, opts \\ []) do
data =
opts
|> Keyword.put_new(:query, &query/2)
|> Keyword.put_new(:run_batch, &run_batch(repo, &1, &2, &3, &4, &5))
opts = Keyword.take(opts, [:timeout])
%__MODULE__{repo: repo, options: opts}
|> struct(data)
end
@doc """
Default implementation for loading a batch. Handles looking up records by
column
"""
@spec run_batch(
repo :: Ecto.Repo.t(),
queryable :: Ecto.Queryable.t(),
query :: Ecto.Query.t(),
col :: any,
inputs :: [any],
repo_opts :: repo_opts
) :: [any]
def run_batch(repo, queryable, query, col, inputs, repo_opts) do
results = load_rows(col, inputs, queryable, query, repo, repo_opts)
grouped_results = group_results(results, col)
for value <- inputs do
grouped_results
|> Map.get(value, [])
|> Enum.reverse()
end
end
defp load_rows(col, inputs, queryable, query, repo, repo_opts) do
pk = queryable.__schema__(:primary_key)
case query do
%Ecto.Query{limit: limit, offset: offset}
when pk != [col] and (not is_nil(limit) or not is_nil(offset)) ->
load_rows_lateral(col, inputs, queryable, query, repo, repo_opts)
_ ->
query
|> where([q], field(q, ^col) in ^inputs)
|> repo.all(repo_opts)
end
end
defp load_rows_lateral(col, inputs, queryable, query, repo, repo_opts) do
# Approximate a postgres unnest with a subquery
inputs_query =
queryable
|> where([q], field(q, ^col) in ^inputs)
|> select(^[col])
|> distinct(true)
inner_query =
query
|> where([q], field(q, ^col) == field(parent_as(:input), ^col))
|> exclude(:preload)
results =
from(input in subquery(inputs_query), as: :input)
|> join(:inner_lateral, [], q in subquery(inner_query), on: true)
|> select([_input, q], q)
|> repo.all(repo_opts)
case query.preloads do
[] -> results
# Preloads can't be used in a subquery, using Repo.preload instead
preloads -> repo.preload(results, preloads, repo_opts)
end
end
defp group_results(results, col) do
results
|> Enum.reduce(%{}, fn result, grouped ->
value = Map.get(result, col)
Map.update(grouped, value, [result], &[result | &1])
end)
end
defp query(schema, _) do
schema
end
defimpl Dataloader.Source do
def run(source) do
results =
Dataloader.async_safely(__MODULE__, :run_batches, [source],
async?: Dataloader.Source.async?(source)
)
results =
Map.merge(source.results, results, fn _, {:ok, v1}, {:ok, v2} ->
{:ok, Map.merge(v1, v2)}
end)
%{source | results: results, batches: %{}}
end
def fetch(source, batch_key, item) do
{batch_key, item_key, _item} =
batch_key
|> normalize_key(source.default_params)
|> get_keys(item)
with {:ok, batch} <- Map.fetch(source.results, batch_key) do
fetch_item_from_batch(batch, item_key)
else
:error ->
{:error, "Unable to find batch #{inspect(batch_key)}"}
end
end
defp fetch_item_from_batch(tried_and_failed = {:error, _reason}, _item_key),
do: tried_and_failed
defp fetch_item_from_batch({:ok, batch}, item_key) do
case Map.fetch(batch, item_key) do
:error -> {:error, "Unable to find item #{inspect(item_key)} in batch"}
result -> result
end
end
def put(source, _batch, _item, %Ecto.Association.NotLoaded{}) do
source
end
def put(source, batch, item, result) do
batch = normalize_key(batch, source.default_params)
{batch_key, item_key, _item} = get_keys(batch, item)
results =
Map.update(
source.results,
batch_key,
{:ok, %{item_key => result}},
fn {:ok, map} -> {:ok, Map.put(map, item_key, result)} end
)
%{source | results: results}
end
def load(source, batch, item) do
{batch_key, item_key, item} =
batch
|> normalize_key(source.default_params)
|> get_keys(item)
if fetched?(source.results, batch_key, item_key) do
source
else
entry = {item_key, item}
update_in(source.batches, fn batches ->
Map.update(batches, batch_key, MapSet.new([entry]), &MapSet.put(&1, entry))
end)
end
end
defp fetched?(results, batch_key, item_key) do
case results do
%{^batch_key => {:ok, %{^item_key => _}}} -> true
_ -> false
end
end
def pending_batches?(%{batches: batches}) do
batches != %{}
end
def timeout(%{options: options}) do
options[:timeout]
end
def async?(%{repo: repo}) do
not repo.in_transaction?()
end
defp chase_down_queryable([field], schema) do
case schema.__schema__(:association, field) do
%{queryable: queryable} ->
queryable
%Ecto.Association.HasThrough{through: through} ->
chase_down_queryable(through, schema)
val ->
raise """
Valid association #{field} not found on schema #{inspect(schema)}
Got: #{inspect(val)}
"""
end
end
defp chase_down_queryable([field | fields], schema) do
case schema.__schema__(:association, field) do
%{queryable: queryable} ->
chase_down_queryable(fields, queryable)
%Ecto.Association.HasThrough{through: [through_field | through_fields]} ->
[through_field | through_fields ++ fields]
|> chase_down_queryable(schema)
end
end
defp get_keys({assoc_field, opts}, %schema{} = record) when is_atom(assoc_field) do
validate_queryable(schema)
primary_keys = schema.__schema__(:primary_key)
id =
if primary_keys == [] do
record
|> :erlang.term_to_iovec()
|> :erlang.md5()
else
Enum.map(primary_keys, &Map.get(record, &1))
end
queryable = chase_down_queryable([assoc_field], schema)
{{:assoc, schema, self(), assoc_field, queryable, opts}, id, record}
end
defp get_keys({{cardinality, queryable}, opts}, value) when is_atom(queryable) do
validate_queryable(queryable)
{_, col, value} = normalize_value(queryable, value)
{{:queryable, self(), queryable, cardinality, col, opts}, value, value}
end
defp get_keys({queryable, opts}, value) when is_atom(queryable) do
validate_queryable(queryable)
case normalize_value(queryable, value) do
{:primary, col, value} ->
{{:queryable, self(), queryable, :one, col, opts}, value, value}
{:not_primary, col, _value} ->
raise """
Cardinality required unless using primary key
The non-primary key column specified was: #{inspect(col)}
"""
end
end
defp get_keys(key, item) do
raise """
Invalid: #{inspect(key)}
#{inspect(item)}
The batch key must either be a schema module, or an association name.
"""
end
defp validate_queryable(queryable) do
unless {:__schema__, 1} in queryable.__info__(:functions) do
raise "The given module - #{queryable} - is not an Ecto schema."
end
rescue
_ in UndefinedFunctionError ->
raise Dataloader.GetError, """
The given atom - #{inspect(queryable)} - is not a module.
This can happen if you intend to pass an Ecto struct in your call to
`dataloader/4` but pass something other than a struct.
"""
end
defp normalize_value(queryable, [{col, value}]) do
case queryable.__schema__(:primary_key) do
[^col] ->
{:primary, col, value}
_ ->
{:not_primary, col, value}
end
end
defp normalize_value(queryable, value) do
[primary_key] = queryable.__schema__(:primary_key)
{:primary, primary_key, value}
end
# This code was totally OK until cardinalities showed up. Now it's ugly :(
# It is however correct, which is nice.
@cardinalities [:one, :many]
defp normalize_key({cardinality, queryable}, default_params)
when cardinality in @cardinalities do
normalize_key({{cardinality, queryable}, []}, default_params)
end
defp normalize_key({cardinality, queryable, params}, default_params)
when cardinality in @cardinalities do
normalize_key({{cardinality, queryable}, params}, default_params)
end
defp normalize_key({key, params}, default_params) do
{key, Enum.into(params, default_params)}
end
defp normalize_key(key, default_params) do
{key, default_params}
end
def run_batches(source) do
options = [
timeout: source.options[:timeout] || Dataloader.default_timeout(),
on_timeout: :kill_task
]
batches = Enum.to_list(source.batches)
results =
batches
|> maybe_async_stream(
fn batch ->
id = :erlang.unique_integer()
system_time = System.system_time()
start_time_mono = System.monotonic_time()
emit_start_event(id, system_time, batch)
batch_result = run_batch(batch, source)
emit_stop_event(id, start_time_mono, batch)
batch_result
end,
options,
Dataloader.Source.async?(source)
)
|> Enum.map(fn
{:ok, {_key, result}} -> {:ok, result}
{:exit, reason} -> {:error, reason}
end)
batches
|> Enum.map(fn {key, _set} -> key end)
|> Enum.zip(results)
|> Map.new()
end
defp maybe_async_stream(batches, fun, options, true) do
async_stream(batches, fun, options)
end
defp maybe_async_stream(batches, fun, _options, _) do
Enum.map(batches, fn batch ->
try do
{:ok, fun.(batch)}
rescue
e ->
{:exit, e}
end
end)
end
defp run_batch(
{{:queryable, pid, queryable, cardinality, col, opts} = key, entries},
source
) do
inputs = Enum.map(entries, &elem(&1, 0))
query = source.query.(queryable, opts)
repo_opts = Keyword.put(source.repo_opts, :caller, pid)
cardinality_mapper = cardinality_mapper(cardinality, queryable)
coerced_inputs =
if type = queryable.__schema__(:type, col) do
for input <- inputs do
{:ok, input} = Ecto.Type.cast(type, input)
input
end
else
inputs
end
results =
queryable
|> source.run_batch.(query, col, coerced_inputs, repo_opts)
|> Enum.map(cardinality_mapper)
results =
inputs
|> Enum.zip(results)
|> Map.new()
{key, results}
end
defp run_batch({{:assoc, schema, pid, field, queryable, opts} = key, records}, source) do
{ids, records} = Enum.unzip(records)
query = source.query.(queryable, opts) |> Ecto.Queryable.to_query()
repo_opts = Keyword.put(source.repo_opts, :caller, pid)
empty = schema |> struct |> Map.fetch!(field)
records = records |> Enum.map(&Map.put(&1, field, empty))
results =
if query.limit || query.offset do
records
|> preload_lateral(field, query, source.repo, repo_opts)
else
records
|> source.repo.preload([{field, query}], repo_opts)
end
results = results |> Enum.map(&Map.get(&1, field))
{key, Map.new(Enum.zip(ids, results))}
end
def preload_lateral([], _assoc, _query, _opts), do: []
def preload_lateral([%schema{} = struct | _] = structs, assoc, query, repo, repo_opts) do
[pk] = schema.__schema__(:primary_key)
# Carry the database prefix across from already-loaded records if not already set
repo_opts = Keyword.put_new(repo_opts, :prefix, struct.__meta__.prefix)
assocs = expand_assocs(schema, [assoc])
query_excluding_preloads = exclude(query, :preload)
inner_query =
assocs
|> Enum.reverse()
|> build_preload_lateral_query(query_excluding_preloads, :join_first)
|> maybe_distinct(assocs)
results =
from(x in schema,
as: :parent,
inner_lateral_join: y in subquery(inner_query),
on: true,
where: field(x, ^pk) in ^Enum.map(structs, &Map.get(&1, pk)),
select: {field(x, ^pk), y}
)
|> repo.all(repo_opts)
results =
case query.preloads do
[] ->
results
# Preloads can't be used in a subquery, using Repo.preload instead
preloads ->
{keys, vals} = Enum.unzip(results)
vals = repo.preload(vals, preloads, repo_opts)
Enum.zip(keys, vals)
end
{keyed, default} =
case schema.__schema__(:association, assoc) do
%{cardinality: :one} ->
{results |> Map.new(), nil}
%{cardinality: :many} ->
{Enum.group_by(results, fn {k, _} -> k end, fn {_, v} -> v end), []}
end
structs
|> Enum.map(&Map.put(&1, assoc, Map.get(keyed, Map.get(&1, pk), default)))
end
defp expand_assocs(_schema, []), do: []
defp expand_assocs(schema, [assoc | rest]) do
case schema.__schema__(:association, assoc) do
%Ecto.Association.HasThrough{through: through} ->
expand_assocs(schema, through ++ rest)
a ->
[a | expand_assocs(a.queryable, rest)]
end
end
defp build_preload_lateral_query(
[%Ecto.Association.ManyToMany{} = assoc],
query,
:join_first
) do
[{owner_join_key, owner_key}, {related_join_key, related_key}] = assoc.join_keys
join_query =
query
|> join(:inner, [x], y in ^assoc.join_through,
on: field(x, ^related_key) == field(y, ^related_join_key)
)
|> where([..., x], field(x, ^owner_join_key) == field(parent_as(:parent), ^owner_key))
binds_count = Ecto.Query.Builder.count_binds(join_query)
join_query
|> Ecto.Association.combine_joins_query(assoc.where, 0)
|> Ecto.Association.combine_joins_query(assoc.join_where, binds_count - 1)
end
defp build_preload_lateral_query(
[%Ecto.Association.ManyToMany{} = assoc],
query,
:join_last
) do
[{owner_join_key, owner_key}, {related_join_key, related_key}] = assoc.join_keys
join_query =
query
|> join(:inner, [..., x], y in ^assoc.join_through,
on: field(x, ^related_key) == field(y, ^related_join_key)
)
|> where([..., x], field(x, ^owner_join_key) == field(parent_as(:parent), ^owner_key))
binds_count = Ecto.Query.Builder.count_binds(join_query)
join_query
|> Ecto.Association.combine_joins_query(assoc.where, binds_count - 2)
|> Ecto.Association.combine_joins_query(assoc.join_where, binds_count - 1)
end
defp build_preload_lateral_query([assoc], query, :join_first) do
query
|> where([x], field(x, ^assoc.related_key) == field(parent_as(:parent), ^assoc.owner_key))
|> Ecto.Association.combine_assoc_query(assoc.where)
end
defp build_preload_lateral_query([assoc], query, :join_last) do
join_query =
query
|> where(
[..., x],
field(x, ^assoc.related_key) == field(parent_as(:parent), ^assoc.owner_key)
)
binds_count = Ecto.Query.Builder.count_binds(join_query)
join_query
|> Ecto.Association.combine_joins_query(assoc.where, binds_count - 1)
end
defp build_preload_lateral_query(
[%Ecto.Association.ManyToMany{} = assoc | rest],
query,
:join_first
) do
[{owner_join_key, owner_key}, {related_join_key, related_key}] = assoc.join_keys
join_query =
query
|> join(:inner, [x], y in ^assoc.join_through,
on: field(x, ^related_key) == field(y, ^related_join_key)
)
|> join(:inner, [..., x], y in ^assoc.owner,
on: field(x, ^owner_join_key) == field(y, ^owner_key)
)
binds_count = Ecto.Query.Builder.count_binds(join_query)
query =
join_query
|> Ecto.Association.combine_joins_query(assoc.where, 0)
|> Ecto.Association.combine_joins_query(assoc.join_where, binds_count - 2)
build_preload_lateral_query(rest, query, :join_last)
end
defp build_preload_lateral_query(
[%Ecto.Association.ManyToMany{} = assoc | rest],
query,
:join_last
) do
[{owner_join_key, owner_key}, {related_join_key, related_key}] = assoc.join_keys
join_query =
query
|> join(:inner, [..., x], y in ^assoc.join_through,
on: field(x, ^related_key) == field(y, ^related_join_key)
)
|> join(:inner, [..., x], y in ^assoc.owner,
on: field(x, ^owner_join_key) == field(y, ^owner_key)
)
binds_count = Ecto.Query.Builder.count_binds(join_query)
query =
join_query
|> Ecto.Association.combine_joins_query(assoc.where, binds_count - 3)
|> Ecto.Association.combine_joins_query(assoc.join_where, binds_count - 2)
build_preload_lateral_query(rest, query, :join_last)
end
defp build_preload_lateral_query(
[assoc | rest],
query,
:join_first
) do
query =
query
|> join(:inner, [x], y in ^assoc.owner,
on: field(x, ^assoc.related_key) == field(y, ^assoc.owner_key)
)
|> Ecto.Association.combine_joins_query(assoc.where, 0)
build_preload_lateral_query(rest, query, :join_last)
end
defp build_preload_lateral_query(
[assoc | rest],
query,
:join_last
) do
binds_count = Ecto.Query.Builder.count_binds(query)
join_query =
query
|> Ecto.Association.combine_joins_query(assoc.where, binds_count - 1)
|> join(:inner, [..., x], y in ^assoc.owner,
on: field(x, ^assoc.related_key) == field(y, ^assoc.owner_key)
)
build_preload_lateral_query(rest, join_query, :join_last)
end
defp maybe_distinct(%Ecto.Query{distinct: distinct} = query, _) when not is_nil(distinct),
do: query
defp maybe_distinct(query, [%Ecto.Association.Has{}, %Ecto.Association.BelongsTo{} | _]),
do: distinct(query, true)
defp maybe_distinct(query, [%Ecto.Association.ManyToMany{} | _]), do: distinct(query, true)
defp maybe_distinct(query, [_assoc | rest]), do: maybe_distinct(query, rest)
defp maybe_distinct(query, []), do: query
defp emit_start_event(id, system_time, batch) do
:telemetry.execute(
[:dataloader, :source, :batch, :run, :start],
%{system_time: system_time},
%{id: id, batch: batch}
)
end
defp emit_stop_event(id, start_time_mono, batch) do
:telemetry.execute(
[:dataloader, :source, :batch, :run, :stop],
%{duration: System.monotonic_time() - start_time_mono},
%{id: id, batch: batch}
)
end
defp cardinality_mapper(:many, _) do
fn
value when is_list(value) -> value
value -> [value]
end
end
defp cardinality_mapper(:one, queryable) do
fn
[] ->
nil
[value] ->
value
other when is_list(other) ->
raise Ecto.MultipleResultsError, queryable: queryable, count: length(other)
other ->
other
end
end
# Optionally use `async_stream/3` function from
# `opentelemetry_process_propagator` if available
if Code.ensure_loaded?(OpentelemetryProcessPropagator.Task) do
@spec async_stream(Enumerable.t(), (term -> term), keyword) :: Enumerable.t()
defdelegate async_stream(items, fun, opts), to: OpentelemetryProcessPropagator.Task
else
@spec async_stream(Enumerable.t(), (term -> term), keyword) :: Enumerable.t()
defdelegate async_stream(items, fun, opts), to: Task
end
end
end
end