Ecto.Query.API.selected_as/1 with a variable

I’m currently working on an adapter behaviour for Flop to open it up to data sources beyond Ecto. For this, I need to separate the generic parts of the library from the Ecto-specific parts.

Flop has the concept of alias fields, which work together with aliases set with Ecto.Query.API.selected_as/2. You can define available alias fields for a schema like this:

@derive {
  Flop.Schema,
  filterable: [],
  sortable: [:pet_count],
  alias_fields: [:pet_count]
}

Then you can define a base query that sets up an alias with selected_as/1 like this:

Owner
|> join(:left, [o], p in assoc(o, :pets), as: :pets)
|> group_by([o], o.id)
|> select(
  [o, pets: p],
  {o.id, p.id |> count() |> selected_as(:pet_count)}
)
|> Flop.validate_and_run(params, for: Owner)

And with that setup, Flop would turn a parameter map like this into an order_by clause on the query:

params = %{order_by: [:pet_count]}

In the current implementation, this is accomplished by having the protocol compile a function clause for each alias field:

for name <- alias_fields do
  quote do
    def apply_order_by(_struct, q, {direction, unquote(name)}) do
      order_by(q, [{^direction, selected_as(unquote(name))}])
    end
  end
end

This works fine, but for the adapter behaviour, I need to move the parts about alias fields from the protocol to the Ecto adapter, since alias fields only make sense in an Ecto context. The protocol should only include generic logic that is common to all adapters.

Instead of compiling a function like above, I’d like the Ecto adapter to just define a function that takes the query and field information and returns an updated query. This works fine for the other field types that the Ecto adapter supports as order fields (normal schema fields, join fields, compound fields), but I’m having trouble getting this to work with selected_as/1.

I tried to define this function clause for alias fields:

defp apply_order_by_field(q, {direction, field}, %FieldInfo{
      extra: %{type: :alias}
    }, _) do
  order_by(q, [{^direction, selected_as(field)}])
end

Note that I’m trying to pass a variable to selected_as instead of an atom. This results in:

** (Ecto.Query.CompileError) selected_as/1 expects `name` to be an atom, got `{:field, [line: 291], nil}`
    (ecto 3.10.3) expanding macro: Ecto.Query.order_by/2

Pinning the variable results in:

** (Ecto.Query.CompileError) selected_as/1 expects `name` to be an atom, got `{:^, [line: 291], [{:field, [line: 291], nil}]}`
    (ecto 3.10.3) expanding macro: Ecto.Query.order_by/2

So it seems like selected_as doesn’t support variables at the moment. Is there any workaround for this situation at the moment? Is this something that could be changed in Ecto?

For more context, here’s the WIP Flop PR.

1 Like

Support for interpolated names in selected_as was added to Ecto by @greg-rychlewski in this PR. :heart:

2 Likes