Skip to content
Permalink
Browse files

Add Repo.first and Repo.last

  • Loading branch information...
josevalim committed Jan 31, 2016
1 parent 9325c3e commit c3ba2297a0b43e5fcdd6e4b863f372e496ef76e7
Showing with 135 additions and 71 deletions.
  1. +15 −22 integration_test/cases/repo.exs
  2. +3 −3 lib/ecto.ex
  3. +52 −6 lib/ecto/repo.ex
  4. +64 −39 lib/ecto/repo/queryable.ex
  5. +1 −1 lib/ecto/schema.ex
@@ -60,7 +60,7 @@ defmodule Ecto.Integration.RepoTest do
loaded_meta = put_in meta.state, :loaded
assert %Post{__meta__: ^loaded_meta} = TestRepo.insert!(post)

post = TestRepo.one(Post)
post = TestRepo.first(Post)
assert post.__meta__.state == :loaded
assert post.inserted_at
end
@@ -428,33 +428,26 @@ defmodule Ecto.Integration.RepoTest do
end
end

test "one(!)" do
test "first(!) and last(!)" do
post1 = TestRepo.insert!(%Post{title: "1", text: "hai"})
post2 = TestRepo.insert!(%Post{title: "2", text: "hai"})

assert post1 == TestRepo.one(from p in Post, where: p.id == ^post1.id)
assert post2 == TestRepo.one(from p in Post, where: p.id == ^to_string post2.id) # With casting
assert nil == TestRepo.one(from p in Post, where: is_nil(p.id))
assert post1 == TestRepo.first(Post)
assert post2 == TestRepo.last(Post)

assert post1 == TestRepo.one!(from p in Post, where: p.id == ^post1.id)
assert post2 == TestRepo.one!(from p in Post, where: p.id == ^to_string post2.id) # With casting
query = from p in Post, order_by: p.title
assert post1 == TestRepo.first(query)
assert post2 == TestRepo.last(query)

assert_raise Ecto.NoResultsError, fn ->
TestRepo.one!(from p in Post, where: is_nil(p.id))
end
end

test "one(!) with multiple results" do
assert %Post{} = TestRepo.insert!(%Post{title: "hai"})
assert %Post{} = TestRepo.insert!(%Post{title: "hai"})
query = from p in Post, order_by: [desc: p.title], limit: 10
assert post2 == TestRepo.first(query)
assert post1 == TestRepo.last(query)

assert_raise Ecto.MultipleResultsError, fn ->
TestRepo.one(from p in Post, where: p.title == "hai")
end

assert_raise Ecto.MultipleResultsError, fn ->
TestRepo.one!(from p in Post, where: p.title == "hai")
end
query = from p in Post, where: is_nil(p.id)
refute TestRepo.first(query)
refute TestRepo.last(query)
assert_raise Ecto.NoResultsError, fn -> TestRepo.first!(query) end
assert_raise Ecto.NoResultsError, fn -> TestRepo.last!(query) end
end

test "insert all" do
@@ -266,9 +266,9 @@ defmodule Ecto do
end
Besides `Repo.all/1` which returns all entries, repositories also
provide `Repo.one/1` which returns one entry or nil, `Repo.one!/1`
which returns one entry or raises and `Repo.get/2` which fetches
entries for a particular ID.
provide `Repo.first/1` which returns one entry or nil, `Repo.first!/1`
which returns one entry or raises, `Repo.get/2` which fetches
entries for a particular ID and more.
Finally, if you need a escape hatch, Ecto provides fragments
(see `Ecto.Query.API.fragment/1`) to inject SQL (and non-SQL)
@@ -148,13 +148,33 @@ defmodule Ecto.Repo do
end

def one(queryable, opts \\ []) do
IO.puts :stderr, "warning: #{inspect __MODULE__}.one/2 is deprecated, " <>
"please use all/2 or first/2 accordingly\n" <> Exception.format_stacktrace
Ecto.Repo.Queryable.one(__MODULE__, @adapter, queryable, opts)
end

def one!(queryable, opts \\ []) do
IO.puts :stderr, "warning: #{inspect __MODULE__}.one!/2 is deprecated, " <>
"please use all/2 or first!/2 accordingly\n" <> Exception.format_stacktrace
Ecto.Repo.Queryable.one!(__MODULE__, @adapter, queryable, opts)
end

def first(queryable, opts \\ []) do
Ecto.Repo.Queryable.first(__MODULE__, @adapter, queryable, opts)
end

def first!(queryable, opts \\ []) do
Ecto.Repo.Queryable.first!(__MODULE__, @adapter, queryable, opts)
end

def last(queryable, opts \\ []) do
Ecto.Repo.Queryable.last(__MODULE__, @adapter, queryable, opts)
end

def last!(queryable, opts \\ []) do
Ecto.Repo.Queryable.last!(__MODULE__, @adapter, queryable, opts)
end

def insert_all(schema_or_source, entries, opts \\ []) do
Ecto.Repo.Schema.insert_all(__MODULE__, @adapter, schema_or_source, entries, opts)
end
@@ -292,26 +312,52 @@ defmodule Ecto.Repo do
@callback get_by!(Ecto.Queryable.t, Keyword.t | Map.t, Keyword.t) :: Ecto.Schema.t | nil | no_return

@doc """
Fetches a single result from the query.
Fetches the first result from the query.
Returns `nil` if no result was found. Raises if more than one entry.
The query will be automatically ordered by the primary key
if there is no order_by statement. Limit is always set to 1.
Returns `nil` if no result was found.
## Options
See the "Shared options" section at the module documentation.
"""
@callback one(Ecto.Queryable.t, Keyword.t) :: Ecto.Schema.t | nil | no_return
@callback first(Ecto.Queryable.t, Keyword.t) :: Ecto.Schema.t | nil

@doc """
Similar to `one/2` but raises `Ecto.NoResultsError` if no record was found.
Similar to `first/2` but raises `Ecto.NoResultsError` if no record was found.
Raises if more than one entry.
## Options
See the "Shared options" section at the module documentation.
"""
@callback first!(Ecto.Queryable.t, Keyword.t) :: Ecto.Schema.t | nil | no_return

@doc """
Fetches the last result from the query.
The query ordering will be automatically reversed, with ASC
columns becoming DESC columns (and vice-versa) and limit is set
to 1. If there is no ordering, it is ordered decreasingly by
primary key.
Returns `nil` if no result was found.
## Options
See the "Shared options" section at the module documentation.
"""
@callback last(Ecto.Queryable.t, Keyword.t) :: Ecto.Schema.t | nil

@doc """
Similar to `last/2` but raises `Ecto.NoResultsError` if no record was found.
## Options
See the "Shared options" section at the module documentation.
"""
@callback one!(Ecto.Queryable.t, Keyword.t) :: Ecto.Schema.t | nil | no_return
@callback last!(Ecto.Queryable.t, Keyword.t) :: Ecto.Schema.t | nil | no_return

@doc """
Preloads all associations on the given struct or structs.
@@ -8,23 +8,14 @@ defmodule Ecto.Repo.Queryable do

require Ecto.Query

@doc """
Implementation for `Ecto.Repo.all/2`
"""
def all(repo, adapter, queryable, opts) when is_list(opts) do
execute(:all, repo, adapter, queryable, opts) |> elem(1)
end

@doc """
Implementation for `Ecto.Repo.get/3`
"""
def get(repo, adapter, queryable, id, opts) do
one(repo, adapter, query_for_get(repo, queryable, id), opts)
end

@doc """
Implementation for `Ecto.Repo.get!/3`
"""
def get!(repo, adapter, queryable, id, opts) do
one!(repo, adapter, query_for_get(repo, queryable, id), opts)
end
@@ -37,35 +28,38 @@ defmodule Ecto.Repo.Queryable do
one!(repo, adapter, query_for_get_by(repo, queryable, clauses), opts)
end

@doc """
Implementation for `Ecto.Repo.one/2`
"""
def first(repo, adapter, queryable, opts) do
one(repo, adapter, query_for_first(repo, queryable), opts)
end

def first!(repo, adapter, queryable, opts) do
one!(repo, adapter, query_for_first(repo, queryable), opts)
end

def last(repo, adapter, queryable, opts) do
one(repo, adapter, query_for_last(repo, queryable), opts)
end

def last!(repo, adapter, queryable, opts) do
one!(repo, adapter, query_for_last(repo, queryable), opts)
end

def one(repo, adapter, queryable, opts) do
IO.puts :stderr, "warning: #{inspect repo}.one/2 is deprecated, " <>
"please use all/2 or first/2 accordingly\n" <> Exception.format_stacktrace
case all(repo, adapter, queryable, opts) do
[one] -> one
[] -> nil
other -> raise Ecto.MultipleResultsError, queryable: queryable, count: length(other)
end
end

@doc """
Implementation for `Ecto.Repo.one!/2`
"""
def one!(repo, adapter, queryable, opts) do
IO.puts :stderr, "warning: #{inspect repo}.one/2 is deprecated, " <>
"please use all/2 or first!/2 accordingly\n" <> Exception.format_stacktrace
case all(repo, adapter, queryable, opts) do
[one] -> one
[] -> raise Ecto.NoResultsError, queryable: queryable
other -> raise Ecto.MultipleResultsError, queryable: queryable, count: length(other)
end
end

@doc """
Runtime callback for `Ecto.Repo.update_all/3`
"""
def update_all(repo, adapter, queryable, [], opts) when is_list(opts) do
update_all(repo, adapter, queryable, opts)
end
@@ -79,9 +73,6 @@ defmodule Ecto.Repo.Queryable do
execute(:update_all, repo, adapter, queryable, opts)
end

@doc """
Implementation for `Ecto.Repo.delete_all/2`
"""
def delete_all(repo, adapter, queryable, opts) when is_list(opts) do
execute(:delete_all, repo, adapter, queryable, opts)
end
@@ -209,29 +200,63 @@ defmodule Ecto.Repo.Queryable do

defp query_for_get(repo, queryable, id) do
query = Queryable.to_query(queryable)
Ecto.Query.from(x in query, where: field(x, ^assert_pk!(repo, query)) == ^id)
end

defp query_for_get_by(_repo, queryable, clauses) do
Ecto.Query.where(queryable, [], ^Enum.to_list(clauses))
end

defp query_for_first(repo, queryable) do
query = %{Queryable.to_query(queryable) | limit: limit()}
case query do
%{order_bys: []} ->
%{query | order_bys: [order_by_pk(repo, query, :asc)]}
%{} ->
query
end
end

defp query_for_last(repo, queryable) do
query = %{Queryable.to_query(queryable) | limit: limit()}
update_in query.order_bys, fn
[] ->
[order_by_pk(repo, query, :desc)]
order_bys ->
for %{expr: expr} = order_by <- order_bys do
%{order_by | expr:
Enum.map(expr, fn
{:desc, ast} -> {:asc, ast}
{:asc, ast} -> {:desc, ast}
end)}
end
end
end

defp limit do
%Ecto.Query.QueryExpr{expr: 1, params: [], file: __ENV__.file, line: __ENV__.line}
end

defp order_by_pk(repo, query, dir) do
%Ecto.Query.QueryExpr{expr: [{dir, {{:., [], [{:&, [], [0]}, assert_pk!(repo, query)]}, [], []}}],
params: [], file: __ENV__.file, line: __ENV__.line}
end

defp assert_pk!(repo, query) do
schema = assert_schema!(query)
case schema.__schema__(:primary_key) do
[pk] ->
Ecto.Query.from(x in query, where: field(x, ^pk) == ^id)
[pk] -> pk
pks ->
raise ArgumentError,
"#{inspect repo}.get/2 requires the schema #{inspect schema} " <>
"to have exactly one primary key, got: #{inspect pks}"
end
end

defp query_for_get_by(_repo, queryable, clauses) do
Ecto.Query.where(queryable, [], ^Enum.to_list(clauses))
end

defp assert_schema!(%{from: {_source, schema}}) when schema != nil, do: schema
defp assert_schema!(query) do
case query.from do
{_source, schema} when schema != nil ->
schema
_ ->
raise Ecto.QueryError,
query: query,
message: "expected a from expression with a schema"
end
raise Ecto.QueryError,
query: query,
message: "expected a from expression with a schema"
end
end
@@ -171,7 +171,7 @@ defmodule Ecto.Schema do
# Now in your code
%User{data: %{"foo" => "bar"}} |> Repo.insert!
%User{data: %{"foo" => value}} = Repo.one(User)
%User{data: %{"foo" => value}} = Repo.first(User)
value #=> "bar"
Keep in mind that we advise the map keys to be strings or integers

0 comments on commit c3ba229

Please sign in to comment.
You can’t perform that action at this time.