Skip to content

Commit

Permalink
Allow schema to be used for values list types (#4553)
Browse files Browse the repository at this point in the history
  • Loading branch information
greg-rychlewski authored Dec 3, 2024
1 parent 4d3c5ee commit 38453a4
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 7 deletions.
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

0 comments on commit 38453a4

Please sign in to comment.