@@ -2256,4 +2256,52 @@ defmodule AshSql.AggregateTest do
22562256 "Calculation was not loaded when using page(count: true) with aggregates"
22572257 end
22582258 end
2259+
2260+ describe "join_filters in aggregate calculations" do
2261+ test "Ash.Filter structs in join_filters are properly converted to Ecto expressions" do
2262+ # This test reproduces the bug where Ash.Filter structs in join_filters
2263+ # are not properly converted to Ecto dynamic expressions, causing:
2264+ # ** (Ecto.Query.CastError) value `#Ash.Filter<...>` in `where` cannot be cast to type :boolean
2265+ #
2266+ # The root cause is in ash_sql/lib/expr.ex - when a BooleanExpression contains
2267+ # an Ash.Filter struct as an operand, the private do_dynamic_expr/default_dynamic_expr
2268+ # functions don't have a clause to handle it, so the Ash.Filter is passed directly
2269+ # to Ecto instead of being converted to a dynamic expression.
2270+
2271+ author =
2272+ Author
2273+ |> Ash.Changeset . for_create ( :create , % { first_name: "Test" , last_name: "Author" } )
2274+ |> Ash . create! ( )
2275+
2276+ post =
2277+ Post
2278+ |> Ash.Changeset . for_create ( :create , % { title: "test" } )
2279+ |> Ash.Changeset . manage_relationship ( :author , author , type: :append_and_remove )
2280+ |> Ash . create! ( )
2281+
2282+ comment =
2283+ Comment
2284+ |> Ash.Changeset . for_create ( :create , % { title: "comment" , likes: 5 } )
2285+ |> Ash.Changeset . manage_relationship ( :post , post , type: :append_and_remove )
2286+ |> Ash.Changeset . manage_relationship ( :author , author , type: :append_and_remove )
2287+ |> Ash . create! ( )
2288+
2289+ Rating
2290+ |> Ash.Changeset . for_create ( :create , % { score: 10 , resource_id: comment . id } )
2291+ |> Ash.Changeset . set_context ( % { data_layer: % { table: "comment_ratings" } } )
2292+ |> Ash . create! ( )
2293+
2294+ # This triggers the bug - loading a calculation that uses join_filters with actor reference.
2295+ # The join_filter `expr(author_id == ^actor(:id))` gets resolved to an Ash.Filter struct
2296+ # which is then combined with other filters in a BooleanExpression.
2297+ # We use authorize?: false to bypass Post's organization-based authorization policies.
2298+ assert { :ok , [ loaded_post ] } =
2299+ Post
2300+ |> Ash.Query . filter ( id == ^ post . id )
2301+ |> Ash.Query . load ( :max_rating_with_join_filter )
2302+ |> Ash . read ( actor: author , authorize?: false )
2303+
2304+ assert loaded_post . max_rating_with_join_filter == 10
2305+ end
2306+ end
22592307end
0 commit comments