Skip to content

Commit aaf6b2f

Browse files
soundmonsterLeo B
authored and
Leo B
committed
Support associations on composite foreign keys
1 parent 58ce5f0 commit aaf6b2f

17 files changed

+922
-162
lines changed

Diff for: Earthfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ integration-test-base:
6060
apk del .build-dependencies && rm -f msodbcsql*.sig mssql-tools*.apk
6161
ENV PATH="/opt/mssql-tools/bin:${PATH}"
6262

63-
GIT CLONE https://github.com/elixir-ecto/ecto_sql.git /src/ecto_sql
63+
GIT CLONE --branch composite_foreign_keys https://github.com/soundmonster/ecto_sql.git /src/ecto_sql
6464
WORKDIR /src/ecto_sql
6565
RUN mix deps.get
6666

Diff for: integration_test/cases/assoc.exs

+46
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule Ecto.Integration.AssocTest do
1010
alias Ecto.Integration.PostUser
1111
alias Ecto.Integration.Comment
1212
alias Ecto.Integration.Permalink
13+
alias Ecto.Integration.CompositePk
1314

1415
test "has_many assoc" do
1516
p1 = TestRepo.insert!(%Post{title: "1"})
@@ -55,6 +56,22 @@ defmodule Ecto.Integration.AssocTest do
5556
assert p2.id == pid2
5657
end
5758

59+
test "belongs_to assoc with composite key" do
60+
TestRepo.insert!(%CompositePk{a: 2, b: 1, name: "foo"})
61+
TestRepo.insert!(%CompositePk{a: 2, b: 2, name: "bar"})
62+
TestRepo.insert!(%CompositePk{a: 2, b: 3, name: "unused"})
63+
64+
p1 = TestRepo.insert!(%Post{title: "first", composite_a: 2, composite_b: 1})
65+
p2 = TestRepo.insert!(%Post{title: "none"})
66+
p3 = TestRepo.insert!(%Post{title: "second", composite_a: 2, composite_b: 2})
67+
68+
assert [c1, c2] = TestRepo.all Ecto.assoc([p1, p2, p3], :composite)
69+
assert c1.a == 2
70+
assert c1.b == 1
71+
assert c2.a == 2
72+
assert c2.b == 2
73+
end
74+
5875
test "has_many through assoc" do
5976
p1 = TestRepo.insert!(%Post{})
6077
p2 = TestRepo.insert!(%Post{})
@@ -725,6 +742,27 @@ defmodule Ecto.Integration.AssocTest do
725742
assert perma.post_id == nil
726743
end
727744

745+
test "belongs_to changeset assoc on composite key" do
746+
changeset =
747+
%CompositePk{a: 1, b: 2}
748+
|> Ecto.Changeset.change()
749+
|> Ecto.Changeset.put_assoc(:posts, [%Post{title: "1"}])
750+
751+
composite = TestRepo.insert!(changeset)
752+
assert [post] = composite.posts
753+
assert post.id
754+
assert post.composite_a == composite.a
755+
assert post.composite_b == composite.b
756+
assert post.title == "1"
757+
# TODO Repo.get should work with composite keys somehow, right?
758+
# composite = TestRepo.get! from(Composite, preload: [:post]), [composite.a, composite.b]
759+
# assert composite.post.title == "1"
760+
761+
post = TestRepo.get! from(Post, preload: [:composite]), post.id
762+
assert post.composite.a == 1
763+
assert post.composite.b == 2
764+
end
765+
728766
test "inserting struct with associations" do
729767
tree = %Permalink{
730768
url: "root",
@@ -750,6 +788,14 @@ defmodule Ecto.Integration.AssocTest do
750788
assert Enum.all?(tree.post.comments, & &1.id)
751789
end
752790

791+
test "inserting struct with associations on composite keys" do
792+
# creates nested belongs_to
793+
%Post{composite: composite} =
794+
TestRepo.insert! %Post{title: "1", composite: %CompositePk{a: 1, b: 2, name: "name"}}
795+
796+
assert %CompositePk{a: 1, b: 2, name: "name"} = composite
797+
end
798+
753799
test "inserting struct with empty associations" do
754800
permalink = TestRepo.insert!(%Permalink{url: "root", post: nil})
755801
assert permalink.post == nil

Diff for: integration_test/cases/preload.exs

+76
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule Ecto.Integration.PreloadTest do
66

77
alias Ecto.Integration.Post
88
alias Ecto.Integration.Comment
9+
alias Ecto.Integration.CompositePk
910
alias Ecto.Integration.Item
1011
alias Ecto.Integration.Permalink
1112
alias Ecto.Integration.User
@@ -341,6 +342,25 @@ defmodule Ecto.Integration.PreloadTest do
341342
assert [] = pe3.comments
342343
end
343344

345+
test "preload composite foreign key with function" do
346+
c11 = TestRepo.insert!(%CompositePk{a: 1, b: 1, name: "11"})
347+
c12 = TestRepo.insert!(%CompositePk{a: 1, b: 2, name: "12"})
348+
c22 = TestRepo.insert!(%CompositePk{a: 2, b: 2, name: "22"})
349+
c33 = TestRepo.insert!(%CompositePk{a: 3, b: 3, name: "33"})
350+
351+
TestRepo.insert!(%Post{title: "1", composite_a: 1, composite_b: 1})
352+
TestRepo.insert!(%Post{title: "2", composite_a: 1, composite_b: 1})
353+
TestRepo.insert!(%Post{title: "3", composite_a: 1, composite_b: 2})
354+
TestRepo.insert!(%Post{title: "4", composite_a: 2, composite_b: 2})
355+
356+
assert [ce12, ce11, ce33, ce22] = TestRepo.preload([c12, c11, c33, c22],
357+
posts: fn _ -> TestRepo.all(Post) end)
358+
assert [%Post{title: "1"}, %Post{title: "2"}] = ce11.posts
359+
assert [%Post{title: "3"}] = ce12.posts
360+
assert [%Post{title: "4"}] = ce22.posts
361+
assert [] = ce33.posts
362+
end
363+
344364
test "preload many_to_many with function" do
345365
p1 = TestRepo.insert!(%Post{title: "1"})
346366
p2 = TestRepo.insert!(%Post{title: "2"})
@@ -389,6 +409,50 @@ defmodule Ecto.Integration.PreloadTest do
389409
assert p3.users == [%{id: uid1}, %{id: uid4}]
390410
end
391411

412+
test "preload many_to_many on composite foreign keys with function" do
413+
c11 = TestRepo.insert!(%CompositePk{a: 1, b: 1, name: "11"})
414+
c12 = TestRepo.insert!(%CompositePk{a: 1, b: 2, name: "12"})
415+
c22 = TestRepo.insert!(%CompositePk{a: 2, b: 2, name: "22"})
416+
417+
TestRepo.insert_all "composite_pk_composite_pk", [[a_1: 1, b_1: 1, a_2: 1, b_2: 2],
418+
[a_1: 1, b_1: 1, a_2: 2, b_2: 2],
419+
[a_1: 1, b_1: 2, a_2: 1, b_2: 1],
420+
[a_1: 2, b_1: 2, a_2: 2, b_2: 2]]
421+
422+
wrong_preloader = fn composite_ids ->
423+
composite_ids_a = Enum.map(composite_ids, &Enum.at(&1, 0))
424+
composite_ids_b = Enum.map(composite_ids, &Enum.at(&1, 1))
425+
TestRepo.all(
426+
from c in CompositePk,
427+
join: cc in "composite_pk_composite_pk",
428+
where: cc.a_1 in ^composite_ids_a and cc.b_1 in ^composite_ids_b and cc.a_2 == c.a and cc.b_2 == c.b,
429+
order_by: [c.a, c.b],
430+
select: map(c, [:name])
431+
)
432+
end
433+
434+
assert_raise RuntimeError, ~r/invalid custom preload for `composites` on `Ecto.Integration.CompositePk`/, fn ->
435+
TestRepo.preload([c11, c12, c22], composites: wrong_preloader)
436+
end
437+
438+
right_preloader = fn composite_ids ->
439+
composite_ids_a = Enum.map(composite_ids, &Enum.at(&1, 0))
440+
composite_ids_b = Enum.map(composite_ids, &Enum.at(&1, 1))
441+
TestRepo.all(
442+
from c in CompositePk,
443+
join: cc in "composite_pk_composite_pk",
444+
where: cc.a_1 in ^composite_ids_a and cc.b_1 in ^composite_ids_b and cc.a_2 == c.a and cc.b_2 == c.b,
445+
order_by: [c.a, c.b],
446+
select: {[cc.a_1, cc.b_1], map(c, [:name])}
447+
)
448+
end
449+
450+
[c11, c12, c22] = TestRepo.preload([c11, c12, c22], composites: right_preloader)
451+
assert c11.composites == [%{name: "12"}, %{name: "22"}]
452+
assert c12.composites == [%{name: "11"}]
453+
assert c22.composites == [%{name: "22"}]
454+
end
455+
392456
test "preload with query" do
393457
p1 = TestRepo.insert!(%Post{title: "1"})
394458
p2 = TestRepo.insert!(%Post{title: "2"})
@@ -604,6 +668,18 @@ defmodule Ecto.Integration.PreloadTest do
604668
assert TestRepo.preload(updated, [:author], force: true).author == nil
605669
end
606670

671+
test "preload raises with association over composite foreign key is set but without id" do
672+
p1 = TestRepo.insert!(%Post{title: "1"})
673+
c11 = TestRepo.insert!(%CompositePk{a: 1, b: 1, name: "11"})
674+
updated = %{p1 | composite: c11, composite_a: nil, composite_b: nil}
675+
676+
assert ExUnit.CaptureLog.capture_log(fn ->
677+
assert TestRepo.preload(updated, [:composite]).composite == c11
678+
end) =~ ~r/its association keys `\[composite_a, composite_b\]` are nil/
679+
680+
assert TestRepo.preload(updated, [:composite], force: true).composite == nil
681+
end
682+
607683
test "preload skips already loaded for cardinality one" do
608684
%Post{id: pid} = TestRepo.insert!(%Post{title: "1"})
609685

Diff for: integration_test/cases/repo.exs

+18
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,24 @@ defmodule Ecto.Integration.RepoTest do
152152
assert TestRepo.all(PostUserCompositePk) == []
153153
end
154154

155+
@tag :composite_pk
156+
# TODO this needs a better name
157+
test "insert, update and delete with associated composite pk #2" do
158+
composite = TestRepo.insert!(%CompositePk{a: 1, b: 2, name: "name"})
159+
post = TestRepo.insert!(%Post{title: "post title", composite: composite})
160+
161+
assert post.composite_a == 1
162+
assert post.composite_b == 2
163+
assert TestRepo.get_by!(CompositePk, [a: 1, b: 2]) == composite
164+
165+
post = post |> Ecto.Changeset.change(composite: nil) |> TestRepo.update!
166+
assert is_nil(post.composite_a)
167+
assert is_nil(post.composite_b)
168+
169+
TestRepo.delete!(post)
170+
assert TestRepo.all(CompositePk) == [composite]
171+
end
172+
155173
@tag :invalid_prefix
156174
test "insert, update and delete with invalid prefix" do
157175
post = TestRepo.insert!(%Post{})

Diff for: integration_test/support/schemas.exs

+6
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ defmodule Ecto.Integration.Post do
5454
has_one :update_permalink, Ecto.Integration.Permalink, foreign_key: :post_id, on_delete: :delete_all, on_replace: :update
5555
has_many :comments_authors, through: [:comments, :author]
5656
belongs_to :author, Ecto.Integration.User
57+
belongs_to :composite, Ecto.Integration.CompositePk,
58+
foreign_key: [:composite_a, :composite_b], references: [:a, :b], type: [:integer, :integer], on_replace: :nilify
5759
many_to_many :users, Ecto.Integration.User,
5860
join_through: "posts_users", on_delete: :delete_all, on_replace: :delete
5961
many_to_many :ordered_users, Ecto.Integration.User, join_through: "posts_users", preload_order: [desc: :name]
@@ -291,6 +293,10 @@ defmodule Ecto.Integration.CompositePk do
291293
field :a, :integer, primary_key: true
292294
field :b, :integer, primary_key: true
293295
field :name, :string
296+
has_many :posts, Ecto.Integration.Post, foreign_key: [:composite_a, :composite_b], references: [:a, :b]
297+
many_to_many :composites, Ecto.Integration.CompositePk,
298+
join_through: "composite_pk_composite_pk", join_keys: [[a_1: :a, b_1: :b], [a_2: :a, b_2: :b]],
299+
on_delete: :delete_all, on_replace: :delete
294300
end
295301
def changeset(schema, params) do
296302
cast(schema, params, ~w(a b name)a)

Diff for: lib/ecto.ex

+9-4
Original file line numberDiff line numberDiff line change
@@ -510,10 +510,15 @@ defmodule Ecto do
510510
refl = %{owner_key: owner_key} = Ecto.Association.association_from_schema!(schema, assoc)
511511

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

518523
case assocs do
519524
[] ->

0 commit comments

Comments
 (0)