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

Add Repo.reload/2 and Repo.reload!/2 #3416

Merged
merged 5 commits into from
Oct 8, 2020
Merged

Add Repo.reload/2 and Repo.reload!/2 #3416

merged 5 commits into from
Oct 8, 2020

Conversation

v0idpwn
Copy link
Member

@v0idpwn v0idpwn commented Sep 20, 2020

Closes #3389
I'm still polishing, got to fix some things regarding code quality. Still, the API proposal is this. Do I need to change something?

case schema.__schema__(:primary_key) do
[pk] ->
keys = Enum.map(structs, &get_pk!(&1, pk))
preloads = Keyword.get(opts, :preload, [])
Copy link
Member

Choose a reason for hiding this comment

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

Honestly, I don't think we need to support this option. Doing a |> Repo.preload(...) after is only very few characters more verbose than , preload: .

Choose a reason for hiding this comment

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

@josevalim wouldn't |> Repo.preload() imply another trip to the database while from(..., preload: ...) does it in one pass? Or they're always equivalent?

Copy link
Member

Choose a reason for hiding this comment

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

Preload always issues separates queries, that’s what it is about, so both forms are equivalent!

Choose a reason for hiding this comment

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

That's nice to know! I thought the form in Ecto.Query would optimize into a join of some kind

Comment on lines 91 to 95
if result_count < expected_count do
raise Ecto.TooFewResultsError, queryable: query, min: expected_count, count: result_count
end

result
Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately there is no guarantee result will be in the same order as structs. So you need to do something like this:

for struct <- structs do
  struct_pk = Map.fetch!(struct, pk)
  Enum.find(result, &Map.fetch!(&1, pk) == struct_pk) || raise "could not find"
end

This means you don't need the Enum.count nor the exception above, as the above will guarantee all entries have a match.

Copy link
Member

Choose a reason for hiding this comment

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

If we guarantee same ordering in reload!/2, should we also do the same for reload/2?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, it should be for both!

:ok

rejected ->
raise ArgumentError, "Expected a struct or a list of structs, received #{inspect(rejected)}"
Copy link
Member

Choose a reason for hiding this comment

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

Messages should start in lowercase.

Comment on lines 550 to 551
defp is_struct?(%{__struct__: _, __meta__: _}), do: true
defp is_struct?(_), do: false
Copy link
Member

Choose a reason for hiding this comment

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

We typically use is_ or ? but not both. In this case, since it is not a guard, ? works better. :)

Suggested change
defp is_struct?(%{__struct__: _, __meta__: _}), do: true
defp is_struct?(_), do: false
defp schema?(%{__struct__: _, __meta__: _}), do: true
defp schema?(_), do: false

Choose a reason for hiding this comment

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

@josevalim I have a conceptual question: does Map.put(%{}, :__struct__, ...) imply that the result is a struct?

In my conception, matching on %_{__meta__: _} would be a little bit more semantic in this regard, but I really don't know if there is a practical difference

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 subset match, so you can match as above, but you shouldn’t build the struct manually as you may miss some fields. In any case your syntax is definitely nicer!

@josevalim
Copy link
Member

It is looking great! I have dropped some comments.

@v0idpwn
Copy link
Member Author

v0idpwn commented Sep 20, 2020

Thanks, @josevalim, I'll fix those ASAP :)

Comment on lines 554 to 561
struct
|> Map.fetch!(pk)
|> case do
nil ->
raise ArgumentError, "Ecto.Repo.reload/2 expects existent structs, found a `nil` id"
key ->
key
end
Copy link
Member

Choose a reason for hiding this comment

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

Would this be more concise?

Suggested change
struct
|> Map.fetch!(pk)
|> case do
nil ->
raise ArgumentError, "Ecto.Repo.reload/2 expects existent structs, found a `nil` id"
key ->
key
end
case struct do
%{^pk => key} when key != nil -> key
_ -> raise ArgumentError, "Ecto.Repo.reload/2 expects existent structs"
end

@@ -485,4 +536,28 @@ defmodule Ecto.Repo.Queryable do
query: query,
message: "expected a from expression with a schema"
end

defp assert_structs!(structs) when is_list(structs) do
Copy link
Member

Choose a reason for hiding this comment

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

Shall we guard against the case when users input different struct types in the list?

Repo.reload([%Post{}, %Comment{}])

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll cover this case

@@ -220,6 +220,33 @@ defmodule Ecto.RepoTest do
end
end

describe "reload" do
test "raises when input have a `nil` pk" do
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
test "raises when input have a `nil` pk" do
test "raises when input structs do not have valid primary keys" do

maybe?

Comment on lines 91 to 95
if result_count < expected_count do
raise Ecto.TooFewResultsError, queryable: query, min: expected_count, count: result_count
end

result
Copy link
Member

Choose a reason for hiding this comment

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

If we guarantee same ordering in reload!/2, should we also do the same for reload/2?

lib/ecto/repo/queryable.ex Outdated Show resolved Hide resolved
@@ -428,6 +459,26 @@ defmodule Ecto.Repo.Queryable do
Query.where(queryable, [], ^Enum.to_list(clauses))
end

defp query_for_reload([head| _] = structs, opts) do
Copy link
Member

@qcam qcam Sep 21, 2020

Choose a reason for hiding this comment

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

Suggested change
defp query_for_reload([head| _] = structs, opts) do
defp query_for_reload([%schema{} | _] = structs, opts) do

maybe? Worse error message in case of a list of normal maps however.

@v0idpwn
Copy link
Member Author

v0idpwn commented Sep 28, 2020

Hi! I just fixed most of the suggestions. I think there is room for improvement in the code, but I didn't figure out how to do it.

@v0idpwn
Copy link
Member Author

v0idpwn commented Sep 28, 2020

@josevalim let me know if you wanna merge without a squash, I may improve the commits to make a better history :)

@v0idpwn
Copy link
Member Author

v0idpwn commented Oct 2, 2020

Bump :)

(just in case you lost the notification)

lib/ecto/repo.ex Outdated
Comment on lines 677 to 682
* `:prefix` - The prefix to run the query on (such as the schema path
in Postgres or the database in MySQL). This will be applied to all `from`
and `join`s in the query that did not have a prefix previously given
either via the `:prefix` option on `join`/`from` or via `@schema_prefix`
in the schema. For more information see the "Query Prefix" section of the
`Ecto.Query` documentation.
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we should accept this option, rather, we should use the prefix from the structs themselves. You can find it under the __meta__ field.

Copy link
Member

Choose a reason for hiding this comment

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

You can test this by using Ecto.put_meta(struct, :prefix, "another") in test/ecto/repo_test.exs and then using assert_receive to check to which prefix the query went to. :)

If that doesn't work, have an integration test that sets Ecto.put_meta(struct, :prefix, "invalid") and try to reload it and see that it will fail.

lib/ecto/repo.ex Outdated
Comment on lines 672 to 673
When using with lists, ordering is guaranteed to be kept. Results not found in
the database will be returned as `nil`.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
When using with lists, ordering is guaranteed to be kept. Results not found in
the database will be returned as `nil`.
When using with lists, it is expected that all of the structs in the list belong
to the same schema. Ordering is guaranteed to be kept. Results not found in the
database will be returned as `nil`.

Comment on lines 546 to 555
with [] <- Enum.reject(structs, &schema?/1),
true <- Enum.all?(structs, &(&1.__struct__ == head.__struct__)) do
:ok
else
rejected when is_list(rejected) ->
raise ArgumentError, "expected a struct or a list of structs, received #{inspect(rejected)}"

false ->
raise ArgumentError, "expected an homogenous list, received different struct types"
end
Copy link
Member

@josevalim josevalim Oct 3, 2020

Choose a reason for hiding this comment

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

This can be clearer as conditionals:

if not Enum.all?(structs, &schema?/1) do
  raise ArgumentError, "expected a struct or a list of structs, received #{inspect(rejected)}"
end

if not Enum.all?(structs, &(&1.__struct__ == head.__struct__)) do
  raise ArgumentError, "expected an homogenous list, received different struct types"
end

:ok

end
end

defp query_for_reload(struct), do: query_for_reload([struct])
Copy link
Member

Choose a reason for hiding this comment

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

I would remove this clause and call it as query_for_reload([struct]) in the reload/reload! functions. :)

@josevalim
Copy link
Member

Awesome work @v0idpwn! I have added some minor comments and a note about prefix handling. In particular, I think you need to do this:

%{query | prefix: head.__meta__.prefix}

I have added some notes on how to write the test for it.

@v0idpwn
Copy link
Member Author

v0idpwn commented Oct 7, 2020

Just pushed those fixes :)

@josevalim josevalim merged commit 0ec6688 into elixir-ecto:master Oct 8, 2020
@josevalim
Copy link
Member

💚 💙 💜 💛 ❤️

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.

Add Repo.reload and Repo.reload!
4 participants