diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 824a7d9fe3..a63bfa28ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,11 @@ jobs: fail-fast: false matrix: include: + - elixir: "1.18.1" + otp: "27.2" + lint: lint - elixir: "1.17.3" otp: "27.1" - lint: lint - elixir: "1.17.3" otp: "25.0.4" - elixir: "1.14.5" diff --git a/test/ecto/query/builder_test.exs b/test/ecto/query/builder_test.exs index 59163d0d3e..0e871244aa 100644 --- a/test/ecto/query/builder_test.exs +++ b/test/ecto/query/builder_test.exs @@ -1,4 +1,4 @@ -Code.require_file "../../../integration_test/support/types.exs", __DIR__ +Code.require_file("../../../integration_test/support/types.exs", __DIR__) defmodule Ecto.Query.BuilderTest do use ExUnit.Case, async: true @@ -12,127 +12,416 @@ defmodule Ecto.Query.BuilderTest do end test "escape" do - assert {Macro.escape(quote do &0.y() end), []} == - escape(quote do x.y() end, [x: 0], __ENV__) + assert {Macro.escape( + quote do + &0.y() + end + ), + []} == + escape( + quote do + x.y() + end, + [x: 0], + __ENV__ + ) import Kernel, except: [>: 2] - assert {Macro.escape(quote do &0.y() > &0.z() end), []} == - escape(quote do x.y() > x.z() end, [x: 0], __ENV__) - assert {Macro.escape(quote do &0.y() > &1.z() end), []} == - escape(quote do x.y() > y.z() end, [x: 0, y: 1], __ENV__) + assert {Macro.escape( + quote do + &0.y() > &0.z() + end + ), + []} == + escape( + quote do + x.y() > x.z() + end, + [x: 0], + __ENV__ + ) + + assert {Macro.escape( + quote do + &0.y() > &1.z() + end + ), + []} == + escape( + quote do + x.y() > y.z() + end, + [x: 0, y: 1], + __ENV__ + ) import Kernel, except: [+: 2, +: 1] - assert {Macro.escape(quote do &0.y() + &1.z() end), []} == - escape(quote do x.y() + y.z() end, [x: 0, y: 1], __ENV__) - assert {Macro.escape(quote do avg(0) end), []} == - escape(quote do avg(0) end, [], __ENV__) + assert {Macro.escape( + quote do + &0.y() + &1.z() + end + ), + []} == + escape( + quote do + x.y() + y.z() + end, + [x: 0, y: 1], + __ENV__ + ) + + assert {Macro.escape( + quote do + avg(0) + end + ), + []} == + escape( + quote do + avg(0) + end, + [], + __ENV__ + ) assert {quote(do: ~s"123"), []} == - escape(quote do ~s"123" end, [], __ENV__) - - assert {{:%, [], [Ecto.Query.Tagged, {:%{}, [], [value: {:<<>>, [], [0, 1, 2]}, type: {:||, _, [{:&&, _, [{:is_binary, _, [{:<<>>, [], [0, 1, 2]}]}, :binary]}, :bitstring]}]}]}, []} = - escape(quote do <<0, 1, 2>> end, [], __ENV__) + escape( + quote do + ~s"123" + end, + [], + __ENV__ + ) + + assert {{:%, [], + [ + Ecto.Query.Tagged, + {:%{}, [], + [ + value: {:<<>>, [], [0, 1, 2]}, + type: + {:||, _, + [{:&&, _, [{:is_binary, _, [{:<<>>, [], [0, 1, 2]}]}, :binary]}, :bitstring]} + ]} + ]}, + []} = + escape( + quote do + <<0, 1, 2>> + end, + [], + __ENV__ + ) assert {:some_atom, []} == - escape(quote do :some_atom end, [], __ENV__) + escape( + quote do + :some_atom + end, + [], + __ENV__ + ) assert quote(do: &0.z()) == - escape(quote do field(x, :z) end, [x: 0], __ENV__) - |> elem(0) - |> Code.eval_quoted([], __ENV__) - |> elem(0) + escape( + quote do + field(x, :z) + end, + [x: 0], + __ENV__ + ) + |> elem(0) + |> Code.eval_quoted([], __ENV__) + |> elem(0) assert {{:., [], [{:&, [], [0]}, "z"]}, [], []} == - escape(quote do field(x, "z") end, [x: 0], __ENV__) - |> elem(0) - |> Code.eval_quoted([], __ENV__) - |> elem(0) - - assert {Macro.escape(quote do -&0.y() end), []} == - escape(quote do -x.y() end, [x: 0], __ENV__) + escape( + quote do + field(x, "z") + end, + [x: 0], + __ENV__ + ) + |> elem(0) + |> Code.eval_quoted([], __ENV__) + |> elem(0) + + assert {Macro.escape( + quote do + -&0.y() + end + ), + []} == + escape( + quote do + -x.y() + end, + [x: 0], + __ENV__ + ) end test "escape json_extract_path" do expected = {Macro.escape(quote do: json_extract_path(&0.y(), ["a", "b"])), []} - actual = escape(quote do json_extract_path(x.y, ["a", "b"]) end, [x: 0], __ENV__) + + actual = + escape( + quote do + json_extract_path(x.y, ["a", "b"]) + end, + [x: 0], + __ENV__ + ) + assert actual == expected expected = {Macro.escape(quote do: json_extract_path(&0.y(), ["a", "b"])), []} - actual = escape(quote do json_extract_path(field(x, :y), ["a", "b"]) end, [x: 0], __ENV__) + + actual = + escape( + quote do + json_extract_path(field(x, :y), ["a", "b"]) + end, + [x: 0], + __ENV__ + ) + assert actual == expected - actual = escape(quote do x.y["a"]["b"] end, [x: 0], __ENV__) + actual = + escape( + quote do + x.y["a"]["b"] + end, + [x: 0], + __ENV__ + ) + assert actual == expected expected = {Macro.escape(quote do: json_extract_path(as(:x).y(), ["a", "b"])), []} - actual = escape(quote do json_extract_path(as(:x).y, ["a", "b"]) end, [], __ENV__) + + actual = + escape( + quote do + json_extract_path(as(:x).y, ["a", "b"]) + end, + [], + __ENV__ + ) + assert actual == expected expected = {Macro.escape(quote do: json_extract_path(parent_as(:x).y(), ["a", "b"])), []} - actual = escape(quote do json_extract_path(parent_as(:x).y, ["a", "b"]) end, [], __ENV__) + + actual = + escape( + quote do + json_extract_path(parent_as(:x).y, ["a", "b"]) + end, + [], + __ENV__ + ) + assert actual == expected expected = {Macro.escape(quote do: json_extract_path(as(:x).y(), ["a", "b"])), []} - actual = escape(quote do as(:x).y["a"]["b"] end, [], __ENV__) + + actual = + escape( + quote do + as(:x).y["a"]["b"] + end, + [], + __ENV__ + ) + assert actual == expected expected = {Macro.escape(quote do: json_extract_path(parent_as(:x).y(), ["a", "b"])), []} - actual = escape(quote do parent_as(:x).y["a"]["b"] end, [], __ENV__) + + actual = + escape( + quote do + parent_as(:x).y["a"]["b"] + end, + [], + __ENV__ + ) + assert actual == expected expected = {Macro.escape(quote do: json_extract_path(&0.y(), ["a", 0])), []} - actual = escape(quote do x.y["a"][0] end, [x: 0], __ENV__) + + actual = + escape( + quote do + x.y["a"][0] + end, + [x: 0], + __ENV__ + ) + assert actual == expected expected = {Macro.escape(quote do: json_extract_path(&0.y(), [0, "a"])), []} - actual = escape(quote do x.y[0]["a"] end, [x: 0], __ENV__) + + actual = + escape( + quote do + x.y[0]["a"] + end, + [x: 0], + __ENV__ + ) + assert actual == expected assert_raise Ecto.Query.CompileError, "`x` is not a valid json field", fn -> - escape(quote do json_extract_path(x, ["a"]) end, [x: 0], __ENV__) + escape( + quote do + json_extract_path(x, ["a"]) + end, + [x: 0], + __ENV__ + ) end assert_raise Ecto.Query.CompileError, "`x[\"a\"]` is not a valid query expression", fn -> - escape(quote do x["a"] end, [x: 0], __ENV__) + escape( + quote do + x["a"] + end, + [x: 0], + __ENV__ + ) end - assert_raise Ecto.Query.CompileError, ~r/expected JSON path to contain literal strings.*got: `a`/, fn -> - escape(quote do x.y[a] end, [x: 0], __ENV__) - end + assert_raise Ecto.Query.CompileError, + ~r/expected JSON path to contain literal strings.*got: `a`/, + fn -> + escape( + quote do + x.y[a] + end, + [x: 0], + __ENV__ + ) + end - assert_raise Ecto.Query.CompileError, "expected JSON path to be a literal list or interpolated value, got: `bad`", fn -> - escape(quote do json_extract_path(x.y, bad) end, [x: 0], __ENV__) - end + assert_raise Ecto.Query.CompileError, + "expected JSON path to be a literal list or interpolated value, got: `bad`", + fn -> + escape( + quote do + json_extract_path(x.y, bad) + end, + [x: 0], + __ENV__ + ) + end end test "escape fragments" do - assert {Macro.escape(quote do fragment({:raw, "date_add("}, {:expr, &0.created_at()}, - {:raw, ", "}, {:expr, ^0}, {:raw, ")"}) end), [{0, :any}]} == - escape(quote do fragment("date_add(?, ?)", p.created_at(), ^0) end, [p: 0], __ENV__) + assert {Macro.escape( + quote do + fragment( + {:raw, "date_add("}, + {:expr, &0.created_at()}, + {:raw, ", "}, + {:expr, ^0}, + {:raw, ")"} + ) + end + ), + [{0, :any}]} == + escape( + quote do + fragment("date_add(?, ?)", p.created_at(), ^0) + end, + [p: 0], + __ENV__ + ) + + assert {Macro.escape( + quote do + fragment({:raw, ""}, {:expr, ^0}, {:raw, "::text"}) + end + ), + [{0, :any}]} == + escape( + quote do + fragment(~S"?::text", ^0) + end, + [p: 0], + __ENV__ + ) + + assert {Macro.escape( + quote do + fragment({:raw, "query?("}, {:expr, &0.created_at()}, {:raw, ")"}) + end + ), + []} == + escape( + quote do + fragment("query\\?(?)", p.created_at()) + end, + [p: 0], + __ENV__ + ) + + assert {Macro.escape( + quote do + fragment(title: [foo: ^0]) + end + ), + [{0, :any}]} == + escape( + quote do + fragment(title: [foo: ^0]) + end, + [], + __ENV__ + ) - assert {Macro.escape(quote do fragment({:raw, ""}, {:expr, ^0}, {:raw, "::text"}) end), [{0, :any}]} == - escape(quote do fragment(~S"?::text", ^0) end, [p: 0], __ENV__) - - assert {Macro.escape(quote do fragment({:raw, "query?("}, {:expr, &0.created_at()}, - {:raw, ")"}) end), []} == - escape(quote do fragment("query\\?(?)", p.created_at()) end, [p: 0], __ENV__) - - assert {Macro.escape(quote do fragment(title: [foo: ^0]) end), [{0, :any}]} == - escape(quote do fragment(title: [foo: ^0]) end, [], __ENV__) - - assert_raise Ecto.Query.CompileError, ~r"fragment\(...\) does not allow strings to be interpolated", fn -> - escape(quote do fragment(:invalid) end, [], __ENV__) - end + assert_raise Ecto.Query.CompileError, + ~r"fragment\(...\) does not allow strings to be interpolated", + fn -> + escape( + quote do + fragment(:invalid) + end, + [], + __ENV__ + ) + end assert_raise Ecto.Query.CompileError, ~r"expects extra arguments in the same amount of question marks in string. It received 0 extra argument\(s\) but expected 1", - fn -> escape(quote do fragment("?") end, [], __ENV__) end + fn -> + escape( + quote do + fragment("?") + end, + [], + __ENV__ + ) + end assert_raise Ecto.Query.CompileError, ~r"expects extra arguments in the same amount of question marks in string. It received 1 extra argument\(s\) but expected 0", - fn -> escape(quote do fragment("", 1) end, [], __ENV__) end + fn -> + escape( + quote do + fragment("", 1) + end, + [], + __ENV__ + ) + end end defmacro my_first_value(expr) do @@ -142,14 +431,14 @@ defmodule Ecto.Query.BuilderTest do end test "escape over with window name" do - assert {Macro.escape(quote(do: over(count(&0.id()), :w))), []} == - escape(quote(do: count(x.id()) |> over(:w)), [x: 0], __ENV__) + assert {Macro.escape(quote(do: over(count(&0.id()), :w))), []} == + escape(quote(do: count(x.id()) |> over(:w)), [x: 0], __ENV__) - assert {Macro.escape(quote(do: over(nth_value(&0.id(), 1), :w))), []} == - escape(quote(do: nth_value(x.id(), 1) |> over(:w)), [x: 0], __ENV__) + assert {Macro.escape(quote(do: over(nth_value(&0.id(), 1), :w))), []} == + escape(quote(do: nth_value(x.id(), 1) |> over(:w)), [x: 0], __ENV__) - assert {Macro.escape(quote(do: over(nth_value(&0.id(), 1), :w))), []} == - escape(quote(do: my_first_value(x.id()) |> over(:w)), [x: 0], __ENV__) + assert {Macro.escape(quote(do: over(nth_value(&0.id(), 1), :w))), []} == + escape(quote(do: my_first_value(x.id()) |> over(:w)), [x: 0], __ENV__) end defmacro my_custom_field(p) do @@ -165,67 +454,207 @@ defmodule Ecto.Query.BuilderTest do end test "escape over with window parts" do - assert {Macro.escape(quote(do: over(row_number(), []))), []} == - escape(quote(do: over(row_number())), [], __ENV__) + assert {Macro.escape(quote(do: over(row_number(), []))), []} == + escape(quote(do: over(row_number())), [], __ENV__) - assert {Macro.escape(quote(do: over(nth_value(&0.id(), 1), []))), []} == - escape(quote(do: over(my_first_value(x.id()))), [x: 0], __ENV__) + assert {Macro.escape(quote(do: over(nth_value(&0.id(), 1), []))), []} == + escape(quote(do: over(my_first_value(x.id()))), [x: 0], __ENV__) assert {Macro.escape(quote(do: over(nth_value(&0.id(), 1), order_by: [asc: &0.id()]))), []} == - escape(quote(do: nth_value(x.id(), 1) |> over(order_by: x.id())), [x: 0], __ENV__) - - assert {Macro.escape(quote(do: over(nth_value(&0.id(), 1), order_by: [desc: &0.id(), asc: fragment({:raw, "lower("}, {:expr, &0.title()}, {:raw, ")"}), asc: nth_value(&0.links(), 1)]))), []} == - escape(quote(do: nth_value(x.id(), 1) |> over(order_by: my_complex_order(x))), [x: 0], __ENV__) + escape(quote(do: nth_value(x.id(), 1) |> over(order_by: x.id())), [x: 0], __ENV__) + + assert {Macro.escape( + quote( + do: + over(nth_value(&0.id(), 1), + order_by: [ + desc: &0.id(), + asc: fragment({:raw, "lower("}, {:expr, &0.title()}, {:raw, ")"}), + asc: nth_value(&0.links(), 1) + ] + ) + ) + ), + []} == + escape( + quote(do: nth_value(x.id(), 1) |> over(order_by: my_complex_order(x))), + [x: 0], + __ENV__ + ) assert {Macro.escape(quote(do: over(nth_value(&0.id(), 1), partition_by: [&0.id()]))), []} == - escape(quote(do: nth_value(x.id(), 1) |> over(partition_by: x.id())), [x: 0], __ENV__) - - assert {Macro.escape(quote(do: over(nth_value(&0.id(), 1), frame: fragment({:raw, "ROWS"})))), []} == - escape(quote(do: nth_value(x.id(), 1) |> over(frame: fragment("ROWS"))), [x: 0], __ENV__) + escape( + quote(do: nth_value(x.id(), 1) |> over(partition_by: x.id())), + [x: 0], + __ENV__ + ) + + assert {Macro.escape(quote(do: over(nth_value(&0.id(), 1), frame: fragment({:raw, "ROWS"})))), + []} == + escape( + quote(do: nth_value(x.id(), 1) |> over(frame: fragment("ROWS"))), + [x: 0], + __ENV__ + ) assert_raise Ecto.Query.CompileError, ~r"windows definitions given to over/2 do not allow interpolations at the root", fn -> - escape(quote(do: nth_value(x.id(), 1) |> over(order_by: ^foo)), [x: 0], __ENV__) - end + escape( + quote(do: nth_value(x.id(), 1) |> over(order_by: ^foo)), + [x: 0], + __ENV__ + ) + end import Kernel, except: [is_nil: 1] - assert {Macro.escape(quote(do: over(filter(avg(&0.value()), is_nil(&0.flag())), []))), []} == - escape(quote(do: avg(x.value()) |> filter(is_nil(x.flag())) |> over([])), [x: 0], __ENV__) + + assert {Macro.escape(quote(do: over(filter(avg(&0.value()), is_nil(&0.flag())), []))), []} == + escape( + quote(do: avg(x.value()) |> filter(is_nil(x.flag())) |> over([])), + [x: 0], + __ENV__ + ) end test "escape type cast" do import Kernel, except: [+: 2, +: 1] - assert {Macro.escape(quote do type(&0.y() + &1.z(), :decimal) end), []} == - escape(quote do type(x.y() + y.z(), :decimal) end, [x: 0, y: 1], __ENV__) - - assert {Macro.escape(quote do type(&0.y(), :decimal) end), []} == - escape(quote do type(field(x, :y), :decimal) end, [x: 0], __ENV__) - - assert {Macro.escape(quote do type(&0.y(), :"Elixir.Ecto.UUID") end), []} == - escape(quote do type(field(x, :y), Ecto.UUID) end, [x: 0], __ENV__) - - assert {Macro.escape(quote do type(&0.y(), :"Elixir.Ecto.UUID") end), []} == - escape(quote do type(field(x, :y), Ecto.UUID) end, [x: 0], {__ENV__, %{}}) - - assert {Macro.escape(quote do type(sum(&0.y()), :decimal) end), []} == - escape(quote do type(sum(x.y()), :decimal) end, [x: 0], {__ENV__, %{}}) - assert {Macro.escape(quote do type(count(), :decimal) end), []} == - escape(quote do type(count(), :decimal) end, [x: 0], {__ENV__, %{}}) + assert {Macro.escape( + quote do + type(&0.y() + &1.z(), :decimal) + end + ), + []} == + escape( + quote do + type(x.y() + y.z(), :decimal) + end, + [x: 0, y: 1], + __ENV__ + ) + + assert {Macro.escape( + quote do + type(&0.y(), :decimal) + end + ), + []} == + escape( + quote do + type(field(x, :y), :decimal) + end, + [x: 0], + __ENV__ + ) + + assert {Macro.escape( + quote do + type(&0.y(), :"Elixir.Ecto.UUID") + end + ), + []} == + escape( + quote do + type(field(x, :y), Ecto.UUID) + end, + [x: 0], + __ENV__ + ) + + assert {Macro.escape( + quote do + type(&0.y(), :"Elixir.Ecto.UUID") + end + ), + []} == + escape( + quote do + type(field(x, :y), Ecto.UUID) + end, + [x: 0], + {__ENV__, %{}} + ) + + assert {Macro.escape( + quote do + type(sum(&0.y()), :decimal) + end + ), + []} == + escape( + quote do + type(sum(x.y()), :decimal) + end, + [x: 0], + {__ENV__, %{}} + ) + + assert {Macro.escape( + quote do + type(count(), :decimal) + end + ), + []} == + escape( + quote do + type(count(), :decimal) + end, + [x: 0], + {__ENV__, %{}} + ) import Kernel, except: [>: 2] - assert {Macro.escape(quote do type(filter(sum(&0.y()), &0.y() > &0.z()), :decimal) end), []} == - escape(quote do type(filter(sum(x.y()), x.y() > x.z()), :decimal) end, [x: 0], {__ENV__, %{}}) - assert {Macro.escape(quote do type(over(fragment({:raw, "array_agg("}, {:expr, &0.id()}, {:raw, ")"}), :y), {:array, :"Elixir.Ecto.UUID"}) end), []} == - escape(quote do type(over(fragment("array_agg(?)", x.id()), :y), {:array, Ecto.UUID}) end, [x: 0], {__ENV__, %{}}) + assert {Macro.escape( + quote do + type(filter(sum(&0.y()), &0.y() > &0.z()), :decimal) + end + ), + []} == + escape( + quote do + type(filter(sum(x.y()), x.y() > x.z()), :decimal) + end, + [x: 0], + {__ENV__, %{}} + ) + + assert {Macro.escape( + quote do + type( + over(fragment({:raw, "array_agg("}, {:expr, &0.id()}, {:raw, ")"}), :y), + {:array, :"Elixir.Ecto.UUID"} + ) + end + ), + []} == + escape( + quote do + type(over(fragment("array_agg(?)", x.id()), :y), {:array, Ecto.UUID}) + end, + [x: 0], + {__ENV__, %{}} + ) end test "escape parameterized types" do - parameterized_type = Ecto.ParameterizedType.init(ParameterizedPrefixedString, prefix: "p") - assert {Macro.escape(quote do type(&0.y(), unquote(parameterized_type)) end), []} == - escape(quote do type(field(x, :y), unquote(parameterized_type)) end, [x: 0], __ENV__) + parameterized_type = + Macro.escape(Ecto.ParameterizedType.init(ParameterizedPrefixedString, prefix: "p")) + + assert {Macro.escape( + quote do + type(&0.y(), unquote(parameterized_type)) + end + ), + []} == + escape( + quote do + type(field(x, :y), unquote(parameterized_type)) + end, + [x: 0], + __ENV__ + ) end defmacro wrapped_sum(a) do @@ -233,36 +662,59 @@ defmodule Ecto.Query.BuilderTest do end test "escape type cast with macro" do - assert {Macro.escape(quote do type(sum(&0.y()), :integer) end), []} == - escape(quote do type(wrapped_sum(x.y()), :integer) end, [x: 0], __ENV__) + assert {Macro.escape( + quote do + type(sum(&0.y()), :integer) + end + ), + []} == + escape( + quote do + type(wrapped_sum(x.y()), :integer) + end, + [x: 0], + __ENV__ + ) end test "escape type checks" do - assert_raise Ecto.Query.CompileError, ~r"It returns a value of type :boolean but a value of type :integer is expected", fn -> - escape(quote(do: ^1 == ^2), :integer, %{}, [], __ENV__) - end + assert_raise Ecto.Query.CompileError, + ~r"It returns a value of type :boolean but a value of type :integer is expected", + fn -> + escape(quote(do: ^1 == ^2), :integer, %{}, [], __ENV__) + end - assert_raise Ecto.Query.CompileError, ~r"It returns a value of type :boolean but a value of type :integer is expected", fn -> - escape(quote(do: 1 > 2), :integer, %{}, [], __ENV__) - end + assert_raise Ecto.Query.CompileError, + ~r"It returns a value of type :boolean but a value of type :integer is expected", + fn -> + escape(quote(do: 1 > 2), :integer, %{}, [], __ENV__) + end end test "escape raise" do - assert_raise Ecto.Query.CompileError, ~r"is not a valid query expression. Only literal binaries and strings are allowed", fn -> - escape(quote(do: "#{x}"), [], __ENV__) - end + assert_raise Ecto.Query.CompileError, + ~r"is not a valid query expression. Only literal binaries and strings are allowed", + fn -> + escape(quote(do: "#{x}"), [], __ENV__) + end - assert_raise Ecto.Query.CompileError, ~r"short-circuit operators are not supported: `&&`", fn -> - escape(quote(do: true && false), [], __ENV__) - end + assert_raise Ecto.Query.CompileError, + ~r"short-circuit operators are not supported: `&&`", + fn -> + escape(quote(do: true && false), [], __ENV__) + end - assert_raise Ecto.Query.CompileError, ~r"`1 = 1` is not a valid query expression. The match operator is not supported: `=`", fn -> - escape(quote(do: 1 = 1), [], __ENV__) - end + assert_raise Ecto.Query.CompileError, + ~r"`1 = 1` is not a valid query expression. The match operator is not supported: `=`", + fn -> + escape(quote(do: 1 = 1), [], __ENV__) + end - assert_raise Ecto.Query.CompileError, ~r"`unknown\(1, 2\)` is not a valid query expression", fn -> - escape(quote(do: unknown(1, 2)), [], __ENV__) - end + assert_raise Ecto.Query.CompileError, + ~r"`unknown\(1, 2\)` is not a valid query expression", + fn -> + escape(quote(do: unknown(1, 2)), [], __ENV__) + end assert_raise Ecto.Query.CompileError, ~r"unbound variable", fn -> escape(quote(do: x.y), [], __ENV__) @@ -272,15 +724,21 @@ defmodule Ecto.Query.BuilderTest do escape(quote(do: x.y == 1), [], __ENV__) end - assert_raise Ecto.Query.CompileError, ~r"expected literal atom or string or interpolated value.*got: `var`", fn -> - escape(quote(do: field(x, var)), [x: 0], __ENV__) |> elem(0) |> Code.eval_quoted([], __ENV__) - end + assert_raise Ecto.Query.CompileError, + ~r"expected literal atom or string or interpolated value.*got: `var`", + fn -> + escape(quote(do: field(x, var)), [x: 0], __ENV__) + |> elem(0) + |> Code.eval_quoted([], __ENV__) + end assert_raise Ecto.Query.CompileError, ~r"make sure that you have required\n the module or imported the relevant function", fn -> - escape(quote(do: Foo.bar(x)), [x: 0], __ENV__) |> elem(0) |> Code.eval_quoted([], __ENV__) - end + escape(quote(do: Foo.bar(x)), [x: 0], __ENV__) + |> elem(0) + |> Code.eval_quoted([], __ENV__) + end assert_raise Ecto.Query.CompileError, ~r"unknown window function lag/0", fn -> escape(quote(do: over(lag())), [], __ENV__) @@ -289,11 +747,12 @@ defmodule Ecto.Query.BuilderTest do test "doesn't escape interpolation" do import Kernel, except: [>: 2, ++: 2] + assert {Macro.escape(quote(do: ^0)), [{quote(do: 1 > 2), :any}]} == - escape(quote(do: ^(1 > 2)), [], __ENV__) + escape(quote(do: ^(1 > 2)), [], __ENV__) assert {Macro.escape(quote(do: ^0)), [{quote(do: [] ++ []), :any}]} == - escape(quote(do: ^([] ++ [])), [], __ENV__) + escape(quote(do: ^([] ++ [])), [], __ENV__) end defp params(quoted, type, vars \\ []) do @@ -303,22 +762,22 @@ defmodule Ecto.Query.BuilderTest do test "infers the type for parameter" do assert [{_, :integer}] = - params(quote(do: ^1 == 2), :any) + params(quote(do: ^1 == 2), :any) assert [{_, :integer}] = - params(quote(do: 2 == ^1), :any) + params(quote(do: 2 == ^1), :any) assert [{_, :any}, {_, :any}] = - params(quote(do: ^1 == ^2), :any) + params(quote(do: ^1 == ^2), :any) assert [{_, {0, :title}}] = - params(quote(do: ^1 == p.title), :any, [p: 0]) + params(quote(do: ^1 == p.title), :any, p: 0) assert [{_, :boolean}] = - params(quote(do: ^1 and true), :any) + params(quote(do: ^1 and true), :any) assert [{_, :boolean}] = - params(quote(do: ^1), :boolean) + params(quote(do: ^1), :boolean) end test "returns the type for quoted query expression" do @@ -349,9 +808,9 @@ defmodule Ecto.Query.BuilderTest do assert quoted_type({:max, [], [1]}, []) == :integer assert quoted_type({:avg, [], [1]}, []) == :any - assert quoted_type({{:., [], [{:p, [], Elixir}, :title]}, [], []}, [p: 0]) == {0, :title} - assert quoted_type({:field, [], [{:p, [], Elixir}, :title]}, [p: 0]) == {0, :title} - assert quoted_type({:field, [], [{:p, [], Elixir}, {:^, [], [:title]}]}, [p: 0]) == {0, :title} + assert quoted_type({{:., [], [{:p, [], Elixir}, :title]}, [], []}, p: 0) == {0, :title} + assert quoted_type({:field, [], [{:p, [], Elixir}, :title]}, p: 0) == {0, :title} + assert quoted_type({:field, [], [{:p, [], Elixir}, {:^, [], [:title]}]}, p: 0) == {0, :title} assert quoted_type({:unknown, [], []}, []) == :any end @@ -360,17 +819,79 @@ defmodule Ecto.Query.BuilderTest do env = {__ENV__, :ok} assert validate_type!({:array, :string}, [], env) == {:array, :string} - assert validate_type!(quote do ^:string end, [], env) == :string - assert validate_type!(quote do Ecto.UUID end, [], env) == Ecto.UUID - assert validate_type!(quote do :string end, [], env) == :string - assert validate_type!(quote do x.title end, [x: 0], env) == {0, :title} - assert validate_type!(quote do field(x, :title) end, [x: 0], env) == {0, :title} - assert validate_type!(quote do field(x, ^:title) end, [x: 0], env) == {0, :title} - assert validate_type!(quote do field(x, "title") end, [x: 0], env) == {0, "title"} - assert validate_type!(quote do field(x, ^"title") end, [x: 0], env) == {0, "title"} + + assert validate_type!( + quote do + ^:string + end, + [], + env + ) == :string + + assert validate_type!( + quote do + Ecto.UUID + end, + [], + env + ) == Ecto.UUID + + assert validate_type!( + quote do + :string + end, + [], + env + ) == :string + + assert validate_type!( + quote do + x.title + end, + [x: 0], + env + ) == {0, :title} + + assert validate_type!( + quote do + field(x, :title) + end, + [x: 0], + env + ) == {0, :title} + + assert validate_type!( + quote do + field(x, ^:title) + end, + [x: 0], + env + ) == {0, :title} + + assert validate_type!( + quote do + field(x, "title") + end, + [x: 0], + env + ) == {0, "title"} + + assert validate_type!( + quote do + field(x, ^"title") + end, + [x: 0], + env + ) == {0, "title"} assert_raise Ecto.Query.CompileError, ~r"^type/2 expects an alias, atom", fn -> - validate_type!(quote do "string" end, [x: 0], env) + validate_type!( + quote do + "string" + end, + [x: 0], + env + ) end end end diff --git a/test/ecto/query_test.exs b/test/ecto/query_test.exs index 1647e89291..30583edff4 100644 --- a/test/ecto/query_test.exs +++ b/test/ecto/query_test.exs @@ -1,4 +1,4 @@ -Code.require_file "../support/eval_helpers.exs", __DIR__ +Code.require_file("../support/eval_helpers.exs", __DIR__) defmodule Ecto.QueryTest.Macros do defmacro macro_equal(column, value) do @@ -9,8 +9,7 @@ defmodule Ecto.QueryTest.Macros do defmacro macro_map(key) do quote do - %{"1" => unquote(key), - "2" => unquote(key)} + %{"1" => unquote(key), "2" => unquote(key)} end end @@ -29,6 +28,7 @@ defmodule Ecto.QueryTest do defmodule Schema do use Ecto.Schema + schema "schema" do end end @@ -56,27 +56,30 @@ defmodule Ecto.QueryTest do test "does not allow nils in comparison at compile time" do assert_raise Ecto.Query.CompileError, - ~r"comparison with nil in `p.id == nil` is forbidden as it is unsafe", fn -> - quote_and_eval from p in "posts", where: p.id == nil - end + ~r"comparison with nil in `p.id == nil` is forbidden as it is unsafe", + fn -> + quote_and_eval(from p in "posts", where: p.id == nil) + end end test "does not allow interpolated nils at runtime" do id = nil assert_raise ArgumentError, - ~r"nil given for `id`. comparison with nil is forbidden as it is unsafe", fn -> - from p in "posts", where: [id: ^id] - end + ~r"nil given for `id`. comparison with nil is forbidden as it is unsafe", + fn -> + from p in "posts", where: [id: ^id] + end assert_raise ArgumentError, - ~r"comparing `p.id` with `nil` is forbidden as it is unsafe", fn -> - from p in "posts", where: p.id == ^id - end + ~r"comparing `p.id` with `nil` is forbidden as it is unsafe", + fn -> + from p in "posts", where: p.id == ^id + end end test "allows arbitrary parentheses in where" do - _ = from(p in "posts", where: (not is_nil(p.title))) + _ = from(p in "posts", where: not is_nil(p.title)) end @statuses [:draft, :published] @@ -87,7 +90,9 @@ defmodule Ecto.QueryTest do %Ecto.Query.Tagged{value: :published, type: {0, :status}} ] - [%{expr: {:in, [], [_, actual_in]}}] = from(p in "posts", where: p.status in @statuses).wheres + [%{expr: {:in, [], [_, actual_in]}}] = + from(p in "posts", where: p.status in @statuses).wheres + assert actual_in == expected_in end end @@ -183,7 +188,7 @@ defmodule Ecto.QueryTest do describe "bindings" do test "are not required by macros" do _ = from(p in "posts") |> limit(1) - _ = from(p in "posts") |> order_by([asc: :title]) + _ = from(p in "posts") |> order_by(asc: :title) _ = from(p in "posts") |> where(title: "foo") _ = from(p in "posts") |> having(title: "foo") _ = from(p in "posts") |> offset(1) @@ -193,15 +198,16 @@ defmodule Ecto.QueryTest do _ = from(p in "posts") |> distinct(true) assert ExUnit.CaptureIO.capture_io(:stderr, fn -> - _ = quote_and_eval(from(p in "posts") |> join(:inner, "comments")) - end) =~ ~s'missing `:on` in join on "comments", defaulting to `on: true`' + _ = quote_and_eval(from(p in "posts") |> join(:inner, "comments")) + end) =~ ~s'missing `:on` in join on "comments", defaulting to `on: true`' end test "must be a list of variables" do assert_raise Ecto.Query.CompileError, - "binding list should contain only variables or `{as, var}` tuples, got: 0", fn -> - quote_and_eval select(%Query{}, [0], 1) - end + "binding list should contain only variables or `{as, var}` tuples, got: 0", + fn -> + quote_and_eval(select(%Query{}, [0], 1)) + end end test "ignore unbound _ var" do @@ -257,18 +263,20 @@ defmodule Ecto.QueryTest do describe "trailing bindings (...)" do test "match on last bindings" do - query = "posts" |> join(:inner, [], "comments", on: true) |> join(:inner, [], "votes", on: true) + query = + "posts" |> join(:inner, [], "comments", on: true) |> join(:inner, [], "votes", on: true) + assert select(query, [..., v], v).select.expr == - {:&, [], [2]} + {:&, [], [2]} assert select(query, [p, ..., v], {p, v}).select.expr == - {:{}, [], [{:&, [], [0]}, {:&, [], [2]}]} + {:{}, [], [{:&, [], [0]}, {:&, [], [2]}]} assert select(query, [p, c, v, ...], v).select.expr == - {:&, [], [2]} + {:&, [], [2]} assert select(query, [..., c, v], {c, v}).select.expr == - {:{}, [], [{:&, [], [1]}, {:&, [], [2]}]} + {:{}, [], [{:&, [], [1]}, {:&, [], [2]}]} end test "match on last bindings with multiple constructs" do @@ -290,20 +298,23 @@ defmodule Ecto.QueryTest do |> join(:inner, [..., c], v in "votes", on: c.id == v.id) assert hd(tl(query.joins)).on.expr == - {:==, [], [ - {{:., [], [{:&, [], [1]}, :id]}, [], []}, - {{:., [], [{:&, [], [2]}, :id]}, [], []} - ]} + {:==, [], + [ + {{:., [], [{:&, [], [1]}, :id]}, [], []}, + {{:., [], [{:&, [], [2]}, :id]}, [], []} + ]} end test "match on last bindings on keyword query" do posts = "posts" query = from [..., p] in posts, join: c in "comments", on: p.id == c.id + assert hd(query.joins).on.expr == - {:==, [], [ - {{:., [], [{:&, [], [0]}, :id]}, [], []}, - {{:., [], [{:&, [], [1]}, :id]}, [], []} - ]} + {:==, [], + [ + {{:., [], [{:&, [], [0]}, :id]}, [], []}, + {{:., [], [{:&, [], [1]}, :id]}, [], []} + ]} end test "dynamic in :on takes new binding when ... is used" do @@ -311,7 +322,7 @@ defmodule Ecto.QueryTest do query = from p in "posts", join: c in "comments", on: ^join_on assert inspect(query) == - ~s[#Ecto.Query] + ~s[#Ecto.Query] end end @@ -319,9 +330,15 @@ defmodule Ecto.QueryTest do test "assigns a name to a join" do query = from(p in "posts", - join: b in "blogs", on: true, - join: c in "comments", on: true, as: :comment, - join: l in "links", on: l.valid, as: :link) + join: b in "blogs", + on: true, + join: c in "comments", + on: true, + as: :comment, + join: l in "links", + on: l.valid, + as: :link + ) assert %{comment: 2, link: 3} == query.aliases end @@ -348,7 +365,7 @@ defmodule Ecto.QueryTest do end test "assigns a name to a subquery source" do - posts_query = from p in "posts" + posts_query = from(p in "posts") query = from p in subquery(posts_query), as: :post assert %{post: 0} == query.aliases @@ -356,7 +373,9 @@ defmodule Ecto.QueryTest do end test "assign to source fails when non-atom name passed" do - message = ~r/`as` must be a compile time atom or an interpolated value using \^, got: "post"/ + message = + ~r/`as` must be a compile time atom or an interpolated value using \^, got: "post"/ + assert_raise Ecto.Query.CompileError, message, fn -> quote_and_eval(from(p in "posts", as: "post")) end @@ -370,6 +389,7 @@ defmodule Ecto.QueryTest do test "crashes on duplicate as for keyword query" do message = ~r"`as` keyword was given more than once" + assert_raise Ecto.Query.CompileError, message, fn -> quote_and_eval(from(p in "posts", join: b in "blogs", on: true, as: :foo, as: :bar)) end @@ -377,23 +397,41 @@ defmodule Ecto.QueryTest do test "crashes on assigning the same name twice at compile time" do message = ~r"alias `:foo` already exists" + assert_raise Ecto.Query.CompileError, message, fn -> quote_and_eval( - from(p in "posts", join: b in "blogs", on: true, as: :foo, join: c in "comments", on: true, as: :foo) + from(p in "posts", + join: b in "blogs", + on: true, + as: :foo, + join: c in "comments", + on: true, + as: :foo + ) ) end end test "crashes on assigning the same name twice at runtime" do message = ~r"alias `:foo` already exists" + assert_raise Ecto.Query.CompileError, message, fn -> query = "posts" - from(p in query, join: b in "blogs", on: true, as: :foo, join: c in "comments", on: true, as: :foo) + + from(p in query, + join: b in "blogs", + on: true, + as: :foo, + join: c in "comments", + on: true, + as: :foo + ) end end test "crashes on assigning the same name twice when aliasing source" do message = ~r"alias `:foo` already exists" + assert_raise Ecto.Query.CompileError, message, fn -> query = from p in "posts", join: b in "blogs", on: true, as: :foo from(p in query, as: :foo) @@ -402,6 +440,7 @@ defmodule Ecto.QueryTest do test "crashes on assigning the name to source when it already has one" do message = ~r"can't apply alias `:foo`, binding in `from` is already aliased to `:post`" + assert_raise Ecto.Query.CompileError, message, fn -> query = from p in "posts", as: :post from(p in query, as: :foo) @@ -415,7 +454,7 @@ defmodule Ecto.QueryTest do |> where([comment: c], c.id == 0) assert inspect(query) == - ~s[#Ecto.Query] + ~s[#Ecto.Query] end test "match on binding by name for source" do @@ -424,7 +463,7 @@ defmodule Ecto.QueryTest do |> where([post: p], p.id == 0) assert inspect(query) == - ~s[#Ecto.Query] + ~s[#Ecto.Query] end test "match on binding by name for source and join" do @@ -435,7 +474,7 @@ defmodule Ecto.QueryTest do |> update([comment: c], set: [id: c.id + 1]) assert inspect(query) == - ~s{#Ecto.Query} + ~s{#Ecto.Query} end test "match on binding by name with ... in the middle" do @@ -446,7 +485,7 @@ defmodule Ecto.QueryTest do |> where([p, ..., authors: a], a.id == 0) assert inspect(query) == - ~s[#Ecto.Query] + ~s[#Ecto.Query] end test "crashes on non-existing binding" do @@ -459,12 +498,13 @@ defmodule Ecto.QueryTest do test "crashes on bind not in tail of the list" do message = ~r"tuples must be at the end of the binding list" + assert_raise Ecto.Query.CompileError, message, fn -> - quote_and_eval( - "posts" - |> join(:inner, [p], c in "comments", on: true, as: :comment) - |> where([{:comment, c}, p], c.id == 0) - ) + quote_and_eval( + "posts" + |> join(:inner, [p], c in "comments", on: true, as: :comment) + |> where([{:comment, c}, p], c.id == 0) + ) end end @@ -477,7 +517,7 @@ defmodule Ecto.QueryTest do |> where([{^assoc, c}], c.id == 0) assert inspect(query) == - ~s[#Ecto.Query] + ~s[#Ecto.Query] end test "dynamic in :on takes new binding when alias is used" do @@ -485,7 +525,7 @@ defmodule Ecto.QueryTest do query = from p in "posts", join: c in "comments", as: :comment, on: ^join_on assert inspect(query) == - ~s[#Ecto.Query] + ~s[#Ecto.Query] end end @@ -506,7 +546,10 @@ defmodule Ecto.QueryTest do test "are assigned from a variable" do from_prefix = "hello" join_prefix = "world" - query = from p in "posts", prefix: ^from_prefix, join: "comments", on: true, prefix: ^join_prefix + + query = + from p in "posts", prefix: ^from_prefix, join: "comments", on: true, prefix: ^join_prefix + assert query.from.prefix == from_prefix assert hd(query.joins).prefix == join_prefix end @@ -535,6 +578,7 @@ defmodule Ecto.QueryTest do posts = from "posts", prefix: "hello" message = "can't apply prefix `\"world\"`, `from` is already prefixed to `\"hello\"`" + assert_raise Ecto.Query.CompileError, message, fn -> from posts, prefix: "world" end @@ -603,19 +647,22 @@ defmodule Ecto.QueryTest do test "are supported through from/2" do # queries need to be on the same line or == won't work assert from(p in "posts", select: 1 < 2) == from(p in "posts", []) |> select([p], 1 < 2) - assert from(p in "posts", where: 1 < 2) == from(p in "posts", []) |> where([p], 1 < 2) - assert (from(p in "posts") |> select([p], p.title)) == from(p in "posts", select: p.title) + assert from(p in "posts", where: 1 < 2) == from(p in "posts", []) |> where([p], 1 < 2) + assert from(p in "posts") |> select([p], p.title) == from(p in "posts", select: p.title) end test "are built at compile time with binaries" do quoted = quote do from(p in "posts", - join: b in "blogs", on: true, - join: c in "comments", on: c.text == "", - limit: 0, - where: p.id == 0 and b.id == 0 and c.id == 0, - select: p) + join: b in "blogs", + on: true, + join: c in "comments", + on: c.text == "", + limit: 0, + where: p.id == 0 and b.id == 0 and c.id == 0, + select: p + ) end assert {:%{}, _, list} = Macro.expand(quoted, __ENV__) @@ -626,11 +673,14 @@ defmodule Ecto.QueryTest do quoted = quote do from(p in Post, - join: b in Blog, on: true, - join: c in Comment, on: c.text == "", - limit: 0, - where: p.id == 0 and b.id == 0 and c.id == 0, - select: p) + join: b in Blog, + on: true, + join: c in Comment, + on: c.text == "", + limit: 0, + where: p.id == 0 and b.id == 0 and c.id == 0, + select: p + ) end assert {:%{}, _, list} = Macro.expand(quoted, __ENV__) @@ -642,6 +692,7 @@ defmodule Ecto.QueryTest do from(p in "posts", join: c in assoc(p, :comments), select: p) message = ~r"`on` keyword must immediately follow a join" + assert_raise Ecto.Query.CompileError, message, fn -> quote_and_eval(from(c in "comments", on: c.text == "", select: c)) end @@ -654,8 +705,10 @@ defmodule Ecto.QueryTest do query = from(p in "posts", - join: b in "blogs", on: true, - join: c in "comments", on: true, + join: b in "blogs", + on: true, + join: c in "comments", + on: true, where: p.id == 0 and b.id == 0, or_where: c.id == 0, order_by: p.title, @@ -763,14 +816,14 @@ defmodule Ecto.QueryTest do test "removes join qualifiers" do base = %Ecto.Query{} - inner_query = from p in "posts", inner_join: b in "blogs", on: true - cross_query = from p in "posts", cross_join: b in "blogs" + inner_query = from p in "posts", inner_join: b in "blogs", on: true + cross_query = from p in "posts", cross_join: b in "blogs" cross_lateral_query = from p in "posts", cross_lateral_join: b in "blogs" - left_query = from p in "posts", left_join: b in "blogs", on: true - right_query = from p in "posts", right_join: b in "blogs", on: true - full_query = from p in "posts", full_join: b in "blogs", on: true + left_query = from p in "posts", left_join: b in "blogs", on: true + right_query = from p in "posts", right_join: b in "blogs", on: true + full_query = from p in "posts", full_join: b in "blogs", on: true inner_lateral_query = from p in "posts", inner_lateral_join: b in "blogs", on: true - left_lateral_query = from p in "posts", left_lateral_join: b in "blogs", on: true + left_lateral_query = from p in "posts", left_lateral_join: b in "blogs", on: true refute inner_query.joins == base.joins refute cross_query.joins == base.joins @@ -808,7 +861,8 @@ defmodule Ecto.QueryTest do test "removes join qualifiers with named bindings" do query = - from p in "posts", as: :base, + from p in "posts", + as: :base, inner_join: bi in "blogs", on: true, as: :blogs_i, @@ -895,10 +949,10 @@ defmodule Ecto.QueryTest do right = dynamic([posts], posts.is_draft == false) assert inspect(dynamic(^left and ^right)) == - inspect(dynamic([posts], posts.is_public == true and posts.is_draft == false)) + inspect(dynamic([posts], posts.is_public == true and posts.is_draft == false)) assert inspect(dynamic(^left or ^right)) == - inspect(dynamic([posts], posts.is_public == true or posts.is_draft == false)) + inspect(dynamic([posts], posts.is_public == true or posts.is_draft == false)) end test "can be used to merge dynamics with subquery" do @@ -911,10 +965,14 @@ defmodule Ecto.QueryTest do dynamic_with_subquery = dynamic([posts], posts.id in subquery(subquery)) assert inspect(dynamic(^dynamic and ^dynamic_with_subquery)) == - inspect(dynamic([posts], posts.is_public == true and posts.id in subquery(subquery))) + inspect( + dynamic([posts], posts.is_public == true and posts.id in subquery(subquery)) + ) assert inspect(dynamic(^dynamic_with_subquery or ^dynamic)) == - inspect(dynamic([posts], posts.id in subquery(subquery) or posts.is_public == true)) + inspect( + dynamic([posts], posts.id in subquery(subquery) or posts.is_public == true) + ) end test "can be used to merge two dynamics with named bindings" do @@ -924,7 +982,9 @@ defmodule Ecto.QueryTest do query = from p in "post", as: :post assert inspect(where(query, ^dynamic(^left and ^right))) == - inspect(where(query, [post: post], post.is_public == true and post.is_draft == false)) + inspect( + where(query, [post: post], post.is_public == true and post.is_draft == false) + ) end test "can be used to merge two dynamics with subquery that reuse named binding" do @@ -940,40 +1000,77 @@ defmodule Ecto.QueryTest do query = from p in "post", as: :post assert inspect(where(query, ^dynamic(^dynamic and ^dynamic_with_subquery))) == - inspect(where(query, [post: post], post.is_public == ^true and post.id in subquery(subquery))) + inspect( + where( + query, + [post: post], + post.is_public == ^true and post.id in subquery(subquery) + ) + ) assert inspect(where(query, ^dynamic(^dynamic_with_subquery or ^dynamic))) == - inspect(where(query, [post: post], post.id in subquery(subquery) or post.is_public == ^true)) - - assert inspect(where(query, ^dynamic(^dynamic_with_subquery and ^dynamic and ^dynamic_not_in))) == - inspect(where(query, [post: post], post.id in subquery(subquery) and post.is_public == ^true and post.foo not in ^[1, 2, 3])) + inspect( + where( + query, + [post: post], + post.id in subquery(subquery) or post.is_public == ^true + ) + ) + + assert inspect( + where(query, ^dynamic(^dynamic_with_subquery and ^dynamic and ^dynamic_not_in)) + ) == + inspect( + where( + query, + [post: post], + post.id in subquery(subquery) and post.is_public == ^true and + post.foo not in ^[1, 2, 3] + ) + ) end test "merges with precedence" do left = dynamic([posts], posts.is_public == true) right = dynamic([posts], posts.is_draft == false) - assert inspect(dynamic(^left or ^left and ^right)) == - inspect(dynamic([posts], posts.is_public == true or (posts.is_public == true and posts.is_draft == false))) - - assert inspect(dynamic(^left and ^left or ^right)) == - inspect(dynamic([posts], (posts.is_public == true and posts.is_public == true) or posts.is_draft == false)) + assert inspect(dynamic(^left or (^left and ^right))) == + inspect( + dynamic( + [posts], + posts.is_public == true or + (posts.is_public == true and posts.is_draft == false) + ) + ) + + assert inspect(dynamic((^left and ^left) or ^right)) == + inspect( + dynamic( + [posts], + (posts.is_public == true and posts.is_public == true) or + posts.is_draft == false + ) + ) end end describe "fragment/1" do test "raises at runtime when interpolation is not a keyword list" do - assert_raise ArgumentError, ~r/fragment\(...\) does not allow strings to be interpolated/s, fn -> - clause = ["1 = ?"] - from p in "posts", where: fragment(^clause) - end + assert_raise ArgumentError, + ~r/fragment\(...\) does not allow strings to be interpolated/s, + fn -> + clause = ["1 = ?"] + from p in "posts", where: fragment(^clause) + end end test "raises at runtime when interpolation is a binary string" do - assert_raise ArgumentError, ~r/fragment\(...\) does not allow strings to be interpolated/, fn -> - clause = "1 = ?" - from p in "posts", where: fragment(^clause) - end + assert_raise ArgumentError, + ~r/fragment\(...\) does not allow strings to be interpolated/, + fn -> + clause = "1 = ?" + from p in "posts", where: fragment(^clause) + end end test "supports identifiers" do @@ -1039,17 +1136,24 @@ defmodule Ecto.QueryTest do test "keeps UTF-8 encoding" do assert inspect(from p in "posts", where: fragment("héllò")) == - ~s[#Ecto.Query] + ~s[#Ecto.Query] end end describe "has_named_binding?/1" do test "returns true if query has a named binding" do query = - from(p in "posts", as: :posts, - join: b in "blogs", on: true, - join: c in "comments", on: true, as: :comment, - join: l in "links", on: l.valid, as: :link) + from(p in "posts", + as: :posts, + join: b in "blogs", + on: true, + join: c in "comments", + on: true, + as: :comment, + join: l in "links", + on: l.valid, + as: :link + ) assert has_named_binding?(query, :posts) assert has_named_binding?(query, :comment) @@ -1069,13 +1173,13 @@ defmodule Ecto.QueryTest do test "casts queryable to query" do assert_raise Protocol.UndefinedError, - ~r"protocol Ecto.Queryable not implemented for \[\]", + ~r"protocol Ecto.Queryable not implemented", fn -> has_named_binding?([], :posts) end end test "is_named_binding/2 guard" do named_binding_query = from p in "posts", as: :posts - no_binding_query = from p in "posts" + no_binding_query = from(p in "posts") assert is_named_binding(named_binding_query, :posts) refute is_named_binding(no_binding_query, :posts) @@ -1113,8 +1217,7 @@ defmodule Ecto.QueryTest do test "does not execute a function when query has a named binding" do query = - from(p in "posts", as: :posts, - join: c in "comments", on: true, as: :comments) + from(p in "posts", as: :posts, join: c in "comments", on: true, as: :comments) fun = fn _query -> @@ -1144,7 +1247,7 @@ defmodule Ecto.QueryTest do test "casts queryable to query" do assert_raise Protocol.UndefinedError, - ~r"protocol Ecto.Queryable not implemented for \[\]", + ~r"protocol Ecto.Queryable not implemented", fn -> with_named_binding([], :posts, & &1) end end end @@ -1154,8 +1257,9 @@ defmodule Ecto.QueryTest do order_bys = [asc: :inserted_at, desc: :id] reversed_order_bys = [desc: :inserted_at, asc: :id] q = from(p in "posts") + assert inspect(reverse_order(order_by(q, ^order_bys))) == - inspect(order_by(q, ^reversed_order_bys)) + inspect(order_by(q, ^reversed_order_bys)) end test "reverses by primary key with no order" do diff --git a/test/ecto/schema_test.exs b/test/ecto/schema_test.exs index b0b91583ad..15e9c27b96 100644 --- a/test/ecto/schema_test.exs +++ b/test/ecto/schema_test.exs @@ -260,7 +260,7 @@ defmodule Ecto.SchemaTest do end test "embedded schema does not have metadata" do - refute match?(%{__meta__: _}, %EmbeddedSchema{}) + refute Map.has_key?(%EmbeddedSchema{}, :__meta__) end test "embedded redacted_fields" do diff --git a/test/support/test_repo.exs b/test/support/test_repo.exs index 26e6bc0dd9..01754db61a 100644 --- a/test/support/test_repo.exs +++ b/test/support/test_repo.exs @@ -11,9 +11,9 @@ defmodule Ecto.TestAdapter do end def init(opts) do - :ecto = opts[:otp_app] - "user" = opts[:username] - "pass" = opts[:password] + :ecto = opts[:otp_app] + "user" = opts[:username] + "pass" = opts[:password] "hello" = opts[:database] "local" = opts[:hostname] @@ -21,7 +21,7 @@ defmodule Ecto.TestAdapter do end def checkout(mod, _opts, fun) do - send self(), {:checkout, fun} + send(self(), {:checkout, fun}) Process.put({mod, :checked_out?}, true) try do @@ -37,11 +37,11 @@ defmodule Ecto.TestAdapter do ## Types - def loaders({:map, _}, type), do: [&Ecto.Type.embedded_load(type, &1, :json)] + def loaders({:map, _}, type), do: [&Ecto.Type.embedded_load(type, &1, :json)] def loaders(:binary_id, type), do: [Ecto.UUID, type] def loaders(_primitive, type), do: [type] - def dumpers({:map, _}, type), do: [&Ecto.Type.embedded_dump(type, &1, :json)] + def dumpers({:map, _}, type), do: [&Ecto.Type.embedded_dump(type, &1, :json)] def dumpers(:binary_id, type), do: [type, Ecto.UUID] def dumpers(_primitive, type), do: [type] @@ -54,18 +54,18 @@ defmodule Ecto.TestAdapter do def prepare(operation, query), do: {:nocache, {operation, query}} def execute(_, _, {:nocache, {:all, query}}, _, _) do - send self(), {:all, query} + send(self(), {:all, query}) Process.get(:test_repo_all_results) || results_for_all_query(query) end def execute(_, _meta, {:nocache, {op, query}}, _params, _opts) do - send self(), {op, query} + send(self(), {op, query}) {1, nil} end def stream(_, _meta, {:nocache, {:all, query}}, _params, _opts) do Stream.map([:execute], fn :execute -> - send self(), {:stream, query} + send(self(), {:stream, query}) results_for_all_query(query) end) end @@ -97,9 +97,16 @@ defmodule Ecto.TestAdapter do end def insert(_, %{context: nil, prefix: prefix} = meta, fields, on_conflict, returning, _opts) do - meta = Map.merge(meta, %{fields: fields, on_conflict: on_conflict, returning: returning, prefix: prefix}) + meta = + Map.merge(meta, %{ + fields: fields, + on_conflict: on_conflict, + returning: returning, + prefix: prefix + }) + send(self(), {:insert, meta}) - {:ok, Enum.zip(returning, 1..length(returning))} + {:ok, Enum.zip(returning, 1..length(returning)//1)} end def insert(_, %{context: context}, _fields, _on_conflict, _returning, _opts) do @@ -110,7 +117,7 @@ defmodule Ecto.TestAdapter do def update(_, %{context: nil} = meta, [_ | _] = changes, filters, returning, _opts) do meta = Map.merge(meta, %{changes: changes, filters: filters, returning: returning}) send(self(), {:update, meta}) - {:ok, Enum.zip(returning, 1..length(returning))} + {:ok, Enum.zip(returning, 1..length(returning)//1)} end def update(_, %{context: context}, [_ | _], _filters, _returning, _opts) do @@ -120,7 +127,7 @@ defmodule Ecto.TestAdapter do def delete(_, %{context: nil} = meta, filters, returning, _opts) do meta = Map.merge(meta, %{filters: filters, returning: returning}) send(self(), {:delete, meta}) - {:ok, Enum.zip(returning, 1..length(returning))} + {:ok, Enum.zip(returning, 1..length(returning)//1)} end def delete(_, %{context: context}, _filters, _returning, _opts) do @@ -132,7 +139,8 @@ defmodule Ecto.TestAdapter do def transaction(mod, _opts, fun) do # Makes transactions "trackable" in tests Process.put({mod, :in_transaction?}, true) - send self(), {:transaction, fun} + send(self(), {:transaction, fun}) + try do {:ok, fun.()} catch @@ -148,12 +156,12 @@ defmodule Ecto.TestAdapter do end def rollback(_, value) do - send self(), {:rollback, value} - throw {:ecto_rollback, value} + send(self(), {:rollback, value}) + throw({:ecto_rollback, value}) end end -Application.put_env(:ecto, Ecto.TestRepo, [user: "invalid"]) +Application.put_env(:ecto, Ecto.TestRepo, user: "invalid") defmodule Ecto.TestRepo do use Ecto.Repo, otp_app: :ecto, adapter: Ecto.TestAdapter