Skip to content

Commit 05c0e68

Browse files
committed
wip: first draft of a unit test to reproduce this type of issue:
1) test Thing.create! create works without associating other things (ThingTest) test/thing_test.exs:20 ** (Ash.Error.Unknown) Bread Crumbs: > Exception raised in: Things.get_by_id Unknown Error * ** (RuntimeError) Error while building reference: all_other_things.associated_at (ash_sql 0.2.38) lib/expr.ex:1796: AshSql.Expr.default_dynamic_expr/6 (ash_sql 0.2.38) lib/expr.ex:88: AshSql.Expr.default_dynamic_expr/6
1 parent 6da0cf0 commit 05c0e68

File tree

7 files changed

+472
-0
lines changed

7 files changed

+472
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
{
2+
"attributes": [
3+
{
4+
"allow_nil?": false,
5+
"default": "nil",
6+
"generated?": false,
7+
"primary_key?": false,
8+
"references": null,
9+
"size": null,
10+
"source": "role",
11+
"type": "text"
12+
},
13+
{
14+
"allow_nil?": true,
15+
"default": "nil",
16+
"generated?": false,
17+
"primary_key?": false,
18+
"references": null,
19+
"size": null,
20+
"source": "was_cancelled_at",
21+
"type": "utc_datetime"
22+
},
23+
{
24+
"allow_nil?": false,
25+
"default": "nil",
26+
"generated?": false,
27+
"primary_key?": true,
28+
"references": {
29+
"deferrable": false,
30+
"destination_attribute": "id",
31+
"destination_attribute_default": null,
32+
"destination_attribute_generated": null,
33+
"index?": false,
34+
"match_type": null,
35+
"match_with": null,
36+
"multitenancy": {
37+
"attribute": null,
38+
"global": null,
39+
"strategy": null
40+
},
41+
"name": "co_authored_posts_author_id_fkey",
42+
"on_delete": null,
43+
"on_update": null,
44+
"primary_key?": true,
45+
"schema": "public",
46+
"table": "authors"
47+
},
48+
"size": null,
49+
"source": "author_id",
50+
"type": "uuid"
51+
},
52+
{
53+
"allow_nil?": false,
54+
"default": "nil",
55+
"generated?": false,
56+
"primary_key?": true,
57+
"references": {
58+
"deferrable": false,
59+
"destination_attribute": "id",
60+
"destination_attribute_default": null,
61+
"destination_attribute_generated": null,
62+
"index?": false,
63+
"match_type": null,
64+
"match_with": null,
65+
"multitenancy": {
66+
"attribute": null,
67+
"global": null,
68+
"strategy": null
69+
},
70+
"name": "co_authored_posts_post_id_fkey",
71+
"on_delete": null,
72+
"on_update": null,
73+
"primary_key?": true,
74+
"schema": "public",
75+
"table": "posts"
76+
},
77+
"size": null,
78+
"source": "post_id",
79+
"type": "uuid"
80+
}
81+
],
82+
"base_filter": null,
83+
"check_constraints": [],
84+
"custom_indexes": [],
85+
"custom_statements": [],
86+
"has_create_action": true,
87+
"hash": "E1F2AC3AED1987928E3A2446584C268EC54D0BCA616D81A495F4AB26E3999444",
88+
"identities": [],
89+
"multitenancy": {
90+
"attribute": null,
91+
"global": null,
92+
"strategy": null
93+
},
94+
"repo": "Elixir.AshPostgres.TestRepo",
95+
"schema": null,
96+
"table": "co_authored_posts"
97+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
defmodule AshPostgres.TestRepo.Migrations.MigrateResources44 do
2+
@moduledoc """
3+
Updates resources based on their most recent snapshots.
4+
5+
This file was autogenerated with `mix ash_postgres.generate_migrations`
6+
"""
7+
8+
use Ecto.Migration
9+
10+
def up do
11+
create table(:co_authored_posts, primary_key: false) do
12+
add(:role, :text, null: false)
13+
add(:was_cancelled_at, :utc_datetime)
14+
15+
add(
16+
:author_id,
17+
references(:authors,
18+
column: :id,
19+
name: "co_authored_posts_author_id_fkey",
20+
type: :uuid,
21+
prefix: "public"
22+
),
23+
primary_key: true,
24+
null: false
25+
)
26+
27+
add(
28+
:post_id,
29+
references(:posts,
30+
column: :id,
31+
name: "co_authored_posts_post_id_fkey",
32+
type: :uuid,
33+
prefix: "public"
34+
),
35+
primary_key: true,
36+
null: false
37+
)
38+
end
39+
end
40+
41+
def down do
42+
drop(constraint(:co_authored_posts, "co_authored_posts_author_id_fkey"))
43+
44+
drop(constraint(:co_authored_posts, "co_authored_posts_post_id_fkey"))
45+
46+
drop(table(:co_authored_posts))
47+
end
48+
end

test/many_to_many_expr_test.exs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
defmodule AshPostgres.ManyToManyExprTest do
2+
use AshPostgres.RepoCase, async: false
3+
4+
alias AshPostgres.Test.Author
5+
alias AshPostgres.Test.CoAuthorPost
6+
alias AshPostgres.Test.Post
7+
8+
require Ash.Query
9+
10+
setup ctx do
11+
main_author =
12+
if ctx[:main_author?],
13+
do: create_author(),
14+
else: nil
15+
16+
co_authors =
17+
if ctx[:co_authors],
18+
do: 1..ctx[:co_authors]
19+
|> Stream.map(&
20+
Author
21+
|> Ash.Changeset.for_create(:create, %{first_name: "John #{&1}", last_name: "Doe"})
22+
|> Ash.create!()
23+
)
24+
|> Enum.into([]),
25+
else: []
26+
27+
%{
28+
main_author: main_author,
29+
co_authors: co_authors,
30+
}
31+
end
32+
33+
def create_author(params \\ %{first_name: "John", last_name: "Doe"}) do
34+
Author
35+
|> Ash.Changeset.for_create(:create, params)
36+
|> Ash.create!()
37+
end
38+
39+
def create_post(author) do
40+
Post
41+
|> Ash.Changeset.for_create(:create, %{title: "Post by #{author.first_name}"})
42+
|> Ash.create!()
43+
end
44+
45+
def create_co_author_post(author, post, role) do
46+
CoAuthorPost
47+
|> Ash.Changeset.for_create(:create, %{author_id: author.id, post_id: post.id, role: role})
48+
|> Ash.create!()
49+
end
50+
51+
def get_author!(author_id) do
52+
Author
53+
|> Ash.Query.new()
54+
|> Ash.Query.filter(id == ^author_id)
55+
|> Ash.Query.load([:all_co_authored_posts, :cancelled_co_authored_posts, :editor_of, :writer_of])
56+
|> Ash.read_one!()
57+
end
58+
59+
def get_co_author_post!(a_id, p_id) do
60+
CoAuthorPost
61+
|> Ash.Query.new()
62+
|> Ash.Query.filter(author_id == ^a_id and post_id == ^p_id)
63+
|> Ash.read_one!()
64+
end
65+
66+
def get_post!(post_id) do
67+
Post.get_by_id!(post_id, load: [:co_author_posts, :co_authors_unfiltered, :co_authors])
68+
end
69+
70+
def cancel(author, post) do
71+
get_co_author_post!(author.id, post.id)
72+
|> CoAuthorPost.cancel()
73+
end
74+
75+
def uncancel(author, post) do
76+
get_co_author_post!(author.id, post.id)
77+
|> CoAuthorPost.uncancel()
78+
end
79+
80+
describe "manual join-resource insertion" do
81+
@tag main_author?: true
82+
@tag co_authors: 3
83+
test "filter on many_to_many relationship using parent works as expected - basic",
84+
%{
85+
main_author: main_author,
86+
co_authors: co_authors,
87+
} do
88+
post = create_post(main_author)
89+
90+
[first_ca, second_ca, third_ca] = co_authors
91+
92+
# Add first co-author
93+
create_co_author_post(first_ca, post, :editor)
94+
95+
first_ca = get_author!(first_ca.id)
96+
post = get_post!(post.id)
97+
98+
assert Enum.count(post.co_authors) == 1
99+
assert Enum.count(first_ca.all_co_authored_posts) == 1
100+
assert Enum.count(first_ca.editor_of) == 1
101+
assert Enum.count(first_ca.writer_of) == 0
102+
assert Enum.count(first_ca.cancelled_co_authored_posts) == 0
103+
104+
# Add second co-author
105+
create_co_author_post(second_ca, post, :writer)
106+
107+
second_ca = get_author!(second_ca.id)
108+
post = get_post!(post.id)
109+
110+
assert Enum.count(post.co_authors) == 2
111+
assert Enum.count(second_ca.all_co_authored_posts) == 1
112+
assert Enum.count(second_ca.editor_of) == 0
113+
assert Enum.count(second_ca.writer_of) == 1
114+
assert Enum.count(second_ca.cancelled_co_authored_posts) == 0
115+
116+
# Add third co-author
117+
create_co_author_post(third_ca, post, :proof_reader)
118+
119+
third_ca = get_author!(third_ca.id)
120+
post = get_post!(post.id)
121+
122+
assert Enum.count(post.co_authors) == 3
123+
assert Enum.count(third_ca.all_co_authored_posts) == 1
124+
assert Enum.count(third_ca.editor_of) == 0
125+
assert Enum.count(third_ca.writer_of) == 0
126+
assert Enum.count(third_ca.cancelled_co_authored_posts) == 0
127+
end
128+
129+
@tag main_author?: true
130+
@tag co_authors: 4
131+
test "filter on many_to_many relationship using parent works as expected - cancelled",
132+
%{
133+
main_author: main_author,
134+
co_authors: co_authors,
135+
} do
136+
first_post = create_post(main_author)
137+
second_post = create_post(main_author)
138+
139+
[first_ca, second_ca, third_ca, fourth_ca] = co_authors
140+
141+
# Add first co-author
142+
create_co_author_post(first_ca, first_post, :editor)
143+
create_co_author_post(first_ca, second_post, :writer)
144+
145+
first_ca = get_author!(first_ca.id)
146+
first_post = get_post!(first_post.id)
147+
148+
assert Enum.count(first_post.co_authors) == 1
149+
assert Enum.count(first_post.co_authors_unfiltered) == 1
150+
151+
assert Enum.count(first_ca.all_co_authored_posts) == 2
152+
assert Enum.count(first_ca.editor_of) == 1
153+
assert Enum.count(first_ca.writer_of) == 1
154+
assert Enum.count(first_ca.cancelled_co_authored_posts) == 0
155+
156+
# Add second co-author
157+
create_co_author_post(second_ca, first_post, :proof_reader)
158+
create_co_author_post(second_ca, second_post, :writer)
159+
160+
second_ca = get_author!(second_ca.id)
161+
first_post = get_post!(first_post.id)
162+
second_post = get_post!(second_post.id)
163+
164+
assert Enum.count(second_post.co_authors) == 2
165+
assert Enum.count(second_post.co_authors_unfiltered) == 2
166+
167+
assert Enum.count(second_ca.all_co_authored_posts) == 2
168+
assert Enum.count(second_ca.editor_of) == 0
169+
assert Enum.count(second_ca.writer_of) == 1
170+
assert Enum.count(second_ca.cancelled_co_authored_posts) == 0
171+
172+
# Add third co-author
173+
create_co_author_post(third_ca, first_post, :proof_reader)
174+
create_co_author_post(third_ca, second_post, :proof_reader)
175+
cancel(third_ca, second_post)
176+
177+
third_ca = get_author!(third_ca.id)
178+
first_post = get_post!(first_post.id)
179+
second_post = get_post!(second_post.id)
180+
181+
assert Enum.count(first_post.co_authors) == 3
182+
assert Enum.count(first_post.co_authors_unfiltered) == 3
183+
assert Enum.count(second_post.co_authors) == 2
184+
assert Enum.count(second_post.co_authors_unfiltered) == 3
185+
186+
assert Enum.count(third_ca.all_co_authored_posts) == 2
187+
assert Enum.count(third_ca.editor_of) == 0
188+
assert Enum.count(third_ca.writer_of) == 0
189+
assert Enum.count(third_ca.cancelled_co_authored_posts) == 1
190+
191+
# Add fourth co-author
192+
create_co_author_post(fourth_ca, first_post, :proof_reader)
193+
create_co_author_post(fourth_ca, second_post, :editor)
194+
cancel(fourth_ca, first_post)
195+
cancel(fourth_ca, second_post)
196+
197+
fourth_ca = get_author!(fourth_ca.id)
198+
first_post = get_post!(first_post.id)
199+
second_post = get_post!(second_post.id)
200+
201+
assert Enum.count(first_post.co_authors) == 3
202+
assert Enum.count(first_post.co_authors_unfiltered) == 4
203+
assert Enum.count(second_post.co_authors) == 2
204+
assert Enum.count(second_post.co_authors_unfiltered) == 4
205+
206+
assert Enum.count(fourth_ca.all_co_authored_posts) == 2
207+
assert Enum.count(fourth_ca.editor_of) == 1
208+
assert Enum.count(fourth_ca.writer_of) == 0
209+
assert Enum.count(fourth_ca.cancelled_co_authored_posts) == 2
210+
end
211+
end
212+
end

test/support/domain.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule AshPostgres.Test.Domain do
33
use Ash.Domain
44

55
resources do
6+
resource(AshPostgres.Test.CoAuthorPost)
67
resource(AshPostgres.Test.Post)
78
resource(AshPostgres.Test.Comment)
89
resource(AshPostgres.Test.IntegerPost)

0 commit comments

Comments
 (0)