1defmodule Ecto.AssociationTest do
2  use ExUnit.Case, async: true
3  doctest Ecto.Association
4
5  import Ecto
6  import Ecto.Query, only: [from: 2]
7
8  alias __MODULE__.Author
9  alias __MODULE__.Comment
10  alias __MODULE__.CommentWithPrefix
11  alias __MODULE__.Permalink
12  alias __MODULE__.Post
13  alias __MODULE__.PostWithPrefix
14  alias __MODULE__.Summary
15  alias __MODULE__.Email
16  alias __MODULE__.Profile
17  alias __MODULE__.AuthorPermalink
18
19  defmodule Post do
20    use Ecto.Schema
21
22    schema "posts" do
23      field :title, :string
24
25      has_many :comments, Comment
26      has_one :permalink, Permalink
27      has_many :permalinks, Permalink
28      belongs_to :author, Author, defaults: [title: "World!"]
29      belongs_to :summary, Summary
30    end
31  end
32
33  defmodule Comment do
34    use Ecto.Schema
35
36    schema "comments" do
37      field :text, :string
38
39      belongs_to :post, Post
40      has_one :permalink, Permalink
41      has_one :post_author, through: [:post, :author]       # belongs -> belongs
42      has_one :post_permalink, through: [:post, :permalink] # belongs -> one
43    end
44  end
45
46  defmodule Permalink do
47    use Ecto.Schema
48
49    schema "permalinks" do
50      field :url, :string
51      many_to_many :authors, Author, join_through: AuthorPermalink, defaults: [title: "m2m!"]
52      has_many :author_emails, through: [:authors, :emails]
53    end
54  end
55
56  defmodule PostWithPrefix do
57    use Ecto.Schema
58    @schema_prefix "my_prefix"
59
60    schema "posts" do
61      belongs_to :author, Author
62      has_many :comments_with_prefix, CommentWithPrefix
63    end
64  end
65
66  defmodule CommentWithPrefix do
67    use Ecto.Schema
68    @schema_prefix "my_prefix"
69
70    schema "comments" do
71      belongs_to :posts_with_prefix, Post, foreign_key: :post_with_prefix_id
72    end
73  end
74
75  defmodule Author do
76    use Ecto.Schema
77
78    schema "authors" do
79      field :title, :string
80      has_many :posts, Post, on_replace: :delete
81      has_many :posts_comments, through: [:posts, :comments]    # many -> many
82      has_many :posts_permalinks, through: [:posts, :permalink] # many -> one
83      has_many :emails, {"users_emails", Email}
84      has_one :profile, {"users_profiles", Profile},
85        defaults: [name: "default"], on_replace: :delete
86      many_to_many :permalinks, {"custom_permalinks", Permalink},
87        join_through: "authors_permalinks"
88      has_many :posts_with_prefix, PostWithPrefix
89      has_many :comments_with_prefix, through: [:posts_with_prefix, :comments_with_prefix]
90    end
91  end
92
93  defmodule AuthorPermalink do
94    use Ecto.Schema
95
96    schema "authors_permalinks" do
97      field :author_id
98      field :permalink_id
99    end
100  end
101
102  defmodule Summary do
103    use Ecto.Schema
104
105    schema "summaries" do
106      has_one :post, Post, defaults: [title: "default"], on_replace: :nilify
107      has_many :posts, Post, on_replace: :nilify
108      has_one :post_author, through: [:post, :author]        # one -> belongs
109      has_many :post_comments, through: [:post, :comments]   # one -> many
110    end
111  end
112
113  defmodule Email do
114    use Ecto.Schema
115
116    schema "emails" do
117      belongs_to :author, {"post_authors", Author}
118    end
119  end
120
121  defmodule Profile do
122    use Ecto.Schema
123
124    schema "profiles" do
125      field :name
126      belongs_to :author, Author
127      belongs_to :summary, Summary
128    end
129  end
130
131  test "has many" do
132    assoc = Post.__schema__(:association, :comments)
133
134    assert inspect(Ecto.Association.Has.joins_query(assoc)) ==
135           inspect(from p in Post, join: c in Comment, on: c.post_id == p.id)
136
137    assert inspect(Ecto.Association.Has.assoc_query(assoc, nil, [])) ==
138           inspect(from c in Comment, where: c.post_id in ^[])
139
140    assert inspect(Ecto.Association.Has.assoc_query(assoc, nil, [1, 2, 3])) ==
141           inspect(from c in Comment, where: c.post_id in ^[1, 2, 3])
142  end
143
144  test "has many with specified source" do
145    assoc = Author.__schema__(:association, :emails)
146
147    assert inspect(Ecto.Association.Has.joins_query(assoc)) ==
148           inspect(from a in Author, join: e in {"users_emails", Email}, on: e.author_id == a.id)
149
150    assert inspect(Ecto.Association.Has.assoc_query(assoc, nil, [])) ==
151           inspect(from e in {"users_emails", Email}, where: e.author_id in ^[])
152
153    assert inspect(Ecto.Association.Has.assoc_query(assoc, nil, [1, 2, 3])) ==
154           inspect(from e in {"users_emails", Email}, where: e.author_id in ^[1, 2, 3])
155  end
156
157  test "has many custom assoc query" do
158    assoc = Post.__schema__(:association, :comments)
159    query = from c in Comment, limit: 5
160    assert inspect(Ecto.Association.Has.assoc_query(assoc, query, [1, 2, 3])) ==
161           inspect(from c in Comment, where: c.post_id in ^[1, 2, 3], limit: 5)
162  end
163
164  test "has one" do
165    assoc = Post.__schema__(:association, :permalink)
166
167    assert inspect(Ecto.Association.Has.joins_query(assoc)) ==
168           inspect(from p in Post, join: c in Permalink, on: c.post_id == p.id)
169
170    assert inspect(Ecto.Association.Has.assoc_query(assoc, nil, [])) ==
171           inspect(from c in Permalink, where: c.post_id in ^[])
172
173    assert inspect(Ecto.Association.Has.assoc_query(assoc, nil, [1])) ==
174           inspect(from c in Permalink, where: c.post_id == ^1)
175
176    assert inspect(Ecto.Association.Has.assoc_query(assoc, nil, [1, 2, 3])) ==
177           inspect(from c in Permalink, where: c.post_id in ^[1, 2, 3])
178  end
179
180  test "has one with specified source" do
181    assoc = Author.__schema__(:association, :profile)
182
183    assert inspect(Ecto.Association.Has.joins_query(assoc)) ==
184           inspect(from a in Author, join: p in {"users_profiles", Profile}, on: p.author_id == a.id)
185
186    assert inspect(Ecto.Association.Has.assoc_query(assoc, nil, [])) ==
187           inspect(from p in {"users_profiles", Profile}, where: p.author_id in ^[])
188
189    assert inspect(Ecto.Association.Has.assoc_query(assoc, nil, [1, 2, 3])) ==
190           inspect(from p in {"users_profiles", Profile}, where: p.author_id in ^[1, 2, 3])
191  end
192
193  test "has one custom assoc query" do
194    assoc = Post.__schema__(:association, :permalink)
195    query = from c in Permalink, limit: 5
196    assert inspect(Ecto.Association.Has.assoc_query(assoc, query, [1, 2, 3])) ==
197           inspect(from c in Permalink, where: c.post_id in ^[1, 2, 3], limit: 5)
198  end
199
200  test "belongs to" do
201    assoc = Post.__schema__(:association, :author)
202
203    assert inspect(Ecto.Association.BelongsTo.joins_query(assoc)) ==
204           inspect(from p in Post, join: a in Author, on: a.id == p.author_id)
205
206    assert inspect(Ecto.Association.BelongsTo.assoc_query(assoc, nil, [])) ==
207           inspect(from a in Author, where: a.id in ^[])
208
209    assert inspect(Ecto.Association.BelongsTo.assoc_query(assoc, nil, [1])) ==
210           inspect(from a in Author, where: a.id == ^1)
211
212    assert inspect(Ecto.Association.BelongsTo.assoc_query(assoc, nil, [1, 2, 3])) ==
213           inspect(from a in Author, where: a.id in ^[1, 2, 3])
214  end
215
216  test "belongs to with specified source" do
217    assoc = Email.__schema__(:association, :author)
218
219    assert inspect(Ecto.Association.BelongsTo.joins_query(assoc)) ==
220           inspect(from e in Email, join: a in {"post_authors", Author}, on: a.id == e.author_id)
221
222    assert inspect(Ecto.Association.BelongsTo.assoc_query(assoc, nil, [])) ==
223           inspect(from a in {"post_authors", Author}, where: a.id in ^[])
224
225    assert inspect(Ecto.Association.BelongsTo.assoc_query(assoc, nil, [1])) ==
226           inspect(from a in {"post_authors", Author}, where: a.id == ^1)
227
228    assert inspect(Ecto.Association.BelongsTo.assoc_query(assoc, nil, [1, 2, 3])) ==
229           inspect(from a in {"post_authors", Author}, where: a.id in ^[1, 2, 3])
230  end
231
232  test "belongs to custom assoc query" do
233    assoc = Post.__schema__(:association, :author)
234    query = from a in Author, limit: 5
235    assert inspect(Ecto.Association.BelongsTo.assoc_query(assoc, query, [1, 2, 3])) ==
236           inspect(from a in Author, where: a.id in ^[1, 2, 3], limit: 5)
237  end
238
239  test "many to many" do
240    assoc = Permalink.__schema__(:association, :authors)
241
242    assert inspect(Ecto.Association.ManyToMany.joins_query(assoc)) ==
243           inspect(from p in Permalink,
244                    join: m in AuthorPermalink, on: m.permalink_id == p.id,
245                    join: a in Author, on: m.author_id == a.id)
246
247    assert inspect(Ecto.Association.ManyToMany.assoc_query(assoc, nil, [])) ==
248           inspect(from a in Author,
249                    join: p in Permalink, on: p.id in ^[],
250                    join: m in AuthorPermalink, on: m.permalink_id == p.id,
251                    where: m.author_id == a.id)
252
253    assert inspect(Ecto.Association.ManyToMany.assoc_query(assoc, nil, [1, 2, 3])) ==
254           inspect(from a in Author,
255                    join: p in Permalink, on: p.id in ^[1, 2, 3],
256                    join: m in AuthorPermalink, on: m.permalink_id == p.id,
257                    where: m.author_id == a.id)
258  end
259
260  test "many to many with specified source" do
261    assoc = Author.__schema__(:association, :permalinks)
262
263    assert inspect(Ecto.Association.ManyToMany.joins_query(assoc)) ==
264           inspect(from a in Author,
265                    join: m in "authors_permalinks", on: m.author_id == a.id,
266                    join: p in {"custom_permalinks", Permalink}, on: m.permalink_id == p.id)
267
268    assert inspect(Ecto.Association.ManyToMany.assoc_query(assoc, nil, [])) ==
269           inspect(from p in {"custom_permalinks", Permalink},
270                    join: a in Author, on: a.id in ^[],
271                    join: m in "authors_permalinks", on: m.author_id == a.id,
272                    where: m.permalink_id == p.id)
273
274    assert inspect(Ecto.Association.ManyToMany.assoc_query(assoc, nil, [1, 2, 3])) ==
275           inspect(from p in {"custom_permalinks", Permalink},
276                    join: a in Author, on: a.id in ^[1, 2, 3],
277                    join: m in "authors_permalinks", on: m.author_id == a.id,
278                    where: m.permalink_id == p.id)
279  end
280
281  test "many to many custom assoc query" do
282    assoc = Permalink.__schema__(:association, :authors)
283    query = from a in Author, limit: 5
284    assert inspect(Ecto.Association.ManyToMany.assoc_query(assoc, query, [1, 2, 3])) ==
285           inspect(from a in Author,
286                    join: p in Permalink, on: p.id in ^[1, 2, 3],
287                    join: m in AuthorPermalink, on: m.permalink_id == p.id,
288                    where: m.author_id == a.id, limit: 5)
289  end
290
291  test "has many through many to many" do
292    assoc = Author.__schema__(:association, :posts_comments)
293
294    assert inspect(Ecto.Association.HasThrough.joins_query(assoc)) ==
295           inspect(from a in Author, join: p in assoc(a, :posts), join: c in assoc(p, :comments))
296
297    assert inspect(Ecto.Association.HasThrough.assoc_query(assoc, nil, [1,2,3])) ==
298           inspect(from c in Comment, join: p in Post, on: p.author_id in ^[1, 2, 3],
299                        where: c.post_id == p.id, distinct: true)
300  end
301
302  test "has many through many to one" do
303    assoc = Author.__schema__(:association, :posts_permalinks)
304
305    assert inspect(Ecto.Association.HasThrough.joins_query(assoc)) ==
306           inspect(from a in Author, join: p in assoc(a, :posts), join: c in assoc(p, :permalink))
307
308    assert inspect(Ecto.Association.HasThrough.assoc_query(assoc, nil, [1,2,3])) ==
309           inspect(from l in Permalink, join: p in Post, on: p.author_id in ^[1, 2, 3],
310                        where: l.post_id == p.id, distinct: true)
311  end
312
313  test "has one through belongs to belongs" do
314    assoc = Comment.__schema__(:association, :post_author)
315
316    assert inspect(Ecto.Association.HasThrough.joins_query(assoc)) ==
317           inspect(from c in Comment, join: p in assoc(c, :post), join: a in assoc(p, :author))
318
319    assert inspect(Ecto.Association.HasThrough.assoc_query(assoc, nil, [1,2,3])) ==
320           inspect(from a in Author, join: p in Post, on: p.id in ^[1, 2, 3],
321                        where: a.id == p.author_id, distinct: true)
322  end
323
324  test "has one through belongs to one" do
325    assoc = Comment.__schema__(:association, :post_permalink)
326
327    assert inspect(Ecto.Association.HasThrough.joins_query(assoc)) ==
328           inspect(from c in Comment, join: p in assoc(c, :post), join: l in assoc(p, :permalink))
329
330    assert inspect(Ecto.Association.HasThrough.assoc_query(assoc, nil, [1,2,3])) ==
331           inspect(from l in Permalink, join: p in Post, on: p.id in ^[1, 2, 3],
332                        where: l.post_id == p.id, distinct: true)
333  end
334
335  test "has many through one to many" do
336    assoc = Summary.__schema__(:association, :post_comments)
337
338    assert inspect(Ecto.Association.HasThrough.joins_query(assoc)) ==
339           inspect(from s in Summary, join: p in assoc(s, :post), join: c in assoc(p, :comments))
340
341    assert inspect(Ecto.Association.HasThrough.assoc_query(assoc, nil, [1,2,3])) ==
342           inspect(from c in Comment, join: p in Post, on: p.summary_id in ^[1, 2, 3],
343                        where: c.post_id == p.id, distinct: true)
344  end
345
346  test "has one through one to belongs" do
347    assoc = Summary.__schema__(:association, :post_author)
348
349    assert inspect(Ecto.Association.HasThrough.joins_query(assoc)) ==
350           inspect(from s in Summary, join: p in assoc(s, :post), join: a in assoc(p, :author))
351
352    assert inspect(Ecto.Association.HasThrough.assoc_query(assoc, nil, [1,2,3])) ==
353           inspect(from a in Author, join: p in Post, on: p.summary_id in ^[1, 2, 3],
354                        where: a.id == p.author_id, distinct: true)
355  end
356
357  test "has many through custom assoc many to many query" do
358    assoc = Author.__schema__(:association, :posts_comments)
359    query = from c in Comment, where: c.text == "foo", limit: 5
360    assert inspect(Ecto.Association.HasThrough.assoc_query(assoc, query, [1,2,3])) ==
361           inspect(from c in Comment, join: p in Post,
362                        on: p.author_id in ^[1, 2, 3],
363                        where: c.post_id == p.id, where: c.text == "foo",
364                        distinct: true, limit: 5)
365
366    query = from c in {"custom", Comment}, where: c.text == "foo", limit: 5
367    assert inspect(Ecto.Association.HasThrough.assoc_query(assoc, query, [1,2,3])) ==
368           inspect(from c in {"custom", Comment}, join: p in Post,
369                        on: p.author_id in ^[1, 2, 3],
370                        where: c.post_id == p.id, where: c.text == "foo",
371                        distinct: true, limit: 5)
372
373    query = from c in Comment, join: p in assoc(c, :permalink), limit: 5
374    assert inspect(Ecto.Association.HasThrough.assoc_query(assoc, query, [1,2,3])) ==
375           inspect(from c in Comment, join: p0 in Permalink, on: p0.comment_id == c.id,
376                        join: p1 in Post, on: p1.author_id in ^[1, 2, 3],
377                        where: c.post_id == p1.id,
378                        distinct: true, limit: 5)
379  end
380
381  test "has many through many to many and has many" do
382    assoc = Permalink.__schema__(:association, :author_emails)
383    assert inspect(Ecto.Association.HasThrough.assoc_query(assoc, nil, [1, 2, 3])) ==
384           inspect(from e in {"users_emails", Email},
385                        join: p in Permalink, on: p.id in ^[1, 2, 3],
386                        join: ap in AuthorPermalink, on: ap.permalink_id == p.id,
387                        join: a in Author, on: ap.author_id == a.id,
388                        where: e.author_id == a.id, distinct: true)
389  end
390
391  ## Integration tests through Ecto
392
393  test "build/2" do
394    # has many
395    assert build_assoc(%Post{id: 1}, :comments) ==
396           %Comment{post_id: 1}
397
398    # has one
399    assert build_assoc(%Summary{id: 1}, :post) ==
400           %Post{summary_id: 1, title: "default"}
401
402    # belongs to
403    assert build_assoc(%Post{id: 1}, :author) ==
404           %Author{title: "World!"}
405
406    # many to many
407    assert build_assoc(%Permalink{id: 1}, :authors) ==
408           %Author{title: "m2m!"}
409
410    assert_raise ArgumentError, ~r"cannot build through association `:post_author`", fn ->
411      build_assoc(%Comment{}, :post_author)
412    end
413  end
414
415  test "build/2 with custom source" do
416    email = build_assoc(%Author{id: 1}, :emails)
417    assert email.__meta__.source == {nil, "users_emails"}
418
419    profile = build_assoc(%Author{id: 1}, :profile)
420    assert profile.__meta__.source == {nil, "users_profiles"}
421
422    profile = build_assoc(%Email{id: 1}, :author)
423    assert profile.__meta__.source == {nil, "post_authors"}
424
425    permalink = build_assoc(%Author{id: 1}, :permalinks)
426    assert permalink.__meta__.source == {nil, "custom_permalinks"}
427  end
428
429  test "build/3 with custom attributes" do
430    # has many
431    assert build_assoc(%Post{id: 1}, :comments, text: "Awesome!") ==
432           %Comment{post_id: 1, text: "Awesome!"}
433
434    assert build_assoc(%Post{id: 1}, :comments, %{text: "Awesome!"}) ==
435           %Comment{post_id: 1, text: "Awesome!"}
436
437    # has one
438    assert build_assoc(%Post{id: 1}, :comments, post_id: 2) ==
439           %Comment{post_id: 1}
440
441    # belongs to
442    assert build_assoc(%Post{id: 1}, :author, title: "Hello!") ==
443           %Author{title: "Hello!"}
444
445    # 2 belongs to
446    with author_post = build_assoc(%Author{id: 1}, :posts),
447         author_and_summary_post = build_assoc(%Summary{id: 2}, :posts, author_post) do
448      assert author_and_summary_post.author_id == 1
449      assert author_and_summary_post.summary_id == 2
450    end
451
452    # many to many
453    assert build_assoc(%Permalink{id: 1}, :authors, title: "Hello!") ==
454           %Author{title: "Hello!"}
455
456    # Overriding defaults
457    assert build_assoc(%Summary{id: 1}, :post, title: "Hello").title == "Hello"
458
459    # Should not allow overriding of __meta__
460    meta = %{__meta__: %{source: {nil, "posts"}}}
461    comment = build_assoc(%Post{id: 1}, :comments, meta)
462    assert comment.__meta__.source == {nil, "comments"}
463  end
464
465  test "sets association to loaded/not loaded" do
466    refute Ecto.assoc_loaded?(%Post{}.comments)
467    assert Ecto.assoc_loaded?(%Post{comments: []}.comments)
468  end
469
470  test "assoc/2" do
471    assert inspect(assoc(%Post{id: 1}, :comments)) ==
472           inspect(from c in Comment, where: c.post_id == ^1)
473
474    assert inspect(assoc([%Post{id: 1}, %Post{id: 2}], :comments)) ==
475           inspect(from c in Comment, where: c.post_id in ^[1, 2])
476  end
477
478  test "assoc/2 with prefixes" do
479    author = %Author{id: 1}
480    assert Ecto.assoc(author, :posts_with_prefix).prefix == "my_prefix"
481    assert Ecto.assoc(author, :comments_with_prefix).prefix == "my_prefix"
482  end
483
484  test "assoc/2 filters nil ids" do
485    assert inspect(assoc([%Post{id: 1}, %Post{id: 2}, %Post{}], :comments)) ==
486           inspect(from c in Comment, where: c.post_id in ^[1, 2])
487  end
488
489  test "assoc/2 fails on empty list" do
490    assert_raise ArgumentError, ~r"cannot retrieve association :whatever for empty list", fn ->
491      assoc([], :whatever)
492    end
493  end
494
495  test "assoc/2 fails on missing association" do
496    assert_raise ArgumentError, ~r"does not have association :whatever", fn ->
497      assoc([%Post{}], :whatever)
498    end
499  end
500
501  test "assoc/2 fails on heterogeneous collections" do
502    assert_raise ArgumentError, ~r"expected a homogeneous list containing the same struct", fn ->
503      assoc([%Post{}, %Comment{}], :comments)
504    end
505  end
506
507  ## Preloader
508
509  alias Ecto.Repo.Preloader
510
511  test "preload: normalizer" do
512    assert Preloader.normalize(:foo, nil, []) ==
513           [foo: {nil, nil, []}]
514    assert Preloader.normalize([foo: :bar], nil, []) ==
515           [foo: {nil, nil, [bar: {nil, nil, []}]}]
516    assert Preloader.normalize([foo: [:bar, baz: :bat], this: :that], nil, []) ==
517           [this: {nil, nil, [that: {nil, nil, []}]},
518            foo: {nil, nil, [baz: {nil, nil, [bat: {nil, nil, []}]},
519                             bar: {nil, nil, []}]}]
520  end
521
522  test "preload: normalize with query" do
523    query = from(p in Post, limit: 1)
524    assert Preloader.normalize([foo: query], nil, []) ==
525           [foo: {nil, query, []}]
526    assert Preloader.normalize([foo: {query, :bar}], nil, []) ==
527           [foo: {nil, query, [bar: {nil, nil, []}]}]
528    assert Preloader.normalize([foo: {query, bar: :baz}], nil, []) ==
529           [foo: {nil, query, [bar: {nil, nil, [baz: {nil, nil, []}]}]}]
530  end
531
532  test "preload: normalize with take" do
533    assert Preloader.normalize([:foo], [foo: :id], []) ==
534           [foo: {[:id], nil, []}]
535    assert Preloader.normalize([foo: :bar], [foo: :id], []) ==
536           [foo: {[:id], nil, [bar: {nil, nil, []}]}]
537    assert Preloader.normalize([foo: :bar], [foo: [:id, bar: :id]], []) ==
538           [foo: {[:id, bar: :id], nil, [bar: {[:id], nil, []}]}]
539    assert Preloader.normalize([foo: [bar: :baz]], [foo: [:id, bar: :id]], []) ==
540           [foo: {[:id, bar: :id], nil, [bar: {[:id], nil, [baz: {nil, nil, []}]}]}]
541  end
542
543  test "preload: raises on invalid preload" do
544    assert_raise ArgumentError, ~r"invalid preload `123` in `123`", fn ->
545      Preloader.normalize(123, nil, 123)
546    end
547
548    assert_raise ArgumentError, ~r"invalid preload `{:bar, :baz}` in", fn ->
549      Preloader.normalize([foo: {:bar, :baz}], nil, []) == [foo: [bar: []]]
550    end
551  end
552
553  defp expand(schema, preloads, take \\ nil) do
554    Preloader.expand(schema, Preloader.normalize(preloads, take, preloads), {%{}, %{}})
555  end
556
557  test "preload: expand" do
558    assert {%{comments: {{:assoc, %Ecto.Association.Has{}, {0, :post_id}}, nil, nil, []},
559              permalink: {{:assoc, %Ecto.Association.Has{}, {0, :post_id}}, nil, nil, []}},
560            %{}} =
561           expand(Post, [:comments, :permalink])
562
563    assert {%{post: {{:assoc, %Ecto.Association.BelongsTo{}, {0, :id}}, nil, nil, [author: {nil, nil, []}]}},
564            %{}} =
565           expand(Comment, [post: :author])
566
567    assert {%{post: {{:assoc, %Ecto.Association.BelongsTo{}, {0, :id}}, nil, nil,
568              [author: {nil, nil, []}, permalink: {nil, nil, []}]}},
569            %{}} =
570           expand(Comment, [:post, post: :author, post: :permalink])
571
572    assert {%{posts: {{:assoc, %Ecto.Association.Has{}, {0, :author_id}}, nil, nil,
573                      [comments: {nil, nil, [post: {nil, nil, []}]}]}},
574            %{posts_comments: {:through, %Ecto.Association.HasThrough{}, [:posts, :comments]}}} =
575           expand(Author, [posts_comments: :post])
576
577    assert {%{posts: {{:assoc, %Ecto.Association.Has{}, {0, :author_id}}, nil, nil,
578                      [comments: _, comments: _]}},
579            %{posts_comments: {:through, %Ecto.Association.HasThrough{}, [:posts, :comments]}}} =
580           expand(Author, [:posts, posts_comments: :post, posts: [comments: :post]])
581  end
582
583  test "preload: expand with queries" do
584    query = from(c in Comment, limit: 1)
585    assert {%{permalink: {{:assoc, %Ecto.Association.Has{}, {0, :post_id}}, nil, nil, []},
586              comments: {{:assoc, %Ecto.Association.Has{}, {0, :post_id}}, nil, ^query, []}},
587            %{}} =
588           expand(Post, [:permalink, comments: query])
589
590    assert {%{posts: {{:assoc, %Ecto.Association.Has{}, {0, :author_id}}, nil, nil,
591                      [comments: {nil, ^query, [post: {nil, nil, []}]}]}},
592            %{posts_comments: {:through, %Ecto.Association.HasThrough{}, [:posts, :comments]}}} =
593           expand(Author, [posts_comments: {query, :post}])
594  end
595
596  test "preload: expand with take" do
597    assert {%{permalink: {{:assoc, %Ecto.Association.Has{}, {0, :post_id}}, [:id], nil, []},
598              comments: {{:assoc, %Ecto.Association.Has{}, {0, :post_id}}, nil, nil, []}},
599            %{}} =
600           expand(Post, [:permalink, :comments], [permalink: :id])
601
602    assert {%{posts: {{:assoc, %Ecto.Association.Has{}, {0, :author_id}}, nil, nil,
603                      [comments: {[:id], nil, [post: {nil, nil, []}]}]}},
604            %{posts_comments: {:through, %Ecto.Association.HasThrough{}, [:posts, :comments]}}} =
605           expand(Author, [posts_comments: :post], [posts_comments: :id])
606  end
607
608  test "preload: expand raises on duplicated entries" do
609    message = ~r"cannot preload `comments` as it has been supplied more than once with different queries"
610    assert_raise ArgumentError, message, fn ->
611      expand(Post, [comments: from(c in Comment, limit: 2),
612                    comments: from(c in Comment, limit: 1)])
613    end
614  end
615
616  describe "after_compile_validation/2" do
617    defp after_compile_validation(assoc, name, opts) do
618      defmodule Sample do
619        use Ecto.Schema
620
621        schema "sample" do
622          opts = [cardinality: :one] ++ opts
623          throw assoc.after_compile_validation(assoc.struct(__MODULE__, name, opts),
624                                               %{__ENV__ | context_modules: [Ecto.AssociationTest]})
625        end
626      end
627    catch
628      result -> result
629    end
630
631    test "for has" do
632      assert after_compile_validation(Ecto.Association.Has, :post,
633                                      cardinality: :one, queryable: __MODULE__) ==
634             :ok
635
636      assert after_compile_validation(Ecto.Association.Has, :post,
637                                      cardinality: :one, queryable: Unknown) ==
638             {:error, "associated schema Unknown does not exist"}
639
640      assert after_compile_validation(Ecto.Association.Has, :post,
641                                      cardinality: :one, queryable: Ecto.Changeset) ==
642             {:error, "associated module Ecto.Changeset is not an Ecto schema"}
643
644      assert after_compile_validation(Ecto.Association.Has, :post,
645                                      cardinality: :one, queryable: Post) ==
646             {:error, "associated schema Ecto.AssociationTest.Post does not have field `sample_id`"}
647
648      assert after_compile_validation(Ecto.Association.Has, :post,
649                                      cardinality: :one, queryable: Post, foreign_key: :author_id) ==
650             :ok
651    end
652
653    test "for belongs_to" do
654      assert after_compile_validation(Ecto.Association.BelongsTo, :sample,
655                                      foreign_key: :post_id, queryable: __MODULE__) ==
656             :ok
657
658      assert after_compile_validation(Ecto.Association.BelongsTo, :sample,
659                                      foreign_key: :post_id, queryable: Unknown) ==
660             {:error, "associated schema Unknown does not exist"}
661
662      assert after_compile_validation(Ecto.Association.BelongsTo, :sample,
663                                      foreign_key: :post_id, queryable: Ecto.Changeset) ==
664             {:error, "associated module Ecto.Changeset is not an Ecto schema"}
665
666      assert after_compile_validation(Ecto.Association.BelongsTo, :sample,
667                                      foreign_key: :post_id, references: :non_id, queryable: Post) ==
668             {:error, "associated schema Ecto.AssociationTest.Post does not have field `non_id`"}
669
670      assert after_compile_validation(Ecto.Association.BelongsTo, :sample,
671                                      foreign_key: :post_id, references: :id, queryable: Post) ==
672             :ok
673    end
674
675    test "for many_to_many" do
676      assert after_compile_validation(Ecto.Association.ManyToMany, :sample,
677                                      join_through: "join", queryable: __MODULE__) ==
678             :ok
679
680      assert after_compile_validation(Ecto.Association.ManyToMany, :sample,
681                                      join_through: "join", queryable: Unknown) ==
682             {:error, "associated schema Unknown does not exist"}
683
684      assert after_compile_validation(Ecto.Association.ManyToMany, :sample,
685                                      join_through: "join", queryable: Ecto.Changeset) ==
686             {:error, "associated module Ecto.Changeset is not an Ecto schema"}
687
688      assert after_compile_validation(Ecto.Association.ManyToMany, :sample,
689                                      join_through: "join", queryable: Post) ==
690             :ok
691    end
692  end
693end
694