Skip to content

Commit 4c2f560

Browse files
author
Leo Batyuk
committed
Wip use lists for storing keys in assoc structs
1 parent 869116e commit 4c2f560

File tree

5 files changed

+138
-64
lines changed

5 files changed

+138
-64
lines changed

lib/ecto.ex

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -508,10 +508,15 @@ defmodule Ecto do
508508
refl = %{owner_key: owner_key} = Ecto.Association.association_from_schema!(schema, assoc)
509509

510510
values =
511-
Enum.uniq for(struct <- structs,
512-
assert_struct!(schema, struct),
513-
key = Map.fetch!(struct, owner_key),
514-
do: key)
511+
structs
512+
|> Enum.filter(&assert_struct!(schema, &1))
513+
|> Enum.map(fn struct ->
514+
owner_key
515+
# TODO remove List.wrap once all assocs use lists
516+
|> List.wrap
517+
|> Enum.map(&Map.fetch!(struct, &1))
518+
end)
519+
|> Enum.uniq
515520

516521
case assocs do
517522
[] ->

lib/ecto/association.ex

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ defmodule Ecto.Association do
3535
required(:cardinality) => :one | :many,
3636
required(:relationship) => :parent | :child,
3737
required(:owner) => atom,
38-
required(:owner_key) => atom | list(atom),
38+
required(:owner_key) => list(atom),
3939
required(:field) => atom,
4040
required(:unique) => boolean,
4141
optional(atom) => any}
@@ -236,8 +236,15 @@ defmodule Ecto.Association do
236236
# for the final WHERE clause with values.
237237
{_, query, _, dest_out_key} = Enum.reduce(joins, {source, query, counter, source.out_key}, fn curr_rel, {prev_rel, query, counter, _} ->
238238
related_queryable = curr_rel.schema
239-
240-
next = join(query, :inner, [{src, counter}], dest in ^related_queryable, on: field(src, ^prev_rel.out_key) == field(dest, ^curr_rel.in_key))
239+
# TODO remove this once all relations store keys in lists
240+
in_keys = List.wrap(curr_rel.in_key)
241+
out_keys = List.wrap(prev_rel.out_key)
242+
next = query
243+
# join on the first field of the foreign key
244+
|> join(:inner, [{src, counter}], dest in ^related_queryable, on: field(src, ^hd(out_keys)) == field(dest, ^hd(in_keys)))
245+
# add the rest of the foreign key fields, if any
246+
|> composite_joins_query(counter, counter + 1, tl(out_keys), tl(in_keys))
247+
# consider where clauses on assocs
241248
|> combine_joins_query(curr_rel.where, counter + 1)
242249

243250
{curr_rel, next, counter + 1, curr_rel.out_key}
@@ -321,6 +328,16 @@ defmodule Ecto.Association do
321328
end)
322329
end
323330

331+
# TODO docs
332+
def composite_joins_query(query, _binding_src, _binding_dst, [], []) do
333+
query
334+
end
335+
def composite_joins_query(query, binding_src, binding_dst, [src_key | src_keys], [dst_key | dst_keys]) do
336+
# TODO
337+
[query, binding_src, binding_dst, [src_key | src_keys], [dst_key | dst_keys]] |> IO.inspect(label: :composite_joins_query)
338+
query
339+
end
340+
324341
@doc """
325342
Add the default assoc query where clauses to a join.
326343
@@ -336,6 +353,16 @@ defmodule Ecto.Association do
336353
%{query | joins: joins ++ [%{join_expr | on: %{join_on | expr: expr, params: params}}]}
337354
end
338355

356+
# TODO docs
357+
def composite_assoc_query(query, _binding_src, [], []) do
358+
query
359+
end
360+
def composite_assoc_query(query, binding_dst, [dst_key | dst_keys], [value | values]) do
361+
# TODO
362+
[query, binding_dst, [dst_key | dst_keys], [value | values]] |> IO.inspect(label: :composite_assoc_query)
363+
query
364+
end
365+
339366
@doc """
340367
Add the default assoc query where clauses a provided query.
341368
"""
@@ -633,6 +660,10 @@ defmodule Ecto.Association do
633660

634661
defp primary_key!(nil), do: []
635662
defp primary_key!(struct), do: Ecto.primary_key!(struct)
663+
664+
def missing_fields(queryable, related_key) do
665+
Enum.filter related_key, &is_nil(queryable.__schema__(:type, &1))
666+
end
636667
end
637668

638669
defmodule Ecto.Association.Has do
@@ -645,8 +676,8 @@ defmodule Ecto.Association.Has do
645676
* `field` - The name of the association field on the schema
646677
* `owner` - The schema where the association was defined
647678
* `related` - The schema that is associated
648-
* `owner_key` - The key on the `owner` schema used for the association
649-
* `related_key` - The key on the `related` schema used for the association
679+
* `owner_key` - The list of columns that form the key on the `owner` schema used for the association
680+
* `related_key` - The list of columns that form the key on the `related` schema used for the association
650681
* `queryable` - The real query to use for querying association
651682
* `on_delete` - The action taken on associations when schema is deleted
652683
* `on_replace` - The action taken on associations when schema is replaced
@@ -674,8 +705,8 @@ defmodule Ecto.Association.Has do
674705
{:error, "associated schema #{inspect queryable} does not exist"}
675706
not function_exported?(queryable, :__schema__, 2) ->
676707
{:error, "associated module #{inspect queryable} is not an Ecto schema"}
677-
is_nil queryable.__schema__(:type, related_key) ->
678-
{:error, "associated schema #{inspect queryable} does not have field `#{related_key}`"}
708+
[] != (missing_fields = Ecto.Association.missing_fields(queryable, related_key)) ->
709+
{:error, "associated schema #{inspect queryable} does not have field(s) `#{inspect missing_fields}`"}
679710
true ->
680711
:ok
681712
end
@@ -687,12 +718,13 @@ defmodule Ecto.Association.Has do
687718
cardinality = Keyword.fetch!(opts, :cardinality)
688719
related = Ecto.Association.related_from_query(queryable, name)
689720

690-
ref =
721+
refs =
691722
module
692723
|> Module.get_attribute(:primary_key)
693724
|> get_ref(opts[:references], name)
725+
|> List.wrap()
694726

695-
for ref <- List.wrap(ref) do
727+
for ref <- refs do
696728
unless Module.get_attribute(module, :ecto_fields)[ref] do
697729
raise ArgumentError, "schema does not have the field #{inspect ref} used by " <>
698730
"association #{inspect name}, please set the :references option accordingly"
@@ -728,13 +760,19 @@ defmodule Ecto.Association.Has do
728760
raise ArgumentError, "expected `:where` for #{inspect name} to be a keyword list, got: `#{inspect where}`"
729761
end
730762

763+
foreign_key = case opts[:foreign_key] do
764+
nil -> Enum.map(refs, &Ecto.Association.association_key(module, &1))
765+
key when is_atom(key) -> [key]
766+
keys when is_list(keys) -> keys
767+
end
768+
731769
%__MODULE__{
732770
field: name,
733771
cardinality: cardinality,
734772
owner: module,
735773
related: related,
736-
owner_key: ref,
737-
related_key: opts[:foreign_key] || Ecto.Association.association_key(module, ref),
774+
owner_key: refs,
775+
related_key: foreign_key,
738776
queryable: queryable,
739777
on_delete: on_delete,
740778
on_replace: on_replace,
@@ -759,19 +797,23 @@ defmodule Ecto.Association.Has do
759797

760798
@impl true
761799
def joins_query(%{related_key: related_key, owner: owner, owner_key: owner_key, queryable: queryable} = assoc) do
762-
from(o in owner, join: q in ^queryable, on: field(q, ^related_key) == field(o, ^owner_key))
800+
# TODO find out how to handle a dynamic list of fields here
801+
from(o in owner, join: q in ^queryable, on: field(q, ^hd(related_key)) == field(o, ^hd(owner_key)))
802+
|> Ecto.Association.composite_joins_query(0, 1, tl(related_key), tl(owner_key))
763803
|> Ecto.Association.combine_joins_query(assoc.where, 1)
764804
end
765805

766806
@impl true
767807
def assoc_query(%{related_key: related_key, queryable: queryable} = assoc, query, [value]) do
768-
from(x in (query || queryable), where: field(x, ^related_key) == ^value)
808+
from(x in (query || queryable), where: field(x, ^hd(related_key)) == ^hd(value))
809+
|> Ecto.Association.composite_assoc_query(0, tl(related_key), tl(value))
769810
|> Ecto.Association.combine_assoc_query(assoc.where)
770811
end
771812

772813
@impl true
773814
def assoc_query(%{related_key: related_key, queryable: queryable} = assoc, query, values) do
774-
from(x in (query || queryable), where: field(x, ^related_key) in ^values)
815+
from(x in (query || queryable), where: field(x, ^hd(related_key)) in ^Enum.map(values, &hd/1))
816+
|> Ecto.Association.composite_assoc_query(0, tl(related_key), Enum.map(values, &tl/1))
775817
|> Ecto.Association.combine_assoc_query(assoc.where)
776818
end
777819

@@ -1000,16 +1042,16 @@ defmodule Ecto.Association.BelongsTo do
10001042
{:error, "associated schema #{inspect queryable} does not exist"}
10011043
not function_exported?(queryable, :__schema__, 2) ->
10021044
{:error, "associated module #{inspect queryable} is not an Ecto schema"}
1003-
is_nil queryable.__schema__(:type, related_key) ->
1004-
{:error, "associated schema #{inspect queryable} does not have field `#{related_key}`"}
1045+
[] != (missing_fields = Ecto.Association.missing_fields(queryable, related_key)) ->
1046+
{:error, "associated schema #{inspect queryable} does not have field(s) `#{inspect missing_fields}`"}
10051047
true ->
10061048
:ok
10071049
end
10081050
end
10091051

10101052
@impl true
10111053
def struct(module, name, opts) do
1012-
ref = if ref = opts[:references], do: ref, else: :id
1054+
refs = if ref = opts[:references], do: List.wrap(ref), else: [:id]
10131055
queryable = Keyword.fetch!(opts, :queryable)
10141056
related = Ecto.Association.related_from_query(queryable, name)
10151057
on_replace = Keyword.get(opts, :on_replace, :raise)
@@ -1031,8 +1073,8 @@ defmodule Ecto.Association.BelongsTo do
10311073
field: name,
10321074
owner: module,
10331075
related: related,
1034-
owner_key: Keyword.fetch!(opts, :foreign_key),
1035-
related_key: ref,
1076+
owner_key: List.wrap(Keyword.fetch!(opts, :foreign_key)),
1077+
related_key: refs,
10361078
queryable: queryable,
10371079
on_replace: on_replace,
10381080
defaults: defaults,
@@ -1049,19 +1091,22 @@ defmodule Ecto.Association.BelongsTo do
10491091

10501092
@impl true
10511093
def joins_query(%{related_key: related_key, owner: owner, owner_key: owner_key, queryable: queryable} = assoc) do
1052-
from(o in owner, join: q in ^queryable, on: field(q, ^related_key) == field(o, ^owner_key))
1094+
from(o in owner, join: q in ^queryable, on: field(q, ^hd(related_key)) == field(o, ^hd(owner_key)))
1095+
|> Ecto.Association.composite_joins_query(0, 1, tl(related_key), tl(owner_key))
10531096
|> Ecto.Association.combine_joins_query(assoc.where, 1)
10541097
end
10551098

10561099
@impl true
10571100
def assoc_query(%{related_key: related_key, queryable: queryable} = assoc, query, [value]) do
1058-
from(x in (query || queryable), where: field(x, ^related_key) == ^value)
1101+
from(x in (query || queryable), where: field(x, ^hd(related_key)) == ^hd(value))
1102+
|> Ecto.Association.composite_assoc_query(0, tl(related_key), tl(value))
10591103
|> Ecto.Association.combine_assoc_query(assoc.where)
10601104
end
10611105

10621106
@impl true
10631107
def assoc_query(%{related_key: related_key, queryable: queryable} = assoc, query, values) do
1064-
from(x in (query || queryable), where: field(x, ^related_key) in ^values)
1108+
from(x in (query || queryable), where: field(x, ^hd(related_key)) in ^Enum.map(values, &hd/1))
1109+
|> Ecto.Association.composite_assoc_query(0, tl(related_key), Enum.map(values, &tl/1))
10651110
|> Ecto.Association.combine_assoc_query(assoc.where)
10661111
end
10671112

@@ -1282,11 +1327,12 @@ defmodule Ecto.Association.ManyToMany do
12821327

12831328
owner_key_type = owner.__schema__(:type, owner_key)
12841329

1330+
# TODO fix the hd(values)
12851331
# We only need to join in the "join table". Preload and Ecto.assoc expressions can then filter
12861332
# by &1.join_owner_key in ^... to filter down to the associated entries in the related table.
12871333
from(q in (query || queryable),
12881334
join: j in ^join_through, on: field(q, ^related_key) == field(j, ^join_related_key),
1289-
where: field(j, ^join_owner_key) in type(^values, {:in, ^owner_key_type})
1335+
where: field(j, ^join_owner_key) in type(^hd(values), {:in, ^owner_key_type})
12901336
)
12911337
|> Ecto.Association.combine_assoc_query(assoc.where)
12921338
|> Ecto.Association.combine_joins_query(assoc.join_where, 1)

lib/ecto/repo/preloader.ex

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ defmodule Ecto.Repo.Preloader do
3939
end
4040

4141
def preload(structs, repo_name, preloads, opts) when is_list(structs) do
42+
# IO.inspect([preloads: preloads, opts: opts], label: :preload__list)
4243
normalize_and_preload_each(structs, repo_name, preloads, opts[:take], opts)
4344
end
4445

4546
def preload(struct, repo_name, preloads, opts) when is_map(struct) do
47+
# IO.inspect([preloads: preloads, opts: opts], label: :preload__map)
4648
normalize_and_preload_each([struct], repo_name, preloads, opts[:take], opts) |> hd()
4749
end
4850

@@ -108,6 +110,7 @@ defmodule Ecto.Repo.Preloader do
108110
[
109111
fn opts ->
110112
fetch_query(fetch_ids, assoc, repo_name, query, prefix, related_key, take, opts)
113+
# |> IO.inspect(label: :fetch_query)
111114
end
112115
| queries
113116
]
@@ -170,10 +173,15 @@ defmodule Ecto.Repo.Preloader do
170173
acc
171174
struct, {fetch_ids, loaded_ids, loaded_structs} ->
172175
assert_struct!(module, struct)
173-
%{^owner_key => id, ^field => value} = struct
176+
%{^field => value} = struct
177+
# TODO remove List.wrap
178+
ids = owner_key |> List.wrap |> Enum.map(fn key ->
179+
%{^key => id} = struct
180+
id
181+
end)
174182
loaded? = Ecto.assoc_loaded?(value) and not force?
175183

176-
if loaded? and is_nil(id) and not Ecto.Changeset.Relation.empty?(assoc, value) do
184+
if loaded? and Enum.any?(ids, &is_nil/1) and not Ecto.Changeset.Relation.empty?(assoc, value) do
177185
Logger.warn """
178186
association `#{field}` for `#{inspect(module)}` has a loaded value but \
179187
its association key `#{owner_key}` is nil. This usually means one of:
@@ -185,20 +193,22 @@ defmodule Ecto.Repo.Preloader do
185193
"""
186194
end
187195

196+
binding() |> Keyword.take(~w/ids loaded? struct value field owner_key card/a) |> IO.inspect(label: :fetch_ids)
188197
cond do
189198
card == :one and loaded? ->
190-
{fetch_ids, [id | loaded_ids], [value | loaded_structs]}
199+
{fetch_ids, [ids | loaded_ids], [value | loaded_structs]}
191200
card == :many and loaded? ->
192-
{fetch_ids, [{id, length(value)} | loaded_ids], value ++ loaded_structs}
193-
is_nil(id) ->
201+
{fetch_ids, [{ids, length(value)} | loaded_ids], value ++ loaded_structs}
202+
Enum.any? ids, &is_nil/1 ->
194203
{fetch_ids, loaded_ids, loaded_structs}
195204
true ->
196-
{[id | fetch_ids], loaded_ids, loaded_structs}
205+
{[ids | fetch_ids], loaded_ids, loaded_structs}
197206
end
198207
end
199208
end
200209

201210
defp fetch_query(ids, assoc, _repo_name, query, _prefix, related_key, _take, _opts) when is_function(query, 1) do
211+
binding() |> Keyword.take(~w(query ids related_key)a) |> IO.inspect(label: :fetch_query_first)
202212
# Note we use an explicit sort because we don't want
203213
# to reorder based on the struct. Only the ID.
204214
ids
@@ -211,20 +221,21 @@ defmodule Ecto.Repo.Preloader do
211221

212222
defp fetch_query(ids, %{cardinality: card} = assoc, repo_name, query, prefix, related_key, take, opts) do
213223
query = assoc.__struct__.assoc_query(assoc, query, Enum.uniq(ids))
214-
field = related_key_to_field(query, related_key)
224+
fields = related_key_to_fields(query, related_key)
215225

216226
# Normalize query
217227
query = %{Ecto.Query.Planner.ensure_select(query, take || true) | prefix: prefix}
218228

219229
# Add the related key to the query results
220-
query = update_in query.select.expr, &{:{}, [], [field, &1]}
230+
query = update_in query.select.expr, &{:{}, [], [fields, &1]}
231+
binding() |> Keyword.take(~w(fields query ids related_key take)a) |> IO.inspect(label: :fetch_query_second)
221232

222233
# If we are returning many results, we must sort by the key too
223234
query =
224235
case card do
225236
:many ->
226237
update_in query.order_bys, fn order_bys ->
227-
[%Ecto.Query.QueryExpr{expr: preload_order(assoc, query, field), params: [],
238+
[%Ecto.Query.QueryExpr{expr: preload_order(assoc, query, fields), params: [],
228239
file: __ENV__.file, line: __ENV__.line}|order_bys]
229240
end
230241
:one ->
@@ -288,6 +299,13 @@ defmodule Ecto.Repo.Preloader do
288299
[{:asc, related_field} | custom_order_by]
289300
end
290301

302+
defp related_key_to_fields(query, {pos, keys}) do
303+
Enum.map keys, fn key ->
304+
{{:., [], [{:&, [], [related_key_pos(query, pos)]}, key]}, [], []}
305+
end
306+
end
307+
308+
# TODO deprecated
291309
defp related_key_to_field(query, {pos, key, field_type}) do
292310
field_ast = related_key_to_field(query, {pos, key})
293311

@@ -352,16 +370,19 @@ defmodule Ecto.Repo.Preloader do
352370

353371
defp load_assoc({:assoc, assoc, ids}, struct) do
354372
%{field: field, owner_key: owner_key, cardinality: cardinality} = assoc
355-
key = Map.fetch!(struct, owner_key)
356-
357-
loaded =
358-
case ids do
359-
%{^key => value} -> value
360-
_ when cardinality == :many -> []
361-
_ -> nil
362-
end
373+
binding() |> Keyword.take(~w(struct ids owner_key)a) |> IO.inspect(label: :load_assoc)
374+
Enum.reduce owner_key, struct, fn owner_key_field, struct ->
375+
key = Map.fetch!(struct, owner_key_field)
376+
377+
loaded =
378+
case ids do
379+
%{^key => value} -> value
380+
_ when cardinality == :many -> []
381+
_ -> nil
382+
end
363383

364-
Map.put(struct, field, loaded)
384+
Map.put(struct, field, loaded)
385+
end
365386
end
366387

367388
defp load_through({:through, assoc, throughs}, struct) do

0 commit comments

Comments
 (0)