Edit: Full example of a working lateral join.
iex(1)> alias MusicDB.{Repo,Album,Track}
[MusicDB.Repo, MusicDB.Album, MusicDB.Track]
iex(2)> import Ecto.Query
Ecto.Query
iex(3)> compact_rest = fn
...(3)> {nil,nil,nil,[]} ->
...(3)> []
...(3)> {_, album, tracks, rest} = init ->
...(3)> [Map.put(album, :tracks, :lists.reverse(tracks)) | rest]
...(3)> end
#Function<6.127694169/1 in :erl_eval.expr/5>
iex(4)> next_acc = fn
...(4)> (%{id: id} = a, t, {id, album, tracks, rest}) ->
...(4)> {id, album, [t|tracks], rest}
...(4)> (%{id: id} = a, t, acc) ->
...(4)> {id, a, [t], compact_rest.(acc)}
...(4)> end
#Function<18.127694169/3 in :erl_eval.expr/5>
iex(5)> row_to_acc = fn (%{album: album, track: track}, acc) -> next_acc.(album, track, acc) end
#Function<12.127694169/2 in :erl_eval.expr/5>
iex(6)> make_result = fn rows ->
...(6)> rows
...(6)> |> List.foldl({nil,nil,nil,[]}, row_to_acc)
...(6)> |> compact_rest.()
...(6)> |> :lists.reverse()
...(6)> end
#Function<6.127694169/1 in :erl_eval.expr/5>
iex(7)> album_ids = [1,2]
[1, 2]
iex(8)> album_query = from(a in Album, [where: a.id in ^album_ids])
#Ecto.Query<from a in MusicDB.Album, where: a.id in ^[1, 2]>
iex(9)> query = from(q in subquery(album_query), [
...(9)> left_lateral_join: t in fragment("SELECT * FROM tracks AS t WHERE t.album_id = ? ORDER BY t.inserted_at DESC NULLS LAST LIMIT ?", q.id,3), on: true,
...(9)> select: %{album: %{id: q.id, artist_id: q.artist_id, title: q.title}},
...(9)> select_merge: %{track: %{id: t.id, index: t.index, title: t.title, duration: t.duration}}
...(9)> ])
#Ecto.Query<from a in subquery(from a in MusicDB.Album,
where: a.id in ^[1, 2]),
left_join_lateral: f in fragment("SELECT * FROM tracks AS t WHERE t.album_id = ? ORDER BY t.inserted_at DESC NULLS LAST LIMIT ?", a.id, 3),
on: true,
select: %{album: %{id: a.id, artist_id: a.artist_id, title: a.title}, track: %{id: f.id, index: f.index, title: f.title, duration: f.duration}}>
iex(10)> results = Repo.all(query)
18:31:36.947 [debug] QUERY OK db=2.5ms
SELECT s0."id", s0."artist_id", s0."title", f1."id", f1."index", f1."title", f1."duration" FROM (SELECT a0."id" AS "id", a0."title" AS "title", a0."inserted_at" AS "inserted_at", a0."updated_at" AS "updated_at", a0."artist_id" AS "artist_id" FROM "albums" AS a0 WHERE (a0."id" = ANY($1))) AS s0 LEFT OUTER JOIN LATERAL (SELECT * FROM tracks AS t WHERE t.album_id = s0."id" ORDER BY t.inserted_at DESC NULLS LAST LIMIT 3) AS f1 ON TRUE [[1, 2]]
[
%{
album: %{artist_id: 1, id: 1, title: "Kind Of Blue"},
track: %{duration: 481, id: 5, index: 5, title: "Flamenco Sketches"}
},
%{
album: %{artist_id: 1, id: 1, title: "Kind Of Blue"},
track: %{duration: 693, id: 4, index: 4, title: "All Blues"}
},
%{
album: %{artist_id: 1, id: 1, title: "Kind Of Blue"},
track: %{duration: 327, id: 3, index: 3, title: "Blue In Green"}
},
%{
album: %{artist_id: 1, id: 2, title: "Cookin' At The Plugged Nickel"},
track: %{duration: 1061, id: 10, index: 5, title: "No Blues"}
},
%{
album: %{artist_id: 1, id: 2, title: "Cookin' At The Plugged Nickel"},
track: %{duration: 754, id: 9, index: 4, title: "Miles"}
},
%{
album: %{artist_id: 1, id: 2, title: "Cookin' At The Plugged Nickel"},
track: %{duration: 896, id: 8, index: 3, title: "Walkin'"}
}
]
iex(11)> result = make_result.(results)
[
%{
artist_id: 1,
id: 1,
title: "Kind Of Blue",
tracks: [
%{duration: 481, id: 5, index: 5, title: "Flamenco Sketches"},
%{duration: 693, id: 4, index: 4, title: "All Blues"},
%{duration: 327, id: 3, index: 3, title: "Blue In Green"}
]
},
%{
artist_id: 1,
id: 2,
title: "Cookin' At The Plugged Nickel",
tracks: [
%{duration: 1061, id: 10, index: 5, title: "No Blues"},
%{duration: 754, id: 9, index: 4, title: "Miles"},
%{duration: 896, id: 8, index: 3, title: "Walkin'"}
]
}
]
iex(12)>
Raw SQL that served as the template:
SELECT a1.id AS album_id, a1.artist_id, a1.title AS album_title, t1.id AS track_id, t1.index, t1.title, t1.duration FROM
(SELECT id, artist_id, title FROM albums WHERE id in (1,2)) a1
LEFT JOIN LATERAL
(SELECT id, index, title, duration FROM tracks WHERE album_id = a1.id ORDER BY inserted_at DESC NULLS LAST LIMIT 3) t1
ON true;