Repository: elixir-ecto/ecto
Pull request #2499 (2018-04-07 16:42:30)

Support naming query `from` with `as`
Refs #2389 

This PR adds support for using `as` for naming query source.

Example:

```elixir
from(p in Post, as: :post)
|> where([post: p], p.title == "hello")
```


Patch:
diff --git a/lib/ecto/query.ex b/lib/ecto/query.ex
index e649c96e..e56030d2 100644
--- a/lib/ecto/query.ex
+++ b/lib/ecto/query.ex
@@ -312,7 +312,7 @@ defmodule Ecto.Query do
 
   defmodule FromExpr do
     @moduledoc false
-    defstruct [:source]
+    defstruct [:source, :as]
   end
 
   defmodule DynamicExpr do
@@ -592,7 +592,8 @@ defmodule Ecto.Query do
       raise ArgumentError, "second argument to `from` must be a compile time keyword list"
     end
 
-    {quoted, binds, count_bind} = From.build(expr, __CALLER__)
+    {kw, as} = collect_as(kw)
+    {quoted, binds, count_bind} = From.build(expr, __CALLER__, as)
     from(kw, __CALLER__, count_bind, quoted, to_query_binds(binds))
   end
 
@@ -674,6 +675,9 @@ defmodule Ecto.Query do
   defp collect_on_and_as(other, on, as),
     do: {other, on, as}
 
+  defp collect_as([{:as, as} | t]), do: {t, as}
+  defp collect_as(t), do: {t, nil}
+
   @doc """
   A join query expression.
 
diff --git a/lib/ecto/query/builder/from.ex b/lib/ecto/query/builder/from.ex
index 6dc51004..da69631e 100644
--- a/lib/ecto/query/builder/from.ex
+++ b/lib/ecto/query/builder/from.ex
@@ -49,8 +49,12 @@ defmodule Ecto.Query.Builder.From do
   If possible, it does all calculations at compile time to avoid
   runtime work.
   """
-  @spec build(Macro.t, Macro.Env.t) :: {Macro.t, Keyword.t, non_neg_integer | nil}
-  def build(query, env) do
+  @spec build(Macro.t, Macro.Env.t, atom) :: {Macro.t, Keyword.t, non_neg_integer | nil}
+  def build(query, env, as) do
+    if not is_atom(as) do
+      Builder.error! "`as` must be a compile time atom, got: `#{Macro.to_string(as)}`"
+    end
+
     {query, binds} = escape(query, env)
 
     {count_bind, quoted} =
@@ -60,15 +64,15 @@ defmodule Ecto.Query.Builder.From do
           # dependencies between modules are added
           source = quote do: unquote(schema).__schema__(:source)
           prefix = quote do: unquote(schema).__schema__(:prefix)
-          {1, query(prefix, source, schema)}
+          {1, query(prefix, source, schema, as)}
 
         source when is_binary(source) ->
           # When a binary is used, there is no schema
-          {1, query(nil, source, nil)}
+          {1, query(nil, source, nil, as)}
 
         {source, schema} when is_binary(source) and is_atom(schema) ->
           prefix = quote do: unquote(schema).__schema__(:prefix)
-          {1, query(prefix, source, schema)}
+          {1, query(prefix, source, schema, as)}
 
         other ->
           {nil, other}
@@ -78,13 +82,15 @@ defmodule Ecto.Query.Builder.From do
     {quoted, binds, count_bind}
   end
 
-  defp query(prefix, source, schema) do
+  defp query(prefix, source, schema, as) do
+    aliases = if as, do: [{as, 0}], else: []
+
     {:%, [], [Ecto.Query,
               {:%{}, [],
                [from: {:%, [], [Ecto.Query.FromExpr,
-                                {:%{}, [], [source: {source, schema}]}]},
+                                {:%{}, [], [source: {source, schema}, as: as]}]},
                 prefix: prefix,
-                aliases: {:%{}, [], []}]}]}
+                aliases: {:%{}, [], aliases}]}]}
   end
 
   defp expand_from({left, right}, env) do
diff --git a/lib/ecto/query/inspect.ex b/lib/ecto/query/inspect.ex
index 9e54e1ed..697a7f3e 100644
--- a/lib/ecto/query/inspect.ex
+++ b/lib/ecto/query/inspect.ex
@@ -69,7 +69,9 @@ defimpl Inspect, for: Ecto.Query do
   end
 
   defp bound_from(nil, name), do: ["from #{name} in query"]
-  defp bound_from(%{source: source}, name), do: ["from #{name} in #{inspect_source source}"]
+  defp bound_from(%{source: source, as: as}, name) do
+    ["from #{name} in #{inspect_source source}"] ++ kw_inspect(:as, as)
+  end
 
   defp inspect_source(%Ecto.Query{} = query), do: "^" <> inspect(query)
   defp inspect_source(%Ecto.SubQuery{query: query}), do: "subquery(#{to_string query})"


Test patch:
diff --git a/integration_test/cases/repo.exs b/integration_test/cases/repo.exs
index 2263e737..79ffabba 100644
--- a/integration_test/cases/repo.exs
+++ b/integration_test/cases/repo.exs
@@ -47,6 +47,16 @@ defmodule Ecto.Integration.RepoTest do
     end
   end
 
+  test "fetch using named from" do
+    TestRepo.insert!(%Post{title: "hello"})
+
+    query =
+      from(p in Post, as: :post)
+      |> where([post: p], p.title == "hello")
+
+    assert [_] = TestRepo.all query
+  end
+
   test "fetch without schema" do
     %Post{} = TestRepo.insert!(%Post{title: "title1"})
     %Post{} = TestRepo.insert!(%Post{title: "title2"})
diff --git a/test/ecto/query_test.exs b/test/ecto/query_test.exs
index 8a160dca..0370bb9a 100644
--- a/test/ecto/query_test.exs
+++ b/test/ecto/query_test.exs
@@ -221,6 +221,19 @@ defmodule Ecto.QueryTest do
       assert %{comment: 2, link: 3} == query.aliases
     end
 
+    test "assigns a name to query source" do
+      query = from p in "posts", as: :post
+
+      assert %{post: 0} == query.aliases
+    end
+
+    test "assign to source fails when non-atom name passed" do
+      message = ~r"`as` must be a compile time atom, got: `\"post\"`"
+      assert_raise Ecto.Query.CompileError, message, fn ->
+        quote_and_eval(from(p in "posts", as: "post"))
+      end
+    end
+
     test "crashes on duplicate as for keyword query" do
       message = ~r"`as` keyword was given more than once to the same join"
       assert_raise Ecto.Query.CompileError, message, fn ->
@@ -253,6 +266,15 @@ defmodule Ecto.QueryTest do
         ~s[#Ecto.Query<from p in \"posts\", join: c in \"comments\", as: :comment, on: true, where: c.id == 0>]
     end
 
+    test "match on binding by name for source" do
+      query =
+        from(p in "posts", as: :post)
+        |> where([post: p], p.id == 0)
+
+      assert inspect(query) ==
+        ~s[#Ecto.Query<from p in \"posts\", as: :post, where: p.id == 0>]
+    end
+
     test "match on binding by name with ... in the middle" do
       query =
         "posts"