-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Support for Subqueries and Quantifiers in Comparison, and Exists #3465
Conversation
Hi @GabrielAlacchi! Welcome and thanks for the PR! I think the PR is moving in the correct direction but I don't think we need to make the quantifier part of the subquery. Instead, you want to do something similar to def escape({quantifier, _, [subquery]} = expr, type, params_acc, vars, env) when quantifier in [:all, :any] do
{subquery, params_acc} = escape_subquery(right, type, params_acc, vars, env)
{{:{}, [], [quantifier, [], [subquery]]}, params_acc}
end This should be enough (or close enough) to make it work! Plus you don't have to worry about validating the Once you do this, we just need docs on Ecto.Query.API and the second test you added to filter_test.exs :) |
7053284
to
a5b8c0a
Compare
59516d2
to
fd1fbfb
Compare
Hey @josevalim thanks for the follow up. It definitely comes down to the philosophy of whether you'd like Ecto to enforce correct queries at compile time or just let the database complain if any of these statements are used in the wrong place. I was calling I still needed to modify |
lib/ecto/query/builder.ex
Outdated
@@ -281,7 +281,7 @@ defmodule Ecto.Query.Builder do | |||
rtype = quoted_type(left, vars) | |||
|
|||
{left, params_acc} = escape(left, ltype, params_acc, vars, env) | |||
{right, params_acc} = escape(right, rtype, params_acc, vars, env) | |||
{right, params_acc} = escape_subquery(right, rtype, params_acc, vars, env) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need this change. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is needed to support the case where we don't want to use a quantifier. For example:
avg_rating = from(p in Post, select: avg(p.rating))
from(p in Post, where: p.rating > subquery(avg_rating))
Without this we get
** (Ecto.Query.CompileError) `subquery(query)` is not a valid query expression.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, you also want to support row constructors. I see. :) I would skip it for now since we don't support the ROW
operation built-in. We can always add it later in the future though if someone has a proper use case. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you're referring too, yes row constructors are related to this work. I didn't know you could do this type of comparison with multiple fields using row constructors https://dev.mysql.com/doc/refman/8.0/en/row-subqueries.html
For now I'm just dealing with the scalar case where the subquery must return a single column and be compared to a scalar expression. I use this a lot for use cases like finding "above average" rows, rows that are in certain percentiles and stuff like that.
By the way, I also tested all these queries locally with an old PHX project and they generate valid SQL without any adapter changes =).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I don't want to work on supporting ROW constructors yet, I've never seen them used constructively in this context so it's best to just use a fragment for that kind of thing, but the scalar comparison is not too difficult to support with the groundwork that's already been laid out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If those queries have to return a single column and a single row, then it is equivalent foo >= all(sub)
anyway, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I guess databases support both syntaxes for backwards compatibility, we don't necessarily need to. Since it's one less thing to maintain and worry about I will drop the >= subquery
syntax.
lib/ecto/query/planner.ex
Outdated
@@ -1105,16 +1105,47 @@ defmodule Ecto.Query.Planner do | |||
{{:in, in_meta, [left, right]}, acc} | |||
end | |||
|
|||
defp prewalk({:in, in_meta, [left, {:subquery, i}]}, kind, query, expr, acc, adapter) do | |||
# Case when a subquery is at the Right Hand Side of either an in or quantifier based expression | |||
defp prewalk({in_or_comp, op_meta, [left, {:subquery, i}]}, kind, query, expr, acc, adapter) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can reverse these changes too to what they were! (i.e. check only for :in
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also letting comparison operators go into this block for the same reasons as above. It's not necessarily to use a quantifier when comparing with a subquery. That's why I'm handling the case where a subquery without a quantifier is at the RHS of an "in" or comparison operator the same way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the above. I missed you wanted to support row constructors too but I would like to skip the row constructors one for now. :)
test/ecto/query/planner_test.exs
Outdated
case where.expr do | ||
{:>=, _, [_, {:all, _, [%Ecto.SubQuery{}] }]} -> | ||
:ok | ||
|
||
_ -> | ||
raise "planner did not replace {:subquery, index} with an Ecto.SubQuery struct in quantified comparison" | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
case where.expr do | |
{:>=, _, [_, {:all, _, [%Ecto.SubQuery{}] }]} -> | |
:ok | |
_ -> | |
raise "planner did not replace {:subquery, index} with an Ecto.SubQuery struct in quantified comparison" | |
end | |
assert {:>=, _, [_, {:all, _, [%Ecto.SubQuery{}] }]} = where.expr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please do similar change below. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't know about this syntax, thanks!
test/ecto/query/planner_test.exs
Outdated
end | ||
|
||
assert_raise Ecto.QueryError, fn -> | ||
from(p in Post, where: p.id > subquery(s)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
from(p in Post, where: p.id > subquery(s)) | |
from(p in Post, where: p.id > any(s)) |
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is valid SQL assuming the subquery returns a single column and a single row. If the database finds that the subquery returns more than 1 row it'll throw a cardinality exception. I'm simply testing that this syntax correctly throws a QueryError in the case that there's >1 field in the select.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, in this case I would rather have folks using all/any with proper limit, rather than support a syntax designed for row constructs which we don't properly handle. :D
@josevalim Ok I dropped the row constructor syntax. I will add documentation next and squash the branch. Should the docs for |
Yes, and exists too! |
9896622
to
6a3fe4a
Compare
Added some docs and squashed. |
038d27c
to
74608a0
Compare
Awesome job as your first contribution @GabrielAlacchi, thank you! ❤️ |
💚 💙 💜 💛 ❤️ |
Obrigado @josevalim! You went to USP right? My GF is from Brazil and studied CS there =) |
Yes, I am from Poli-USP! Please send her regards. :) |
She sends regards as well and respectfully says #racacaaso #chupapoli Whatever that means? |
@GabrielAlacchi Thanks for the PR. I'm trying to use the I would love to write:
but I can't, can I? I also tried:
but I get:
Can I get more hints on this? Cheers |
Where have you defined the named binding? See more info here: https://hexdocs.pm/ecto/Ecto.Query.html#module-named-bindings |
Thanks @josevalim for your reactivity! Indeed it does work with:
Any chance to make it work with Otherwise I need to fetch association module, foreign and related key, which doesn't feel DRY at all... Cheers |
Besides, I'm trying to pass the
|
This is my first contribution to the elixir ecosystem as a whole, and it likely won't be my last!
This work builds on #3264 which introduced the
expr IN (subquery)
syntax toEcto.Query
. This laid out the groundwork for also supporting comparison operators with subqueries, including ANY and ALL quantifiers. I will also submit a PR toecto_sql
to implement the SQL in the adapters if this is merged.Postgres, MySQL, and most others (I believe) support these operations.
My Proposed DSL Syntax
Currently we can perform an IN subquery operation as follows
I simply implemented comparison with quantifiers as
and of course
>=
can be substituted for any comparison operator! You can also re-use the same subquery syntax if you are expecting a single row to be returned from the subqueryImplementation
I implemented this by including a
:quantifier
field in theEcto.SubQuery
struct. This signals to the adapter that it should include this quantifier when converting the subquery to SQL, and if the underlying database doesn't support quantifiers then it can raise an exception.