Skip to content

Commit f4b8462

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 f4b8462

File tree

6 files changed

+428
-1
lines changed

6 files changed

+428
-1
lines changed
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: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,34 @@ defmodule AshPostgres.CalculationTest do
111111
|> Ash.read!()
112112
end
113113

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

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

test/complex_calculation_test.exs

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