Phoenix/Ecto - query for match in array of objects - postgresql

(Ecto.Query.CompileError) any(l.signers) is not a valid query expression.
(from l in Listing, where: "xxxxxx#gmail.com" == any(l.signers))
|> Repo.all()

Ecto.Query.API.in/2 is supposed to be used to cover postgresql ANY selector.
where: ^mail_addr in l.signers
It is assumed that l.signers is an enumerable.

select id,address,owners from listings where 'xxxxxx#gmail.com' = ANY(signers);
This query is working in PostgreSQL.

Related

Ecto - casting an array of strings to integers in a fragment

In an Ecto query I'm running against Postgres (13.6), I need to interpolate a list of ids into a fragment - this is generally not something I have a problem with, but in this case, the list of ids is being received as a list of strings that need to be cast to integers (or, more specifically, BIGINT). The query that I think that I need is as follows, which the troublesome bit being ANY(ARRAY?::BIGINT[]):
ModelA
|> where(
[ma],
fragment(
"EXISTS (SELECT * FROM model_b mb WHERE mb.a_id = ? AND mb.c_id = ANY(ARRAY?::BIGINT[]))",
a.id,
^c_ids
)
)
where c_ids would be a list like ["1449441579", "2345556834"]
However, when I run this, I get the error
(Postgrex.Error) ERROR 42703 (undefined_column) column "array$4" does not exist
referring to the generated SQL
ANY(ARRAY$4::BIGINT[])
Of course, I could convert the array of c_ids to integers beforehand in my app code, but I'd like to see if I can get it to cast in the query itself.
Writing the fragment in straight SQL works out just fine:
SELECT * FROM model_b mb WHERE mb.a_id = 1 AND mb.c_id = ANY(ARRAY['1449441579', '2345556834']::BIGINT[]);
What is the idiomatic way to get this kind of array casting to work in an Ecto fragment? Many thanks.
Just to codify my comment, I would do the integer conversion before the query. You can use the dynamic macro to support IN queries:
import Ecto.Query
alias YourApp.Repo
alias YourApp.SomeSchema, as: ModelA
strs = ["1", "2", "3"]
ids = Enum.map(strs, fn x -> String.to_integer(x) end)
conditions = dynamic([tbl], tbl.id in ^ids)
Repo.all(
from ma in ModelA,
where: ^conditions
)
|> IO.inspect()
I did find a post that led me to a potential solution here - https://elixirforum.com/t/interpolating-lists-into-ecto-query-fragment-1/16690/7 - however, I'm not convinced that this is optimal for me.
By using Enum.join and converting my initial array of strings into a string and then allowing Postgres to cast that string to an array of bigints, it worked out:
ModelA
|> where(
[ma],
fragment(
"EXISTS (SELECT * FROM model_b mb WHERE mb.a_id = ? AND mb.c_id = ANY(ARRAY?::BIGINT[]))",
a.id,
^Enum.join(c_ids, ",")
)
)
So I'm posting it here for reference and because it does "work", but I feel sort of silly doing this, because I specifically was trying to avoid processing the list before passing it to PG... so yeah I'd still really like to hear about the "right" way to do this...

Ecto: How to update all records with a different random number

I have a table in postgresql and i want to update the value of column "points" for all the records with a random number. In other languages we could loop over all the db records but how can i do it with ecto? I tried this:
Repo.all(from u in User, update: User.changeset(u, %{points: :rand.uniform(100)}))
but it outputs the following error:
== Compilation error in file lib/hello_remote/user.ex ==
** (Ecto.Query.CompileError) malformed update `User.changeset(u, %{points: :rand.uniform(100)})` in query expression, expected a keyword list with set/push/pop as keys with field-value pairs as values
expanding macro: Ecto.Query.update/3
lib/hello_remote/user.ex:30: HelloRemote.User.update_points/0
expanding macro: Ecto.Query.from/2
lib/hello_remote/user.ex:30: HelloRemote.User.update_points/0
(elixir 1.10.4) lib/kernel/parallel_compiler.ex:304: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
I've also tried this:
from(u in User)
|> Repo.update_all(set: [points: Enum.random(0..100)])
but it updates all the records with the same value
You can use fragment/1 with update_all/3, calling a PostgreSQL function to calculate the random values, for example:
update(User, set: [points: fragment("floor(random()*100)")])
|> Repo.update_all([])
I don't think it is possible with the update_all function. The function was created to update many rows with the same value.
You can create a loop and update each record separately, it's not nice but a working solution.
User
|> Repo.all()
|> Enum.each(fn user -> update_user(user) end)
def update_user(user) do
user
|> Ecto.Changeset.cast(%{"points" => Enum.random(0..100)}, [:points])
|> Repo.update!()
end
If you would like to do it one call you can construct a raw SQL query and use that.

Ecto query to also include records with no has_many associated records?

I wrote this query to find all records that are not in Florida.
query =
from papa in Papa,
inner_join: account in assoc(papa, :account),
inner_join: location in assoc(account, :locations),
where: account.email == ^"myapp#example.com",
where: papa.status == ^"active",
where: location.papa_id == papa.id, <--- Some of these Papas have ZERO locations.
where: location.state != ^"FL",
group_by: [papa.id, location.state, account.id],
distinct: [papa.id],
select: [
papa.id,
papa.member_id,
papa.full_name,
account.id,
account.full_name,
location.state,
papa.status
]
It's returning correctly all the Papa records with no locations in Florida. Unfortunately it's skipping the Papa records that have literally NO location records.
How can I also include these records in my Ecto query?
Once you have
inner_join: location in assoc(account, :locations),
the where clause
where: location.papa_id == papa.id
is redundant, since it’s implied by joining an assoc. If you want to include all the records from papas, you need to use left_join (See the diagram here for the visualization of what is returned for different joins.)
So, get rid of where: location.papa_id == papa.id and change join clause for location to:
left_join: location in assoc(account, :locations),

Ecto query to grab all values that satisfy all values in array_aggregator not just any?

Wonder if someone can help me with an array aggregator issue
I’ve got a query that does a join using a joining table then it filters down all values that are inside a given array and filters out values that are in another array.
The code looks like this:
Product
|> join(:inner, [j], jt in "job_tech", on: j.id == jt.product_id)
|> join(:inner, [j, jt], t in Tech, on: jt.ingredient_id == t.id)
|> group_by([j], j.id)
|> having_good_ingredients(good_ingredients)
|> not_having_bad_ingredients(bad_ingredients)
With having_good_ingredients look like this:
def having_good_ingredients(query, good_ingredients) do
if Enum.count(good_ingredients) > 0 do
query
|> having(fragment("array_agg(t2.name) && (?)::varchar[]", ^good_ingredients))
else
query
end
end
This works but it’ll grab all values that satisfy any of the values in the good_stacks array where I want them to only satisfy if all of the stacks work
i.e. if I’ve got [A, C] in my array, I want to return values that have A AND C, not just A and not just C.
Anyone have any ideas?
I believe you want to use the #> operator, instead of the overlap && operator:
having(fragment("array_agg(t2.name) #> (?)::varchar[]", ^good_ingredients))
Reference: https://www.postgresql.org/docs/current/functions-array.html#ARRAY-OPERATORS-TABLE

Get results from HQL query with the same order as given list

I'm trying make a query with HQL that will stay with the same order as given list of IDs. I know it's possible with SQL but I can't find any way to do it with HQL (and I cannot do it with native SQL because I got many joins)
Example
fingerIds = [3,1,10,4]
SELECT p FROM People p
JOIN FETCH p.fingers f
WHERE f.id IN :fingerIds
DB: PostgreSQL 10.4
Hibernate: 4.3.11.Final
Eg. Given list of IDs: [3,1,10,4]
Actual result's order: [1,3,4,10]
Expected result's order: [3,1,10,4]
You can obtain the order by adding to your query the keyword FIELD, in your example:
SELECT p FROM People p
JOIN FETCH p.fingers f
WHERE f.id IN :fingerIds
ORDER BY FIELD(f.ID,3,1,10,4)
Ofc you can replace the numbers with your variable :fingerIds
You can find more about that command here.
Returns the index (position) of str in the str1, str2, str3, ... list. Returns 0 if str is not found.