Skip to content

Commit f270bbe

Browse files
authored
test: reproduce parent() scoping bug in nested exists with calculation (#717)
When a calculation on resource B uses `parent()` inside an unrelated `exists()`, and resource A references that calculation via `exists(relationship_to_b, b_calculation)`, the `parent()` references incorrectly resolve to resource A instead of resource B. Example: Post has `exists(Author, first_name == parent(title))` where parent(title) should refer to Post.title. When Author uses `exists(posts, that_post_calculation)`, the generated SQL produces `authors.title` instead of `posts.title`.
1 parent d5f8e92 commit f270bbe

File tree

3 files changed

+75
-0
lines changed

3 files changed

+75
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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.NestedExistsParentScopeTest do
6+
use AshPostgres.RepoCase, async: false
7+
8+
alias AshPostgres.Test.{Author, Post}
9+
10+
require Ash.Query
11+
12+
describe "nested exists with calculation containing parent() inside unrelated exists" do
13+
test "parent() references in a calculation are scoped to the calculation's own resource, not the outer exists" do
14+
# Setup: Author "Alice" with a post titled "Alice"
15+
# The post's `has_matching_author_by_unrelated_exists` calculation checks:
16+
# exists(Author, first_name == parent(title))
17+
# When used standalone on the post, parent(title) refers to Post.title — works fine.
18+
#
19+
# Author's `has_post_matching_author_via_nested_exists` calculation checks:
20+
# exists(posts, has_matching_author_by_unrelated_exists)
21+
# This inlines the Post calculation. The bug: parent(title) gets scoped to
22+
# Author instead of Post, generating SQL like `authors.title` which doesn't exist.
23+
24+
author =
25+
Author
26+
|> Ash.Changeset.for_create(:create, %{first_name: "Alice"})
27+
|> Ash.create!()
28+
29+
Post
30+
|> Ash.Changeset.for_create(:create, %{title: "Alice", author_id: author.id})
31+
|> Ash.create!()
32+
33+
# This should work: the post titled "Alice" matches the author named "Alice"
34+
assert %{has_post_matching_author_via_nested_exists: true} =
35+
Ash.load!(author, [:has_post_matching_author_via_nested_exists])
36+
end
37+
38+
test "the inner calculation works correctly when loaded directly on the child resource" do
39+
# Sanity check: the Post calculation works fine when not nested
40+
author =
41+
Author
42+
|> Ash.Changeset.for_create(:create, %{first_name: "Bob"})
43+
|> Ash.create!()
44+
45+
post =
46+
Post
47+
|> Ash.Changeset.for_create(:create, %{title: "Bob", author_id: author.id})
48+
|> Ash.create!()
49+
50+
assert %{has_matching_author_by_unrelated_exists: true} =
51+
Ash.load!(post, [:has_matching_author_by_unrelated_exists])
52+
end
53+
end
54+
end

test/support/resources/author.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,15 @@ defmodule AshPostgres.Test.Author do
206206
expr(exists(posts, title == parent(first_name)))
207207
)
208208

209+
# When used standalone on a Post, parent(title) correctly refers to the Post.
210+
# But when used inside exists(posts, ...) from Author, parent(title) incorrectly
211+
# resolves to Author (which doesn't have a title column).
212+
calculate(
213+
:has_post_matching_author_via_nested_exists,
214+
:boolean,
215+
expr(exists(posts, has_matching_author_by_unrelated_exists))
216+
)
217+
209218
calculate(:profile_description_calc, :string, expr(profile.description), allow_nil?: true)
210219

211220
calculate(:true_if_actor_in_context, :boolean, TrueIfActorInContext)

test/support/resources/post.ex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,18 @@ defmodule AshPostgres.Test.Post do
10361036
expr(exists(followers, name == "fred"))
10371037
)
10381038

1039+
# Used to test nested exists with parent() — see nested_exists_test.exs
1040+
calculate(
1041+
:has_matching_author_by_unrelated_exists,
1042+
:boolean,
1043+
expr(
1044+
exists(
1045+
AshPostgres.Test.Author,
1046+
first_name == parent(title)
1047+
)
1048+
)
1049+
)
1050+
10391051
calculate(
10401052
:composite_origin,
10411053
AshPostgres.Test.CompositePoint,

0 commit comments

Comments
 (0)