How to preload many_to_many via a preloader function?

Hey all,
As an exercise (and as a part of a bigger effort to get rid of an older Rails app), I’ve been doing preloading of part of my Ecto records through preloader functions – in this particular case, fetching them from a warmed-up cache.

Normally, passing a function that accepts a list of IDs is enough and I already did that successfully with belongs_to associations, but when the association is has_many or many_to_many, the function receives a list containing only one ID, and that’s the ID of the object whose associated objects I am trying to preload through a function.

The official docs (https://hexdocs.pm/ecto/Ecto.Query.html#preload/3) are not helping me in this case because I want to directly fetch association IDs from cache; having the source ID (the object that contains the associated objects) is not an info I can use, unless I also cache back-references when warming up the cache.

What am I missing?


Example:
orders <=> orders_promotions <=> promotions

preload_promotions = fn(ids) ->
  IO.inspect(ids)
  PromotionCache.get_multiple(ids)
end

from(x in Order, where: x.id == 10)
|> preload(promotions: ^preload_promotions)

I get several errors and my iex is restarted:

** (EXIT from #PID<0.844.0>) evaluator process exited with reason: an exception was raised:
    ** (BadMapError) expected a map, got: nil
        (stdlib) :maps.get(:id, nil)
        (ecto) lib/ecto/repo/preloader.ex:156: anonymous fn/2 in Ecto.Repo.Preloader.fetch_query/8
        (elixir) lib/enum.ex:1270: Enum."-map/2-lists^map/1-0-"/2
        (ecto) lib/ecto/repo/preloader.ex:156: Ecto.Repo.Preloader.fetch_query/8
        (ecto) lib/ecto/repo/preloader.ex:119: Ecto.Repo.Preloader.preload_assoc/10
        (elixir) lib/task/supervised.ex:85: Task.Supervised.do_apply/2
        (elixir) lib/task/supervised.ex:36: Task.Supervised.reply/5
        (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3

18:29:06.844 [error] Task #PID<0.1686.0> started from #PID<0.844.0> terminating
** (BadMapError) expected a map, got: nil
    (stdlib) :maps.get(:id, nil)
    (ecto) lib/ecto/repo/preloader.ex:156: anonymous fn/2 in Ecto.Repo.Preloader.fetch_query/8
    (elixir) lib/enum.ex:1270: Enum."-map/2-lists^map/1-0-"/2
    (ecto) lib/ecto/repo/preloader.ex:156: Ecto.Repo.Preloader.fetch_query/8
    (ecto) lib/ecto/repo/preloader.ex:119: Ecto.Repo.Preloader.preload_assoc/10
    (elixir) lib/task/supervised.ex:85: Task.Supervised.do_apply/2
    (elixir) lib/task/supervised.ex:36: Task.Supervised.reply/5
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Function: &:erlang.apply/2
    Args: [#Function<11.39838178/2 in Ecto.Repo.Preloader.preload_each/4>, [{{:assoc, %Ecto.Association.ManyToMany{cardinality: :many, defaults: [], field: :promotions, join_keys: [order_id: :id, promotion_id: :id], join_through: "spree_orders_promotions", on_cast: nil, on_delete: :nothing, on_replace: :raise, owner: Njoy.Schema.Order, owner_key: :id, queryable: Njoy.Schema.Promotion, related: Njoy.Schema.Promotion, relationship: :child, unique: false}, {-2, :id}}, nil, #Function<1.28834455/1 in Njoy.Order.preload_with_cache/1>, []}, [caller: #PID<0.844.0>]]]
Interactive Elixir (1.5.2) - press Ctrl+C to exit (type h() ENTER for help)