Select_merge, map, and selected_as

I’m trying to dynamically build up a query. Imagine I have two ecto schemas, e.g.

defmodule Make do
  use Ecto.Schema
  
  schema "makes" do
    field :name, :string
    ....
  end
end 
defmodule Model do
  use Ecto.Schema

  schema "models" do
    field :name, :string
    ...
    belongs_to :make, Make
  end
  ... 
end

I want to end up with a query like:

from m in Model, join: a in assoc(m, :make), select: %{model_name: m.name, make_name: m.name}

, but I am generating the query from some metadata rather than explicitly, e.g. [model: [:name, make: name]].

I can generate the query with joins and select/select_merge as needed to return just the fields I am interested in, but I cannot figure out how to combine select_merge, map, and selected_as to allow me to avoid name collisions in my select. I’ve bumped into my limits on macros, and I could use some help, please.

I’m basically stuck on a call as follows, that works but does not deal with naming collisions:

select_merge(query, [{^assoc, x}], map(x, ^[field]))

I can see from the ast of the query that I want to end up with a chunk like:

     select: {:%{}, [],
      [
        model_name: {{:., [], [{:m, [], Elixir}, :name]}, [no_parens: true], []},
        make_name: {{:., [], [{:a, [], Elixir}, :name]}, [no_parens: true], []}
      ]}

Where my select expr becomes:

%Ecto.Query.SelectExpr{
  expr: {:%{}, [],
   [
     model_name: {{:., [], [{:&, [], [0]}, :name]}, [], []},
     make_name: {{:., [], [{:&, [], [1]}, :name]}, [], []}
   ]},
  file: "iex",
  line: 1,
  fields: nil,
  params: [],
  take: %{},
  subqueries: [],
  aliases: %{}
}

Thanks in advance for any help!

Hello and welcome,

I wrapped your code with 3 ``` to format it.

Please use markdown markup if You want to add code :slight_smile:

Thank you, you’re kind! I’ll do that going forward.

1 Like

I was able to get something to work. I couldn’t figure out how to use map and selected_as, but I built up my select clause dynamically instead. I’m pretty sure it doesn’t match orthodoxy for ecto, but it might help someone with macros, so I thought I would post it here for future reference:

defmodule AliasedSelect do
  import Ecto.Query, only: [select: 3]
  defmacro aliased_select(query, expr) do
    meta = [line: __CALLER__.line]
    {binding_expr, mapped_expr} = 
      expr
      |> Enum.with_index()
      |> Enum.reduce({[], []}, fn {{alias, fields}, index}, acc -> bind_aliased_fields(acc, index, alias, fields, meta) end)
    quote do
      select(unquote(query), unquote(Enum.reverse(binding_expr)), unquote({:%{}, meta, mapped_expr}))
    end
  end

  def bind_aliased_fields({bindings, expr}, index, alias, fields, meta) when is_list(fields) do 
    var = {:"v#{index}", meta, nil}
    fields
    |> Enum.map(&alias_field(alias, var, &1, meta))
    |> Enum.concat(expr)
    |> then(&{[var | bindings], &1})
  end

  def alias_field(alias, var, field, [line: line] = meta) do
    {:"#{alias}_#{field}", {{:., meta, [var, field]}, [no_parens: true, line: line], []}} 
  end
end

defmodule Foo do
  import Ecto.Query, only: [from: 2, select: 3, select_merge: 3]
  import AliasedSelect
  alias Harmoni.Equipment.Model
  def foo do
    query = 
      from m in Model, as: :model, 
      join: a in assoc(m, :make), as: :make
    
    query
    |> aliased_select([model: [:id, :name], make: [:id, :name]])
  end
end

Foo.foo()
|> IO.inspect()

Results in a query like:

#Ecto.Query<from m0 in Harmoni.Equipment.Model, as: :model,
 join: m1 in assoc(m0, :make), as: :make,
 select: %{make_id: m1.id, make_name: m1.name, model_id: m0.id, model_name: m0.name}>

I might be the only one that cares, but when I concatenate the binding, it should be a named binding:

  def bind_aliased_fields({bindings, expr}, index, alias, fields, meta) when is_list(fields) do 
    var = {:"v#{index}", meta, nil}
    fields
    |> Enum.map(&alias_field(alias, var, &1, meta))
    |> Enum.concat(expr)
    |> then(&{[{alias, var} | bindings], &1})
  end