Preloads mess with order in has many through

Hi,

I have a problem where Ecto preloads are adding an unwanted and unspecified ORDER BY to the query. Hope I’m sharing enough info for someone to tell what’s going wrong, but here we go:

  def playlist_with_productions(playlist_id, organisation_id, limit) do
    productions = AccessibleContent.get_accessible_productions_for_playlist(organisation_id, playlist_id, limit)

    query = from playlist in Playlist,
      where: playlist.id == ^playlist_id,
      preload: [productions: ^productions]

    IO.puts "START WITH PRELOAD"
    playlist = Repo.one(query)
    IO.puts "END WITH PRELOAD"

    IO.puts "START WITHOUT PRELOAD"
    productions = Repo.all(productions)
    playlist = Map.put(playlist, :productions, productions)
    IO.puts "END WITHOUT PRELOAD"

    playlist
  end

This generates a bunch of SQL, but the version WITHOUT preloads, or I guess perhaps you could call it a manual preload, is correct. The one WITH preloads is not, it returns a bunch of productions, but the ordering is slighty off.

Here’s the Queryable using for (manual) preloading

def get_accessible_productions_for_playlist(organisation_id, playlist_id, limit) do
    query =
        from production in Production,
      join: pp in assoc(production, :playlist_productions),
      join: show in assoc(production, :show),
      join: channel in assoc(show, :channel),
      join: read_access_organisation in assoc(channel, :read_access_organisations),
      where: pp.playlist_id == ^playlist_id,
      where: production.publication_status == "published",
      where: read_access_organisation.id == ^organisation_id,
      order_by: [asc: pp.position],
      preload: ^preload_production()

    query
      |> limit(^limit)

  end

The order_by: [asc: pp.position], does not seems to get respected as the version with preloading inserts different SQL. I’ll paste that below.

START WITH PRELOAD

[debug] QUERY OK source="playlists" db=0.8ms
SELECT p0."id", p0."organisation_id", p0."name", p0."display_title", p0."layout_style", p0."summary", p0."logo_imgix_src", p0."position", p0."created_at", p0."updated_at" FROM "playlists" AS p0 WHERE (p0."id" = $1) [953]
[debug] QUERY OK source="playlist_productions" db=1.1ms decode=0.1ms
SELECT p0."id", p0."playlist_id", p0."production_id", p0."position", p0."created_at", p0."updated_at", p0."playlist_id" FROM "playlist_productions" AS p0 WHERE (p0."playlist_id" = $1) ORDER BY p0."playlist_id" [953]
[debug] QUERY OK source="productions" db=4.3ms decode=0.1ms queue=0.1ms
SELECT p0."id", p0."show_id", p0."genre_id", p0."title", p0."description", p0."display_logo", p0."video_time", p0."adaptive_dash_video_url", p0."adaptive_hls_video_url", p0."ads_enabled", p0."clickout_url", p0."clickout_url_time_offset", p0."age_rating", p0."duration_category", p0."ooyala_content_id", p0."live_broadcast", p0."publication_status", p0."published_at", p0."screenshot_imgix_focalpoint", p0."screenshot_imgix_src", p0."origin", p0."player_skin", p0."slug", p0."vas_content_id", p0."vas_content_status", p0."created_at", p0."updated_at", p0."id" FROM "productions" AS p0 INNER JOIN "playlist_productions" AS p1 ON p1."production_id" = p0."id" INNER JOIN "shows" AS s2 ON s2."id" = p0."show_id" INNER JOIN "channels" AS c3 ON c3."id" = s2."channel_id" INNER JOIN "channel_permissions" AS c5 ON c5."channel_id" = c3."id" INNER JOIN "organisations" AS o4 ON o4."id" = c5."organisation_id" WHERE (p1."playlist_id" = $1) AND (p0."publication_status" = 'published') AND (o4."id" = $2) AND (p0."id" = ANY($3)) ORDER BY p1."position" LIMIT $4 [953, 2, [27756, 35351, 37719, 25266, 24608, 28503, 24563, 24590, 31399, 29114, 25428, 25430, 35114, 24571, 28210, 46256, 47274, 35022, 39692, 28311, 42131, 27263, 25928, 24583, 42484, 28870, 45110, 27491, 45796, 38839, 46262, 26933, 32287, 28820, 32222, 43204, 32757, 33073, 30397, 26610, 36015, 31795, 30894, 33534, 47701, 46276, 39145, ...], 6]
[debug] QUERY OK source="genres" db=0.3ms
SELECT g0."id", g0."key", g0."position", g0."id" FROM "genres" AS g0 WHERE (g0."id" = ANY($1)) [[3, 6]]
[debug] QUERY OK source="progressive_video_urls" db=0.4ms decode=0.1ms
SELECT p0."id", p0."production_id", p0."label", p0."url", p0."created_at", p0."updated_at", p0."production_id" FROM "progressive_video_urls" AS p0 WHERE (p0."production_id" = ANY($1)) ORDER BY p0."production_id" [[44383, 44706, 45110, 46670, 42131, 47274]]
[debug] QUERY OK source="shows" db=0.5ms
SELECT s0."id", s0."channel_id", s0."genre_id", s0."title", s0."publication_status", s0."published_at", s0."summary", s0."ads_forbidden", s0."header_imgix_focalpoint", s0."header_imgix_src", s0."logo_imgix_src", s0."picture_imgix_focalpoint", s0."picture_imgix_src", s0."slug", s0."position", s0."published_productions_count", s0."created_at", s0."updated_at", s0."id" FROM "shows" AS s0 WHERE (s0."id" = $1) [1108]
[debug] QUERY OK source="channels" db=0.4ms
SELECT c0."id", c0."organisation_id", c0."genre_id", c0."highlighted_production_id", c0."name", c0."subtitle", c0."summary_longer", c0."header_imgix_focalpoint", c0."summary", c0."header_imgix_src", c0."logo_imgix_src", c0."logo_with_transparency_imgix_src", c0."publication_status", c0."published_at", c0."ads_forbidden", c0."slug", c0."published_shows_count", c0."created_at", c0."updated_at", c0."id" FROM "channels" AS c0 WHERE (c0."id" = $1) [449]
[debug] QUERY OK source="genres" db=0.3ms
SELECT g0."id", g0."key", g0."position", g0."id" FROM "genres" AS g0 WHERE (g0."id" = $1) [6]
[debug] QUERY OK source="organisations" db=0.3ms
SELECT o0."id", o0."name", o0."vas_brand_label", o0."brand_name", o0."id" FROM "organisations" AS o0 WHERE (o0."id" = $1) [8]
[debug] QUERY OK source="genres" db=1.0ms
SELECT g0."id", g0."key", g0."position", g0."id" FROM "genres" AS g0 WHERE (g0."id" = $1) [3]

END WITH PRELOAD

START WITHOUT PRELOAD

[debug] QUERY OK source=“productions” db=7.6ms
SELECT p0.“id”, p0.“show_id”, p0.“genre_id”, p0.“title”, p0.“description”, p0.“display_logo”, p0.“video_time”, p0.“adaptive_dash_video_url”, p0.“adaptive_hls_video_url”, p0.“ads_enabled”, p0.“clickout_url”, p0.“clickout_url_time_offset”, p0.“age_rating”, p0.“duration_category”, p0.“ooyala_content_id”, p0.“live_broadcast”, p0.“publication_status”, p0.“published_at”, p0.“screenshot_imgix_focalpoint”, p0.“screenshot_imgix_src”, p0.“origin”, p0.“player_skin”, p0.“slug”, p0.“vas_content_id”, p0.“vas_content_status”, p0.“created_at”, p0.“updated_at” FROM “productions” AS p0 INNER JOIN “playlist_productions” AS p1 ON p1.“production_id” = p0.“id” INNER JOIN “shows” AS s2 ON s2.“id” = p0.“show_id” INNER JOIN “channels” AS c3 ON c3.“id” = s2.“channel_id” INNER JOIN “channel_permissions” AS c5 ON c5.“channel_id” = c3.“id” INNER JOIN “organisations” AS o4 ON o4.“id” = c5.“organisation_id” WHERE (p1.“playlist_id” = $1) AND (p0.“publication_status” = ‘published’) AND (o4.“id” = $2) ORDER BY p1.“position” LIMIT $3 [953, 2, 6]
[debug] QUERY OK source=“shows” db=0.3ms
SELECT s0.“id”, s0.“channel_id”, s0.“genre_id”, s0.“title”, s0.“publication_status”, s0.“published_at”, s0.“summary”, s0.“ads_forbidden”, s0.“header_imgix_focalpoint”, s0.“header_imgix_src”, s0.“logo_imgix_src”, s0.“picture_imgix_focalpoint”, s0.“picture_imgix_src”, s0.“slug”, s0.“position”, s0.“published_productions_count”, s0.“created_at”, s0.“updated_at”, s0.“id” FROM “shows” AS s0 WHERE (s0.“id” = $1) [1108]
[debug] QUERY OK source=“genres” db=0.5ms
SELECT g0.“id”, g0.“key”, g0.“position”, g0.“id” FROM “genres” AS g0 WHERE (g0.“id” = $1) [3]
[debug] QUERY OK source=“genres” db=1.3ms
SELECT g0.“id”, g0.“key”, g0.“position”, g0.“id” FROM “genres” AS g0 WHERE (g0.“id” = ANY($1)) [[3, 6]]
[debug] QUERY OK source=“progressive_video_urls” db=1.4ms
SELECT p0.“id”, p0.“production_id”, p0.“label”, p0.“url”, p0.“created_at”, p0.“updated_at”, p0.“production_id” FROM “progressive_video_urls” AS p0 WHERE (p0.“production_id” = ANY($1)) ORDER BY p0.“production_id” [[44383, 44706, 45110, 46670, 42131, 47274]]
[debug] QUERY OK source=“channels” db=1.9ms
SELECT c0.“id”, c0.“organisation_id”, c0.“genre_id”, c0.“highlighted_production_id”, c0.“name”, c0.“subtitle”, c0.“summary_longer”, c0.“header_imgix_focalpoint”, c0.“summary”, c0.“header_imgix_src”, c0.“logo_imgix_src”, c0.“logo_with_transparency_imgix_src”, c0.“publication_status”, c0.“published_at”, c0.“ads_forbidden”, c0.“slug”, c0.“published_shows_count”, c0.“created_at”, c0.“updated_at”, c0.“id” FROM “channels” AS c0 WHERE (c0.“id” = $1) [449]
[debug] QUERY OK source=“genres” db=0.2ms queue=0.1ms
SELECT g0.“id”, g0.“key”, g0.“position”, g0.“id” FROM “genres” AS g0 WHERE (g0.“id” = $1) [6]
[debug] QUERY OK source=“organisations” db=0.8ms
SELECT o0.“id”, o0.“name”, o0.“vas_brand_label”, o0.“brand_name”, o0.“id” FROM “organisations” AS o0 WHERE (o0.“id” = $1) [8]
END WITHOUT PRELOAD

I really hope someone can tell me why the preload version adds this unwanted ORDER BY:

SELECT p0."id", p0."playlist_id", p0."production_id", p0."position", p0."created_at", p0."updated_at", p0."playlist_id" FROM "playlist_productions" AS p0 WHERE (p0."playlist_id" = $1) ORDER BY p0."playlist_id" [953]

You can pass a query or function for each item preloaded. So, if the order is undesired you can override it.

https://hexdocs.pm/ecto/Ecto.Query.html#preload/3-preload-queries
https://hexdocs.pm/ecto/Ecto.Query.html#preload/3-preload-functions

Thanks Blatyo,

but why is this ignored in the first place?

hmm I see what you mean, so I guess what I have done to fix it is basically a poor man’s version of a preload-function?

You haven’t told the preload to use the join association you’ve already defined. I believe if you did the following it would work as desired.

query =
      from production in Production,
      join: pp in assoc(production, :playlist_productions),
      join: show in assoc(production, :show),
      join: channel in assoc(show, :channel),
      join: read_access_organisation in assoc(channel, :read_access_organisations),
      where: pp.playlist_id == ^playlist_id,
      where: production.publication_status == "published",
      where: read_access_organisation.id == ^organisation_id,
      order_by: [asc: pp.position],
      preload: [playlist_productions: pp]

I changed the last line.