Reputation: 4113
Starting with this query as the basis for an Ecto version:
select folder_id, json_agg(p.*)
from folder_memberships inner join profiles p
on p.id=folder_memberships.profile_id
where folder_id in (1234) group by folder_id;
I've got this code:
# ids=[1234]
from(p in Profile,
join: link in FolderMembership, on: link.profile_id == p.id,
select: [link.folder_id, fragment("json_agg(?) as members", p)],
group_by: link.folder_id,
where: link.folder_id in ^ids
)
|> Repo.all
And that gets me the following error:
== Compilation error on file lib/profile.ex ==
** (Ecto.Query.CompileError) variable `p` is not a valid query expression.
Variables need to be explicitly interpolated in queries with ^
(ecto) expanding macro: Ecto.Query.select/3
I'm sure I'm missing elementary but I'm bonkered if I know what it is. I've tried a number of options, but all the examples I've been able to see do something like fragment("json_agg(?)", p.some_field)
, not p
itself.
Upvotes: 2
Views: 2017
Reputation: 6255
The solution is not perfect because it requires listing all fields explicitly and also doesn't let you exclude fields from the resulting JSON.
# ids=[1234]
from(p in Profile,
join: link in FolderMembership, on: link.profile_id == p.id,
select: [link.folder_id, fragment("json_agg((?, ?, ?)::profiles) as members", p.id, p.name, p.created_at)],
group_by: link.folder_id,
where: link.folder_id in ^ids
)
|> Repo.all
The number of question marks in json_agg
should be exactly the same as the number of columns in the profiles table and also the order of columns in the table should correspond to the order of fragment
arguments. I don't know your schema, so I "made up" 3 columns - I hope you get the idea.
I tried this approach myself on a simplified example (without a join). The source code of the app I used as a playground is there.
defmodule Magic do
import Ecto.Query
alias Badging.{Badge, Repo}
@fields Badge.__schema__(:fields)
@source Badge.__schema__(:source)
@questions Enum.map_join(@fields, ", ", fn _ -> "?" end)
@json_agg "json_agg((#{@questions})::#{@source})"
def run do
fields = Badge.__schema__(:fields)
source = Badge.__schema__(:source)
questions = Enum.map_join(fields, ", ", fn _ -> "?" end)
json_agg = "json_agg((#{questions})::#{source})"
from(
b in Badge,
select: [
b.id,
fragment(
"json_agg((?, ?, ?, ?, ?, ?, ?, ?, ?)::badges)",
b.id,
b.identifier,
b.subject,
b.status,
b.color,
b.svg,
b.svg_downloaded_at,
b.inserted_at,
b.updated_at
)
],
group_by: b.id
) |> Repo.all
end
end
I also made an attempt to make it more flexible by using Badge.__schema__(:fields)
and Badge.__schema__(:source)
, but stumbled upon the inability of fragment
to accept variable number of arguments.
This is what I got so far:
defmodule Magic do
import Ecto.Query
alias Badging.{Badge, Repo}
fields = Badge.__schema__(:fields)
source = Badge.__schema__(:source)
questions = Enum.map_join(fields, ", ", fn _ -> "?" end)
@json_agg "json_agg((#{questions})::#{@source})"
def run do
from(
b in Badge,
select: [
b.id,
fragment(
@json_agg,
field(b, :id), # or just b.id
b.identifier,
b.subject,
b.status,
b.color,
b.svg,
b.svg_downloaded_at,
b.inserted_at,
b.updated_at
)
],
group_by: b.id
) |> Repo.all
end
end
I think technically it is possible to rely on __schema__(:fields)
instead of listing all fields explicitly. The list of fields is known at compile time. I'm just not as good at macros in Elixir/Ecto to do it (yet).
Upvotes: 2