From 18093d619756e4ff2dabed44e4cfcd3a0e8f6617 Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 30 Nov 2024 09:31:24 -0500 Subject: [PATCH 1/4] allow schema for values list type --- integration_test/cases/repo.exs | 11 ++++++++ lib/ecto/query.ex | 14 +++++++++- test/ecto/query/builder/from_test.exs | 39 ++++++++++++++++++++++++--- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/integration_test/cases/repo.exs b/integration_test/cases/repo.exs index 667db70a77..5b3e39360f 100644 --- a/integration_test/cases/repo.exs +++ b/integration_test/cases/repo.exs @@ -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() diff --git a/lib/ecto/query.ex b/lib/ecto/query.ex index 279448eb95..9d3ca7ac08 100644 --- a/lib/ecto/query.ex +++ b/lib/ecto/query.ex @@ -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} -> @@ -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 -> diff --git a/test/ecto/query/builder/from_test.exs b/test/ecto/query/builder/from_test.exs index 8114e587d9..beb0300e69 100644 --- a/test/ecto/query/builder/from_test.exs +++ b/test/ecto/query/builder/from_test.exs @@ -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)) @@ -19,23 +29,32 @@ 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 = Enum.map([:num, :text], &{&1, Schema.__schema__(:type, &1)}) + query = from v in values(values, type_schema) + + assert query.from.source == {:values, [], [types, 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 -> @@ -43,7 +62,19 @@ defmodule Ecto.Query.Builder.FromTest do 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}" From 5613a063a6d216d285bba3b4e2fa7ddb0b8e146f Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 30 Nov 2024 09:35:06 -0500 Subject: [PATCH 2/4] update docs --- lib/ecto/query/api.ex | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/ecto/query/api.ex b/lib/ecto/query/api.ex index 9d2c6ce540..6d20168ca5 100644 --- a/lib/ecto/query/api.ex +++ b/lib/ecto/query/api.ex @@ -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} @@ -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} From e75c0277958a59deb308abcd650d14a5be690042 Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 30 Nov 2024 09:39:01 -0500 Subject: [PATCH 3/4] map ordering --- test/ecto/query/builder/from_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ecto/query/builder/from_test.exs b/test/ecto/query/builder/from_test.exs index beb0300e69..99ca8ca915 100644 --- a/test/ecto/query/builder/from_test.exs +++ b/test/ecto/query/builder/from_test.exs @@ -40,10 +40,10 @@ defmodule Ecto.Query.Builder.FromTest do test "values list source with types defined by schema" do values = [%{num: 1, text: "one"}, %{num: 2, text: "two"}] type_schema = Schema - types = Enum.map([:num, :text], &{&1, Schema.__schema__(:type, &1)}) + types_kw = Enum.map(%{num: :integer, text: :string}, & &1) query = from v in values(values, type_schema) - assert query.from.source == {:values, [], [types, length(values)]} + assert query.from.source == {:values, [], [types_kw, length(values)]} end test "values list source with empty values" do From bae8ca901f311bac6f99807cd1f00491fcafcda7 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 2 Dec 2024 18:15:58 -0500 Subject: [PATCH 4/4] oops --- lib/ecto/query/api.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ecto/query/api.ex b/lib/ecto/query/api.ex index 6d20168ca5..4dfe57d9bd 100644 --- a/lib/ecto/query/api.ex +++ b/lib/ecto/query/api.ex @@ -519,7 +519,7 @@ defmodule Ecto.Query.API do same fields. The second argument is either a map of types or an Ecto schema containing all the - fields in the first argument + 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.