Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions test/aggregate_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@
|> Ash.read_one!()
end

test "it properly applies join criteria on the first term of a two-level path" do

Check failure on line 473 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (15) / mix test

test count it properly applies join criteria on the first term of a two-level path (AshSql.AggregateTest)

Check failure on line 473 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (16) / mix test

test count it properly applies join criteria on the first term of a two-level path (AshSql.AggregateTest)

Check failure on line 473 in test/aggregate_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (14) / mix test

test count it properly applies join criteria on the first term of a two-level path (AshSql.AggregateTest)
author =
Author
|> Ash.Changeset.for_create(:create)
Expand Down Expand Up @@ -2256,4 +2256,70 @@
"Calculation was not loaded when using page(count: true) with aggregates"
end
end

describe "join_filters in aggregate calculations" do
test "Ash.Filter structs in join_filters are properly converted to Ecto expressions" do
# This test reproduces the bug where Ash.Filter structs in join_filters
# are not properly converted to Ecto dynamic expressions, causing:
# ** (Ecto.Query.CastError) value `#Ash.Filter<...>` in `where` cannot be cast to type :boolean
#
# The root cause is in ash_sql/lib/expr.ex - when a BooleanExpression contains
# an Ash.Filter struct as an operand, the private do_dynamic_expr/default_dynamic_expr
# functions don't have a clause to handle it, so the Ash.Filter is passed directly
# to Ecto instead of being converted to a dynamic expression.
#
# The bug triggers when authorization policies create Ash.Filter structs that get
# combined with join_filter expressions in BooleanExpressions.

# Set up authorization chain: User -> Organization -> Post -> Comment
org =
Organization
|> Ash.Changeset.for_create(:create, %{name: "Test Org"})
|> Ash.create!()

user =
User
|> Ash.Changeset.for_create(:create, %{})
|> Ash.Changeset.manage_relationship(:organization, org, type: :append_and_remove)
|> Ash.create!()

author =
Author
|> Ash.Changeset.for_create(:create, %{first_name: "Test", last_name: "Author"})
|> Ash.create!()

post =
Post
|> Ash.Changeset.for_create(:create, %{title: "test"})
|> Ash.Changeset.manage_relationship(:organization, org, type: :append_and_remove)
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
|> Ash.create!()

comment =
Comment
|> Ash.Changeset.for_create(:create, %{title: "comment", likes: 5})
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|> Ash.Changeset.manage_relationship(:author, author, type: :append_and_remove)
|> Ash.create!()

Rating
|> Ash.Changeset.for_create(:create, %{score: 10, resource_id: comment.id})
|> Ash.Changeset.set_context(%{data_layer: %{table: "comment_ratings"}})
|> Ash.create!()

# This triggers the bug - loading a calculation that uses join_filters with actor reference.
# The join_filter `expr(author_id == ^actor(:id))` gets resolved to an Ash.Filter struct
# which is then combined with authorization policy filters in a BooleanExpression.
assert {:ok, [loaded_post]} =
Post
|> Ash.Query.filter(id == ^post.id)
|> Ash.Query.load(:max_rating_with_join_filter)
|> Ash.read(actor: user)

# The rating won't match because the comment's author_id doesn't match user.id,
# but the important thing is the query executes without CastError
assert is_nil(loaded_post.max_rating_with_join_filter) or
loaded_post.max_rating_with_join_filter == 10
end
end
end
15 changes: 15 additions & 0 deletions test/support/resources/post.ex
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ defmodule AshPostgres.Test.Post do
attribute :keyword_map, :keyword do
public?(true)
allow_nil?(true)

constraints(
fields: [
display_template: [
Expand All @@ -630,6 +631,7 @@ defmodule AshPostgres.Test.Post do
]
)
end

attribute(:list_of_stuff, {:array, :map}, public?: true)
attribute(:uniq_one, :string, public?: true)
attribute(:uniq_two, :string, public?: true)
Expand Down Expand Up @@ -1211,6 +1213,19 @@ defmodule AshPostgres.Test.Post do

calculate(:past_datetime1?, :boolean, expr(now() > datetime))
calculate(:past_datetime2?, :boolean, expr(datetime <= now()))

# Test case for join_filters bug where Ash.Filter structs are not converted to Ecto expressions
calculate(
:max_rating_with_join_filter,
:integer,
expr(
max([:comments, :ratings],
query: [filter: expr(score > 0)],
field: :score,
join_filters: %{comments: expr(author_id == ^actor(:id))}
)
)
)
end

aggregates do
Expand Down
Loading