Skip to content
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

[WIP] Add support for named joins #2411

Closed
wants to merge 11 commits into from

Conversation

zoldar
Copy link
Member

@zoldar zoldar commented Feb 9, 2018

Closes #2389

I took a stab at implementing support for named joins as discussed in #2389, using the ideas from initial post and #2389 (comment).

Example usage:

query =
  Post
  |> join(:inner, [p], {:comment, c} in assoc(p, :comments))
  |> order_by([comment: c], c.id)
  |> select([p, comment: c], {p, c})

Marking it as work in progress because I'm definitely not sure whether

  1. the API is acceptable
  2. it's implemented optimally.

Other than that, I'll want to refactor Query.Builder.escape_binding for sure, because there's a lot of redundancy and excessive nesting at the moment. Also, docs will need update eventually as well when/if the proposed change gets into acceptable shape.

Feedback appreciated.

{query, vars ++ tail_fn.(tail)}
{tail, named} ->
mapped_binds =
Enum.map(named, fn {k, name} -> {k, quote(do: bind_mappings[unquote(name)])} end)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function body is nested too deep (max depth is 2, was 3).

@sourcelevel-bot
Copy link

Ebert has finished reviewing this Pull Request and has found:

  • 1 possible new issue (including those that may have been commented here).

You can see more details about this review at https://ebertapp.io/github/elixir-ecto/ecto/pulls/2411.

@josevalim
Copy link
Member

josevalim commented Feb 10, 2018

Thanks for the PR @zoldar! ❤️ 💚 💙 💛 💜

This is great I have a couple suggestions though.

  1. I still prefer join c in "comments", as: :comments because it allows us to also use the :as option when generating SQL queries. It is more verbose but it is also clearer. For example, imagine reading join: {:comments, c} in {"comments", Comment} by using tuples it is really hard to figure out what is what.

  2. The way you compute the names right now is very expensive because it is linear based on the number of joins. My suggestion is to add a new field to Ecto.Query called aliases and store it there. Seeing if a field exists or not should be a simple lookup.

Because this PR is potentially quite long, I would recommend breaking this into smaller tasks and we work step by step towards the end result:

The first step is to make Ecto.Query.join/5 support keyword list as last argument. Today join/5 expects the last argument to a list of expressions:

join(Post, :left, [p], c in assoc(p, :comments), c.public)

We need to change it to support a keyword list as last argument and emit a warning if a keyword list is not given. So we want this:

join(Post, :left, [p], c in assoc(p, :comments), on: c.public)

This will also be necessary for us to support index hinting (#2392) in the future. But for now we will support only :on anything else will raise.

Now that we made joins extensible, the second step is to add support to :as in both join and from. The aliases should be stored in the query, so lookups are fast. Trying to register an already given name should be an error. Trying to use a name that does not exist should be an error too.

Finally, as third step, we can include the :as option in the generated SQL statements too.

Thoughts?

@@ -268,6 +268,42 @@ defmodule Ecto.QueryTest do
end
end

describe "named joins" do
test "assigns a name to a join" do
Copy link
Member

@josevalim josevalim Feb 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those tests are very fragile because they are asserting on the AST. The reason why we test them above is because we want to check if the query is optimizable but we are not worried about optimization here. My recommendation is to build actual queries and then traverse query.joins (or query.aliases if we follow my proposal) checking for the generated aliases, like you did in the dynamic tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is also very important to test what happens when we give an alias that already exists. We also need to check what happens when you try to use an alias that does not exist.

@@ -124,6 +124,13 @@ defmodule Ecto.Query.BuilderTest do
escape(quote(do: ^([] ++ [])), [], __ENV__)
end

test "escape_binding with named binding in the input" do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fragile test too, we don't need to check the generated AST on expansion.

@@ -66,5 +72,29 @@ defmodule Ecto.Query.Builder.DynamicTest do
"&0.foo() == ^0 and &1.bar() == ^1 and &0.baz() == ^2"
assert params == [{"foo", {0, :foo}}, {"bar", {1, :bar}}, {"baz", {0, :baz}}]
end

test "with ... binding in the middle" do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those tests are beautiful! I would recommend moving them to query_test.exs as there is nothing specific to fragments about them. Have them work directly on the query and then check the result of query.joins (or query.aliases according to my proposal).

@zoldar
Copy link
Member Author

zoldar commented Feb 10, 2018

@josevalim Thank you for the feedback. All the points you made are valid to me and make sense. If nobody else has any objections, I'd start with stage one, as described by you, some time next week.

@zoldar
Copy link
Member Author

zoldar commented Feb 11, 2018

I need a word of advice. When I've started working on introducing keyword opts in join/5, I've hit an issue with the fact that join actually allows to accept keyword list for on expression here:

https://github.com/zoldar/ecto/blob/cea93504d0dd609ad92733692e5bdd6830b685a9/test/ecto/query/builder/join_test.exs#L23

In order for this to work, I guess it would be necessary to tell apart the case where it's meant to be treated as on expression from the one where it's to be treated as a keyword list of options. I'm not sure whether there's a sensible solution for that. The on expression could very well to refer to fields such as on or as. Any ideas?

@josevalim
Copy link
Member

@zoldar yeah, you will have to check that it is either an empty list or it has as or on in it. There is a very small chance of a conflict but this is a major release so we can afford it.

Theoretically we could just go and break everything but that would be annoying for users. :)

@zoldar
Copy link
Member Author

zoldar commented Feb 16, 2018

Closing in favor of plan laid out in #2411 (comment) and #2428. Leaving for reference though.

@zoldar zoldar closed this Feb 16, 2018
@zoldar zoldar mentioned this pull request Feb 19, 2018
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants