diff --git a/priv/resource_snapshots/test_repo/chats/20251030040520.json b/priv/resource_snapshots/test_repo/chats/20251030040520.json new file mode 100644 index 00000000..173de42e --- /dev/null +++ b/priv/resource_snapshots/test_repo/chats/20251030040520.json @@ -0,0 +1,74 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "chats_last_read_message_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "messages" + }, + "scale": null, + "size": null, + "source": "last_read_message_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "78989357AB74584B9A18249E307C842D17AA96B7DD790EAADC9FA1C6422388B5", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "chats" +} \ No newline at end of file diff --git a/priv/test_repo/migrations/20251030040520_add_last_read_message_to_chats.exs b/priv/test_repo/migrations/20251030040520_add_last_read_message_to_chats.exs new file mode 100644 index 00000000..fffd5ad4 --- /dev/null +++ b/priv/test_repo/migrations/20251030040520_add_last_read_message_to_chats.exs @@ -0,0 +1,31 @@ +defmodule AshPostgres.TestRepo.Migrations.AddLastReadMessageToChats do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:chats) do + add( + :last_read_message_id, + references(:messages, + column: :id, + name: "chats_last_read_message_id_fkey", + type: :uuid, + prefix: "public" + ) + ) + end + end + + def down do + drop(constraint(:chats, "chats_last_read_message_id_fkey")) + + alter table(:chats) do + remove(:last_read_message_id) + end + end +end diff --git a/test/aggregate_test.exs b/test/aggregate_test.exs index ad66b36e..52bad4f3 100644 --- a/test/aggregate_test.exs +++ b/test/aggregate_test.exs @@ -5,7 +5,7 @@ defmodule AshSql.AggregateTest do use AshPostgres.RepoCase, async: false import ExUnit.CaptureIO - alias AshPostgres.Test.{Author, Comment, Organization, Post, Rating, User} + alias AshPostgres.Test.{Author, Chat, Comment, Organization, Post, Rating, User} require Ash.Query import Ash.Expr @@ -1860,4 +1860,63 @@ defmodule AshSql.AggregateTest do assert Enum.at(results, 0).count_of_comments == 3 assert Enum.at(results, 1).count_of_comments == 2 end + + describe "aggregate with parent filter and limited select" do + test "FAILS when combining select() + limit() with aggregate using parent() in filter" do + # BUG: When using select() + limit() with an aggregate that uses parent() + # in its filter, the query generation creates a subquery that's missing the parent + # fields, causing a SQL error. + # + # This bug was found in ash_graphql where GraphQL list queries with pagination + # would fail when loading aggregates that use parent() in filters. + # + # The bug requires BOTH conditions: + # 1. select() limits which fields are included (e.g., only :id) + # 2. limit() causes a subquery to be generated + # 3. An aggregate filter references parent() fields that aren't in select() + # + # Without BOTH select() and limit(), the query works fine (see tests below). + # + # Current error: + # ERROR 42703 (undefined_column) column s0.last_read_message_id does not exist + # + # Generated query: + # SELECT s0."id", coalesce(s1."unread_message_count"::bigint, ...) + # FROM (SELECT sc0."id" AS "id" FROM "chats" AS sc0 LIMIT 10) AS s0 + # LEFT OUTER JOIN LATERAL ( + # SELECT ... FROM "messages" WHERE ... s0."last_read_message_id" ... # <- field not in subquery! + # ) AS s1 ON TRUE + # + # Expected fix: Ash should automatically include parent() referenced fields + # (like last_read_message_id) in the subquery even if not explicitly selected. + + Chat + |> Ash.Query.select(:id) + |> Ash.Query.load(:unread_message_count) + |> Ash.Query.limit(10) + |> Ash.read!() + end + + test "works WITHOUT select() - limit alone doesn't cause the bug" do + Chat + |> Ash.Query.load(:unread_message_count) + |> Ash.Query.limit(10) + |> Ash.read!() + end + + test "works WITHOUT limit() - select alone doesn't cause the bug" do + Chat + |> Ash.Query.select(:id) + |> Ash.Query.load(:unread_message_count) + |> Ash.read!() + end + + test "works when selecting the parent() referenced field explicitly (workaround)" do + Chat + |> Ash.Query.select([:id, :last_read_message_id]) + |> Ash.Query.load(:unread_message_count) + |> Ash.Query.limit(10) + |> Ash.read!() + end + end end diff --git a/test/support/resources/chat.ex b/test/support/resources/chat.ex index c237fe42..afe2bec1 100644 --- a/test/support/resources/chat.ex +++ b/test/support/resources/chat.ex @@ -24,6 +24,12 @@ defmodule AshPostgres.Test.Chat do end relationships do + belongs_to :last_read_message, AshPostgres.Test.Message do + allow_nil?(true) + public?(true) + attribute_writable?(true) + end + has_many :messages, AshPostgres.Test.Message do public?(true) end @@ -41,4 +47,11 @@ defmodule AshPostgres.Test.Chat do sort(sent_at: :desc) end end + + aggregates do + count :unread_message_count, :messages do + public?(true) + filter(expr(is_nil(parent(last_read_message_id)) or id > parent(last_read_message_id))) + end + end end