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

Allow schema to be used for values list types #4553

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions integration_test/cases/repo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2207,6 +2207,17 @@ defmodule Ecto.Integration.RepoTest do
assert TestRepo.all(query) == Enum.map(values, &{&1, &1.bid})
end

test "all with schema types" do
uuid_module = uuid_module(TestRepo.__adapter__())
uuid = uuid_module.generate()

raw_values = [%{bid: uuid, visits: "1"}, %{bid: uuid, visits: "2"}]
casted_values = [%{bid: uuid, visits: 1}, %{bid: uuid, visits: 2}]
types = Post
query = from v in values(raw_values, types)
assert TestRepo.all(query) == casted_values
end

test "all with join" do
uuid_module = uuid_module(TestRepo.__adapter__())
uuid = uuid_module.generate()
Expand Down
14 changes: 13 additions & 1 deletion lib/ecto/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ defmodule Ecto.Query do
MapSet.to_list(fields)
end

defp types!(fields, types) do
defp types!(fields, types) when is_map(types) do
Enum.map(fields, fn field ->
case types do
%{^field => type} ->
Expand All @@ -521,6 +521,18 @@ defmodule Ecto.Query do
end)
end

defp types!(fields, schema) when is_atom(schema) do
Enum.map(fields, fn field ->
if type = schema.__schema__(:type, field) do
{field, type}
else
raise ArgumentError,
"values/2 must declare the type for every field. " <>
"The type was not given for field `#{field}`"
end
end)
end

defp params!(values_list, types) do
Enum.reduce(values_list, [], fn values, params ->
Enum.reduce(types, params, fn {field, type}, params ->
Expand Down
18 changes: 16 additions & 2 deletions lib/ecto/query/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -518,13 +518,15 @@ defmodule Ecto.Query.API do
An error is raised if the list is empty or if every map does not have exactly the
same fields.

The second argument is a map of types corresponding to the fields in the first argument.
The second argument is either a map of types or an Ecto schema containing all the
fields in the first argument.

Each field must be given a type or an error is raised. Any type that can be specified in
a schema may be used.

Queries using a values list are not cacheable by Ecto.

## Select example
## Select with map types example

values = [%{id: 1, text: "abc"}, %{id: 2, text: "xyz"}]
types = %{id: :integer, text: :string}
Expand All @@ -536,6 +538,18 @@ defmodule Ecto.Query.API do

Repo.all(query)

## Select with schema types example

values = [%{id: 1, text: "abc"}, %{id: 2, text: "xyz"}]
types = ValuesSchema

query =
from v1 in values(values, types),
join: v2 in values(values, types),
on: v1.id == v2.id

Repo.all(query)

## Delete example
values = [%{id: 1, text: "abc"}, %{id: 2, text: "xyz"}]
types = %{id: :integer, text: :string}
Expand Down
39 changes: 35 additions & 4 deletions test/ecto/query/builder/from_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ defmodule Ecto.Query.Builder.FromTest do

import Ecto.Query

defmodule Schema do
use Ecto.Schema

schema "schema" do
field :num, :integer
field :text, :string
end
end


defmacro from_macro(left, right) do
quote do
fragment("? <> ?", unquote(left), unquote(right))
Expand All @@ -19,31 +29,52 @@ defmodule Ecto.Query.Builder.FromTest do
end

test "values list source" do
# Valid input
values = [%{num: 1, text: "one"}, %{num: 2, text: "two"}]
types = %{num: :integer, text: :string}
query = from v in values(values, types)

types_kw = Enum.map(types, & &1)
assert query.from.source == {:values, [], [types_kw, length(values)]}
end

test "values list source with types defined by schema" do
values = [%{num: 1, text: "one"}, %{num: 2, text: "two"}]
type_schema = Schema
types_kw = Enum.map(%{num: :integer, text: :string}, & &1)
query = from v in values(values, type_schema)

assert query.from.source == {:values, [], [types_kw, length(values)]}
end

# Empty values
test "values list source with empty values" do
msg = "must provide a non-empty list to values/2"

assert_raise ArgumentError, msg, fn ->
from v in values([], %{})
end
end


# Missing type
test "values list source with missing types" do
msg = "values/2 must declare the type for every field. The type was not given for field `text`"

assert_raise ArgumentError, msg, fn ->
values = [%{num: 1, text: "one"}, %{num: 2, text: "two"}]
types = %{num: :integer}
from v in values(values, types)
end
end

test "values list source with missing schema types" do
msg = "values/2 must declare the type for every field. The type was not given for field `not_a_field`"

assert_raise ArgumentError, msg, fn ->
values = [%{not_a_field: 1}]
types = Schema
from v in values(values, types)
end
end

test "values list source with inconsistent fields across entries" do
# Missing field
msg = "each member of a values list must have the same fields. Missing field `text` in %{num: 2}"

Expand Down
Loading