Repository: elixir-ecto/ecto
Pull request #2437 (2018-02-21 12:53:47)

Fix select_merge/2 regression
Related to #2436 

AndrewDryga: After merging, I can submit the same PR to the branch `v2.2` so we can release an update.

Patch:
diff --git a/lib/ecto/query.ex b/lib/ecto/query.ex
index 85fd1ddf..a3b93f77 100644
--- a/lib/ecto/query.ex
+++ b/lib/ecto/query.ex
@@ -305,7 +305,7 @@ defmodule Ecto.Query do
   specially on insert, use `Ecto.put_meta/2`.
   """
 
-  defstruct [prefix: nil, sources: nil, from: nil, joins: [], aliases: nil, wheres: [], select: nil,
+  defstruct [prefix: nil, sources: nil, from: nil, joins: [], aliases: %{}, wheres: [], select: nil,
              order_bys: [], limit: nil, offset: nil, group_bys: [], updates: [],
              havings: [], preloads: [], assocs: [], distinct: nil, lock: nil]
   @type t :: %__MODULE__{}
@@ -332,7 +332,7 @@ defmodule Ecto.Query do
 
   defmodule JoinExpr do
     @moduledoc false
-    defstruct [:qual, :source, :on, :file, :line, :assoc, :alias, :ix, params: []]
+    defstruct [:qual, :source, :on, :file, :line, :assoc, :ix, params: []]
   end
 
   defmodule Tagged do
@@ -624,9 +624,8 @@ defmodule Ecto.Query do
 
   defp from([{join, expr}|t], env, count_bind, quoted, binds) when join in @joins do
     qual = join_qual(join)
-    {t, as} = pop_join_as(t)
-    {t, on} = collect_on(t, nil)
-    {quoted, binds, count_bind} = Join.build(quoted, qual, binds, expr, count_bind, env, as: as, on: on)
+    {t, on, as} = collect_on_and_as(t, nil, nil)
+    {quoted, binds, count_bind} = Join.build(quoted, qual, binds, expr, count_bind, on, as, env)
     from(t, env, count_bind, quoted, to_query_binds(binds))
   end
 
@@ -659,15 +658,16 @@ defmodule Ecto.Query do
   defp join_qual(:left_lateral_join), do: :left_lateral
   defp join_qual(:inner_lateral_join), do: :inner_lateral
 
-  defp pop_join_as([{:as, expr}|t]), do: {t, expr}
-  defp pop_join_as(t), do: {t, nil}
-
-  defp collect_on([{:on, expr}|t], nil),
-    do: collect_on(t, expr)
-  defp collect_on([{:on, expr}|t], acc),
-    do: collect_on(t, {:and, [], [acc, expr]})
-  defp collect_on(other, acc),
-    do: {other, acc}
+  defp collect_on_and_as([{:on, on} | t], nil, as),
+    do: collect_on_and_as(t, on, as)
+  defp collect_on_and_as([{:on, expr} | t], on, as),
+    do: collect_on_and_as(t, {:and, [], [on, expr]}, as)
+  defp collect_on_and_as([{:as, as} | t], on, nil),
+    do: collect_on_and_as(t, on, as)
+  defp collect_on_and_as([{:as, _} | _], _, _),
+    do: Builder.error! "`as` keyword was given more than once to the same join"
+  defp collect_on_and_as(other, on, as),
+    do: {other, on, as}
 
   @doc """
   A join query expression.
@@ -766,18 +766,18 @@ defmodule Ecto.Query do
 
   """
   defmacro join(query, qual, binding \\ [], expr, opts \\ []) do
-    opts =
+    {on, as} =
       case parse_join_opts(opts) do
         {:opts, opts} ->
-          opts
+          {Keyword.get(opts, :on), Keyword.get(opts, :as)}
         {:expr, on_expr} ->
           IO.warn "Passing raw `on` expression as the last argument to Ecto.Query.join/5 is deprecated. " <>
             "Please use :on keyword option instead."
-          [on: on_expr]
+          {on_expr, nil}
       end
 
     query
-    |> Join.build(qual, binding, expr, nil, __CALLER__, opts)
+    |> Join.build(qual, binding, expr, nil, on, as, __CALLER__)
     |> elem(0)
   end
 
@@ -793,7 +793,7 @@ defmodule Ecto.Query do
       {opts, opts} ->
         {:opts, opts}
       {list, _} ->
-        raise ArgumentError, "invalid option(s) passed to Ecto.Query.join/5: " <>
+        raise ArgumentError, "invalid options passed to Ecto.Query.join/5: " <>
           "#{inspect(Keyword.keys(list) -- @join_opts)}. Valid options: #{inspect @join_opts}."
     end
   end
diff --git a/lib/ecto/query/builder.ex b/lib/ecto/query/builder.ex
index cc528d40..a0676a9f 100644
--- a/lib/ecto/query/builder.ex
+++ b/lib/ecto/query/builder.ex
@@ -509,64 +509,60 @@ defmodule Ecto.Query.Builder do
 
   ## Examples
 
-      iex> escape_binding(%Ecto.Query{}, quote do: [x, y, z])
+      iex> escape_binding(%Ecto.Query{}, quote(do: [x, y, z]), __ENV__)
       {%Ecto.Query{}, [x: 0, y: 1, z: 2]}
 
-      iex> escape_binding(%Ecto.Query{}, quote do: [{x, 0}, {z, 2}])
+      iex> escape_binding(%Ecto.Query{}, quote(do: [{x, 0}, {z, 2}]), __ENV__)
       {%Ecto.Query{}, [x: 0, z: 2]}
 
-      iex> escape_binding(%Ecto.Query{}, quote do: [x, y, x])
+      iex> escape_binding(%Ecto.Query{}, quote(do: [x, y, x]), __ENV__)
       ** (Ecto.Query.CompileError) variable `x` is bound twice
 
-      iex> escape_binding(%Ecto.Query{}, quote do: [a, b, :foo])
-      ** (Ecto.Query.CompileError) binding list should contain only variables or `{:bind_name, var}` tuples, got: :foo
+      iex> escape_binding(%Ecto.Query{}, quote(do: [a, b, :foo]), __ENV__)
+      ** (Ecto.Query.CompileError) binding list should contain only variables or `{as, var}` tuples, got: :foo
 
   """
-  @spec escape_binding(Macro.t | Ecto.Query.t, list) :: {Macro.t | Ecto.Query.t, Keyword.t}
-  def escape_binding(query, binding) when is_list(binding) do
-    vars = binding |> Enum.with_index |> Enum.map(&escape_bind(&1))
-
-    with :ok <- assert_no_duplicate_bind(vars),
-         {positional_vars, named_vars} <- Enum.split_while(vars, & !named_bind?(&1)),
-         :ok <- assert_named_binds_in_tail(named_vars),
-         {query, positional_binds} <- calculate_positional_binds(query, positional_vars),
-         {query, named_binds} <- calculate_named_binds(query, named_vars) do
-      {query, positional_binds ++ named_binds}
-    else
-      {:error, {:duplicate_bind, var}} ->
-        error! "variable `#{var}` is bound twice"
-      {:error, :named_binds_not_in_tail} ->
-        error! "named binds in the form of `{:bind_name, var}` tuples must be at the end " <>
-          "of the binding list, got: #{Macro.to_string(binding)}"
-    end
-   end
-  def escape_binding(_query, bind) do
-    error! "binding should be list of variables and `{:bind_name, var}` tuples " <>
-      "at the end, got: #{Macro.to_string(bind)}"
+  @spec escape_binding(Macro.t, list, Macro.Env.t) :: {Macro.t, Keyword.t}
+  def escape_binding(query, binding, env) when is_list(binding) do
+    vars = binding |> Enum.with_index |> Enum.map(&escape_bind/1)
+    assert_no_duplicate_binding!(vars)
+
+    {positional_vars, named_vars} = Enum.split_while(vars, &not named_bind?(&1))
+    assert_named_binds_in_tail!(named_vars, binding)
+
+    {query, positional_binds} = calculate_positional_binds(query, positional_vars)
+    {query, named_binds} = calculate_named_binds(query, named_vars, env)
+    {query, positional_binds ++ named_binds}
+  end
+  def escape_binding(_query, bind, _env) do
+    error! "binding should be list of variables and `{as, var}` tuples " <>
+             "at the end, got: #{Macro.to_string(bind)}"
   end
 
-  defp assert_named_binds_in_tail(named_vars) do
+  defp named_bind?({kind, _, _}), do: kind == :named
+
+  defp assert_named_binds_in_tail!(named_vars, binding) do
     if Enum.all?(named_vars, &named_bind?/1) do
       :ok
     else
-      {:error, :named_binds_not_in_tail}
+      error! "named binds in the form of `{as, var}` tuples must be at the end " <>
+               "of the binding list, got: #{Macro.to_string(binding)}"
     end
   end
 
-  defp named_bind?({_, {:named, _}}), do: true
-  defp named_bind?(_), do: false
+  defp assert_no_duplicate_binding!(vars) do
+    bound_vars = for {_, var, _} <- vars, var != :_, do: var
 
-  defp assert_no_duplicate_bind(vars) do
-    bound_vars = vars |> Keyword.keys |> Enum.filter(& &1 != :_)
     case bound_vars -- Enum.uniq(bound_vars) do
       []  -> :ok
-      dup -> {:error, {:duplicate_bind, hd(dup)}}
+      [var | _] -> error! "variable `#{var}` is bound twice"
     end
   end
 
   defp calculate_positional_binds(query, vars) do
-    case Enum.split_while(vars, & elem(&1, 0) != :...) do
+    case Enum.split_while(vars, &elem(&1, 1) != :...) do
       {vars, []} ->
+        vars = for {:pos, var, count} <- vars, do: {var, count}
         {query, vars}
       {vars, [_ | tail]} ->
         query =
@@ -575,45 +571,56 @@ defmodule Ecto.Query.Builder do
             escape_count = Ecto.Query.Builder.count_binds(query)
             query
           end
+
         tail =
           tail
           |> Enum.with_index(-length(tail))
-          |> Enum.map(fn {{k, _}, count} -> {k, quote(do: escape_count + unquote(count))} end)
+          |> Enum.map(fn {{:pos, k, _}, count} -> {k, quote(do: escape_count + unquote(count))} end)
+
+        vars = for {:pos, var, count} <- vars, do: {var, count}
         {query, vars ++ tail}
     end
   end
 
-  defp calculate_named_binds(query, []), do: {query, []}
-  defp calculate_named_binds(query, vars) do
+  def calculate_named_binds(query, [], _env), do: {query, []}
+  def calculate_named_binds(query, vars, env) do
     query =
       quote do
-        query = Ecto.Queryable.to_query(unquote(query))
-        aliases = query.aliases || %{}
-        query
+        %{aliases: aliases} = query = Ecto.Queryable.to_query(unquote(query))
       end
 
-    vars = Enum.map(vars, fn {k, {_, name}} ->
-      ix = quote do
-        case Map.fetch(aliases, unquote(name)) do
-          {:ok, ix} -> ix
-          :error -> Ecto.Query.Builder.error! "there is no bind named `#{inspect unquote(name)}`"
-        end
+    vars =
+      for {:named, key, name} <- vars do
+        ix =
+          quote do
+            case Map.fetch(aliases, unquote(name)) do
+              {:ok, ix} ->
+                ix
+
+              :error ->
+                raise Ecto.QueryError,
+                  message: "unknown bind name `#{inspect unquote(name)}`",
+                  query: query,
+                  file: unquote(env.file),
+                  line: unquote(env.line)
+            end
+          end
+
+        {key, ix}
       end
-      {k, ix}
-    end)
 
     {query, vars}
   end
 
   defp escape_bind({{{var, _, context}, ix}, _}) when is_atom(var) and is_atom(context),
-    do: {var, ix}
+    do: {:pos, var, ix}
   defp escape_bind({{var, _, context}, ix}) when is_atom(var) and is_atom(context),
-    do: {var, ix}
+    do: {:pos, var, ix}
   defp escape_bind({{name, {var, _, context}}, _ix}) when is_atom(name) and is_atom(var) and is_atom(context),
-    do: {var, {:named, name}}
+    do: {:named, var, name}
   defp escape_bind({bind, _ix}),
     do: error!("binding list should contain only variables or " <>
-          "`{:bind_name, var}` tuples, got: #{Macro.to_string(bind)}")
+          "`{as, var}` tuples, got: #{Macro.to_string(bind)}")
 
   defp try_expansion(expr, type, params, vars, %Macro.Env{} = env) do
     try_expansion(expr, type, params, vars, {env, &escape/5})
@@ -889,11 +896,11 @@ defmodule Ecto.Query.Builder do
 
   # Unescapes an `Ecto.Query` struct.
   defp unescape_query({:%, _, [Query, {:%{}, _, list}]}) do
-    struct(Query, list)
+    struct(Query, unescape_aliases(list))
   end
   defp unescape_query({:%{}, _, list} = ast) do
     if List.keyfind(list, :__struct__, 0) == {:__struct__, Query} do
-      Enum.into(list, %{})
+      Enum.into(unescape_aliases(list), %{})
     else
       ast
     end
@@ -902,9 +909,25 @@ defmodule Ecto.Query.Builder do
     other
   end
 
+  defp unescape_aliases(query) do
+    case List.keytake(query, :aliases, 0) do
+      {{:aliases, {:%{}, _, aliases}}, query} -> [aliases: Map.new(aliases)] ++ query
+      _ -> query
+    end
+  end
+
   # Escapes an `Ecto.Query` and associated structs.
   defp escape_query(%Query{} = query),
-    do: {:%{}, [], Map.to_list(query)}
+    do: {:%{}, [], escape_aliases(query)}
   defp escape_query(other),
     do: other
+
+  defp escape_aliases(%{aliases: aliases} = query) do
+    query = Map.to_list(Map.delete(query, :aliases))
+
+    case aliases do
+      %{} -> [aliases: {:%{}, [], Map.to_list(aliases)}] ++ query
+      aliases -> [aliases: aliases] ++ query
+    end
+  end
 end
diff --git a/lib/ecto/query/builder/distinct.ex b/lib/ecto/query/builder/distinct.ex
index 071a2860..5ad38eb1 100644
--- a/lib/ecto/query/builder/distinct.ex
+++ b/lib/ecto/query/builder/distinct.ex
@@ -49,7 +49,7 @@ defmodule Ecto.Query.Builder.Distinct do
   """
   @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t
   def build(query, binding, expr, env) do
-    {query, binding} = Builder.escape_binding(query, binding)
+    {query, binding} = Builder.escape_binding(query, binding, env)
     {expr, params} = escape(expr, binding, env)
     params = Builder.escape_params(params)
 
diff --git a/lib/ecto/query/builder/dynamic.ex b/lib/ecto/query/builder/dynamic.ex
index 8d73ba5a..1fa2609e 100644
--- a/lib/ecto/query/builder/dynamic.ex
+++ b/lib/ecto/query/builder/dynamic.ex
@@ -10,7 +10,7 @@ defmodule Ecto.Query.Builder.Dynamic do
   """
   @spec build([Macro.t], Macro.t, Macro.Env.t) :: Macro.t
   def build(binding, expr, env) do
-    {query, vars} = Builder.escape_binding(quote(do: query), binding)
+    {query, vars} = Builder.escape_binding(quote(do: query), binding, env)
     {expr, {params, :acc}} = Builder.escape(expr, :any, {%{}, :acc}, vars, env)
     params = Builder.escape_params(params)
 
diff --git a/lib/ecto/query/builder/filter.ex b/lib/ecto/query/builder/filter.ex
index 08fb64e6..abef9058 100644
--- a/lib/ecto/query/builder/filter.ex
+++ b/lib/ecto/query/builder/filter.ex
@@ -57,7 +57,7 @@ defmodule Ecto.Query.Builder.Filter do
   end
 
   def build(kind, op, query, binding, expr, env) do
-    {query, binding} = Builder.escape_binding(query, binding)
+    {query, binding} = Builder.escape_binding(query, binding, env)
     {expr, params} = escape(kind, expr, 0, binding, env)
     params = Builder.escape_params(params)
 
diff --git a/lib/ecto/query/builder/from.ex b/lib/ecto/query/builder/from.ex
index ca93e907..bf36a229 100644
--- a/lib/ecto/query/builder/from.ex
+++ b/lib/ecto/query/builder/from.ex
@@ -11,34 +11,34 @@ defmodule Ecto.Query.Builder.From do
 
   ## Examples
 
-      iex> escape(quote do: MySchema)
+      iex> escape(quote(do: MySchema), __ENV__)
       {quote(do: MySchema), []}
 
-      iex> escape(quote do: p in posts)
+      iex> escape(quote(do: p in posts), __ENV__)
       {quote(do: posts), [p: 0]}
 
-      iex> escape(quote do: p in {"posts", MySchema})
+      iex> escape(quote(do: p in {"posts", MySchema}), __ENV__)
       {quote(do: {"posts", MySchema}), [p: 0]}
 
-      iex> escape(quote do: [p, q] in posts)
+      iex> escape(quote(do: [p, q] in posts), __ENV__)
       {quote(do: posts), [p: 0, q: 1]}
 
-      iex> escape(quote do: [_, _] in abc)
+      iex> escape(quote(do: [_, _] in abc), __ENV__)
       {quote(do: abc), [_: 0, _: 1]}
 
-      iex> escape(quote do: other)
+      iex> escape(quote(do: other), __ENV__)
       {quote(do: other), []}
 
-      iex> escape(quote do: x() in other)
-      ** (Ecto.Query.CompileError) binding list should contain only variables or `{:bind_name, var}` tuples, got: x()
+      iex> escape(quote(do: x() in other), __ENV__)
+      ** (Ecto.Query.CompileError) binding list should contain only variables or `{as, var}` tuples, got: x()
 
   """
-  @spec escape(Macro.t) :: {Macro.t, Keyword.t}
-  def escape({:in, _, [var, query]}) do
-    Builder.escape_binding(query, List.wrap(var))
+  @spec escape(Macro.t, Macro.Env.t) :: {Macro.t, Keyword.t}
+  def escape({:in, _, [var, query]}, env) do
+    Builder.escape_binding(query, List.wrap(var), env)
   end
 
-  def escape(query) do
+  def escape(query, _env) do
     {query, []}
   end
 
@@ -51,7 +51,7 @@ defmodule Ecto.Query.Builder.From do
   """
   @spec build(Macro.t, Macro.Env.t) :: {Macro.t, Keyword.t, non_neg_integer | nil}
   def build(query, env) do
-    {query, binds} = escape(query)
+    {query, binds} = escape(query, env)
 
     {count_bind, quoted} =
       case expand_from(query, env) do
@@ -79,7 +79,7 @@ defmodule Ecto.Query.Builder.From do
   end
 
   defp query(prefix, source, schema) do
-    {:%, [], [Ecto.Query, {:%{}, [], [from: {source, schema}, prefix: prefix]}]}
+    {:%, [], [Ecto.Query, {:%{}, [], [from: {source, schema}, prefix: prefix, aliases: {:%{}, [], []}]}]}
   end
 
   defp expand_from({left, right}, env) do
diff --git a/lib/ecto/query/builder/group_by.ex b/lib/ecto/query/builder/group_by.ex
index 38bc135c..0f8ef316 100644
--- a/lib/ecto/query/builder/group_by.ex
+++ b/lib/ecto/query/builder/group_by.ex
@@ -73,7 +73,7 @@ defmodule Ecto.Query.Builder.GroupBy do
   """
   @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t
   def build(query, binding, expr, env) do
-    {query, binding} = Builder.escape_binding(query, binding)
+    {query, binding} = Builder.escape_binding(query, binding, env)
     {expr, params} = escape(expr, binding, env)
     params = Builder.escape_params(params)
 
diff --git a/lib/ecto/query/builder/join.ex b/lib/ecto/query/builder/join.ex
index 0d4b7e0d..255473b7 100644
--- a/lib/ecto/query/builder/join.ex
+++ b/lib/ecto/query/builder/join.ex
@@ -117,12 +117,14 @@ defmodule Ecto.Query.Builder.Join do
   If possible, it does all calculations at compile time to avoid
   runtime work.
   """
-  @spec build(Macro.t, atom, [Macro.t], Macro.t, Macro.t, Macro.Env.t, Keyword.t) ::
+  @spec build(Macro.t, atom, [Macro.t], Macro.t, Macro.t, Macro.t, atom, Macro.Env.t) ::
               {Macro.t, Keyword.t, non_neg_integer | nil}
-  def build(query, qual, binding, expr, count_bind, env, opts) do
-    on = opts[:on]
-    alias_name = opts[:as]
-    {query, binding} = Builder.escape_binding(query, binding)
+  def build(query, qual, binding, expr, count_bind, on, as, env) do
+    if not is_atom(as) do
+      Builder.error! "`as` must be a compile time atom, got: `#{Macro.to_string(as)}`"
+    end
+
+    {query, binding} = Builder.escape_binding(query, binding, env)
     {join_bind, join_source, join_assoc, join_params} = escape(expr, binding, env)
     join_params = Builder.escape_params(join_params)
 
@@ -153,81 +155,76 @@ defmodule Ecto.Query.Builder.Join do
         quote(do: unquote(count_bind) + 1)
       end
 
-    query = build_on(on || true, query, binding, count_bind, qual,
-                     join_source, join_assoc, join_params, alias_name, env)
+    query = build_on(on || true, as, query, binding, count_bind, qual,
+                     join_source, join_assoc, join_params, env)
     {query, binding, next_bind}
   end
 
-  def build_on({:^, _, [var]}, query, _binding, count_bind,
-               join_qual, join_source, join_assoc, join_params, alias_name, env) do
+  def build_on({:^, _, [var]}, as, query, _binding, count_bind,
+               join_qual, join_source, join_assoc, join_params, env) do
     quote do
       query = unquote(query)
-      Ecto.Query.Builder.Join.join!(query, unquote(var), unquote(count_bind),
+      Ecto.Query.Builder.Join.join!(query, unquote(var), unquote(as), unquote(count_bind),
                                     unquote(join_qual), unquote(join_source), unquote(join_assoc),
-                                    unquote(alias_name), unquote(join_params), unquote(env.file),
-                                    unquote(env.line))
+                                    unquote(join_params), unquote(env.file), unquote(env.line))
     end
   end
 
-  def build_on(on, query, binding, count_bind,
-               join_qual, join_source, join_assoc, join_params, alias_name, env) do
+  def build_on(on, as, query, binding, count_bind,
+               join_qual, join_source, join_assoc, join_params, env) do
     {on_expr, on_params} = Ecto.Query.Builder.Filter.escape(:on, on, count_bind, binding, env)
     on_params = Builder.escape_params(on_params)
 
     join =
       quote do
         %JoinExpr{qual: unquote(join_qual), source: unquote(join_source),
-                  assoc: unquote(join_assoc), alias: unquote(alias_name), file: unquote(env.file),
+                  assoc: unquote(join_assoc), file: unquote(env.file),
                   line: unquote(env.line), params: unquote(join_params),
                   on: %QueryExpr{expr: unquote(on_expr), params: unquote(on_params),
                                  line: unquote(env.line), file: unquote(env.file)}}
       end
 
-    Builder.apply_query(query, __MODULE__, [join], env)
+    Builder.apply_query(query, __MODULE__, [join, as, count_bind], env)
   end
 
   @doc """
   Applies the join expression to the query.
   """
-  def apply(%Ecto.Query{joins: joins, aliases: aliases} = query, expr) do
-    join_count = Builder.count_binds(query)
-    aliases = update_aliases(aliases, expr, join_count)
-
+  def apply(%Ecto.Query{joins: joins, aliases: aliases} = query, expr, as, count_bind) do
+    aliases = apply_aliases(aliases, as, count_bind)
     %{query | joins: joins ++ [expr], aliases: aliases}
   end
-  def apply(query, expr) do
-    apply(Ecto.Queryable.to_query(query), expr)
+  def apply(query, expr, as, count_bind) do
+    apply(Ecto.Queryable.to_query(query), expr, as, count_bind)
   end
 
-  def update_aliases(aliases, %JoinExpr{alias: nil}, _), do: aliases
-  def update_aliases(aliases, %JoinExpr{alias: name}, join_count) do
-    aliases = aliases || %{}
-
+  def apply_aliases(aliases, nil, _), do: aliases
+  def apply_aliases(aliases = %{}, name, join_count) when is_atom(name) and is_integer(join_count) do
     if Map.has_key?(aliases, name) do
       Builder.error! "alias `#{inspect name}` already exists"
     else
       Map.put(aliases, name, join_count)
     end
   end
-  def update_aliases(aliases, expr, join_count) do
+  def apply_aliases(aliases, name, join_count) do
     quote do
-      Ecto.Query.Builder.Join.update_aliases(unquote(aliases), unquote(expr), unquote(join_count))
+      Ecto.Query.Builder.Join.apply_aliases(unquote(Macro.escape(aliases)), unquote(name), unquote(join_count))
     end
   end
 
   @doc """
   Called at runtime to build a join.
   """
-  def join!(query, expr, count_bind, join_qual, join_source, join_assoc, alias_name, join_params, file, line) do
+  def join!(query, expr, as, count_bind, join_qual, join_source, join_assoc, join_params, file, line) do
     {on_expr, on_params, on_file, on_line} =
       Ecto.Query.Builder.Filter.filter!(:on, query, expr, count_bind, file, line)
 
-    join = %JoinExpr{qual: join_qual, source: join_source, assoc: join_assoc, alias: alias_name,
+    join = %JoinExpr{qual: join_qual, source: join_source, assoc: join_assoc,
                      file: file, line: line, params: join_params,
                      on: %QueryExpr{expr: on_expr, params: on_params,
                                     line: on_line, file: on_file}}
 
-    apply(query, join)
+    apply(query, join, as, count_bind)
   end
 
   defp validate_qual(qual) when is_atom(qual) do
diff --git a/lib/ecto/query/builder/limit_offset.ex b/lib/ecto/query/builder/limit_offset.ex
index bf6c251d..1ac37099 100644
--- a/lib/ecto/query/builder/limit_offset.ex
+++ b/lib/ecto/query/builder/limit_offset.ex
@@ -14,7 +14,7 @@ defmodule Ecto.Query.Builder.LimitOffset do
   """
   @spec build(:limit | :offset, Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t
   def build(type, query, binding, expr, env) do
-    {query, binding} = Builder.escape_binding(query, binding)
+    {query, binding} = Builder.escape_binding(query, binding, env)
     {expr, {params, :acc}} = Builder.escape(expr, :integer, {%{}, :acc}, binding, env)
     params = Builder.escape_params(params)
 
diff --git a/lib/ecto/query/builder/order_by.ex b/lib/ecto/query/builder/order_by.ex
index 71c0870f..799fcff2 100644
--- a/lib/ecto/query/builder/order_by.ex
+++ b/lib/ecto/query/builder/order_by.ex
@@ -110,7 +110,7 @@ defmodule Ecto.Query.Builder.OrderBy do
   """
   @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t
   def build(query, binding, expr, env) do
-    {query, binding} = Builder.escape_binding(query, binding)
+    {query, binding} = Builder.escape_binding(query, binding, env)
     {expr, params} = escape(:order_by, expr, binding, env)
     params = Builder.escape_params(params)
 
diff --git a/lib/ecto/query/builder/preload.ex b/lib/ecto/query/builder/preload.ex
index bc9ac81e..3e981c35 100644
--- a/lib/ecto/query/builder/preload.ex
+++ b/lib/ecto/query/builder/preload.ex
@@ -141,7 +141,7 @@ defmodule Ecto.Query.Builder.Preload do
   """
   @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t
   def build(query, binding, expr, env) do
-    {query, binding} = Builder.escape_binding(query, binding)
+    {query, binding} = Builder.escape_binding(query, binding, env)
     {preloads, assocs} = escape(expr, binding)
     Builder.apply_query(query, __MODULE__, [Enum.reverse(preloads), Enum.reverse(assocs)], env)
   end
diff --git a/lib/ecto/query/builder/select.ex b/lib/ecto/query/builder/select.ex
index 6b4b66e7..7b9a46a0 100644
--- a/lib/ecto/query/builder/select.ex
+++ b/lib/ecto/query/builder/select.ex
@@ -209,7 +209,7 @@ defmodule Ecto.Query.Builder.Select do
   end
 
   def build(kind, query, binding, expr, env) do
-    {query, binding} = Builder.escape_binding(query, binding)
+    {query, binding} = Builder.escape_binding(query, binding, env)
     {expr, {params, take}} = escape(expr, binding, env)
     params = Builder.escape_params(params)
     take   = {:%{}, [], Map.to_list(take)}
diff --git a/lib/ecto/query/builder/update.ex b/lib/ecto/query/builder/update.ex
index 33a788c4..e4a98438 100644
--- a/lib/ecto/query/builder/update.ex
+++ b/lib/ecto/query/builder/update.ex
@@ -88,7 +88,7 @@ defmodule Ecto.Query.Builder.Update do
   """
   @spec build(Macro.t, [Macro.t], Macro.t, Macro.Env.t) :: Macro.t
   def build(query, binding, expr, env) do
-    {query, binding} = Builder.escape_binding(query, binding)
+    {query, binding} = Builder.escape_binding(query, binding, env)
     {compile, runtime, params} = escape(expr, binding, env)
 
     query =
diff --git a/lib/ecto/repo/queryable.ex b/lib/ecto/repo/queryable.ex
index bcabf015..f0084379 100644
--- a/lib/ecto/repo/queryable.ex
+++ b/lib/ecto/repo/queryable.ex
@@ -235,6 +235,9 @@ defmodule Ecto.Repo.Queryable do
         {map, row}
     end
   end
+  defp process(row, {:map, {:source, :from}, args}, nil, _prefix, _adapter) do
+    {Map.new(args), row}
+  end
   defp process(row, {:map, data, args}, from, prefix, adapter) do
     {data, row} = process(row, data, from, prefix, adapter)
     process_update(data, args, row, from, prefix, adapter)


Test patch:
diff --git a/integration_test/cases/repo.exs b/integration_test/cases/repo.exs
index 9a064e22..e16c93bd 100644
--- a/integration_test/cases/repo.exs
+++ b/integration_test/cases/repo.exs
@@ -1120,6 +1120,15 @@ defmodule Ecto.Integration.RepoTest do
       assert [%{title: "2"}] =
              Post |> select([p], %{title: p.title}) |> select_merge([p], %{title: "2"}) |> TestRepo.all()
     end
+
+    test "merge with update" do
+      %Post{} = TestRepo.insert!(%Post{title: "1", counter: 1})
+
+      assert [%Post{title: "1", counter: 2}] =
+        Post |> select([p], merge(p, %{p | counter: 2})) |> TestRepo.all()
+      assert [%Post{title: "1", counter: 2}] =
+        Post |> select([p], p) |> select_merge([p], %{p | counter: 2}) |> TestRepo.all()
+    end
   end
 
   test "query count distinct" do
diff --git a/test/ecto/query/builder/join_test.exs b/test/ecto/query/builder/join_test.exs
index 34fe9f80..ff8ab3be 100644
--- a/test/ecto/query/builder/join_test.exs
+++ b/test/ecto/query/builder/join_test.exs
@@ -121,10 +121,24 @@ defmodule Ecto.Query.Builder.JoinTest do
   end
 
   test "raises on mix of valid and invalid options passed to join/5" do
-    assert_raise ArgumentError, ~r/invalid option\(s\) passed/, fn ->
+    assert_raise ArgumentError, ~r/invalid options passed/, fn ->
       escape(quote do
         join("posts", :left, [p], c in "comments", on: true, foo: :bar)
       end, [], __ENV__)
     end
   end
+
+  test "raises on non-atom as" do
+    assert_raise Ecto.Query.CompileError, ~r/`as` must be a compile time atom/, fn ->
+      escape(quote do
+        join("posts", :left, [p], c in "comments", on: true, as: "string")
+      end, [], __ENV__)
+    end
+
+    assert_raise Ecto.Query.CompileError, ~r/`as` must be a compile time atom/, fn ->
+      escape(quote do
+        join("posts", :left, [p], c in "comments", on: true, as: atom)
+      end, [], __ENV__)
+    end
+  end
 end
diff --git a/test/ecto/query_test.exs b/test/ecto/query_test.exs
index 6f930ba7..cfb5eb26 100644
--- a/test/ecto/query_test.exs
+++ b/test/ecto/query_test.exs
@@ -120,7 +120,7 @@ defmodule Ecto.QueryTest do
 
     test "must be a list of variables" do
       assert_raise Ecto.Query.CompileError,
-                   "binding list should contain only variables or `{:bind_name, var}` tuples, got: 0", fn ->
+                   "binding list should contain only variables or `{as, var}` tuples, got: 0", fn ->
         quote_and_eval select(%Query{}, [0], 1)
       end
     end
@@ -222,18 +222,30 @@ defmodule Ecto.QueryTest do
         from(p in "posts",
           join: b in "blogs",
           join: c in "comments", as: :comment,
-          join: l in "links", as: :link)
+          join: l in "links", on: l.valid, as: :link)
 
       assert %{comment: 2, link: 3} == query.aliases
-      assert [_, %{alias: :comment}, %{alias: :link}] = query.joins
     end
 
-    test "crashes on assigning the same name twice" do
+    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 ->
+        quote_and_eval(from(p in "posts", join: b in "blogs", as: :foo, as: :bar))
+      end
+    end
+
+    test "crashes on assigning the same name twice at compile time" do
       message = ~r"alias `:foo` already exists"
       assert_raise Ecto.Query.CompileError, message, fn ->
-        from(p in "posts",
-          join: b in "blogs", as: :foo,
-          join: c in "comments", as: :foo)
+        quote_and_eval(from(p in "posts", join: b in "blogs", as: :foo, join: c in "comments", as: :foo))
+      end
+    end
+
+    test "crashes on assigning the same name twice at runtime" do
+      message = ~r"alias `:foo` already exists"
+      assert_raise Ecto.Query.CompileError, message, fn ->
+        query = "posts"
+        from(p in query, join: b in "blogs", as: :foo, join: c in "comments", as: :foo)
       end
     end
 
@@ -259,8 +271,7 @@ defmodule Ecto.QueryTest do
     end
 
     test "referring to non-existing binding" do
-      message = ~r"there is no bind named `:nope`"
-      assert_raise Ecto.Query.CompileError, message, fn ->
+      assert_raise Ecto.QueryError, ~r"unknown bind name `:nope`", fn ->
         "posts"
         |> join(:inner, [p], c in "comments", as: :comment)
         |> where([nope: c], c.id == 0)