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
7 changes: 7 additions & 0 deletions lib/data_layer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,13 @@ defmodule AshPostgres.DataLayer do
base_query
end

base_query =
if Map.get(relationship, :offset) do
from(row in base_query, offset: ^relationship.offset)
else
base_query
end

base_query =
cond do
Map.get(relationship, :manual) ->
Expand Down
27 changes: 27 additions & 0 deletions test/load_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,33 @@ defmodule AshPostgres.Test.LoadTest do
|> Map.get(:latest_comment)
end

test "has_one with offset returns the correct record" do
post =
Post
|> Ash.Changeset.for_create(:create, %{title: "title"})
|> Ash.create!()

%{id: first_comment_id} =
Comment
|> Ash.Changeset.for_create(:create, %{title: "first"})
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|> Ash.create!()

:timer.sleep(1)

Comment
|> Ash.Changeset.for_create(:create, %{title: "second"})
|> Ash.Changeset.manage_relationship(:post, post, type: :append_and_remove)
|> Ash.create!()

# second_latest_comment: sort(created_at: :desc), offset(1) => first comment
assert %{id: ^first_comment_id} =
Post
|> Ash.Query.load(:second_latest_comment)
|> Ash.read_one!()
|> Map.get(:second_latest_comment)
end
Comment on lines +68 to +93
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only inserts two comments, so offset(1) happens to leave a single remaining row even if the underlying lateral-join subquery doesn't apply limit: 1. To actually validate the "pick exactly one row after offset" behavior, add a third (or more) comment and assert the loaded second_latest_comment is still exactly the second-by-sort record (not any later record).

Copilot uses AI. Check for mistakes.

test "belongs_to relationships can be loaded" do
assert %Comment{post: %Ash.NotLoaded{type: :relationship}} =
comment =
Expand Down
6 changes: 6 additions & 0 deletions test/support/resources/post.ex
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,12 @@ defmodule AshPostgres.Test.Post do
public?(true)
end

has_one :second_latest_comment, AshPostgres.Test.Comment do
sort(created_at: :desc)
offset(1)
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

second_latest_comment uses sort/1 and offset/1 to pick a single record from the comments collection, but it doesn't set from_many?(true) like latest_comment does. Without from_many?(true), the lateral-join query builder won't apply limit: 1, and for posts with 3+ comments the subquery can return multiple rows (all rows after the offset), making the loaded has_one nondeterministic.

Consider adding from_many?(true) (and keeping the existing sort/offset) so this relationship is guaranteed to resolve to exactly one comment.

Suggested change
offset(1)
offset(1)
from_many?(true)

Copilot uses AI. Check for mistakes.
public?(true)
end

has_many :comments_matching_post_title, AshPostgres.Test.Comment do
public?(true)
filter(expr(title == parent_expr(title)))
Expand Down
Loading