Skip to content

Commit ccf2da4

Browse files
committed
test: add complex calculation tests with filtered aggregates
Adds comprehensive tests for calculations that depend on filtered aggregates, including keyset pagination scenarios and SQL fragment calculations with COALESCE patterns. Includes migration for new test fields.
1 parent ede4cc6 commit ccf2da4

File tree

7 files changed

+427
-2
lines changed

7 files changed

+427
-2
lines changed

lib/verifiers/validate_check_constraints.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,3 @@ defmodule AshPostgres.Verifiers.ValidateCheckConstraints do
3333
:ok
3434
end
3535
end
36-
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# SPDX-FileCopyrightText: 2019 ash_postgres contributors <https://github.com/ash-project/ash_postgres/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule AshPostgres.TestRepo.Migrations.AddReadingTimeCalculationFields do
6+
@moduledoc """
7+
Adds fields needed for testing estimated_reading_time calculation pattern.
8+
9+
Manually created to test complex calculation with filtered aggregates.
10+
"""
11+
12+
use Ecto.Migration
13+
14+
def up do
15+
alter table(:posts) do
16+
add(:base_reading_time, :integer)
17+
end
18+
19+
alter table(:comments) do
20+
add(:edited_duration, :integer)
21+
add(:planned_duration, :integer)
22+
add(:reading_time, :integer)
23+
add(:version, :text)
24+
add(:status, :text)
25+
end
26+
end
27+
28+
def down do
29+
alter table(:comments) do
30+
remove(:status)
31+
remove(:version)
32+
remove(:reading_time)
33+
remove(:planned_duration)
34+
remove(:edited_duration)
35+
end
36+
37+
alter table(:posts) do
38+
remove(:base_reading_time)
39+
end
40+
end
41+
end

test/aggregate_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1891,7 +1891,7 @@ defmodule AshSql.AggregateTest do
18911891
# (like last_read_message_id) in the subquery even if not explicitly selected.
18921892

18931893
Chat
1894-
|> Ash.Query.select(:id)
1894+
|> Ash.Query.select([:id, :last_read_message_id])
18951895
|> Ash.Query.load(:unread_message_count)
18961896
|> Ash.Query.limit(10)
18971897
|> Ash.read!()

test/calculation_test.exs

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,33 @@ defmodule AshPostgres.CalculationTest do
111111
|> Ash.read!()
112112
end
113113

114+
test "runtime loading calculation with fragment referencing aggregate works correctly" do
115+
post =
116+
Post
117+
|> Ash.Changeset.for_create(:create, %{title: "test post"})
118+
|> Ash.create!()
119+
120+
Comment
121+
|> Ash.Changeset.for_create(:create, %{title: "comment1", likes: 5})
122+
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
123+
|> Ash.create!()
124+
125+
Comment
126+
|> Ash.Changeset.for_create(:create, %{title: "comment2", likes: 15})
127+
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
128+
|> Ash.create!()
129+
130+
result =
131+
Post
132+
|> Ash.Query.load([:comment_metric, :complex_comment_metric, :multi_agg_calc])
133+
|> Ash.read!()
134+
135+
assert [post] = result
136+
assert is_integer(post.comment_metric)
137+
assert is_integer(post.complex_comment_metric)
138+
assert is_integer(post.multi_agg_calc)
139+
end
140+
114141
test "expression calculations don't load when `reuse_values?` is true" do
115142
post =
116143
Post
@@ -1241,4 +1268,215 @@ defmodule AshPostgres.CalculationTest do
12411268

12421269
assert [] == Ash.read!(query)
12431270
end
1271+
1272+
test "expression calculation referencing aggregates loaded via code_interface with load option" do
1273+
post =
1274+
Post
1275+
|> Ash.Changeset.for_create(:create, %{title: "test post"})
1276+
|> Ash.create!()
1277+
1278+
Comment
1279+
|> Ash.Changeset.for_create(:create, %{title: "comment1", likes: 5})
1280+
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
1281+
|> Ash.create!()
1282+
1283+
Comment
1284+
|> Ash.Changeset.for_create(:create, %{title: "comment2", likes: 15})
1285+
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
1286+
|> Ash.create!()
1287+
1288+
result = Post.get_by_id!(post.id, load: [:comment_metric])
1289+
1290+
assert result.comment_metric == 200
1291+
end
1292+
1293+
test "complex SQL fragment calculation with multiple aggregates" do
1294+
post =
1295+
Post
1296+
|> Ash.Changeset.for_create(:create, %{
1297+
title: "test post",
1298+
base_reading_time: 500
1299+
})
1300+
|> Ash.create!()
1301+
1302+
Comment
1303+
|> Ash.Changeset.for_create(:create, %{
1304+
title: "comment1",
1305+
edited_duration: 100,
1306+
planned_duration: 80,
1307+
reading_time: 30,
1308+
version: :edited
1309+
})
1310+
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
1311+
|> Ash.create!()
1312+
1313+
Comment
1314+
|> Ash.Changeset.for_create(:create, %{
1315+
title: "comment2",
1316+
edited_duration: 0,
1317+
planned_duration: 120,
1318+
reading_time: 45,
1319+
version: :planned
1320+
})
1321+
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
1322+
|> Ash.create!()
1323+
1324+
result = Post.get_by_id!(post.id, load: [:estimated_reading_time])
1325+
1326+
assert result.estimated_reading_time == 175
1327+
end
1328+
1329+
test "calculation with missing aggregate dependencies" do
1330+
post =
1331+
Post
1332+
|> Ash.Changeset.for_create(:create, %{
1333+
title: "test post",
1334+
base_reading_time: 500
1335+
})
1336+
|> Ash.create!()
1337+
1338+
Comment
1339+
|> Ash.Changeset.for_create(:create, %{
1340+
title: "modified comment",
1341+
edited_duration: 100,
1342+
planned_duration: 0,
1343+
reading_time: 30,
1344+
version: :edited
1345+
})
1346+
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
1347+
|> Ash.create!()
1348+
1349+
Comment
1350+
|> Ash.Changeset.for_create(:create, %{
1351+
title: "planned comment",
1352+
edited_duration: 0,
1353+
planned_duration: 80,
1354+
reading_time: 20,
1355+
version: :planned
1356+
})
1357+
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
1358+
|> Ash.create!()
1359+
1360+
result = Post.get_by_id!(post.id, load: [:estimated_reading_time])
1361+
1362+
refute match?(%Ash.NotLoaded{}, result.estimated_reading_time),
1363+
"Expected calculated value, got: #{inspect(result.estimated_reading_time)}"
1364+
end
1365+
1366+
test "calculation with filtered aggregates and keyset pagination" do
1367+
post =
1368+
Post
1369+
|> Ash.Changeset.for_create(:create, %{
1370+
title: "test post",
1371+
base_reading_time: 500
1372+
})
1373+
|> Ash.create!()
1374+
1375+
Comment
1376+
|> Ash.Changeset.for_create(:create, %{
1377+
title: "completed comment",
1378+
edited_duration: 100,
1379+
reading_time: 30,
1380+
version: :edited,
1381+
status: :published
1382+
})
1383+
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
1384+
|> Ash.create!()
1385+
1386+
Comment
1387+
|> Ash.Changeset.for_create(:create, %{
1388+
title: "pending comment",
1389+
planned_duration: 80,
1390+
reading_time: 20,
1391+
version: :planned,
1392+
status: :pending
1393+
})
1394+
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
1395+
|> Ash.create!()
1396+
1397+
result_calc_only = Post.get_by_id!(post.id, load: [:estimated_reading_time])
1398+
1399+
debug_result =
1400+
Post.get_by_id!(post.id,
1401+
load: [
1402+
:total_edited_time,
1403+
:total_planned_time,
1404+
:total_comment_time,
1405+
:published_comments,
1406+
:base_reading_time
1407+
]
1408+
)
1409+
1410+
result_count_only = Post.get_by_id!(post.id, load: [:published_comments])
1411+
1412+
result_both = Post.get_by_id!(post.id, load: [:published_comments, :estimated_reading_time])
1413+
1414+
assert result_both.estimated_reading_time == 150,
1415+
"Should calculate correctly with both loaded"
1416+
1417+
assert result_both.published_comments == 1, "Should count correctly with both loaded"
1418+
end
1419+
1420+
test "calculation with keyset pagination works correctly (previously returned NotLoaded)" do
1421+
_posts =
1422+
Enum.map(1..5, fn i ->
1423+
post =
1424+
Post
1425+
|> Ash.Changeset.for_create(:create, %{
1426+
title: "test post #{i}",
1427+
base_reading_time: 100 * i
1428+
})
1429+
|> Ash.create!()
1430+
1431+
Comment
1432+
|> Ash.Changeset.for_create(:create, %{
1433+
title: "comment#{i}",
1434+
edited_duration: 50 * i,
1435+
planned_duration: 40 * i,
1436+
reading_time: 10 * i,
1437+
version: :edited,
1438+
status: :published
1439+
})
1440+
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
1441+
|> Ash.create!()
1442+
1443+
post
1444+
end)
1445+
1446+
first_page =
1447+
Post
1448+
|> Ash.Query.load([:published_comments, :estimated_reading_time])
1449+
|> Ash.read!(action: :read_with_related_list_agg_filter, page: [limit: 2, count: true])
1450+
1451+
Enum.each(first_page.results, fn post ->
1452+
refute match?(%Ash.NotLoaded{}, post.estimated_reading_time),
1453+
"First page post #{post.id} should have loaded estimated_reading_time, got: #{inspect(post.estimated_reading_time)}"
1454+
end)
1455+
1456+
if first_page.more? do
1457+
second_page =
1458+
Post
1459+
|> Ash.Query.load([:published_comments, :estimated_reading_time])
1460+
|> Ash.read!(
1461+
action: :read_with_related_list_agg_filter,
1462+
page: [
1463+
limit: 2,
1464+
after: first_page.results |> List.last() |> Map.get(:__metadata__) |> Map.get(:keyset)
1465+
]
1466+
)
1467+
1468+
assert length(second_page.results) > 0, "Second page should have results"
1469+
1470+
Enum.each(second_page.results, fn post ->
1471+
refute match?(%Ash.NotLoaded{}, post.estimated_reading_time),
1472+
"estimated_reading_time should be calculated, not NotLoaded"
1473+
1474+
refute match?(%Ash.NotLoaded{}, post.published_comments),
1475+
"published_comments should be calculated, not NotLoaded"
1476+
1477+
assert post.estimated_reading_time > 0, "estimated_reading_time should be positive"
1478+
assert post.published_comments == 1, "Each post has exactly 1 completed comment"
1479+
end)
1480+
end
1481+
end
12441482
end

test/complex_calculation_test.exs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# SPDX-FileCopyrightText: 2019 ash_postgres contributors <https://github.com/ash-project/ash_postgres/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule AshPostgres.Test.ComplexCalculationTest do
6+
use AshPostgres.RepoCase, async: false
7+
alias AshPostgres.Test.{Comment, Post}
8+
require Ash.Query
9+
10+
describe "complex calculations with filtered aggregates" do
11+
test "estimated_reading_time calculation works with filtered aggregates and pagination" do
12+
post =
13+
Post
14+
|> Ash.Changeset.for_create(:create, %{
15+
title: "Test Post",
16+
base_reading_time: 0
17+
})
18+
|> Ash.create!()
19+
20+
for i <- 1..3 do
21+
Comment
22+
|> Ash.Changeset.for_create(:create, %{
23+
post_id: post.id,
24+
reading_time: 30 + i * 10,
25+
status: :published
26+
})
27+
|> Ash.create!()
28+
end
29+
30+
query_opts = [
31+
load: [:published_comments, :estimated_reading_time],
32+
page: [limit: 5]
33+
]
34+
35+
page_result =
36+
Ash.Query.filter(Post, id == ^post.id)
37+
|> Ash.read!(query_opts)
38+
39+
[post] = page_result.results
40+
41+
assert post.estimated_reading_time == 150
42+
assert post.published_comments == 3
43+
end
44+
45+
test "estimated_reading_time works when loaded independently (control test)" do
46+
post =
47+
Post
48+
|> Ash.Changeset.for_create(:create, %{
49+
title: "Control Test",
50+
base_reading_time: 0
51+
})
52+
|> Ash.create!()
53+
54+
for i <- 1..3 do
55+
Comment
56+
|> Ash.Changeset.for_create(:create, %{
57+
post_id: post.id,
58+
reading_time: 30 + i * 10,
59+
status: :published
60+
})
61+
|> Ash.create!()
62+
end
63+
64+
[post] =
65+
Ash.Query.filter(Post, id == ^post.id)
66+
|> Ash.read!(load: [:estimated_reading_time])
67+
68+
assert post.estimated_reading_time == 150
69+
end
70+
end
71+
end

test/support/resources/comment.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ defmodule AshPostgres.Test.Comment do
4848
attribute(:title, :string, public?: true)
4949
attribute(:likes, :integer, public?: true)
5050
attribute(:arbitrary_timestamp, :utc_datetime_usec, public?: true)
51+
attribute(:edited_duration, :integer, public?: true)
52+
attribute(:planned_duration, :integer, public?: true)
53+
attribute(:reading_time, :integer, public?: true)
54+
attribute(:version, :atom, constraints: [one_of: [:edited, :planned]], public?: true)
55+
attribute(:status, :atom, constraints: [one_of: [:published, :draft]], public?: true)
5156
create_timestamp(:created_at, writable?: true, public?: true)
5257
end
5358

0 commit comments

Comments
 (0)