2254 lines
82 KiB
Elixir
2254 lines
82 KiB
Elixir
|
defmodule Ecto.Integration.RepoTest do
|
||
|
use Ecto.Integration.Case, async: Application.compile_env(:ecto, :async_integration_tests, true)
|
||
|
|
||
|
alias Ecto.Integration.TestRepo
|
||
|
import Ecto.Query
|
||
|
|
||
|
alias Ecto.Integration.Post
|
||
|
alias Ecto.Integration.Order
|
||
|
alias Ecto.Integration.User
|
||
|
alias Ecto.Integration.Comment
|
||
|
alias Ecto.Integration.Permalink
|
||
|
alias Ecto.Integration.Custom
|
||
|
alias Ecto.Integration.Barebone
|
||
|
alias Ecto.Integration.CompositePk
|
||
|
alias Ecto.Integration.PostUserCompositePk
|
||
|
|
||
|
test "returns already started for started repos" do
|
||
|
assert {:error, {:already_started, _}} = TestRepo.start_link()
|
||
|
end
|
||
|
|
||
|
test "supports unnamed repos" do
|
||
|
assert {:ok, pid} = TestRepo.start_link(name: nil)
|
||
|
assert Ecto.Repo.Queryable.all(pid, Post, Ecto.Repo.Supervisor.tuplet(pid, [])) == []
|
||
|
end
|
||
|
|
||
|
test "all empty" do
|
||
|
assert TestRepo.all(Post) == []
|
||
|
assert TestRepo.all(from p in Post) == []
|
||
|
end
|
||
|
|
||
|
test "all with in" do
|
||
|
TestRepo.insert!(%Post{title: "hello"})
|
||
|
|
||
|
# Works without the query cache.
|
||
|
assert_raise Ecto.Query.CastError, fn ->
|
||
|
TestRepo.all(from p in Post, where: p.title in ^nil)
|
||
|
end
|
||
|
|
||
|
assert [] = TestRepo.all from p in Post, where: p.title in []
|
||
|
assert [] = TestRepo.all from p in Post, where: p.title in ["1", "2", "3"]
|
||
|
assert [] = TestRepo.all from p in Post, where: p.title in ^[]
|
||
|
|
||
|
assert [_] = TestRepo.all from p in Post, where: p.title not in []
|
||
|
assert [_] = TestRepo.all from p in Post, where: p.title in ["1", "hello", "3"]
|
||
|
assert [_] = TestRepo.all from p in Post, where: p.title in ["1", ^"hello", "3"]
|
||
|
assert [_] = TestRepo.all from p in Post, where: p.title in ^["1", "hello", "3"]
|
||
|
|
||
|
# Still doesn't work after the query cache.
|
||
|
assert_raise Ecto.Query.CastError, fn ->
|
||
|
TestRepo.all(from p in Post, where: p.title in ^nil)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
test "all using named from" do
|
||
|
TestRepo.insert!(%Post{title: "hello"})
|
||
|
|
||
|
query =
|
||
|
from(p in Post, as: :post)
|
||
|
|> where([post: p], p.title == "hello")
|
||
|
|
||
|
assert [_] = TestRepo.all query
|
||
|
end
|
||
|
|
||
|
test "all without schema" do
|
||
|
%Post{} = TestRepo.insert!(%Post{title: "title1"})
|
||
|
%Post{} = TestRepo.insert!(%Post{title: "title2"})
|
||
|
|
||
|
assert ["title1", "title2"] =
|
||
|
TestRepo.all(from(p in "posts", order_by: p.title, select: p.title))
|
||
|
|
||
|
assert [_] =
|
||
|
TestRepo.all(from(p in "posts", where: p.title == "title1", select: p.id))
|
||
|
end
|
||
|
|
||
|
test "all shares metadata" do
|
||
|
TestRepo.insert!(%Post{title: "title1"})
|
||
|
TestRepo.insert!(%Post{title: "title2"})
|
||
|
|
||
|
[post1, post2] = TestRepo.all(Post)
|
||
|
assert :erts_debug.same(post1.__meta__, post2.__meta__)
|
||
|
|
||
|
[new_post1, new_post2] = TestRepo.all(Post)
|
||
|
assert :erts_debug.same(post1.__meta__, new_post1.__meta__)
|
||
|
assert :erts_debug.same(post2.__meta__, new_post2.__meta__)
|
||
|
end
|
||
|
|
||
|
@tag :invalid_prefix
|
||
|
test "all with invalid prefix" do
|
||
|
assert catch_error(TestRepo.all("posts", prefix: "oops"))
|
||
|
end
|
||
|
|
||
|
test "insert, update and delete" do
|
||
|
post = %Post{title: "insert, update, delete", visits: 1}
|
||
|
meta = post.__meta__
|
||
|
|
||
|
assert %Post{} = inserted = TestRepo.insert!(post)
|
||
|
assert %Post{} = updated = TestRepo.update!(Ecto.Changeset.change(inserted, visits: 2))
|
||
|
|
||
|
deleted_meta = put_in meta.state, :deleted
|
||
|
assert %Post{__meta__: ^deleted_meta} = TestRepo.delete!(updated)
|
||
|
|
||
|
loaded_meta = put_in meta.state, :loaded
|
||
|
assert %Post{__meta__: ^loaded_meta} = TestRepo.insert!(post)
|
||
|
|
||
|
post = TestRepo.one(Post)
|
||
|
assert post.__meta__.state == :loaded
|
||
|
assert post.inserted_at
|
||
|
end
|
||
|
|
||
|
test "insert, update and delete with field source" do
|
||
|
permalink = %Permalink{url: "url"}
|
||
|
assert %Permalink{url: "url"} = inserted =
|
||
|
TestRepo.insert!(permalink)
|
||
|
assert %Permalink{url: "new"} = updated =
|
||
|
TestRepo.update!(Ecto.Changeset.change(inserted, url: "new"))
|
||
|
assert %Permalink{url: "new"} =
|
||
|
TestRepo.delete!(updated)
|
||
|
end
|
||
|
|
||
|
@tag :composite_pk
|
||
|
test "insert, update and delete with composite pk" do
|
||
|
c1 = TestRepo.insert!(%CompositePk{a: 1, b: 2, name: "first"})
|
||
|
c2 = TestRepo.insert!(%CompositePk{a: 1, b: 3, name: "second"})
|
||
|
|
||
|
assert CompositePk |> first |> TestRepo.one == c1
|
||
|
assert CompositePk |> last |> TestRepo.one == c2
|
||
|
|
||
|
changeset = Ecto.Changeset.cast(c1, %{name: "first change"}, ~w(name)a)
|
||
|
c1 = TestRepo.update!(changeset)
|
||
|
assert TestRepo.get_by!(CompositePk, %{a: 1, b: 2}) == c1
|
||
|
|
||
|
TestRepo.delete!(c2)
|
||
|
assert TestRepo.all(CompositePk) == [c1]
|
||
|
|
||
|
assert_raise ArgumentError, ~r"to have exactly one primary key", fn ->
|
||
|
TestRepo.get(CompositePk, [])
|
||
|
end
|
||
|
|
||
|
assert_raise ArgumentError, ~r"to have exactly one primary key", fn ->
|
||
|
TestRepo.get!(CompositePk, [1, 2])
|
||
|
end
|
||
|
end
|
||
|
|
||
|
@tag :composite_pk
|
||
|
test "insert, update and delete with associated composite pk" do
|
||
|
user = TestRepo.insert!(%User{})
|
||
|
post = TestRepo.insert!(%Post{title: "post title"})
|
||
|
|
||
|
user_post = TestRepo.insert!(%PostUserCompositePk{user_id: user.id, post_id: post.id})
|
||
|
assert TestRepo.get_by!(PostUserCompositePk, [user_id: user.id, post_id: post.id]) == user_post
|
||
|
TestRepo.delete!(user_post)
|
||
|
assert TestRepo.all(PostUserCompositePk) == []
|
||
|
end
|
||
|
|
||
|
@tag :invalid_prefix
|
||
|
test "insert, update and delete with invalid prefix" do
|
||
|
post = TestRepo.insert!(%Post{})
|
||
|
changeset = Ecto.Changeset.change(post, title: "foo")
|
||
|
assert catch_error(TestRepo.insert(%Post{}, prefix: "oops"))
|
||
|
assert catch_error(TestRepo.update(changeset, prefix: "oops"))
|
||
|
assert catch_error(TestRepo.delete(changeset, prefix: "oops"))
|
||
|
|
||
|
# Check we can still insert the post after the invalid prefix attempt
|
||
|
assert %Post{id: _} = TestRepo.insert!(%Post{})
|
||
|
end
|
||
|
|
||
|
test "insert and update with changeset" do
|
||
|
# On insert we merge the fields and changes
|
||
|
changeset = Ecto.Changeset.cast(%Post{visits: 13, title: "wrong"},
|
||
|
%{"title" => "hello", "temp" => "unknown"}, ~w(title temp)a)
|
||
|
|
||
|
post = TestRepo.insert!(changeset)
|
||
|
assert %Post{visits: 13, title: "hello", temp: "unknown"} = post
|
||
|
assert %Post{visits: 13, title: "hello", temp: "temp"} = TestRepo.get!(Post, post.id)
|
||
|
|
||
|
# On update we merge only fields, direct schema changes are discarded
|
||
|
changeset = Ecto.Changeset.cast(%{post | visits: 17},
|
||
|
%{"title" => "world", "temp" => "unknown"}, ~w(title temp)a)
|
||
|
|
||
|
assert %Post{visits: 17, title: "world", temp: "unknown"} = TestRepo.update!(changeset)
|
||
|
assert %Post{visits: 13, title: "world", temp: "temp"} = TestRepo.get!(Post, post.id)
|
||
|
end
|
||
|
|
||
|
test "insert and update with empty changeset" do
|
||
|
# On insert we merge the fields and changes
|
||
|
changeset = Ecto.Changeset.cast(%Permalink{}, %{}, ~w())
|
||
|
assert %Permalink{} = permalink = TestRepo.insert!(changeset)
|
||
|
|
||
|
# Assert we can update the same value twice,
|
||
|
# without changes, without triggering stale errors.
|
||
|
changeset = Ecto.Changeset.cast(permalink, %{}, ~w())
|
||
|
assert TestRepo.update!(changeset) == permalink
|
||
|
assert TestRepo.update!(changeset) == permalink
|
||
|
end
|
||
|
|
||
|
@tag :no_primary_key
|
||
|
test "insert with no primary key" do
|
||
|
assert %Barebone{num: nil} = TestRepo.insert!(%Barebone{})
|
||
|
assert %Barebone{num: 13} = TestRepo.insert!(%Barebone{num: 13})
|
||
|
end
|
||
|
|
||
|
@tag :read_after_writes
|
||
|
test "insert and update with changeset read after writes" do
|
||
|
defmodule RAW do
|
||
|
use Ecto.Schema
|
||
|
|
||
|
schema "comments" do
|
||
|
field :text, :string
|
||
|
field :lock_version, :integer, read_after_writes: true
|
||
|
end
|
||
|
end
|
||
|
|
||
|
changeset = Ecto.Changeset.cast(struct(RAW, %{}), %{}, ~w())
|
||
|
|
||
|
# If the field is nil, we will not send it
|
||
|
# and read the value back from the database.
|
||
|
assert %{id: cid, lock_version: 1} = raw = TestRepo.insert!(changeset)
|
||
|
|
||
|
# Set the counter to 11, so we can read it soon
|
||
|
TestRepo.update_all from(u in RAW, where: u.id == ^cid), set: [lock_version: 11]
|
||
|
|
||
|
# We will read back on update too
|
||
|
changeset = Ecto.Changeset.cast(raw, %{"text" => "0"}, ~w(text)a)
|
||
|
assert %{id: ^cid, lock_version: 11, text: "0"} = TestRepo.update!(changeset)
|
||
|
end
|
||
|
|
||
|
test "insert autogenerates for custom type" do
|
||
|
post = TestRepo.insert!(%Post{uuid: nil})
|
||
|
assert byte_size(post.uuid) == 36
|
||
|
assert TestRepo.get_by(Post, uuid: post.uuid) == post
|
||
|
end
|
||
|
|
||
|
@tag :id_type
|
||
|
test "insert autogenerates for custom id type" do
|
||
|
defmodule ID do
|
||
|
use Ecto.Schema
|
||
|
|
||
|
@primary_key {:id, CustomPermalink, autogenerate: true}
|
||
|
schema "posts" do
|
||
|
end
|
||
|
end
|
||
|
|
||
|
id = TestRepo.insert!(struct(ID, id: nil))
|
||
|
assert id.id
|
||
|
assert TestRepo.get_by(ID, id: "#{id.id}-hello") == id
|
||
|
end
|
||
|
|
||
|
@tag :id_type
|
||
|
@tag :assigns_id_type
|
||
|
test "insert with user-assigned primary key" do
|
||
|
assert %Post{id: 1} = TestRepo.insert!(%Post{id: 1})
|
||
|
end
|
||
|
|
||
|
@tag :id_type
|
||
|
@tag :assigns_id_type
|
||
|
test "insert and update with user-assigned primary key in changeset" do
|
||
|
changeset = Ecto.Changeset.cast(%Post{id: 11}, %{"id" => "13"}, ~w(id)a)
|
||
|
assert %Post{id: 13} = post = TestRepo.insert!(changeset)
|
||
|
|
||
|
changeset = Ecto.Changeset.cast(post, %{"id" => "15"}, ~w(id)a)
|
||
|
assert %Post{id: 15} = TestRepo.update!(changeset)
|
||
|
end
|
||
|
|
||
|
test "insert and fetch a schema with utc timestamps" do
|
||
|
datetime = DateTime.from_unix!(System.os_time(:second), :second)
|
||
|
TestRepo.insert!(%User{inserted_at: datetime})
|
||
|
assert [%{inserted_at: ^datetime}] = TestRepo.all(User)
|
||
|
end
|
||
|
|
||
|
test "optimistic locking in update/delete operations" do
|
||
|
import Ecto.Changeset, only: [cast: 3, optimistic_lock: 2]
|
||
|
base_comment = TestRepo.insert!(%Comment{})
|
||
|
|
||
|
changeset_ok =
|
||
|
base_comment
|
||
|
|> cast(%{"text" => "foo.bar"}, ~w(text)a)
|
||
|
|> optimistic_lock(:lock_version)
|
||
|
TestRepo.update!(changeset_ok)
|
||
|
|
||
|
changeset_stale =
|
||
|
base_comment
|
||
|
|> cast(%{"text" => "foo.bat"}, ~w(text)a)
|
||
|
|> optimistic_lock(:lock_version)
|
||
|
|
||
|
assert_raise Ecto.StaleEntryError, fn -> TestRepo.update!(changeset_stale) end
|
||
|
assert_raise Ecto.StaleEntryError, fn -> TestRepo.delete!(changeset_stale) end
|
||
|
end
|
||
|
|
||
|
test "optimistic locking in update operation with nil field" do
|
||
|
import Ecto.Changeset, only: [cast: 3, optimistic_lock: 3]
|
||
|
|
||
|
base_comment =
|
||
|
%Comment{}
|
||
|
|> cast(%{lock_version: nil}, [:lock_version])
|
||
|
|> TestRepo.insert!()
|
||
|
|
||
|
incrementer =
|
||
|
fn
|
||
|
nil -> 1
|
||
|
old_value -> old_value + 1
|
||
|
end
|
||
|
|
||
|
changeset_ok =
|
||
|
base_comment
|
||
|
|> cast(%{"text" => "foo.bar"}, ~w(text)a)
|
||
|
|> optimistic_lock(:lock_version, incrementer)
|
||
|
|
||
|
updated = TestRepo.update!(changeset_ok)
|
||
|
assert updated.text == "foo.bar"
|
||
|
assert updated.lock_version == 1
|
||
|
end
|
||
|
|
||
|
test "optimistic locking in delete operation with nil field" do
|
||
|
import Ecto.Changeset, only: [cast: 3, optimistic_lock: 3]
|
||
|
|
||
|
base_comment =
|
||
|
%Comment{}
|
||
|
|> cast(%{lock_version: nil}, [:lock_version])
|
||
|
|> TestRepo.insert!()
|
||
|
|
||
|
incrementer =
|
||
|
fn
|
||
|
nil -> 1
|
||
|
old_value -> old_value + 1
|
||
|
end
|
||
|
|
||
|
changeset_ok = optimistic_lock(base_comment, :lock_version, incrementer)
|
||
|
TestRepo.delete!(changeset_ok)
|
||
|
|
||
|
refute TestRepo.get(Comment, base_comment.id)
|
||
|
end
|
||
|
|
||
|
@tag :unique_constraint
|
||
|
test "unique constraint" do
|
||
|
changeset = Ecto.Changeset.change(%Post{}, uuid: Ecto.UUID.generate())
|
||
|
{:ok, _} = TestRepo.insert(changeset)
|
||
|
|
||
|
exception =
|
||
|
assert_raise Ecto.ConstraintError, ~r/constraint error when attempting to insert struct/, fn ->
|
||
|
changeset
|
||
|
|> TestRepo.insert()
|
||
|
end
|
||
|
|
||
|
assert exception.message =~ "\"posts_uuid_index\" (unique_constraint)"
|
||
|
assert exception.message =~ "The changeset has not defined any constraint."
|
||
|
assert exception.message =~ "call `unique_constraint/3`"
|
||
|
|
||
|
message = ~r/constraint error when attempting to insert struct/
|
||
|
exception =
|
||
|
assert_raise Ecto.ConstraintError, message, fn ->
|
||
|
changeset
|
||
|
|> Ecto.Changeset.unique_constraint(:uuid, name: :posts_email_changeset)
|
||
|
|> TestRepo.insert()
|
||
|
end
|
||
|
|
||
|
assert exception.message =~ "\"posts_email_changeset\" (unique_constraint)"
|
||
|
|
||
|
{:error, changeset} =
|
||
|
changeset
|
||
|
|> Ecto.Changeset.unique_constraint(:uuid)
|
||
|
|> TestRepo.insert()
|
||
|
assert changeset.errors == [uuid: {"has already been taken", [constraint: :unique, constraint_name: "posts_uuid_index"]}]
|
||
|
assert changeset.data.__meta__.state == :built
|
||
|
end
|
||
|
|
||
|
@tag :unique_constraint
|
||
|
test "unique constraint from association" do
|
||
|
uuid = Ecto.UUID.generate()
|
||
|
post = & %Post{} |> Ecto.Changeset.change(uuid: &1) |> Ecto.Changeset.unique_constraint(:uuid)
|
||
|
|
||
|
{:error, changeset} =
|
||
|
TestRepo.insert %User{
|
||
|
comments: [%Comment{}],
|
||
|
permalink: %Permalink{},
|
||
|
posts: [post.(uuid), post.(uuid), post.(Ecto.UUID.generate())]
|
||
|
}
|
||
|
|
||
|
[_, p2, _] = changeset.changes.posts
|
||
|
assert p2.errors == [uuid: {"has already been taken", [constraint: :unique, constraint_name: "posts_uuid_index"]}]
|
||
|
end
|
||
|
|
||
|
@tag :id_type
|
||
|
@tag :unique_constraint
|
||
|
test "unique constraint with binary_id" do
|
||
|
changeset = Ecto.Changeset.change(%Custom{}, uuid: Ecto.UUID.generate())
|
||
|
{:ok, _} = TestRepo.insert(changeset)
|
||
|
|
||
|
{:error, changeset} =
|
||
|
changeset
|
||
|
|> Ecto.Changeset.unique_constraint(:uuid)
|
||
|
|> TestRepo.insert()
|
||
|
assert changeset.errors == [uuid: {"has already been taken", [constraint: :unique, constraint_name: "customs_uuid_index"]}]
|
||
|
assert changeset.data.__meta__.state == :built
|
||
|
end
|
||
|
|
||
|
test "unique pseudo-constraint violation error message with join table at the repository" do
|
||
|
post =
|
||
|
TestRepo.insert!(%Post{title: "some post"})
|
||
|
|> TestRepo.preload(:unique_users)
|
||
|
|
||
|
user =
|
||
|
TestRepo.insert!(%User{name: "some user"})
|
||
|
|
||
|
# Violate the unique composite index
|
||
|
{:error, changeset} =
|
||
|
post
|
||
|
|> Ecto.Changeset.change
|
||
|
|> Ecto.Changeset.put_assoc(:unique_users, [user, user])
|
||
|
|> TestRepo.update
|
||
|
|
||
|
errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|
||
|
assert errors == %{unique_users: [%{}, %{id: ["has already been taken"]}]}
|
||
|
refute changeset.valid?
|
||
|
end
|
||
|
|
||
|
@tag :join
|
||
|
@tag :unique_constraint
|
||
|
test "unique constraint violation error message with join table in single changeset" do
|
||
|
post =
|
||
|
TestRepo.insert!(%Post{title: "some post"})
|
||
|
|> TestRepo.preload(:constraint_users)
|
||
|
|
||
|
user =
|
||
|
TestRepo.insert!(%User{name: "some user"})
|
||
|
|
||
|
# Violate the unique composite index
|
||
|
{:error, changeset} =
|
||
|
post
|
||
|
|> Ecto.Changeset.change
|
||
|
|> Ecto.Changeset.put_assoc(:constraint_users, [user, user])
|
||
|
|> Ecto.Changeset.unique_constraint(:user,
|
||
|
name: :posts_users_composite_pk_post_id_user_id_index,
|
||
|
message: "has already been assigned")
|
||
|
|> TestRepo.update
|
||
|
|
||
|
errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|
||
|
assert errors == %{constraint_users: [%{}, %{user: ["has already been assigned"]}]}
|
||
|
|
||
|
refute changeset.valid?
|
||
|
end
|
||
|
|
||
|
@tag :join
|
||
|
@tag :unique_constraint
|
||
|
test "unique constraint violation error message with join table and separate changesets" do
|
||
|
post =
|
||
|
TestRepo.insert!(%Post{title: "some post"})
|
||
|
|> TestRepo.preload(:constraint_users)
|
||
|
|
||
|
user = TestRepo.insert!(%User{name: "some user"})
|
||
|
|
||
|
post
|
||
|
|> Ecto.Changeset.change
|
||
|
|> Ecto.Changeset.put_assoc(:constraint_users, [user])
|
||
|
|> TestRepo.update
|
||
|
|
||
|
# Violate the unique composite index
|
||
|
{:error, changeset} =
|
||
|
post
|
||
|
|> Ecto.Changeset.change
|
||
|
|> Ecto.Changeset.put_assoc(:constraint_users, [user])
|
||
|
|> Ecto.Changeset.unique_constraint(:user,
|
||
|
name: :posts_users_composite_pk_post_id_user_id_index,
|
||
|
message: "has already been assigned")
|
||
|
|> TestRepo.update
|
||
|
|
||
|
errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|
||
|
assert errors == %{constraint_users: [%{user: ["has already been assigned"]}]}
|
||
|
|
||
|
refute changeset.valid?
|
||
|
end
|
||
|
|
||
|
@tag :foreign_key_constraint
|
||
|
test "foreign key constraint" do
|
||
|
changeset = Ecto.Changeset.change(%Comment{post_id: 0})
|
||
|
|
||
|
exception =
|
||
|
assert_raise Ecto.ConstraintError, ~r/constraint error when attempting to insert struct/, fn ->
|
||
|
changeset
|
||
|
|> TestRepo.insert()
|
||
|
end
|
||
|
|
||
|
assert exception.message =~ "\"comments_post_id_fkey\" (foreign_key_constraint)"
|
||
|
assert exception.message =~ "The changeset has not defined any constraint."
|
||
|
assert exception.message =~ "call `foreign_key_constraint/3`"
|
||
|
|
||
|
message = ~r/constraint error when attempting to insert struct/
|
||
|
exception =
|
||
|
assert_raise Ecto.ConstraintError, message, fn ->
|
||
|
changeset
|
||
|
|> Ecto.Changeset.foreign_key_constraint(:post_id, name: :comments_post_id_other)
|
||
|
|> TestRepo.insert()
|
||
|
end
|
||
|
|
||
|
assert exception.message =~ "\"comments_post_id_other\" (foreign_key_constraint)"
|
||
|
|
||
|
{:error, changeset} =
|
||
|
changeset
|
||
|
|> Ecto.Changeset.foreign_key_constraint(:post_id)
|
||
|
|> TestRepo.insert()
|
||
|
assert changeset.errors == [post_id: {"does not exist", [constraint: :foreign, constraint_name: "comments_post_id_fkey"]}]
|
||
|
end
|
||
|
|
||
|
@tag :foreign_key_constraint
|
||
|
test "assoc constraint" do
|
||
|
changeset = Ecto.Changeset.change(%Comment{post_id: 0})
|
||
|
|
||
|
exception =
|
||
|
assert_raise Ecto.ConstraintError, ~r/constraint error when attempting to insert struct/, fn ->
|
||
|
changeset
|
||
|
|> TestRepo.insert()
|
||
|
end
|
||
|
|
||
|
assert exception.message =~ "\"comments_post_id_fkey\" (foreign_key_constraint)"
|
||
|
assert exception.message =~ "The changeset has not defined any constraint."
|
||
|
|
||
|
message = ~r/constraint error when attempting to insert struct/
|
||
|
exception =
|
||
|
assert_raise Ecto.ConstraintError, message, fn ->
|
||
|
changeset
|
||
|
|> Ecto.Changeset.assoc_constraint(:post, name: :comments_post_id_other)
|
||
|
|> TestRepo.insert()
|
||
|
end
|
||
|
|
||
|
assert exception.message =~ "\"comments_post_id_other\" (foreign_key_constraint)"
|
||
|
|
||
|
{:error, changeset} =
|
||
|
changeset
|
||
|
|> Ecto.Changeset.assoc_constraint(:post)
|
||
|
|> TestRepo.insert()
|
||
|
assert changeset.errors == [post: {"does not exist", [constraint: :assoc, constraint_name: "comments_post_id_fkey"]}]
|
||
|
end
|
||
|
|
||
|
@tag :foreign_key_constraint
|
||
|
test "no assoc constraint error" do
|
||
|
user = TestRepo.insert!(%User{})
|
||
|
TestRepo.insert!(%Permalink{user_id: user.id})
|
||
|
|
||
|
exception =
|
||
|
assert_raise Ecto.ConstraintError, ~r/constraint error when attempting to delete struct/, fn ->
|
||
|
TestRepo.delete!(user)
|
||
|
end
|
||
|
|
||
|
assert exception.message =~ "\"permalinks_user_id_fkey\" (foreign_key_constraint)"
|
||
|
assert exception.message =~ "The changeset has not defined any constraint."
|
||
|
end
|
||
|
|
||
|
@tag :foreign_key_constraint
|
||
|
test "no assoc constraint with changeset mismatch" do
|
||
|
user = TestRepo.insert!(%User{})
|
||
|
TestRepo.insert!(%Permalink{user_id: user.id})
|
||
|
|
||
|
message = ~r/constraint error when attempting to delete struct/
|
||
|
exception =
|
||
|
assert_raise Ecto.ConstraintError, message, fn ->
|
||
|
user
|
||
|
|> Ecto.Changeset.change
|
||
|
|> Ecto.Changeset.no_assoc_constraint(:permalink, name: :permalinks_user_id_pther)
|
||
|
|> TestRepo.delete()
|
||
|
end
|
||
|
|
||
|
assert exception.message =~ "\"permalinks_user_id_pther\" (foreign_key_constraint)"
|
||
|
end
|
||
|
|
||
|
@tag :foreign_key_constraint
|
||
|
test "no assoc constraint with changeset match" do
|
||
|
user = TestRepo.insert!(%User{})
|
||
|
TestRepo.insert!(%Permalink{user_id: user.id})
|
||
|
|
||
|
{:error, changeset} =
|
||
|
user
|
||
|
|> Ecto.Changeset.change
|
||
|
|> Ecto.Changeset.no_assoc_constraint(:permalink)
|
||
|
|> TestRepo.delete()
|
||
|
assert changeset.errors == [permalink: {"is still associated with this entry", [constraint: :no_assoc, constraint_name: "permalinks_user_id_fkey"]}]
|
||
|
end
|
||
|
|
||
|
@tag :foreign_key_constraint
|
||
|
test "insert and update with embeds during failing child foreign key" do
|
||
|
changeset =
|
||
|
Order
|
||
|
|> struct(%{})
|
||
|
|> order_changeset(%{item: %{price: 10}, permalink: %{post_id: 0}})
|
||
|
|
||
|
{:error, changeset} = TestRepo.insert(changeset)
|
||
|
assert %Ecto.Changeset{} = changeset.changes.item
|
||
|
|
||
|
order =
|
||
|
Order
|
||
|
|> struct(%{})
|
||
|
|> order_changeset(%{})
|
||
|
|> TestRepo.insert!()
|
||
|
|> TestRepo.preload([:permalink])
|
||
|
|
||
|
changeset = order_changeset(order, %{item: %{price: 10}, permalink: %{post_id: 0}})
|
||
|
assert %Ecto.Changeset{} = changeset.changes.item
|
||
|
|
||
|
{:error, changeset} = TestRepo.update(changeset)
|
||
|
assert %Ecto.Changeset{} = changeset.changes.item
|
||
|
end
|
||
|
|
||
|
def order_changeset(order, params) do
|
||
|
order
|
||
|
|> Ecto.Changeset.cast(params, [:permalink_id])
|
||
|
|> Ecto.Changeset.cast_embed(:item, with: &item_changeset/2)
|
||
|
|> Ecto.Changeset.cast_assoc(:permalink, with: &permalink_changeset/2)
|
||
|
end
|
||
|
|
||
|
def item_changeset(item, params) do
|
||
|
item
|
||
|
|> Ecto.Changeset.cast(params, [:price])
|
||
|
end
|
||
|
|
||
|
def permalink_changeset(comment, params) do
|
||
|
comment
|
||
|
|> Ecto.Changeset.cast(params, [:post_id])
|
||
|
|> Ecto.Changeset.assoc_constraint(:post)
|
||
|
end
|
||
|
|
||
|
test "unsafe_validate_unique/4" do
|
||
|
{:ok, inserted_post} = TestRepo.insert(%Post{title: "Greetings", visits: 13})
|
||
|
new_post_changeset = Post.changeset(%Post{}, %{title: "Greetings", visits: 17})
|
||
|
|
||
|
changeset = Ecto.Changeset.unsafe_validate_unique(new_post_changeset, [:title], TestRepo)
|
||
|
assert changeset.errors[:title] ==
|
||
|
{"has already been taken", validation: :unsafe_unique, fields: [:title]}
|
||
|
|
||
|
changeset = Ecto.Changeset.unsafe_validate_unique(new_post_changeset, [:title, :text], TestRepo)
|
||
|
assert changeset.errors[:title] == nil
|
||
|
|
||
|
update_changeset = Post.changeset(inserted_post, %{visits: 17})
|
||
|
changeset = Ecto.Changeset.unsafe_validate_unique(update_changeset, [:title], TestRepo)
|
||
|
assert changeset.errors[:title] == nil # cannot conflict with itself
|
||
|
end
|
||
|
|
||
|
test "unsafe_validate_unique/4 with composite keys" do
|
||
|
{:ok, inserted_post} = TestRepo.insert(%CompositePk{a: 123, b: 456, name: "UniqueName"})
|
||
|
|
||
|
different_pk = CompositePk.changeset(%CompositePk{}, %{name: "UniqueName", a: 789, b: 321})
|
||
|
changeset = Ecto.Changeset.unsafe_validate_unique(different_pk, [:name], TestRepo)
|
||
|
assert changeset.errors[:name] ==
|
||
|
{"has already been taken", validation: :unsafe_unique, fields: [:name]}
|
||
|
|
||
|
partial_pk = CompositePk.changeset(%CompositePk{}, %{name: "UniqueName", a: 789, b: 456})
|
||
|
changeset = Ecto.Changeset.unsafe_validate_unique(partial_pk, [:name], TestRepo)
|
||
|
assert changeset.errors[:name] ==
|
||
|
{"has already been taken", validation: :unsafe_unique, fields: [:name]}
|
||
|
|
||
|
update_changeset = CompositePk.changeset(inserted_post, %{name: "NewName"})
|
||
|
changeset = Ecto.Changeset.unsafe_validate_unique(update_changeset, [:name], TestRepo)
|
||
|
assert changeset.valid?
|
||
|
assert changeset.errors[:name] == nil # cannot conflict with itself
|
||
|
end
|
||
|
|
||
|
test "get(!)" do
|
||
|
post1 = TestRepo.insert!(%Post{title: "1"})
|
||
|
post2 = TestRepo.insert!(%Post{title: "2"})
|
||
|
|
||
|
assert post1 == TestRepo.get(Post, post1.id)
|
||
|
assert post2 == TestRepo.get(Post, to_string post2.id) # With casting
|
||
|
|
||
|
assert post1 == TestRepo.get!(Post, post1.id)
|
||
|
assert post2 == TestRepo.get!(Post, to_string post2.id) # With casting
|
||
|
|
||
|
TestRepo.delete!(post1)
|
||
|
|
||
|
assert TestRepo.get(Post, post1.id) == nil
|
||
|
assert_raise Ecto.NoResultsError, fn ->
|
||
|
TestRepo.get!(Post, post1.id)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
test "get(!) with custom source" do
|
||
|
custom = Ecto.put_meta(%Custom{}, source: "posts")
|
||
|
custom = TestRepo.insert!(custom)
|
||
|
bid = custom.bid
|
||
|
assert %Custom{bid: ^bid, __meta__: %{source: "posts"}} =
|
||
|
TestRepo.get(from(c in {"posts", Custom}), bid)
|
||
|
end
|
||
|
|
||
|
test "get(!) with binary_id" do
|
||
|
custom = TestRepo.insert!(%Custom{})
|
||
|
bid = custom.bid
|
||
|
assert %Custom{bid: ^bid} = TestRepo.get(Custom, bid)
|
||
|
end
|
||
|
|
||
|
test "get_by(!)" do
|
||
|
post1 = TestRepo.insert!(%Post{title: "1", visits: 1})
|
||
|
post2 = TestRepo.insert!(%Post{title: "2", visits: 2})
|
||
|
|
||
|
assert post1 == TestRepo.get_by(Post, id: post1.id)
|
||
|
assert post1 == TestRepo.get_by(Post, title: post1.title)
|
||
|
assert post1 == TestRepo.get_by(Post, id: post1.id, title: post1.title)
|
||
|
assert post2 == TestRepo.get_by(Post, id: to_string(post2.id)) # With casting
|
||
|
assert nil == TestRepo.get_by(Post, title: "hey")
|
||
|
assert nil == TestRepo.get_by(Post, id: post2.id, visits: 3)
|
||
|
|
||
|
assert post1 == TestRepo.get_by!(Post, id: post1.id)
|
||
|
assert post1 == TestRepo.get_by!(Post, title: post1.title)
|
||
|
assert post1 == TestRepo.get_by!(Post, id: post1.id, visits: 1)
|
||
|
assert post2 == TestRepo.get_by!(Post, id: to_string(post2.id)) # With casting
|
||
|
|
||
|
assert post1 == TestRepo.get_by!(Post, %{id: post1.id})
|
||
|
|
||
|
assert_raise Ecto.NoResultsError, fn ->
|
||
|
TestRepo.get_by!(Post, id: post2.id, title: "hey")
|
||
|
end
|
||
|
end
|
||
|
|
||
|
test "reload" do
|
||
|
post1 = TestRepo.insert!(%Post{title: "1", visits: 1})
|
||
|
post2 = TestRepo.insert!(%Post{title: "2", visits: 2})
|
||
|
|
||
|
assert post1 == TestRepo.reload(post1)
|
||
|
assert [post1, post2] == TestRepo.reload([post1, post2])
|
||
|
assert [post1, post2, nil] == TestRepo.reload([post1, post2, %Post{id: 0}])
|
||
|
assert nil == TestRepo.reload(%Post{id: 0})
|
||
|
|
||
|
# keeps order as received in the params
|
||
|
assert [post2, post1] == TestRepo.reload([post2, post1])
|
||
|
|
||
|
TestRepo.update_all(Post, inc: [visits: 1])
|
||
|
|
||
|
assert [%{visits: 2}, %{visits: 3}] = TestRepo.reload([post1, post2])
|
||
|
end
|
||
|
|
||
|
test "reload ignores preloads" do
|
||
|
post = TestRepo.insert!(%Post{title: "1", visits: 1}) |> TestRepo.preload(:comments)
|
||
|
|
||
|
assert %{comments: %Ecto.Association.NotLoaded{}} = TestRepo.reload(post)
|
||
|
end
|
||
|
|
||
|
test "reload!" do
|
||
|
post1 = TestRepo.insert!(%Post{title: "1", visits: 1})
|
||
|
post2 = TestRepo.insert!(%Post{title: "2", visits: 2})
|
||
|
|
||
|
assert post1 == TestRepo.reload!(post1)
|
||
|
assert [post1, post2] == TestRepo.reload!([post1, post2])
|
||
|
|
||
|
assert_raise RuntimeError, ~r"could not reload", fn ->
|
||
|
TestRepo.reload!([post1, post2, %Post{id: -1}])
|
||
|
end
|
||
|
|
||
|
assert_raise Ecto.NoResultsError, fn ->
|
||
|
TestRepo.reload!(%Post{id: -1})
|
||
|
end
|
||
|
|
||
|
assert [post2, post1] == TestRepo.reload([post2, post1])
|
||
|
|
||
|
TestRepo.update_all(Post, inc: [visits: 1])
|
||
|
|
||
|
assert [%{visits: 2}, %{visits: 3}] = TestRepo.reload!([post1, post2])
|
||
|
end
|
||
|
|
||
|
test "first, last and one(!)" do
|
||
|
post1 = TestRepo.insert!(%Post{title: "1"})
|
||
|
post2 = TestRepo.insert!(%Post{title: "2"})
|
||
|
|
||
|
assert post1 == Post |> first |> TestRepo.one
|
||
|
assert post2 == Post |> last |> TestRepo.one
|
||
|
|
||
|
query = from p in Post, order_by: p.title
|
||
|
assert post1 == query |> first |> TestRepo.one
|
||
|
assert post2 == query |> last |> TestRepo.one
|
||
|
|
||
|
query = from p in Post, order_by: [desc: p.title], limit: 10
|
||
|
assert post2 == query |> first |> TestRepo.one
|
||
|
assert post1 == query |> last |> TestRepo.one
|
||
|
|
||
|
query = from p in Post, where: is_nil(p.id)
|
||
|
refute query |> first |> TestRepo.one
|
||
|
refute query |> last |> TestRepo.one
|
||
|
assert_raise Ecto.NoResultsError, fn -> query |> first |> TestRepo.one! end
|
||
|
assert_raise Ecto.NoResultsError, fn -> query |> last |> TestRepo.one! end
|
||
|
end
|
||
|
|
||
|
test "exists?" do
|
||
|
TestRepo.insert!(%Post{title: "1", visits: 2})
|
||
|
TestRepo.insert!(%Post{title: "2", visits: 1})
|
||
|
|
||
|
query = from p in Post, where: not is_nil(p.title), limit: 2
|
||
|
assert query |> TestRepo.exists? == true
|
||
|
|
||
|
query = from p in Post, where: p.title == "1", select: p.title
|
||
|
assert query |> TestRepo.exists? == true
|
||
|
|
||
|
query = from p in Post, where: is_nil(p.id)
|
||
|
assert query |> TestRepo.exists? == false
|
||
|
|
||
|
query = from p in Post, where: is_nil(p.id)
|
||
|
assert query |> TestRepo.exists? == false
|
||
|
|
||
|
query = from(p in Post, select: {p.visits, avg(p.visits)}, group_by: p.visits, having: avg(p.visits) > 1)
|
||
|
assert query |> TestRepo.exists? == true
|
||
|
end
|
||
|
|
||
|
test "aggregate" do
|
||
|
assert TestRepo.aggregate(Post, :max, :visits) == nil
|
||
|
|
||
|
TestRepo.insert!(%Post{visits: 10})
|
||
|
TestRepo.insert!(%Post{visits: 12})
|
||
|
TestRepo.insert!(%Post{visits: 14})
|
||
|
TestRepo.insert!(%Post{visits: 14})
|
||
|
|
||
|
# Barebones
|
||
|
assert TestRepo.aggregate(Post, :max, :visits) == 14
|
||
|
assert TestRepo.aggregate(Post, :min, :visits) == 10
|
||
|
assert TestRepo.aggregate(Post, :count, :visits) == 4
|
||
|
assert "50" = to_string(TestRepo.aggregate(Post, :sum, :visits))
|
||
|
|
||
|
# With order_by
|
||
|
query = from Post, order_by: [asc: :visits]
|
||
|
assert TestRepo.aggregate(query, :max, :visits) == 14
|
||
|
|
||
|
# With order_by and limit
|
||
|
query = from Post, order_by: [asc: :visits], limit: 2
|
||
|
assert TestRepo.aggregate(query, :max, :visits) == 12
|
||
|
end
|
||
|
|
||
|
@tag :decimal_precision
|
||
|
test "aggregate avg" do
|
||
|
TestRepo.insert!(%Post{visits: 10})
|
||
|
TestRepo.insert!(%Post{visits: 12})
|
||
|
TestRepo.insert!(%Post{visits: 14})
|
||
|
TestRepo.insert!(%Post{visits: 14})
|
||
|
|
||
|
assert "12.5" <> _ = to_string(TestRepo.aggregate(Post, :avg, :visits))
|
||
|
end
|
||
|
|
||
|
@tag :inline_order_by
|
||
|
test "aggregate with distinct" do
|
||
|
TestRepo.insert!(%Post{visits: 10})
|
||
|
TestRepo.insert!(%Post{visits: 12})
|
||
|
TestRepo.insert!(%Post{visits: 14})
|
||
|
TestRepo.insert!(%Post{visits: 14})
|
||
|
|
||
|
query = from Post, order_by: [asc: :visits], distinct: true
|
||
|
assert TestRepo.aggregate(query, :count, :visits) == 3
|
||
|
end
|
||
|
|
||
|
@tag :insert_cell_wise_defaults
|
||
|
test "insert all" do
|
||
|
assert {2, nil} = TestRepo.insert_all("comments", [[text: "1"], %{text: "2", lock_version: 2}])
|
||
|
assert {2, nil} = TestRepo.insert_all({"comments", Comment}, [[text: "3"], %{text: "4", lock_version: 2}])
|
||
|
assert [%Comment{text: "1", lock_version: 1},
|
||
|
%Comment{text: "2", lock_version: 2},
|
||
|
%Comment{text: "3", lock_version: 1},
|
||
|
%Comment{text: "4", lock_version: 2}] = TestRepo.all(Comment)
|
||
|
|
||
|
assert {2, nil} = TestRepo.insert_all(Post, [[], []])
|
||
|
assert [%Post{}, %Post{}] = TestRepo.all(Post)
|
||
|
|
||
|
assert {0, nil} = TestRepo.insert_all("posts", [])
|
||
|
assert {0, nil} = TestRepo.insert_all({"posts", Post}, [])
|
||
|
end
|
||
|
|
||
|
@tag :insert_select
|
||
|
test "insert all with query for single fields" do
|
||
|
comment = TestRepo.insert!(%Comment{text: "1", lock_version: 1})
|
||
|
|
||
|
text_query = from(c in Comment, select: c.text, where: [id: ^comment.id, lock_version: 1])
|
||
|
|
||
|
lock_version_query = from(c in Comment, select: c.lock_version, where: [id: ^comment.id])
|
||
|
|
||
|
rows = [
|
||
|
[text: "2", lock_version: lock_version_query],
|
||
|
[lock_version: lock_version_query, text: "3"],
|
||
|
[text: text_query],
|
||
|
[text: text_query, lock_version: lock_version_query],
|
||
|
[lock_version: 6, text: "6"]
|
||
|
]
|
||
|
assert {5, nil} = TestRepo.insert_all(Comment, rows, [])
|
||
|
|
||
|
inserted_rows = Comment
|
||
|
|> where([c], c.id != ^comment.id)
|
||
|
|> TestRepo.all()
|
||
|
|
||
|
assert [%Comment{text: "2", lock_version: 1},
|
||
|
%Comment{text: "3", lock_version: 1},
|
||
|
%Comment{text: "1"},
|
||
|
%Comment{text: "1", lock_version: 1},
|
||
|
%Comment{text: "6", lock_version: 6}] = inserted_rows
|
||
|
end
|
||
|
|
||
|
describe "insert_all with source query" do
|
||
|
@tag :upsert_all
|
||
|
@tag :with_conflict_target
|
||
|
@tag :concat
|
||
|
test "insert_all with query and conflict target" do
|
||
|
{:ok, %Post{id: id}} = TestRepo.insert(%Post{
|
||
|
title: "A generic title"
|
||
|
})
|
||
|
|
||
|
source = from p in Post,
|
||
|
select: %{
|
||
|
title: fragment("concat(?, ?, ?)", p.title, type(^" suffix ", :string), p.id)
|
||
|
}
|
||
|
|
||
|
assert {1, _} = TestRepo.insert_all(Post, source, conflict_target: [:id], on_conflict: :replace_all)
|
||
|
|
||
|
expected_id = id + 1
|
||
|
expected_title = "A generic title suffix #{id}"
|
||
|
|
||
|
assert %Post{title: ^expected_title} = TestRepo.get(Post, expected_id)
|
||
|
end
|
||
|
|
||
|
@tag :returning
|
||
|
@tag :concat
|
||
|
test "insert_all with query and returning" do
|
||
|
{:ok, %Post{id: id}} = TestRepo.insert(%Post{
|
||
|
title: "A generic title"
|
||
|
})
|
||
|
|
||
|
source = from p in Post,
|
||
|
select: %{
|
||
|
title: fragment("concat(?, ?, ?)", p.title, type(^" suffix ", :string), p.id)
|
||
|
}
|
||
|
|
||
|
assert {1, returns} = TestRepo.insert_all(Post, source, returning: [:id, :title])
|
||
|
|
||
|
expected_id = id + 1
|
||
|
expected_title = "A generic title suffix #{id}"
|
||
|
assert [%Post{id: ^expected_id, title: ^expected_title}] = returns
|
||
|
end
|
||
|
|
||
|
@tag :upsert_all
|
||
|
@tag :without_conflict_target
|
||
|
@tag :concat
|
||
|
test "insert_all with query and on_conflict" do
|
||
|
{:ok, %Post{id: id}} = TestRepo.insert(%Post{
|
||
|
title: "A generic title"
|
||
|
})
|
||
|
|
||
|
source = from p in Post,
|
||
|
select: %{
|
||
|
title: fragment("concat(?, ?, ?)", p.title, type(^" suffix ", :string), p.id)
|
||
|
}
|
||
|
|
||
|
assert {1, _} = TestRepo.insert_all(Post, source, on_conflict: :replace_all)
|
||
|
|
||
|
expected_id = id + 1
|
||
|
expected_title = "A generic title suffix #{id}"
|
||
|
|
||
|
assert %Post{title: ^expected_title} = TestRepo.get(Post, expected_id)
|
||
|
end
|
||
|
|
||
|
@tag :concat
|
||
|
test "insert_all with query" do
|
||
|
{:ok, %Post{id: id}} = TestRepo.insert(%Post{
|
||
|
title: "A generic title"
|
||
|
})
|
||
|
|
||
|
source = from p in Post,
|
||
|
select: %{
|
||
|
title: fragment("concat(?, ?, ?)", p.title, type(^" suffix ", :string), p.id)
|
||
|
}
|
||
|
|
||
|
assert {1, _} = TestRepo.insert_all(Post, source)
|
||
|
|
||
|
expected_id = id + 1
|
||
|
expected_title = "A generic title suffix #{id}"
|
||
|
|
||
|
assert %Post{title: ^expected_title} = TestRepo.get(Post, expected_id)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
@tag :invalid_prefix
|
||
|
@tag :insert_cell_wise_defaults
|
||
|
test "insert all with invalid prefix" do
|
||
|
assert catch_error(TestRepo.insert_all(Post, [[], []], prefix: "oops"))
|
||
|
end
|
||
|
|
||
|
@tag :returning
|
||
|
@tag :insert_cell_wise_defaults
|
||
|
test "insert all with returning with schema" do
|
||
|
assert {0, []} = TestRepo.insert_all(Comment, [], returning: true)
|
||
|
assert {0, nil} = TestRepo.insert_all(Comment, [], returning: false)
|
||
|
|
||
|
{2, [c1, c2]} = TestRepo.insert_all(Comment, [[text: "1"], [text: "2"]], returning: [:id, :text])
|
||
|
assert %Comment{text: "1", __meta__: %{state: :loaded}} = c1
|
||
|
assert %Comment{text: "2", __meta__: %{state: :loaded}} = c2
|
||
|
|
||
|
{2, [c1, c2]} = TestRepo.insert_all(Comment, [[text: "3"], [text: "4"]], returning: true)
|
||
|
assert %Comment{text: "3", __meta__: %{state: :loaded}} = c1
|
||
|
assert %Comment{text: "4", __meta__: %{state: :loaded}} = c2
|
||
|
end
|
||
|
|
||
|
@tag :returning
|
||
|
@tag :insert_cell_wise_defaults
|
||
|
test "insert all with returning with schema with field source" do
|
||
|
assert {0, []} = TestRepo.insert_all(Permalink, [], returning: true)
|
||
|
assert {0, nil} = TestRepo.insert_all(Permalink, [], returning: false)
|
||
|
|
||
|
{2, [c1, c2]} = TestRepo.insert_all(Permalink, [[url: "1"], [url: "2"]], returning: [:id, :url])
|
||
|
assert %Permalink{url: "1", __meta__: %{state: :loaded}} = c1
|
||
|
assert %Permalink{url: "2", __meta__: %{state: :loaded}} = c2
|
||
|
|
||
|
{2, [c1, c2]} = TestRepo.insert_all(Permalink, [[url: "3"], [url: "4"]], returning: true)
|
||
|
assert %Permalink{url: "3", __meta__: %{state: :loaded}} = c1
|
||
|
assert %Permalink{url: "4", __meta__: %{state: :loaded}} = c2
|
||
|
end
|
||
|
|
||
|
@tag :returning
|
||
|
@tag :insert_cell_wise_defaults
|
||
|
test "insert all with returning without schema" do
|
||
|
{2, [c1, c2]} = TestRepo.insert_all("comments", [[text: "1"], [text: "2"]], returning: [:id, :text])
|
||
|
assert %{id: _, text: "1"} = c1
|
||
|
assert %{id: _, text: "2"} = c2
|
||
|
|
||
|
assert_raise ArgumentError, fn ->
|
||
|
TestRepo.insert_all("comments", [[text: "1"], [text: "2"]], returning: true)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
@tag :insert_cell_wise_defaults
|
||
|
test "insert all with dumping" do
|
||
|
uuid = Ecto.UUID.generate()
|
||
|
assert {1, nil} = TestRepo.insert_all(Post, [%{uuid: uuid}])
|
||
|
assert [%Post{uuid: ^uuid, title: nil}] = TestRepo.all(Post)
|
||
|
end
|
||
|
|
||
|
@tag :insert_cell_wise_defaults
|
||
|
test "insert all autogenerates for binary_id type" do
|
||
|
custom = TestRepo.insert!(%Custom{bid: nil})
|
||
|
assert custom.bid
|
||
|
assert TestRepo.get(Custom, custom.bid)
|
||
|
assert TestRepo.delete!(custom)
|
||
|
refute TestRepo.get(Custom, custom.bid)
|
||
|
|
||
|
uuid = Ecto.UUID.generate()
|
||
|
assert {2, nil} = TestRepo.insert_all(Custom, [%{uuid: uuid}, %{bid: custom.bid}])
|
||
|
assert [%Custom{bid: bid2, uuid: nil},
|
||
|
%Custom{bid: bid1, uuid: ^uuid}] = Enum.sort_by(TestRepo.all(Custom), & &1.uuid)
|
||
|
assert bid1 && bid2
|
||
|
assert custom.bid != bid1
|
||
|
assert custom.bid == bid2
|
||
|
end
|
||
|
|
||
|
describe "placeholders" do
|
||
|
@describetag :placeholders
|
||
|
|
||
|
test "Repo.insert_all fills in placeholders" do
|
||
|
placeholders = %{foo: 100, bar: "test"}
|
||
|
bar_ph = {:placeholder, :bar}
|
||
|
foo_ph = {:placeholder, :foo}
|
||
|
|
||
|
entries = [
|
||
|
%{intensity: 1.0, title: bar_ph, posted: ~D[2020-12-21], visits: foo_ph},
|
||
|
%{intensity: 2.0, title: bar_ph, posted: ~D[2000-12-21], visits: foo_ph}
|
||
|
] |> Enum.map(&Map.put(&1, :uuid, Ecto.UUID.generate))
|
||
|
|
||
|
TestRepo.insert_all(Post, entries, placeholders: placeholders)
|
||
|
|
||
|
query = from(p in Post, select: {p.intensity, p.title, p.visits})
|
||
|
assert [{1.0, "test", 100}, {2.0, "test", 100}] == TestRepo.all(query)
|
||
|
end
|
||
|
|
||
|
test "Repo.insert_all accepts non-atom placeholder keys" do
|
||
|
placeholders = %{10 => "integer key", {:foo, :bar} => "tuple key"}
|
||
|
entries = [%{text: {:placeholder, 10}}, %{text: {:placeholder, {:foo, :bar}}}]
|
||
|
TestRepo.insert_all(Comment, entries, placeholders: placeholders)
|
||
|
|
||
|
query = from(c in Comment, select: c.text)
|
||
|
assert ["integer key", "tuple key"] == TestRepo.all(query)
|
||
|
end
|
||
|
|
||
|
test "Repo.insert_all fills in placeholders with keyword list entries" do
|
||
|
TestRepo.insert_all(Barebone, [[num: {:placeholder, :foo}]], placeholders: %{foo: 100})
|
||
|
|
||
|
query = from(b in Barebone, select: b.num)
|
||
|
assert [100] == TestRepo.all(query)
|
||
|
end
|
||
|
|
||
|
@tag :upsert_all
|
||
|
@tag :with_conflict_target
|
||
|
test "Repo.insert_all upserts and fills in placeholders with conditioned on_conflict query" do
|
||
|
do_not_update_title = "don't touch me"
|
||
|
|
||
|
posted_value =
|
||
|
from p in Post, where: p.public == ^true and p.id > ^0, select: p.posted, limit: 1
|
||
|
|
||
|
on_conflict =
|
||
|
from p in Post, update: [set: [title: "updated"]], where: p.title != ^do_not_update_title
|
||
|
|
||
|
placeholders = %{visits: 1, title: "title"}
|
||
|
|
||
|
post1 = [
|
||
|
visits: {:placeholder, :visits},
|
||
|
title: {:placeholder, :title},
|
||
|
uuid: Ecto.UUID.generate(),
|
||
|
posted: posted_value
|
||
|
]
|
||
|
|
||
|
post2 = [
|
||
|
title: do_not_update_title,
|
||
|
uuid: Ecto.UUID.generate(),
|
||
|
posted: posted_value
|
||
|
]
|
||
|
|
||
|
assert TestRepo.insert_all(Post, [post1, post2],
|
||
|
placeholders: placeholders,
|
||
|
on_conflict: on_conflict,
|
||
|
conflict_target: [:uuid]
|
||
|
) ==
|
||
|
{2, nil}
|
||
|
|
||
|
# only update first post
|
||
|
assert TestRepo.insert_all(Post, [post1, post2],
|
||
|
placeholders: placeholders,
|
||
|
on_conflict: on_conflict,
|
||
|
conflict_target: [:uuid]
|
||
|
) ==
|
||
|
{1, nil}
|
||
|
|
||
|
assert TestRepo.aggregate(where(Post, title: "updated"), :count) == 1
|
||
|
end
|
||
|
end
|
||
|
|
||
|
test "update all" do
|
||
|
assert post1 = TestRepo.insert!(%Post{title: "1"})
|
||
|
assert post2 = TestRepo.insert!(%Post{title: "2"})
|
||
|
assert post3 = TestRepo.insert!(%Post{title: "3"})
|
||
|
|
||
|
assert {3, nil} = TestRepo.update_all(Post, set: [title: "x"])
|
||
|
|
||
|
assert %Post{title: "x"} = TestRepo.reload(post1)
|
||
|
assert %Post{title: "x"} = TestRepo.reload(post2)
|
||
|
assert %Post{title: "x"} = TestRepo.reload(post3)
|
||
|
|
||
|
assert {3, nil} = TestRepo.update_all("posts", [set: [title: nil]])
|
||
|
|
||
|
assert %Post{title: nil} = TestRepo.reload(post1)
|
||
|
assert %Post{title: nil} = TestRepo.reload(post2)
|
||
|
assert %Post{title: nil} = TestRepo.reload(post3)
|
||
|
end
|
||
|
|
||
|
@tag :invalid_prefix
|
||
|
test "update all with invalid prefix" do
|
||
|
assert catch_error(TestRepo.update_all(Post, [set: [title: "x"]], prefix: "oops"))
|
||
|
end
|
||
|
|
||
|
@tag :returning
|
||
|
test "update all with returning with schema" do
|
||
|
assert %Post{id: id1} = TestRepo.insert!(%Post{title: "1"})
|
||
|
assert %Post{id: id2} = TestRepo.insert!(%Post{title: "2"})
|
||
|
assert %Post{id: id3} = TestRepo.insert!(%Post{title: "3"})
|
||
|
|
||
|
assert {3, posts} = TestRepo.update_all(select(Post, [p], p), [set: [title: "x"]])
|
||
|
|
||
|
[p1, p2, p3] = Enum.sort_by(posts, & &1.id)
|
||
|
assert %Post{id: ^id1, title: "x"} = p1
|
||
|
assert %Post{id: ^id2, title: "x"} = p2
|
||
|
assert %Post{id: ^id3, title: "x"} = p3
|
||
|
|
||
|
assert {3, posts} = TestRepo.update_all(select(Post, [:id, :visits]), [set: [visits: 11]])
|
||
|
|
||
|
[p1, p2, p3] = Enum.sort_by(posts, & &1.id)
|
||
|
assert %Post{id: ^id1, title: nil, visits: 11} = p1
|
||
|
assert %Post{id: ^id2, title: nil, visits: 11} = p2
|
||
|
assert %Post{id: ^id3, title: nil, visits: 11} = p3
|
||
|
end
|
||
|
|
||
|
@tag :returning
|
||
|
test "update all with returning without schema" do
|
||
|
assert %Post{id: id1} = TestRepo.insert!(%Post{title: "1"})
|
||
|
assert %Post{id: id2} = TestRepo.insert!(%Post{title: "2"})
|
||
|
assert %Post{id: id3} = TestRepo.insert!(%Post{title: "3"})
|
||
|
|
||
|
assert {3, posts} = TestRepo.update_all(select("posts", [:id, :title]), [set: [title: "x"]])
|
||
|
|
||
|
[p1, p2, p3] = Enum.sort_by(posts, & &1.id)
|
||
|
assert p1 == %{id: id1, title: "x"}
|
||
|
assert p2 == %{id: id2, title: "x"}
|
||
|
assert p3 == %{id: id3, title: "x"}
|
||
|
end
|
||
|
|
||
|
test "update all with filter" do
|
||
|
assert %Post{id: id1} = TestRepo.insert!(%Post{title: "1"})
|
||
|
assert %Post{id: id2} = TestRepo.insert!(%Post{title: "2"})
|
||
|
assert %Post{id: id3} = TestRepo.insert!(%Post{title: "3"})
|
||
|
|
||
|
query = from(p in Post, where: p.title == "1" or p.title == "2",
|
||
|
update: [set: [visits: ^17]])
|
||
|
assert {2, nil} = TestRepo.update_all(query, set: [title: "x"])
|
||
|
|
||
|
assert %Post{title: "x", visits: 17} = TestRepo.get(Post, id1)
|
||
|
assert %Post{title: "x", visits: 17} = TestRepo.get(Post, id2)
|
||
|
assert %Post{title: "3", visits: nil} = TestRepo.get(Post, id3)
|
||
|
end
|
||
|
|
||
|
test "update all no entries" do
|
||
|
assert %Post{id: id1} = TestRepo.insert!(%Post{title: "1"})
|
||
|
assert %Post{id: id2} = TestRepo.insert!(%Post{title: "2"})
|
||
|
assert %Post{id: id3} = TestRepo.insert!(%Post{title: "3"})
|
||
|
|
||
|
query = from(p in Post, where: p.title == "4")
|
||
|
assert {0, nil} = TestRepo.update_all(query, set: [title: "x"])
|
||
|
|
||
|
assert %Post{title: "1"} = TestRepo.get(Post, id1)
|
||
|
assert %Post{title: "2"} = TestRepo.get(Post, id2)
|
||
|
assert %Post{title: "3"} = TestRepo.get(Post, id3)
|
||
|
end
|
||
|
|
||
|
test "update all increment syntax" do
|
||
|
assert %Post{id: id1} = TestRepo.insert!(%Post{title: "1", visits: 0})
|
||
|
assert %Post{id: id2} = TestRepo.insert!(%Post{title: "2", visits: 1})
|
||
|
|
||
|
# Positive
|
||
|
query = from p in Post, where: not is_nil(p.id), update: [inc: [visits: 2]]
|
||
|
assert {2, nil} = TestRepo.update_all(query, [])
|
||
|
|
||
|
assert %Post{visits: 2} = TestRepo.get(Post, id1)
|
||
|
assert %Post{visits: 3} = TestRepo.get(Post, id2)
|
||
|
|
||
|
# Negative
|
||
|
query = from p in Post, where: not is_nil(p.id), update: [inc: [visits: -1]]
|
||
|
assert {2, nil} = TestRepo.update_all(query, [])
|
||
|
|
||
|
assert %Post{visits: 1} = TestRepo.get(Post, id1)
|
||
|
assert %Post{visits: 2} = TestRepo.get(Post, id2)
|
||
|
end
|
||
|
|
||
|
@tag :id_type
|
||
|
test "update all with casting and dumping on id type field" do
|
||
|
assert %Post{id: id1} = TestRepo.insert!(%Post{})
|
||
|
assert {1, nil} = TestRepo.update_all(Post, set: [counter: to_string(id1)])
|
||
|
assert %Post{counter: ^id1} = TestRepo.get(Post, id1)
|
||
|
end
|
||
|
|
||
|
test "update all with casting and dumping" do
|
||
|
visits = 13
|
||
|
datetime = ~N[2014-01-16 20:26:51]
|
||
|
assert %Post{id: id} = TestRepo.insert!(%Post{})
|
||
|
|
||
|
assert {1, nil} = TestRepo.update_all(Post, set: [visits: visits, inserted_at: datetime])
|
||
|
assert %Post{visits: 13, inserted_at: ^datetime} = TestRepo.get(Post, id)
|
||
|
end
|
||
|
|
||
|
test "delete all" do
|
||
|
assert %Post{} = TestRepo.insert!(%Post{title: "1"})
|
||
|
assert %Post{} = TestRepo.insert!(%Post{title: "2"})
|
||
|
assert %Post{} = TestRepo.insert!(%Post{title: "3"})
|
||
|
|
||
|
assert {3, nil} = TestRepo.delete_all(Post)
|
||
|
assert [] = TestRepo.all(Post)
|
||
|
end
|
||
|
|
||
|
@tag :invalid_prefix
|
||
|
test "delete all with invalid prefix" do
|
||
|
assert catch_error(TestRepo.delete_all(Post, prefix: "oops"))
|
||
|
end
|
||
|
|
||
|
@tag :returning
|
||
|
test "delete all with returning with schema" do
|
||
|
assert %Post{id: id1} = TestRepo.insert!(%Post{title: "1"})
|
||
|
assert %Post{id: id2} = TestRepo.insert!(%Post{title: "2"})
|
||
|
assert %Post{id: id3} = TestRepo.insert!(%Post{title: "3"})
|
||
|
|
||
|
assert {3, posts} = TestRepo.delete_all(select(Post, [p], p))
|
||
|
|
||
|
[p1, p2, p3] = Enum.sort_by(posts, & &1.id)
|
||
|
assert %Post{id: ^id1, title: "1"} = p1
|
||
|
assert %Post{id: ^id2, title: "2"} = p2
|
||
|
assert %Post{id: ^id3, title: "3"} = p3
|
||
|
end
|
||
|
|
||
|
@tag :returning
|
||
|
test "delete all with returning without schema" do
|
||
|
assert %Post{id: id1} = TestRepo.insert!(%Post{title: "1"})
|
||
|
assert %Post{id: id2} = TestRepo.insert!(%Post{title: "2"})
|
||
|
assert %Post{id: id3} = TestRepo.insert!(%Post{title: "3"})
|
||
|
|
||
|
assert {3, posts} = TestRepo.delete_all(select("posts", [:id, :title]))
|
||
|
|
||
|
[p1, p2, p3] = Enum.sort_by(posts, & &1.id)
|
||
|
assert p1 == %{id: id1, title: "1"}
|
||
|
assert p2 == %{id: id2, title: "2"}
|
||
|
assert p3 == %{id: id3, title: "3"}
|
||
|
end
|
||
|
|
||
|
test "delete all with filter" do
|
||
|
assert %Post{} = TestRepo.insert!(%Post{title: "1"})
|
||
|
assert %Post{} = TestRepo.insert!(%Post{title: "2"})
|
||
|
assert %Post{} = TestRepo.insert!(%Post{title: "3"})
|
||
|
|
||
|
query = from(p in Post, where: p.title == "1" or p.title == "2")
|
||
|
assert {2, nil} = TestRepo.delete_all(query)
|
||
|
assert [%Post{}] = TestRepo.all(Post)
|
||
|
end
|
||
|
|
||
|
test "delete all no entries" do
|
||
|
assert %Post{id: id1} = TestRepo.insert!(%Post{title: "1"})
|
||
|
assert %Post{id: id2} = TestRepo.insert!(%Post{title: "2"})
|
||
|
assert %Post{id: id3} = TestRepo.insert!(%Post{title: "3"})
|
||
|
|
||
|
query = from(p in Post, where: p.title == "4")
|
||
|
assert {0, nil} = TestRepo.delete_all(query)
|
||
|
assert %Post{title: "1"} = TestRepo.get(Post, id1)
|
||
|
assert %Post{title: "2"} = TestRepo.get(Post, id2)
|
||
|
assert %Post{title: "3"} = TestRepo.get(Post, id3)
|
||
|
end
|
||
|
|
||
|
test "virtual field" do
|
||
|
assert %Post{id: id} = TestRepo.insert!(%Post{title: "1"})
|
||
|
assert TestRepo.get(Post, id).temp == "temp"
|
||
|
end
|
||
|
|
||
|
## Query syntax
|
||
|
|
||
|
defmodule Foo do
|
||
|
defstruct [:title]
|
||
|
end
|
||
|
|
||
|
describe "query select" do
|
||
|
test "expressions" do
|
||
|
%Post{} = TestRepo.insert!(%Post{title: "1", visits: 13})
|
||
|
|
||
|
assert [{"1", 13}] ==
|
||
|
TestRepo.all(from p in Post, select: {p.title, p.visits})
|
||
|
|
||
|
assert [["1", 13]] ==
|
||
|
TestRepo.all(from p in Post, select: [p.title, p.visits])
|
||
|
|
||
|
assert [%{:title => "1", 3 => 13, "visits" => 13}] ==
|
||
|
TestRepo.all(from p in Post, select: %{
|
||
|
:title => p.title,
|
||
|
"visits" => p.visits,
|
||
|
3 => p.visits
|
||
|
})
|
||
|
|
||
|
assert [%{:title => "1", "1" => 13, "visits" => 13}] ==
|
||
|
TestRepo.all(from p in Post, select: %{
|
||
|
:title => p.title,
|
||
|
p.title => p.visits,
|
||
|
"visits" => p.visits
|
||
|
})
|
||
|
|
||
|
assert [%Foo{title: "1"}] ==
|
||
|
TestRepo.all(from p in Post, select: %Foo{title: p.title})
|
||
|
end
|
||
|
|
||
|
test "map update" do
|
||
|
%Post{} = TestRepo.insert!(%Post{title: "1", visits: 13})
|
||
|
|
||
|
assert [%Post{:title => "new title", visits: 13}] =
|
||
|
TestRepo.all(from p in Post, select: %{p | title: "new title"})
|
||
|
|
||
|
assert [%Post{title: "new title", visits: 13}] =
|
||
|
TestRepo.all(from p in Post, select: %Post{p | title: "new title"})
|
||
|
|
||
|
assert_raise KeyError, fn ->
|
||
|
TestRepo.all(from p in Post, select: %{p | unknown: "new title"})
|
||
|
end
|
||
|
|
||
|
assert_raise BadMapError, fn ->
|
||
|
TestRepo.all(from p in Post, select: %{p.title | title: "new title"})
|
||
|
end
|
||
|
|
||
|
assert_raise BadStructError, fn ->
|
||
|
TestRepo.all(from p in Post, select: %Foo{p | title: p.title})
|
||
|
end
|
||
|
end
|
||
|
|
||
|
test "map update on association" do
|
||
|
p = TestRepo.insert!(%Post{})
|
||
|
TestRepo.insert!(%Comment{post_id: p.id, text: "comment text"})
|
||
|
TestRepo.insert!(%Comment{})
|
||
|
|
||
|
query =
|
||
|
from(c in Comment, left_join: p in Post, on: c.post_id == p.id, select: %{p | temp: c.text})
|
||
|
|
||
|
assert [%Post{:temp => "comment text"}, nil] = TestRepo.all(query)
|
||
|
end
|
||
|
|
||
|
test "take with structs" do
|
||
|
%{id: pid1} = TestRepo.insert!(%Post{title: "1"})
|
||
|
%{id: pid2} = TestRepo.insert!(%Post{title: "2"})
|
||
|
%{id: pid3} = TestRepo.insert!(%Post{title: "3"})
|
||
|
|
||
|
[p1, p2, p3] = Post |> select([p], struct(p, [:title])) |> order_by([:title]) |> TestRepo.all
|
||
|
refute p1.id
|
||
|
assert p1.title == "1"
|
||
|
assert match?(%Post{}, p1)
|
||
|
refute p2.id
|
||
|
assert p2.title == "2"
|
||
|
assert match?(%Post{}, p2)
|
||
|
refute p3.id
|
||
|
assert p3.title == "3"
|
||
|
assert match?(%Post{}, p3)
|
||
|
|
||
|
[p1, p2, p3] = Post |> select([:id]) |> order_by([:id]) |> TestRepo.all
|
||
|
assert %Post{id: ^pid1} = p1
|
||
|
assert %Post{id: ^pid2} = p2
|
||
|
assert %Post{id: ^pid3} = p3
|
||
|
end
|
||
|
|
||
|
test "take with maps" do
|
||
|
%{id: pid1} = TestRepo.insert!(%Post{title: "1"})
|
||
|
%{id: pid2} = TestRepo.insert!(%Post{title: "2"})
|
||
|
%{id: pid3} = TestRepo.insert!(%Post{title: "3"})
|
||
|
|
||
|
[p1, p2, p3] = "posts" |> select([p], map(p, [:title])) |> order_by([:title]) |> TestRepo.all
|
||
|
assert p1 == %{title: "1"}
|
||
|
assert p2 == %{title: "2"}
|
||
|
assert p3 == %{title: "3"}
|
||
|
|
||
|
[p1, p2, p3] = "posts" |> select([:id]) |> order_by([:id]) |> TestRepo.all
|
||
|
assert p1 == %{id: pid1}
|
||
|
assert p2 == %{id: pid2}
|
||
|
assert p3 == %{id: pid3}
|
||
|
end
|
||
|
|
||
|
test "take with preload assocs" do
|
||
|
%{id: pid} = TestRepo.insert!(%Post{title: "post"})
|
||
|
TestRepo.insert!(%Comment{post_id: pid, text: "comment"})
|
||
|
fields = [:id, :title, comments: [:text, :post_id]]
|
||
|
|
||
|
[p] = Post |> preload(:comments) |> select([p], ^fields) |> TestRepo.all
|
||
|
assert %Post{title: "post"} = p
|
||
|
assert [%Comment{text: "comment"}] = p.comments
|
||
|
|
||
|
[p] = Post |> preload(:comments) |> select([p], struct(p, ^fields)) |> TestRepo.all
|
||
|
assert %Post{title: "post"} = p
|
||
|
assert [%Comment{text: "comment"}] = p.comments
|
||
|
|
||
|
[p] = Post |> preload(:comments) |> select([p], map(p, ^fields)) |> TestRepo.all
|
||
|
assert p == %{id: pid, title: "post", comments: [%{text: "comment", post_id: pid}]}
|
||
|
end
|
||
|
|
||
|
test "take with nil preload assoc" do
|
||
|
%{id: cid} = TestRepo.insert!(%Comment{text: "comment"})
|
||
|
fields = [:id, :text, post: [:title]]
|
||
|
|
||
|
[c] = Comment |> preload(:post) |> select([c], ^fields) |> TestRepo.all
|
||
|
assert %Comment{id: ^cid, text: "comment", post: nil} = c
|
||
|
|
||
|
[c] = Comment |> preload(:post) |> select([c], struct(c, ^fields)) |> TestRepo.all
|
||
|
assert %Comment{id: ^cid, text: "comment", post: nil} = c
|
||
|
|
||
|
[c] = Comment |> preload(:post) |> select([c], map(c, ^fields)) |> TestRepo.all
|
||
|
assert c == %{id: cid, text: "comment", post: nil}
|
||
|
end
|
||
|
|
||
|
test "take with join assocs" do
|
||
|
%{id: pid} = TestRepo.insert!(%Post{title: "post"})
|
||
|
%{id: cid} = TestRepo.insert!(%Comment{post_id: pid, text: "comment"})
|
||
|
fields = [:id, :title, comments: [:text, :post_id, :id]]
|
||
|
query = from p in Post, where: p.id == ^pid, join: c in assoc(p, :comments), preload: [comments: c]
|
||
|
|
||
|
p = TestRepo.one(from q in query, select: ^fields)
|
||
|
assert %Post{title: "post"} = p
|
||
|
assert [%Comment{text: "comment"}] = p.comments
|
||
|
|
||
|
p = TestRepo.one(from q in query, select: struct(q, ^fields))
|
||
|
assert %Post{title: "post"} = p
|
||
|
assert [%Comment{text: "comment"}] = p.comments
|
||
|
|
||
|
p = TestRepo.one(from q in query, select: map(q, ^fields))
|
||
|
assert p == %{id: pid, title: "post", comments: [%{text: "comment", post_id: pid, id: cid}]}
|
||
|
end
|
||
|
|
||
|
test "take with single nil column" do
|
||
|
%Post{} = TestRepo.insert!(%Post{title: "1", counter: nil})
|
||
|
assert %{counter: nil} =
|
||
|
TestRepo.one(from p in Post, where: p.title == "1", select: [:counter])
|
||
|
end
|
||
|
|
||
|
test "take with join assocs and single nil column" do
|
||
|
%{id: post_id} = TestRepo.insert!(%Post{title: "1"}, counter: nil)
|
||
|
TestRepo.insert!(%Comment{post_id: post_id, text: "comment"})
|
||
|
assert %{counter: nil} ==
|
||
|
TestRepo.one(from p in Post, join: c in assoc(p, :comments), where: p.title == "1", select: map(p, [:counter]))
|
||
|
end
|
||
|
|
||
|
test "field source" do
|
||
|
TestRepo.insert!(%Permalink{url: "url"})
|
||
|
assert ["url"] = Permalink |> select([p], p.url) |> TestRepo.all()
|
||
|
assert [1] = Permalink |> select([p], count(p.url)) |> TestRepo.all()
|
||
|
end
|
||
|
|
||
|
test "merge" do
|
||
|
date = Date.utc_today()
|
||
|
%Post{id: post_id} = TestRepo.insert!(%Post{title: "1", counter: nil, posted: date, public: false})
|
||
|
|
||
|
# Merge on source
|
||
|
assert [%Post{title: "2"}] =
|
||
|
Post |> select([p], merge(p, %{title: "2"})) |> TestRepo.all()
|
||
|
assert [%Post{title: "2"}] =
|
||
|
Post |> select([p], p) |> select_merge([p], %{title: "2"}) |> TestRepo.all()
|
||
|
|
||
|
# Merge on struct
|
||
|
assert [%Post{title: "2"}] =
|
||
|
Post |> select([p], merge(%Post{title: p.title}, %{title: "2"})) |> TestRepo.all()
|
||
|
assert [%Post{title: "2"}] =
|
||
|
Post |> select([p], %Post{title: p.title}) |> select_merge([p], %{title: "2"}) |> TestRepo.all()
|
||
|
|
||
|
# Merge on map
|
||
|
assert [%{title: "2"}] =
|
||
|
Post |> select([p], merge(%{title: p.title}, %{title: "2"})) |> TestRepo.all()
|
||
|
assert [%{title: "2"}] =
|
||
|
Post |> select([p], %{title: p.title}) |> select_merge([p], %{title: "2"}) |> TestRepo.all()
|
||
|
|
||
|
# Merge on outer join with map
|
||
|
%Permalink{} = TestRepo.insert!(%Permalink{post_id: post_id, url: "Q", title: "Z"})
|
||
|
|
||
|
# left join record is present
|
||
|
assert [%{url: "Q", title: "1", posted: _date}] =
|
||
|
Permalink
|
||
|
|> join(:left, [l], p in Post, on: l.post_id == p.id)
|
||
|
|> select([l, p], merge(l, map(p, ^~w(title posted)a)))
|
||
|
|> TestRepo.all()
|
||
|
|
||
|
assert [%{url: "Q", title: "1", posted: _date}] =
|
||
|
Permalink
|
||
|
|> join(:left, [l], p in Post, on: l.post_id == p.id)
|
||
|
|> select_merge([_l, p], map(p, ^~w(title posted)a))
|
||
|
|> TestRepo.all()
|
||
|
|
||
|
# left join record is not present
|
||
|
assert [%{url: "Q", title: "Z", posted: nil}] =
|
||
|
Permalink
|
||
|
|> join(:left, [l], p in Post, on: l.post_id == p.id and p.public == true)
|
||
|
|> select([l, p], merge(l, map(p, ^~w(title posted)a)))
|
||
|
|> TestRepo.all()
|
||
|
|
||
|
assert [%{url: "Q", title: "Z", posted: nil}] =
|
||
|
Permalink
|
||
|
|> join(:left, [l], p in Post, on: l.post_id == p.id and p.public == true)
|
||
|
|> select_merge([_l, p], map(p, ^~w(title posted)a))
|
||
|
|> TestRepo.all()
|
||
|
end
|
||
|
|
||
|
test "merge with update on self" do
|
||
|
%Post{} = TestRepo.insert!(%Post{title: "1", counter: 1})
|
||
|
|
||
|
assert [%Post{title: "1", counter: 2}] =
|
||
|
Post |> select([p], merge(p, %{p | counter: 2})) |> TestRepo.all()
|
||
|
assert [%Post{title: "1", counter: 2}] =
|
||
|
Post |> select([p], p) |> select_merge([p], %{p | counter: 2}) |> TestRepo.all()
|
||
|
end
|
||
|
|
||
|
test "merge within subquery" do
|
||
|
%Post{} = TestRepo.insert!(%Post{title: "1", counter: 1})
|
||
|
|
||
|
subquery =
|
||
|
Post
|
||
|
|> select_merge([p], %{p | counter: 2})
|
||
|
|> subquery()
|
||
|
|
||
|
assert [%Post{title: "1", counter: 2}] = TestRepo.all(subquery)
|
||
|
end
|
||
|
|
||
|
@tag :selected_as_with_group_by
|
||
|
test "selected_as/2 with group_by" do
|
||
|
TestRepo.insert!(%Post{posted: ~D[2020-12-21], visits: 3})
|
||
|
TestRepo.insert!(%Post{posted: ~D[2020-12-21], visits: 2})
|
||
|
TestRepo.insert!(%Post{posted: ~D[2020-12-20], visits: nil})
|
||
|
|
||
|
query =
|
||
|
from p in Post,
|
||
|
select: %{
|
||
|
posted: selected_as(p.posted, :date),
|
||
|
min_visits: p.visits |> coalesce(0) |> min()
|
||
|
},
|
||
|
group_by: selected_as(:date),
|
||
|
order_by: p.posted
|
||
|
|
||
|
assert [%{posted: ~D[2020-12-20], min_visits: 0}, %{posted: ~D[2020-12-21], min_visits: 2}] =
|
||
|
TestRepo.all(query)
|
||
|
end
|
||
|
|
||
|
@tag :selected_as_with_order_by
|
||
|
test "selected_as/2 with order_by" do
|
||
|
TestRepo.insert!(%Post{posted: ~D[2020-12-21], visits: 3})
|
||
|
TestRepo.insert!(%Post{posted: ~D[2020-12-21], visits: 2})
|
||
|
TestRepo.insert!(%Post{posted: ~D[2020-12-20], visits: nil})
|
||
|
|
||
|
base_query =
|
||
|
from p in Post,
|
||
|
select: %{
|
||
|
posted: p.posted,
|
||
|
min_visits: p.visits |> coalesce(0) |> min() |> selected_as(:min_visits)
|
||
|
},
|
||
|
group_by: p.posted
|
||
|
|
||
|
# ascending order
|
||
|
results = base_query |> order_by(selected_as(:min_visits)) |> TestRepo.all()
|
||
|
|
||
|
assert [%{posted: ~D[2020-12-20], min_visits: 0}, %{posted: ~D[2020-12-21], min_visits: 2}] =
|
||
|
results
|
||
|
|
||
|
# descending order
|
||
|
results = base_query |> order_by([desc: selected_as(:min_visits)]) |> TestRepo.all()
|
||
|
|
||
|
assert [%{posted: ~D[2020-12-21], min_visits: 2}, %{posted: ~D[2020-12-20], min_visits: 0}] =
|
||
|
results
|
||
|
end
|
||
|
|
||
|
@tag :selected_as_with_order_by
|
||
|
test "selected_as/2 respects custom types" do
|
||
|
TestRepo.insert!(%Post{title: "title1", visits: 1})
|
||
|
TestRepo.insert!(%Post{title: "title2"})
|
||
|
uuid = Ecto.UUID.generate()
|
||
|
|
||
|
query =
|
||
|
from p in Post,
|
||
|
select: %{
|
||
|
uuid: type(^uuid, Ecto.UUID) |> selected_as(:uuid),
|
||
|
visits: p.visits |> coalesce(0) |> selected_as(:visits)
|
||
|
},
|
||
|
order_by: [selected_as(:uuid), selected_as(:visits)]
|
||
|
|
||
|
assert [%{uuid: ^uuid, visits: 0}, %{uuid: ^uuid, visits: 1}] = TestRepo.all(query)
|
||
|
end
|
||
|
|
||
|
@tag :selected_as_with_order_by_expression
|
||
|
test "selected_as/2 with order_by expression" do
|
||
|
TestRepo.insert!(%Post{posted: ~D[2020-12-21], visits: 3, intensity: 2.0})
|
||
|
TestRepo.insert!(%Post{posted: ~D[2020-12-20], visits: nil, intensity: 10.0})
|
||
|
|
||
|
results =
|
||
|
from(p in Post,
|
||
|
select: %{
|
||
|
posted: p.posted,
|
||
|
visits: p.visits |> coalesce(0) |> selected_as(:num_visits),
|
||
|
intensity: selected_as(p.intensity, :strength)
|
||
|
},
|
||
|
order_by: [desc: (selected_as(:num_visits) + selected_as(:strength))]
|
||
|
)
|
||
|
|> TestRepo.all()
|
||
|
|
||
|
assert [%{posted: ~D[2020-12-20], visits: 0}, %{posted: ~D[2020-12-21], visits: 3}] =
|
||
|
results
|
||
|
end
|
||
|
|
||
|
@tag :selected_as_with_having
|
||
|
test "selected_as/2 with having" do
|
||
|
TestRepo.insert!(%Post{posted: ~D[2020-12-21], visits: 3})
|
||
|
TestRepo.insert!(%Post{posted: ~D[2020-12-21], visits: 2})
|
||
|
TestRepo.insert!(%Post{posted: ~D[2020-12-20], visits: nil})
|
||
|
|
||
|
results =
|
||
|
from(p in Post,
|
||
|
select: %{
|
||
|
posted: p.posted,
|
||
|
min_visits: p.visits |> coalesce(0) |> min() |> selected_as(:min_visits)
|
||
|
},
|
||
|
group_by: p.posted,
|
||
|
having: selected_as(:min_visits) > 0,
|
||
|
or_having: not(selected_as(:min_visits) > 0),
|
||
|
order_by: p.posted
|
||
|
)
|
||
|
|> TestRepo.all()
|
||
|
|
||
|
assert [%{posted: ~D[2020-12-20], min_visits: 0}, %{posted: ~D[2020-12-21], min_visits: 2}] = results
|
||
|
end
|
||
|
end
|
||
|
|
||
|
@tag :distinct_count
|
||
|
test "query count distinct" do
|
||
|
TestRepo.insert!(%Post{title: "1"})
|
||
|
TestRepo.insert!(%Post{title: "1"})
|
||
|
TestRepo.insert!(%Post{title: "2"})
|
||
|
|
||
|
assert [3] == Post |> select([p], count(p.title)) |> TestRepo.all
|
||
|
assert [2] == Post |> select([p], count(p.title, :distinct)) |> TestRepo.all
|
||
|
end
|
||
|
|
||
|
test "query where interpolation" do
|
||
|
post1 = TestRepo.insert!(%Post{title: "hello"})
|
||
|
post2 = TestRepo.insert!(%Post{title: "goodbye"})
|
||
|
|
||
|
assert [post1, post2] == Post |> where([], []) |> TestRepo.all |> Enum.sort_by(& &1.id)
|
||
|
assert [post1] == Post |> where([], [title: "hello"]) |> TestRepo.all
|
||
|
assert [post1] == Post |> where([], [title: "hello", id: ^post1.id]) |> TestRepo.all
|
||
|
|
||
|
params0 = []
|
||
|
params1 = [title: "hello"]
|
||
|
params2 = [title: "hello", id: post1.id]
|
||
|
assert [post1, post2] == (from Post, where: ^params0) |> TestRepo.all |> Enum.sort_by(& &1.id)
|
||
|
assert [post1] == (from Post, where: ^params1) |> TestRepo.all
|
||
|
assert [post1] == (from Post, where: ^params2) |> TestRepo.all
|
||
|
|
||
|
post3 = TestRepo.insert!(%Post{title: "goodbye", uuid: nil})
|
||
|
params3 = [title: "goodbye", uuid: post3.uuid]
|
||
|
assert [post3] == (from Post, where: ^params3) |> TestRepo.all
|
||
|
end
|
||
|
|
||
|
describe "upsert via insert" do
|
||
|
@describetag :upsert
|
||
|
|
||
|
test "on conflict raise" do
|
||
|
{:ok, inserted} = TestRepo.insert(%Post{title: "first"}, on_conflict: :raise)
|
||
|
assert catch_error(TestRepo.insert(%Post{id: inserted.id, title: "second"}, on_conflict: :raise))
|
||
|
end
|
||
|
|
||
|
test "on conflict ignore" do
|
||
|
post = %Post{title: "first", uuid: Ecto.UUID.generate()}
|
||
|
{:ok, inserted} = TestRepo.insert(post, on_conflict: :nothing)
|
||
|
assert inserted.id
|
||
|
assert inserted.__meta__.state == :loaded
|
||
|
|
||
|
{:ok, not_inserted} = TestRepo.insert(post, on_conflict: :nothing)
|
||
|
assert not_inserted.id == nil
|
||
|
assert not_inserted.__meta__.state == :loaded
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict and associations" do
|
||
|
on_conflict = [set: [title: "second"]]
|
||
|
post = %Post{uuid: Ecto.UUID.generate(),
|
||
|
title: "first", comments: [%Comment{}]}
|
||
|
{:ok, inserted} = TestRepo.insert(post, on_conflict: on_conflict, conflict_target: [:uuid])
|
||
|
assert inserted.id
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict with inc" do
|
||
|
uuid = "6fa459ea-ee8a-3ca4-894e-db77e160355e"
|
||
|
post = %Post{title: "first", uuid: uuid}
|
||
|
{:ok, _} = TestRepo.insert(post)
|
||
|
post = %{title: "upsert", uuid: uuid}
|
||
|
TestRepo.insert_all(Post, [post], on_conflict: [inc: [visits: 1]], conflict_target: :uuid)
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict ignore and conflict target" do
|
||
|
post = %Post{title: "first", uuid: Ecto.UUID.generate()}
|
||
|
{:ok, inserted} = TestRepo.insert(post, on_conflict: :nothing, conflict_target: [:uuid])
|
||
|
assert inserted.id
|
||
|
|
||
|
# Error on non-conflict target
|
||
|
assert catch_error(TestRepo.insert(post, on_conflict: :nothing, conflict_target: [:id]))
|
||
|
|
||
|
# Error on conflict target
|
||
|
{:ok, not_inserted} = TestRepo.insert(post, on_conflict: :nothing, conflict_target: [:uuid])
|
||
|
assert not_inserted.id == nil
|
||
|
end
|
||
|
|
||
|
@tag :without_conflict_target
|
||
|
test "on conflict keyword list" do
|
||
|
on_conflict = [set: [title: "second"]]
|
||
|
post = %Post{title: "first", uuid: Ecto.UUID.generate()}
|
||
|
{:ok, inserted} = TestRepo.insert(post, on_conflict: on_conflict)
|
||
|
assert inserted.id
|
||
|
|
||
|
{:ok, updated} = TestRepo.insert(post, on_conflict: on_conflict)
|
||
|
assert updated.id == inserted.id
|
||
|
assert updated.title != "second"
|
||
|
assert TestRepo.get!(Post, inserted.id).title == "second"
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict keyword list and conflict target" do
|
||
|
on_conflict = [set: [title: "second"]]
|
||
|
post = %Post{title: "first", uuid: Ecto.UUID.generate()}
|
||
|
{:ok, inserted} = TestRepo.insert(post, on_conflict: on_conflict, conflict_target: [:uuid])
|
||
|
assert inserted.id
|
||
|
|
||
|
# Error on non-conflict target
|
||
|
assert catch_error(TestRepo.insert(post, on_conflict: on_conflict, conflict_target: [:id]))
|
||
|
|
||
|
{:ok, updated} = TestRepo.insert(post, on_conflict: on_conflict, conflict_target: [:uuid])
|
||
|
assert updated.id == inserted.id
|
||
|
assert updated.title != "second"
|
||
|
assert TestRepo.get!(Post, inserted.id).title == "second"
|
||
|
end
|
||
|
|
||
|
@tag :returning
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict keyword list and conflict target and returning" do
|
||
|
{:ok, c1} = TestRepo.insert(%Post{})
|
||
|
{:ok, c2} = TestRepo.insert(%Post{id: c1.id}, on_conflict: [set: [id: c1.id]], conflict_target: [:id], returning: [:id, :uuid])
|
||
|
{:ok, c3} = TestRepo.insert(%Post{id: c1.id}, on_conflict: [set: [id: c1.id]], conflict_target: [:id], returning: true)
|
||
|
{:ok, c4} = TestRepo.insert(%Post{id: c1.id}, on_conflict: [set: [id: c1.id]], conflict_target: [:id], returning: false)
|
||
|
|
||
|
assert c2.uuid == c1.uuid
|
||
|
assert c3.uuid == c1.uuid
|
||
|
assert c4.uuid != c1.uuid
|
||
|
end
|
||
|
|
||
|
@tag :returning
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict keyword list and conflict target and returning and field source" do
|
||
|
TestRepo.insert!(%Permalink{url: "old"})
|
||
|
{:ok, c1} = TestRepo.insert(%Permalink{url: "old"},
|
||
|
on_conflict: [set: [url: "new1"]],
|
||
|
conflict_target: [:url],
|
||
|
returning: [:url])
|
||
|
|
||
|
TestRepo.insert!(%Permalink{url: "old"})
|
||
|
{:ok, c2} = TestRepo.insert(%Permalink{url: "old"},
|
||
|
on_conflict: [set: [url: "new2"]],
|
||
|
conflict_target: [:url],
|
||
|
returning: true)
|
||
|
|
||
|
assert c1.url == "new1"
|
||
|
assert c2.url == "new2"
|
||
|
end
|
||
|
|
||
|
@tag :returning
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict ignore and returning" do
|
||
|
post = %Post{title: "first", uuid: Ecto.UUID.generate()}
|
||
|
{:ok, inserted} = TestRepo.insert(post, on_conflict: :nothing, conflict_target: [:uuid])
|
||
|
assert inserted.id
|
||
|
|
||
|
{:ok, not_inserted} = TestRepo.insert(post, on_conflict: :nothing, conflict_target: [:uuid], returning: true)
|
||
|
assert not_inserted.id == nil
|
||
|
end
|
||
|
|
||
|
@tag :without_conflict_target
|
||
|
test "on conflict query" do
|
||
|
on_conflict = from Post, update: [set: [title: "second"]]
|
||
|
post = %Post{title: "first", uuid: Ecto.UUID.generate()}
|
||
|
{:ok, inserted} = TestRepo.insert(post, on_conflict: on_conflict)
|
||
|
assert inserted.id
|
||
|
|
||
|
{:ok, updated} = TestRepo.insert(post, on_conflict: on_conflict)
|
||
|
assert updated.id == inserted.id
|
||
|
assert updated.title != "second"
|
||
|
assert TestRepo.get!(Post, inserted.id).title == "second"
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict query and conflict target" do
|
||
|
on_conflict = from Post, update: [set: [title: "second"]]
|
||
|
post = %Post{title: "first", uuid: Ecto.UUID.generate()}
|
||
|
{:ok, inserted} = TestRepo.insert(post, on_conflict: on_conflict, conflict_target: [:uuid])
|
||
|
assert inserted.id
|
||
|
|
||
|
# Error on non-conflict target
|
||
|
assert catch_error(TestRepo.insert(post, on_conflict: on_conflict, conflict_target: [:id]))
|
||
|
|
||
|
{:ok, updated} = TestRepo.insert(post, on_conflict: on_conflict, conflict_target: [:uuid])
|
||
|
assert updated.id == inserted.id
|
||
|
assert updated.title != "second"
|
||
|
assert TestRepo.get!(Post, inserted.id).title == "second"
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict query having condition" do
|
||
|
post = %Post{title: "first", counter: 1, uuid: Ecto.UUID.generate()}
|
||
|
{:ok, inserted} = TestRepo.insert(post)
|
||
|
|
||
|
on_conflict = from Post, where: [counter: 2], update: [set: [title: "second"]]
|
||
|
|
||
|
insert_options = [
|
||
|
on_conflict: on_conflict,
|
||
|
conflict_target: [:uuid],
|
||
|
stale_error_field: :counter
|
||
|
]
|
||
|
|
||
|
assert {:error, changeset} = TestRepo.insert(post, insert_options)
|
||
|
assert changeset.errors == [counter: {"is stale", [stale: true]}]
|
||
|
|
||
|
assert TestRepo.get!(Post, inserted.id).title == "first"
|
||
|
end
|
||
|
|
||
|
@tag :without_conflict_target
|
||
|
test "on conflict replace_all" do
|
||
|
post = %Post{title: "first", visits: 13, uuid: Ecto.UUID.generate()}
|
||
|
{:ok, inserted} = TestRepo.insert(post, on_conflict: :replace_all)
|
||
|
assert inserted.id
|
||
|
|
||
|
post = %Post{title: "updated", visits: 17, uuid: post.uuid}
|
||
|
post = TestRepo.insert!(post, on_conflict: :replace_all)
|
||
|
assert post.id != inserted.id
|
||
|
assert post.title == "updated"
|
||
|
assert post.visits == 17
|
||
|
|
||
|
assert TestRepo.all(from p in Post, select: {p.id, p.title, p.visits}) ==
|
||
|
[{post.id, "updated", 17}]
|
||
|
assert TestRepo.all(from p in Post, select: count(p.id)) == [1]
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict replace_all and conflict target" do
|
||
|
post = %Post{title: "first", visits: 13, uuid: Ecto.UUID.generate()}
|
||
|
{:ok, inserted} = TestRepo.insert(post, on_conflict: :replace_all, conflict_target: :uuid)
|
||
|
assert inserted.id
|
||
|
|
||
|
post = %Post{title: "updated", visits: 17, uuid: post.uuid}
|
||
|
post = TestRepo.insert!(post, on_conflict: :replace_all, conflict_target: :uuid)
|
||
|
assert post.id != inserted.id
|
||
|
assert post.title == "updated"
|
||
|
assert post.visits == 17
|
||
|
|
||
|
assert TestRepo.all(from p in Post, select: {p.id, p.title, p.visits}) ==
|
||
|
[{post.id, "updated", 17}]
|
||
|
assert TestRepo.all(from p in Post, select: count(p.id)) == [1]
|
||
|
end
|
||
|
end
|
||
|
|
||
|
describe "upsert via insert_all" do
|
||
|
@describetag :upsert_all
|
||
|
|
||
|
test "on conflict raise" do
|
||
|
post = [title: "first", uuid: Ecto.UUID.generate()]
|
||
|
{1, nil} = TestRepo.insert_all(Post, [post], on_conflict: :raise)
|
||
|
assert catch_error(TestRepo.insert_all(Post, [post], on_conflict: :raise))
|
||
|
end
|
||
|
|
||
|
test "on conflict ignore" do
|
||
|
post = [title: "first", uuid: Ecto.UUID.generate()]
|
||
|
assert TestRepo.insert_all(Post, [post], on_conflict: :nothing) == {1, nil}
|
||
|
|
||
|
# PG returns 0, MySQL returns 1
|
||
|
{entries, nil} = TestRepo.insert_all(Post, [post], on_conflict: :nothing)
|
||
|
assert entries == 0 or entries == 1
|
||
|
|
||
|
assert length(TestRepo.all(Post)) == 1
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict ignore and conflict target" do
|
||
|
post = [title: "first", uuid: Ecto.UUID.generate()]
|
||
|
assert TestRepo.insert_all(Post, [post], on_conflict: :nothing, conflict_target: [:uuid]) ==
|
||
|
{1, nil}
|
||
|
|
||
|
# Error on non-conflict target
|
||
|
assert catch_error(TestRepo.insert_all(Post, [post], on_conflict: :nothing, conflict_target: [:id]))
|
||
|
|
||
|
# Error on conflict target
|
||
|
assert TestRepo.insert_all(Post, [post], on_conflict: :nothing, conflict_target: [:uuid]) ==
|
||
|
{0, nil}
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict keyword list and conflict target" do
|
||
|
on_conflict = [set: [title: "second"]]
|
||
|
post = [title: "first", uuid: Ecto.UUID.generate()]
|
||
|
{1, nil} = TestRepo.insert_all(Post, [post], on_conflict: on_conflict, conflict_target: [:uuid])
|
||
|
|
||
|
# Error on non-conflict target
|
||
|
assert catch_error(TestRepo.insert_all(Post, [post], on_conflict: on_conflict, conflict_target: [:id]))
|
||
|
|
||
|
# Error on conflict target
|
||
|
assert TestRepo.insert_all(Post, [post], on_conflict: on_conflict, conflict_target: [:uuid]) ==
|
||
|
{1, nil}
|
||
|
assert TestRepo.all(from p in Post, select: p.title) == ["second"]
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
@tag :returning
|
||
|
test "on conflict keyword list and conflict target and returning and source field" do
|
||
|
on_conflict = [set: [url: "new"]]
|
||
|
permalink = [url: "old"]
|
||
|
|
||
|
assert {1, [%Permalink{url: "old"}]} =
|
||
|
TestRepo.insert_all(Permalink, [permalink],
|
||
|
on_conflict: on_conflict, conflict_target: [:url], returning: [:url])
|
||
|
|
||
|
assert {1, [%Permalink{url: "new"}]} =
|
||
|
TestRepo.insert_all(Permalink, [permalink],
|
||
|
on_conflict: on_conflict, conflict_target: [:url], returning: [:url])
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict query and conflict target" do
|
||
|
on_conflict = from p in Post, where: p.id > ^0, update: [set: [title: "second"]]
|
||
|
post = [title: "first", uuid: Ecto.UUID.generate()]
|
||
|
assert TestRepo.insert_all(Post, [post], on_conflict: on_conflict, conflict_target: [:uuid]) ==
|
||
|
{1, nil}
|
||
|
|
||
|
# Error on non-conflict target
|
||
|
assert catch_error(TestRepo.insert_all(Post, [post], on_conflict: on_conflict, conflict_target: [:id]))
|
||
|
|
||
|
# Error on conflict target
|
||
|
assert TestRepo.insert_all(Post, [post], on_conflict: on_conflict, conflict_target: [:uuid]) ==
|
||
|
{1, nil}
|
||
|
assert TestRepo.all(from p in Post, select: p.title) == ["second"]
|
||
|
end
|
||
|
|
||
|
@tag :insert_select
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict query and insert select and conflict target" do
|
||
|
on_conflict = from p in Post, where: p.id > ^0, update: [set: [title: "second"]]
|
||
|
visits_value = from p in Post, where: p.public == ^true and p.id > ^0, select: p.visits, limit: 1
|
||
|
post = [title: "first", uuid: Ecto.UUID.generate(), visits: visits_value]
|
||
|
assert TestRepo.insert_all(Post, [post], on_conflict: on_conflict, conflict_target: [:uuid]) ==
|
||
|
{1, nil}
|
||
|
|
||
|
# Error on non-conflict target
|
||
|
assert catch_error(TestRepo.insert_all(Post, [post], on_conflict: on_conflict, conflict_target: [:id]))
|
||
|
|
||
|
# Error on conflict target
|
||
|
assert TestRepo.insert_all(Post, [post], on_conflict: on_conflict, conflict_target: [:uuid]) ==
|
||
|
{1, nil}
|
||
|
assert TestRepo.all(from p in Post, select: p.title) == ["second"]
|
||
|
end
|
||
|
|
||
|
@tag :returning
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict query and conflict target and returning" do
|
||
|
on_conflict = from Post, update: [set: [title: "second"]]
|
||
|
post = [title: "first", uuid: Ecto.UUID.generate()]
|
||
|
{1, [%{id: id}]} = TestRepo.insert_all(Post, [post], on_conflict: on_conflict,
|
||
|
conflict_target: [:uuid], returning: [:id])
|
||
|
|
||
|
# Error on non-conflict target
|
||
|
assert catch_error(TestRepo.insert_all(Post, [post], on_conflict: on_conflict,
|
||
|
conflict_target: [:id], returning: [:id]))
|
||
|
|
||
|
# Error on conflict target
|
||
|
{1, [%Post{id: ^id, title: "second"}]} =
|
||
|
TestRepo.insert_all(Post, [post], on_conflict: on_conflict,
|
||
|
conflict_target: [:uuid], returning: [:id, :title])
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "source (without an Ecto schema) on conflict query and conflict target" do
|
||
|
on_conflict = [set: [title: "second"]]
|
||
|
{:ok, uuid} = Ecto.UUID.dump(Ecto.UUID.generate())
|
||
|
post = [title: "first", uuid: uuid]
|
||
|
assert TestRepo.insert_all("posts", [post], on_conflict: on_conflict, conflict_target: [:uuid]) ==
|
||
|
{1, nil}
|
||
|
|
||
|
# Error on non-conflict target
|
||
|
assert catch_error(TestRepo.insert_all("posts", [post], on_conflict: on_conflict, conflict_target: [:id]))
|
||
|
|
||
|
# Error on conflict target
|
||
|
assert TestRepo.insert_all("posts", [post], on_conflict: on_conflict, conflict_target: [:uuid]) ==
|
||
|
{1, nil}
|
||
|
assert TestRepo.all(from p in Post, select: p.title) == ["second"]
|
||
|
end
|
||
|
|
||
|
@tag :without_conflict_target
|
||
|
test "on conflict replace_all" do
|
||
|
post_first = %Post{title: "first", public: true, uuid: Ecto.UUID.generate()}
|
||
|
post_second = %Post{title: "second", public: false, uuid: Ecto.UUID.generate()}
|
||
|
|
||
|
{:ok, post_first} = TestRepo.insert(post_first, on_conflict: :replace_all)
|
||
|
{:ok, post_second} = TestRepo.insert(post_second, on_conflict: :replace_all)
|
||
|
|
||
|
assert post_first.id
|
||
|
assert post_second.id
|
||
|
assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
|
||
|
|
||
|
# Multiple record change value: note IDS are also replaced
|
||
|
changes = [%{id: post_first.id + 2, title: "first_updated",
|
||
|
visits: 1, uuid: post_first.uuid},
|
||
|
%{id: post_second.id + 2, title: "second_updated",
|
||
|
visits: 2, uuid: post_second.uuid}]
|
||
|
|
||
|
TestRepo.insert_all(Post, changes, on_conflict: :replace_all)
|
||
|
assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
|
||
|
|
||
|
updated_first = TestRepo.get(Post, post_first.id + 2)
|
||
|
assert updated_first.title == "first_updated"
|
||
|
assert updated_first.visits == 1
|
||
|
|
||
|
updated_second = TestRepo.get(Post, post_second.id + 2)
|
||
|
assert updated_second.title == "second_updated"
|
||
|
assert updated_second.visits == 2
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict replace_all and conflict_target" do
|
||
|
post_first = %Post{title: "first", public: true, uuid: Ecto.UUID.generate()}
|
||
|
post_second = %Post{title: "second", public: false, uuid: Ecto.UUID.generate()}
|
||
|
|
||
|
{:ok, post_first} = TestRepo.insert(post_first, on_conflict: :replace_all, conflict_target: :uuid)
|
||
|
{:ok, post_second} = TestRepo.insert(post_second, on_conflict: :replace_all, conflict_target: :uuid)
|
||
|
|
||
|
assert post_first.id
|
||
|
assert post_second.id
|
||
|
assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
|
||
|
|
||
|
# Multiple record change value: note IDS are also replaced
|
||
|
changes = [%{id: post_second.id + 1, title: "first_updated",
|
||
|
visits: 1, uuid: post_first.uuid},
|
||
|
%{id: post_second.id + 2, title: "second_updated",
|
||
|
visits: 2, uuid: post_second.uuid}]
|
||
|
|
||
|
TestRepo.insert_all(Post, changes, on_conflict: :replace_all, conflict_target: :uuid)
|
||
|
assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
|
||
|
|
||
|
updated_first = TestRepo.get(Post, post_second.id + 1)
|
||
|
assert updated_first.title == "first_updated"
|
||
|
assert updated_first.visits == 1
|
||
|
|
||
|
updated_second = TestRepo.get(Post, post_second.id + 2)
|
||
|
assert updated_second.title == "second_updated"
|
||
|
assert updated_second.visits == 2
|
||
|
end
|
||
|
|
||
|
@tag :without_conflict_target
|
||
|
test "on conflict replace_all_except" do
|
||
|
post_first = %Post{title: "first", public: true, uuid: Ecto.UUID.generate()}
|
||
|
post_second = %Post{title: "second", public: false, uuid: Ecto.UUID.generate()}
|
||
|
|
||
|
{:ok, post_first} = TestRepo.insert(post_first, on_conflict: {:replace_all_except, [:id]})
|
||
|
{:ok, post_second} = TestRepo.insert(post_second, on_conflict: {:replace_all_except, [:id]})
|
||
|
|
||
|
assert post_first.id
|
||
|
assert post_second.id
|
||
|
assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
|
||
|
|
||
|
# Multiple record change value: note IDS are not replaced
|
||
|
changes = [%{id: post_first.id + 2, title: "first_updated",
|
||
|
visits: 1, uuid: post_first.uuid},
|
||
|
%{id: post_second.id + 2, title: "second_updated",
|
||
|
visits: 2, uuid: post_second.uuid}]
|
||
|
|
||
|
TestRepo.insert_all(Post, changes, on_conflict: {:replace_all_except, [:id]})
|
||
|
assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
|
||
|
|
||
|
updated_first = TestRepo.get(Post, post_first.id)
|
||
|
assert updated_first.title == "first_updated"
|
||
|
assert updated_first.visits == 1
|
||
|
|
||
|
updated_second = TestRepo.get(Post, post_second.id)
|
||
|
assert updated_second.title == "second_updated"
|
||
|
assert updated_second.visits == 2
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict replace_all_except and conflict_target" do
|
||
|
post_first = %Post{title: "first", public: true, uuid: Ecto.UUID.generate()}
|
||
|
post_second = %Post{title: "second", public: false, uuid: Ecto.UUID.generate()}
|
||
|
|
||
|
{:ok, post_first} = TestRepo.insert(post_first, on_conflict: {:replace_all_except, [:id]}, conflict_target: :uuid)
|
||
|
{:ok, post_second} = TestRepo.insert(post_second, on_conflict: {:replace_all_except, [:id]}, conflict_target: :uuid)
|
||
|
|
||
|
assert post_first.id
|
||
|
assert post_second.id
|
||
|
assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
|
||
|
|
||
|
# Multiple record change value: note IDS are not replaced
|
||
|
changes = [%{id: post_first.id + 2, title: "first_updated",
|
||
|
visits: 1, uuid: post_first.uuid},
|
||
|
%{id: post_second.id + 2, title: "second_updated",
|
||
|
visits: 2, uuid: post_second.uuid}]
|
||
|
|
||
|
TestRepo.insert_all(Post, changes, on_conflict: {:replace_all_except, [:id]}, conflict_target: :uuid)
|
||
|
assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
|
||
|
|
||
|
updated_first = TestRepo.get(Post, post_first.id)
|
||
|
assert updated_first.title == "first_updated"
|
||
|
assert updated_first.visits == 1
|
||
|
|
||
|
updated_second = TestRepo.get(Post, post_second.id)
|
||
|
assert updated_second.title == "second_updated"
|
||
|
assert updated_second.visits == 2
|
||
|
end
|
||
|
|
||
|
@tag :with_conflict_target
|
||
|
test "on conflict replace and conflict_target" do
|
||
|
post_first = %Post{title: "first", visits: 10, public: true, uuid: Ecto.UUID.generate()}
|
||
|
post_second = %Post{title: "second", visits: 20, public: false, uuid: Ecto.UUID.generate()}
|
||
|
|
||
|
{:ok, post_first} = TestRepo.insert(post_first, on_conflict: {:replace, [:title, :visits]}, conflict_target: :uuid)
|
||
|
{:ok, post_second} = TestRepo.insert(post_second, on_conflict: {:replace, [:title, :visits]}, conflict_target: :uuid)
|
||
|
|
||
|
assert post_first.id
|
||
|
assert post_second.id
|
||
|
assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
|
||
|
|
||
|
# Multiple record change value: note `public` field is not changed
|
||
|
changes = [%{id: post_first.id, title: "first_updated", visits: 11, public: false, uuid: post_first.uuid},
|
||
|
%{id: post_second.id, title: "second_updated", visits: 21, public: true, uuid: post_second.uuid}]
|
||
|
|
||
|
TestRepo.insert_all(Post, changes, on_conflict: {:replace, [:title, :visits]}, conflict_target: :uuid)
|
||
|
assert TestRepo.all(from p in Post, select: count(p.id)) == [2]
|
||
|
|
||
|
updated_first = TestRepo.get(Post, post_first.id)
|
||
|
assert updated_first.title == "first_updated"
|
||
|
assert updated_first.visits == 11
|
||
|
assert updated_first.public == true
|
||
|
|
||
|
updated_second = TestRepo.get(Post, post_second.id)
|
||
|
assert updated_second.title == "second_updated"
|
||
|
assert updated_second.visits == 21
|
||
|
assert updated_second.public == false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
describe "values list" do
|
||
|
@describetag :values_list
|
||
|
|
||
|
test "all" do
|
||
|
uuid_module = uuid_module(TestRepo.__adapter__())
|
||
|
uuid = uuid_module.generate()
|
||
|
|
||
|
# Without select
|
||
|
values = [%{bid: uuid, visits: 1}, %{bid: uuid, visits: 2}]
|
||
|
types = %{bid: uuid_module, visits: :integer}
|
||
|
query = from v in values(values, types)
|
||
|
assert TestRepo.all(query) == values
|
||
|
|
||
|
# With select
|
||
|
query = select(query, [v], {v, v.bid})
|
||
|
assert TestRepo.all(query) == Enum.map(values, &{&1, &1.bid})
|
||
|
end
|
||
|
|
||
|
test "all with join" do
|
||
|
uuid_module = uuid_module(TestRepo.__adapter__())
|
||
|
uuid = uuid_module.generate()
|
||
|
|
||
|
values1 = [%{bid: uuid, visits: 1}, %{bid: uuid, visits: 2}]
|
||
|
values2 = [%{bid: uuid, visits: 1}]
|
||
|
types = %{bid: uuid_module, visits: :integer}
|
||
|
|
||
|
query =
|
||
|
from v1 in values(values1, types),
|
||
|
join: v2 in values(values2, types),
|
||
|
on: v1.visits == v2.visits
|
||
|
|
||
|
assert TestRepo.all(query) == [%{bid: uuid, visits: 1}]
|
||
|
end
|
||
|
|
||
|
test "delete_all" do
|
||
|
uuid_module = uuid_module(TestRepo.__adapter__())
|
||
|
uuid = uuid_module.generate()
|
||
|
|
||
|
_p1 = TestRepo.insert!(%Post{bid: uuid, visits: 1})
|
||
|
p2 = TestRepo.insert!(%Post{bid: uuid, visits: 5})
|
||
|
|
||
|
values = [%{bid: uuid, visits: 1}, %{bid: nil, visits: 1}, %{bid: uuid, visits: 3}]
|
||
|
types = %{bid: uuid_module, visits: :integer}
|
||
|
|
||
|
query =
|
||
|
from p in Post,
|
||
|
join: v in values(values, types),
|
||
|
on: p.visits == v.visits
|
||
|
|
||
|
assert {1, _} = TestRepo.delete_all(query)
|
||
|
assert TestRepo.all(Post) == [p2]
|
||
|
end
|
||
|
|
||
|
test "update_all" do
|
||
|
uuid_module = uuid_module(TestRepo.__adapter__())
|
||
|
uuid = uuid_module.generate()
|
||
|
|
||
|
TestRepo.insert!(%Post{bid: uuid, visits: 1})
|
||
|
|
||
|
values = [%{bid: uuid, visits: 10}, %{bid: nil, visits: 2}]
|
||
|
types = %{bid: uuid_module, visits: :integer}
|
||
|
|
||
|
query =
|
||
|
from p in Post,
|
||
|
join: v in values(values, types),
|
||
|
on: p.bid == v.bid,
|
||
|
update: [set: [visits: v.visits]]
|
||
|
|
||
|
assert {1, _} = TestRepo.update_all(query, [])
|
||
|
assert [%{visits: 10}] = TestRepo.all(Post)
|
||
|
end
|
||
|
|
||
|
defp uuid_module(Ecto.Adapters.Tds), do: Tds.Ecto.UUID
|
||
|
defp uuid_module(_), do: Ecto.UUID
|
||
|
end
|
||
|
end
|