Skip to content

Commit c2412e1

Browse files
allow atoms (interpolated or not) as source in field/2
1 parent 95b6a29 commit c2412e1

File tree

4 files changed

+46
-1
lines changed

4 files changed

+46
-1
lines changed

lib/ecto/query/api.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,10 @@ defmodule Ecto.Query.API do
607607
query, the two types of names behave the same. However, when referencing
608608
a field from a schema the behaviours are different.
609609
610+
The field source can be given as a binding (`p` in `from p in Post`), a
611+
late binding (`as/1` and `parent_as/1)` or an atom representing a named
612+
binding.
613+
610614
Using an atom to reference a schema field will inherit all the properties from
611615
the schema. For example, the field name will be changed to the value of `:source`
612616
before generating the final query and its type behaviour will be dictated by the

lib/ecto/query/builder.ex

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,12 +713,27 @@ defmodule Ecto.Query.Builder do
713713
{:{}, [], [dot, [], []]}
714714
end
715715

716+
defp escape_field!(source, field, _vars) when is_atom(source) do
717+
as = {:{}, [], [:as, [], [source]]}
718+
field = quoted_atom_or_string!(field, "field/2")
719+
dot = {:{}, [], [:., [], [as, field]]}
720+
{:{}, [], [dot, [], []]}
721+
end
722+
723+
defp escape_field!({:^, _, [_]} = expr, field, _vars) do
724+
value = quoted_atom!(expr, "field/2")
725+
as = {:{}, [], [:as, [], [value]]}
726+
field = quoted_atom_or_string!(field, "field/2")
727+
dot = {:{}, [], [:., [], [as, field]]}
728+
{:{}, [], [dot, [], []]}
729+
end
730+
716731
defp escape_field!(expr, field, _vars) do
717732
error!("""
718733
cannot fetch field `#{Macro.to_string(field)}` from `#{Macro.to_string(expr)}`. Can only fetch fields from:
719734
720735
* sources, such as `p` in `from p in Post`
721-
* named bindings, such as `as(:post)` in `from Post, as: :post`
736+
* named bindings, such as `as(:post)` or `:post` in `from Post, as: :post`
722737
* parent named bindings, such as `parent_as(:post)` in a subquery
723738
""")
724739
end
@@ -1376,6 +1391,12 @@ defmodule Ecto.Query.Builder do
13761391
{{:{}, [], [kind, [], [value]]}, field}
13771392
end
13781393

1394+
def quoted_type({:field, _, [expr, field]}, _vars)
1395+
when is_atom(field) or is_binary(field) do
1396+
source = quoted_atom!(expr, "field/2")
1397+
{{:{}, [], [:as, [], [source]]}, field}
1398+
end
1399+
13791400
# Unquoting code here means the second argument of field will
13801401
# always be unquoted twice, one by the type checking and another
13811402
# in the query itself. We are assuming this is not an issue
@@ -1390,6 +1411,11 @@ defmodule Ecto.Query.Builder do
13901411
{{:{}, [], [kind, [], [value]]}, code}
13911412
end
13921413

1414+
def quoted_type({:field, _, [expr, {:^, _, [code]}]}, _vars) do
1415+
source = quoted_atom!(expr, "field/2")
1416+
{{:{}, [], [:as, [], [source]]}, code}
1417+
end
1418+
13931419
# Interval
13941420
def quoted_type({:datetime_add, _, [_, _, _]}, _vars), do: :naive_datetime
13951421
def quoted_type({:date_add, _, [_, _, _]}, _vars), do: :date

test/ecto/query/builder/select_test.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@ defmodule Ecto.Query.Builder.SelectTest do
268268
ref = dynamic(field(as(^as), ^field))
269269
query = from(b in "blogs", select: ^ref)
270270

271+
ref = dynamic(field(^as, ^field))
272+
query = from(b in "blogs", select: ^ref)
273+
271274
assert Macro.to_string(query.select.expr) == "as(:blog).title()"
272275
end
273276

test/ecto/query/planner_test.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1312,6 +1312,18 @@ defmodule Ecto.Query.PlannerTest do
13121312
assert Macro.to_string(hd(query.wheres).expr) == "&0 . \"visits\"() == ^0"
13131313
assert cast_params == ["123"]
13141314

1315+
{query, cast_params, _, _} =
1316+
from(Post, as: :posts, where: field(^as, :visits) == ^"123") |> normalize_with_params()
1317+
1318+
assert Macro.to_string(hd(query.wheres).expr) == "&0.visits() == ^0"
1319+
assert cast_params == [123]
1320+
1321+
{query, cast_params, _, _} =
1322+
from(Post, as: :posts, where: field(:posts, :visits) == ^"123") |> normalize_with_params()
1323+
1324+
assert Macro.to_string(hd(query.wheres).expr) == "&0.visits() == ^0"
1325+
assert cast_params == [123]
1326+
13151327
assert_raise Ecto.QueryError, ~r/could not find named binding `as\(:posts\)`/, fn ->
13161328
from(Post, where: as(^as).visits == ^"123") |> normalize()
13171329
end

0 commit comments

Comments
 (0)