diff --git a/lib/swiss_schema.ex b/lib/swiss_schema.ex index aec0fb3..c6e73c9 100644 --- a/lib/swiss_schema.ex +++ b/lib/swiss_schema.ex @@ -373,6 +373,36 @@ defmodule SwissSchema do opts :: Keyword.t() ) :: Ecto.Schema.t() + @doc """ + Allow complex queries to be made over the data store. + + `c:query/2` accepts a function to intercept the query and modify as needed. + + ## Examples + + iex> import Ecto.Query, only: [where: 3] + iex> User.query(fn query -> query |> where([u], u.name == "John Lennon") end) + {:ok, [%User{name: "John Lennon"}]} + """ + @doc group: "SwissSchema API" + @callback query(hook :: (Ecto.Query.t() -> Ecto.Query.t()), opts :: Keyword.t()) :: + {:ok, list(Ecto.Schema.t())} | {:error, %Ecto.QueryError{}} + + @doc """ + Allow complex queries to be made over the data store. + + `c:query!/2` accepts a function to intercept the query and modify as needed. + + ## Examples + + iex> import Ecto.Query, only: [where: 2] + iex> User.query!(fn query -> query |> where([u], u.name == "John Lennon") end) + [%User{name: "John Lennon"}] + """ + @doc group: "SwissSchema API" + @callback query!(hook :: (Ecto.Query.t() -> Ecto.Query.t()), opts :: Keyword.t()) :: + list(Ecto.Schema.t()) + @doc """ Returns a lazy enumerable that emits all entries from the data store. @@ -443,6 +473,7 @@ defmodule SwissSchema do end quote do + import Ecto.Query, only: [from: 1, select: 3] @behaviour SwissSchema @_swiss_schema %{ @@ -611,6 +642,37 @@ defmodule SwissSchema do insert_or_update!.(changeset, opts) end + @impl SwissSchema + def query(hook, opts \\ []) + when is_function(hook, 1) do + repo = Keyword.get(opts, :repo, unquote(repo)) + all = Function.capture(repo, :all, 2) + + query = + from(s in __MODULE__) + |> select([s], s) + |> then(hook) + + {:ok, all.(query, opts)} + rescue + error in Ecto.QueryError -> {:error, error} + exception -> reraise exception, __STACKTRACE__ + end + + @impl SwissSchema + def query!(hook, opts \\ []) + when is_function(hook, 1) do + repo = Keyword.get(opts, :repo, unquote(repo)) + all = Function.capture(repo, :all, 2) + + query = + from(s in __MODULE__) + |> select([s], s) + |> then(hook) + + all.(query, opts) + end + @deprecated "Use Ecto.Repo's stream/2 instead" @impl SwissSchema def stream(opts \\ []) do diff --git a/test/swiss_schema_test.exs b/test/swiss_schema_test.exs index eadd0e6..3ce5f53 100644 --- a/test/swiss_schema_test.exs +++ b/test/swiss_schema_test.exs @@ -133,6 +133,10 @@ defmodule SwissSchemaTest do assert function_exported?(SwissSchemaTest.User, :get_by!, 2) end + test "define query/1" do + assert function_exported?(SwissSchemaTest.User, :query, 1) + end + test "define stream/0" do assert function_exported?(SwissSchemaTest.User, :stream, 0) end @@ -574,6 +578,45 @@ defmodule SwissSchemaTest do end end + describe "query/2" do + setup do: Enum.each(1..3, fn i -> user_mock(lucky_number: i) |> Repo.insert!() end) + + test "accept a function to customize the query" do + assert {:ok, [%User{}, %User{}, %User{}]} = User.query(fn q -> q end) + end + + test "captures Ecto.QueryError exceptions and return them as error tuples" do + import Ecto.Query, only: [where: 3] + + assert {:error, %Ecto.QueryError{}} = + User.query(fn q -> q |> where([u], u.wrong_field == 123) end) + end + + test "accepts a custom Ecto repo thru :repo opt" do + 1..3 + |> Enum.map(fn _ -> user_mock() |> Map.drop([:__struct__, :__meta__, :id]) end) + |> then(&Repo2.insert_all(User, &1)) + + assert {:ok, [%User{}, %User{}, %User{}]} = User.query(fn q -> q end, repo: Repo2) + end + end + + describe "query!/2" do + setup do: Enum.each(1..3, fn i -> user_mock(lucky_number: i) |> Repo.insert!() end) + + test "accept a function to customize the query" do + assert [%User{}, %User{}, %User{}] = User.query!(fn q -> q end) + end + + test "accepts a custom Ecto repo thru :repo opt" do + 1..3 + |> Enum.map(fn _ -> user_mock() |> Map.drop([:__struct__, :__meta__, :id]) end) + |> then(&Repo2.insert_all(User, &1)) + + assert [%User{}, %User{}, %User{}] = User.query!(fn q -> q end, repo: Repo2) + end + end + describe "update_all/2" do setup do: Enum.each(1..5, fn i -> user_mock(lucky_number: i) |> Repo.insert!() end)