From 8344c3175ee8cb341874fbe21ff2eab9d2e7d763 Mon Sep 17 00:00:00 2001 From: Jinkyou Son Date: Wed, 25 Feb 2026 17:21:02 +0900 Subject: [PATCH 1/3] feat: support offset option in relationship joins Adds offset support to the SQL join layer for has_one and has_many relationships. This works alongside the offset option being added to Ash core (ash-project/ash#2584). Co-Authored-By: Claude Opus 4.6 --- lib/join.ex | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/join.ex b/lib/join.ex index e7b0df5..f2b8ca2 100644 --- a/lib/join.ex +++ b/lib/join.ex @@ -349,7 +349,8 @@ defmodule AshSql.Join do has_parent_expr? = opts[:require_lateral?] || !!query.__ash_bindings__.context[:data_layer][:has_parent_expr?] || - not is_nil(query.limit) + not is_nil(query.limit) || + not is_nil(query.offset) query = if has_parent_expr? do @@ -457,6 +458,13 @@ defmodule AshSql.Join do |> Ash.Query.unset(:sort) end end) + |> then(fn query -> + if not is_nil(Map.get(relationship, :offset)) do + Ash.Query.offset(query, relationship.offset) + else + query + end + end) |> set_has_parent_expr_context(relationship) |> case do %{valid?: true} = related_query -> @@ -544,14 +552,23 @@ defmodule AshSql.Join do defp limit_from_many( query, - %{from_many?: true, destination: destination}, + %{from_many?: true, destination: destination} = relationship, filter, filter_subquery?, opts ) do + offset = Map.get(relationship, :offset) + if filter_subquery? do + inner_query = + if offset do + from(row in query, limit: 1, offset: ^offset) + else + from(row in query, limit: 1) + end + query = - from(row in Ecto.Query.subquery(from(row in query, limit: 1)), + from(row in Ecto.Query.subquery(inner_query), as: ^query.__ash_bindings__.root_binding ) |> Map.put(:__ash_bindings__, query.__ash_bindings__) @@ -578,7 +595,7 @@ defmodule AshSql.Join do defp limit_from_many( query, - %{limit: limit, destination: destination}, + %{limit: limit, destination: destination} = relationship, filter, filter_subquery?, opts @@ -587,11 +604,19 @@ defmodule AshSql.Join do # Check if query has parent expressions - if so, we can't wrap in a non-lateral subquery # because parent references won't resolve across the subquery boundary has_parent_expr? = !!query.__ash_bindings__.context[:data_layer][:has_parent_expr?] + offset = Map.get(relationship, :offset) if filter_subquery? && !has_parent_expr? do # Wrap the limited query in a subquery, then apply filter on top + inner_query = + if offset do + from(row in query, limit: ^limit, offset: ^offset) + else + from(row in query, limit: ^limit) + end + query = - from(row in Ecto.Query.subquery(from(row in query, limit: ^limit)), + from(row in Ecto.Query.subquery(inner_query), as: ^query.__ash_bindings__.root_binding ) |> Map.put(:__ash_bindings__, query.__ash_bindings__) From 8aa5370d43adcf96fd514659de2aafa0a00fe78c Mon Sep 17 00:00:00 2001 From: Jinkyou Son Date: Thu, 26 Feb 2026 14:28:53 +0900 Subject: [PATCH 2/3] mix credo --- lib/join.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/join.ex b/lib/join.ex index f2b8ca2..dd669f0 100644 --- a/lib/join.ex +++ b/lib/join.ex @@ -459,10 +459,10 @@ defmodule AshSql.Join do end end) |> then(fn query -> - if not is_nil(Map.get(relationship, :offset)) do - Ash.Query.offset(query, relationship.offset) - else + if is_nil(Map.get(relationship, :offset)) do query + else + Ash.Query.offset(query, relationship.offset) end end) |> set_has_parent_expr_context(relationship) From b1e0594740eed0ff70520b3fc9064f59ff4b257b Mon Sep 17 00:00:00 2001 From: Jinkyou Son Date: Thu, 26 Feb 2026 14:30:05 +0900 Subject: [PATCH 3/3] mix format --- lib/expr.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/expr.ex b/lib/expr.ex index 01cd3e9..6d3fc53 100644 --- a/lib/expr.ex +++ b/lib/expr.ex @@ -1345,7 +1345,8 @@ defmodule AshSql.Expr do end bindings = - if no_cast_for_native_value?(left, left_type) or no_cast_for_native_value?(right, right_type) do + if no_cast_for_native_value?(left, left_type) or + no_cast_for_native_value?(right, right_type) do Map.put(bindings, :skip_cast_for_ref?, true) else bindings