This commit is contained in:
2026-07-14 22:50:46 +04:00
parent 27143319e3
commit bd19e0682b
3116 changed files with 467189 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
defmodule Ecto.Integration.AlterTest do
use Ecto.Integration.Case, async: false
alias Ecto.Integration.PoolRepo
defmodule AlterMigrationOne do
use Ecto.Migration
def up do
create table(:alter_col_type) do
add :value, :integer
end
execute "INSERT INTO alter_col_type (value) VALUES (1)"
end
def down do
drop table(:alter_col_type)
end
end
defmodule AlterMigrationTwo do
use Ecto.Migration
def up do
alter table(:alter_col_type) do
modify :value, :numeric
end
end
def down do
alter table(:alter_col_type) do
modify :value, :integer
end
end
end
import Ecto.Query, only: [from: 1, from: 2]
defp run(direction, repo, module) do
Ecto.Migration.Runner.run(repo, repo.config(), 1, module, :forward, direction, direction, log: false)
end
test "reset cache on returning query after alter column type" do
values = from v in "alter_col_type", select: v.value
assert :ok == run(:up, PoolRepo, AlterMigrationOne)
assert PoolRepo.all(values) == [1]
assert :ok == run(:up, PoolRepo, AlterMigrationTwo)
[%Decimal{}] = PoolRepo.all(values)
PoolRepo.transaction(fn() ->
assert [%Decimal{}] = PoolRepo.all(values)
assert :ok == run(:down, PoolRepo, AlterMigrationTwo)
# Optionally fail once with database error when
# already prepared on connection (and clear cache)
try do
PoolRepo.all(values, [mode: :savepoint])
rescue
_ ->
assert PoolRepo.all(values) == [1]
else
result ->
assert result == [1]
end
end)
after
assert :ok == run(:down, PoolRepo, AlterMigrationOne)
end
test "reset cache on parameterized query after alter column type" do
values = from v in "alter_col_type"
assert :ok == run(:up, PoolRepo, AlterMigrationOne)
assert PoolRepo.update_all(values, [set: [value: 2]]) == {1, nil}
assert :ok == run(:up, PoolRepo, AlterMigrationTwo)
assert PoolRepo.update_all(values, [set: [value: 3]]) == {1, nil}
PoolRepo.transaction(fn() ->
assert PoolRepo.update_all(values, [set: [value: Decimal.new(5)]]) == {1, nil}
assert :ok == run(:down, PoolRepo, AlterMigrationTwo)
assert PoolRepo.update_all(values, [set: [value: 6]]) == {1, nil}
end)
after
assert :ok == run(:down, PoolRepo, AlterMigrationOne)
end
end

View File

@@ -0,0 +1,59 @@
defmodule Ecto.Integration.LockTest do
# We can keep this test async as long as it
# is the only one accessing the lock_test table.
use ExUnit.Case, async: true
import Ecto.Query
alias Ecto.Integration.PoolRepo
defmodule LockCounter do
use Ecto.Schema
schema "lock_counters" do
field :count, :integer
end
end
setup do
PoolRepo.delete_all(LockCounter)
:ok
end
test "lock for update" do
%{id: id} = PoolRepo.insert!(%LockCounter{count: 1})
pid = self()
lock_for_update =
Application.get_env(:ecto_sql, :lock_for_update) ||
raise ":lock_for_update not set in :ecto application"
# Here we are manually inserting the lock in the query
# to test multiple adapters. Never do this in actual
# application code: it is not safe and not public.
query = from(lc in LockCounter, where: lc.id == ^id)
query = %{query | lock: lock_for_update}
{:ok, new_pid} =
Task.start_link fn ->
assert_receive :select_for_update, 5000
PoolRepo.transaction(fn ->
[post] = PoolRepo.all(query) # this should block until the other trans. commit
post |> Ecto.Changeset.change(count: post.count + 1) |> PoolRepo.update!
end)
send pid, :updated
end
PoolRepo.transaction(fn ->
[post] = PoolRepo.all(query) # select and lock the row
send new_pid, :select_for_update # signal second process to begin a transaction
post |> Ecto.Changeset.change(count: post.count + 1) |> PoolRepo.update!
end)
assert_receive :updated, 5000
# Final count will be 3 if SELECT ... FOR UPDATE worked and 2 otherwise
assert [%LockCounter{count: 3}] = PoolRepo.all(LockCounter)
end
end

View File

@@ -0,0 +1,857 @@
defmodule Ecto.Integration.LoggingTest do
use Ecto.Integration.Case, async: true
alias Ecto.Integration.TestRepo
alias Ecto.Integration.PoolRepo
alias Ecto.Integration.{Post, Logging, ArrayLogging}
import ExUnit.CaptureLog
import Ecto.Query, only: [from: 2]
describe "telemetry" do
test "dispatches event" do
log = fn event_name, measurements, metadata ->
assert Enum.at(event_name, -1) == :query
assert %{result: {:ok, _res}} = metadata
assert measurements.total_time ==
measurements.query_time + measurements.decode_time + measurements.queue_time
assert measurements.idle_time
send(self(), :logged)
end
Process.put(:telemetry, log)
_ = PoolRepo.all(Post)
assert_received :logged
end
test "dispatches event with stacktrace" do
log = fn _event_name, _measurements, metadata ->
assert %{stacktrace: [_ | _]} = metadata
send(self(), :logged)
end
Process.put(:telemetry, log)
_ = PoolRepo.all(Post, stacktrace: true)
assert_received :logged
end
test "dispatches event with custom options" do
log = fn event_name, _measurements, metadata ->
assert Enum.at(event_name, -1) == :query
assert metadata.options == [:custom_metadata]
send(self(), :logged)
end
Process.put(:telemetry, log)
_ = PoolRepo.all(Post, telemetry_options: [:custom_metadata])
assert_received :logged
end
test "dispatches under another event name" do
log = fn [:custom], measurements, metadata ->
assert %{result: {:ok, _res}} = metadata
assert measurements.total_time ==
measurements.query_time + measurements.decode_time + measurements.queue_time
assert measurements.idle_time
send(self(), :logged)
end
Process.put(:telemetry, log)
_ = PoolRepo.all(Post, telemetry_event: [:custom])
assert_received :logged
end
test "is not dispatched with no event name" do
Process.put(:telemetry, fn _, _ -> raise "never called" end)
_ = TestRepo.all(Post, telemetry_event: nil)
refute_received :logged
end
test "cast params" do
uuid_module =
if TestRepo.__adapter__() == Ecto.Adapters.Tds do
Tds.Ecto.UUID
else
Ecto.UUID
end
uuid = uuid_module.generate()
dumped_uuid = uuid_module.dump!(uuid)
log = fn _event_name, _measurements, metadata ->
assert [dumped_uuid] == metadata.params
assert [uuid] == metadata.cast_params
send(self(), :logged)
end
Process.put(:telemetry, log)
TestRepo.all(from l in Logging, where: l.uuid == ^uuid)
assert_received :logged
end
end
describe "logs" do
@stacktrace_opts [stacktrace: true, log: :error]
defp stacktrace_entry(line) do
~r/↳ anonymous fn\/0 in Ecto.Integration.LoggingTest.\"test logs includes stacktraces\"\/1, at: .*integration_test\/sql\/logging.exs:#{line - 3}/
end
test "when some measurements are nil" do
assert capture_log(fn -> TestRepo.query("BEG", [], log: :error) end) =~
"[error]"
end
test "includes stacktraces" do
assert capture_log(fn ->
TestRepo.all(Post, @stacktrace_opts)
:ok
end) =~ stacktrace_entry(__ENV__.line)
assert capture_log(fn ->
TestRepo.insert(%Post{}, @stacktrace_opts)
:ok
end) =~ stacktrace_entry(__ENV__.line)
assert capture_log(fn ->
# Test cascading options
Ecto.Multi.new()
|> Ecto.Multi.insert(:post, %Post{})
|> TestRepo.transaction(@stacktrace_opts)
:ok
end) =~ stacktrace_entry(__ENV__.line)
assert capture_log(fn ->
# In theory we should point to the call _inside_ run
# but all multi calls point to the transaction starting point.
Ecto.Multi.new()
|> Ecto.Multi.run(:all, fn _, _ -> {:ok, TestRepo.all(Post, @stacktrace_opts)} end)
|> TestRepo.transaction()
:ok
end) =~ stacktrace_entry(__ENV__.line)
out = capture_log(fn ->
TestRepo.all(Post, Keyword.put(@stacktrace_opts, :log_stacktrace_mfa, {Ecto.Adapters.SQL, :first_non_ecto_stacktrace, [2]}))
:ok
end)
assert out =~ stacktrace_entry(__ENV__.line - 2)
# We are a bit liberal with what we expect as we don't want to tie to internal ExUnit code
assert out =~ ~r/ ExUnit.CaptureLog.*/
end
test "with custom log level" do
assert capture_log(fn -> TestRepo.insert!(%Post{title: "1"}, log: :error) end) =~
"[error]"
# We cannot assert on the result because it depends on the suite log level
capture_log(fn ->
TestRepo.insert!(%Post{title: "1"}, log: true)
end)
# But this assertion is always true
assert capture_log(fn ->
TestRepo.insert!(%Post{title: "1"}, log: false)
end) == ""
end
test "with a log: true override when logging is disabled" do
refute capture_log(fn ->
TestRepo.insert!(%Post{title: "1"}, log: true)
end) =~ "an exception was raised logging"
end
test "with unspecified :log option when logging is disabled" do
refute capture_log(fn ->
TestRepo.insert!(%Post{title: "1"})
end) =~ "an exception was raised logging"
end
end
describe "parameter logging" do
@describetag :parameter_logging
@uuid_regex ~r/[0-9a-f]{2}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
@naive_datetime_regex ~r/~N\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\]/
test "for insert_all with query" do
# Source query
int = 1
uuid = Ecto.UUID.generate()
source_query =
from l in Logging,
where: l.int == ^int and l.uuid == ^uuid,
select: %{uuid: l.uuid, int: l.int}
# Ensure parameters are complete and in correct order
log =
capture_log(fn ->
TestRepo.insert_all(Logging, source_query, log: :info)
end)
param_regex = ~r/\[(?<int>.+), \"(?<uuid>.+)\"\]/
param_logs = Regex.named_captures(param_regex, log)
# Query parameters
assert param_logs["int"] == Integer.to_string(int)
assert param_logs["uuid"] == uuid
end
@tag :insert_select
test "for insert_all with entries" do
# Row 1
int = 1
uuid = Ecto.UUID.generate()
uuid_query = from l in Logging, where: l.int == ^int and l.uuid == ^uuid, select: l.uuid
datetime = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
row1 = [
int: int,
uuid: uuid_query,
inserted_at: datetime,
updated_at: datetime
]
# Row 2
int2 = 2
uuid2 = Ecto.UUID.generate()
int_query = from l in Logging, where: l.int == ^int2 and l.uuid == ^uuid2, select: l.int
datetime2 = NaiveDateTime.add(datetime, 1)
row2 = [
int: int_query,
uuid: uuid2,
inserted_at: datetime2,
updated_at: datetime2
]
# Extract the parameters from the log:
# 1. Remove the colour codes
# 2. Remove the log level
# 3. Capture everything inside of the square brackets
log =
capture_log(fn ->
TestRepo.insert_all(Logging, [row1, row2], log: :info)
end)
log = Regex.replace(~r/\e\[[0-9]+m/, log, "")
log = Regex.replace(~r/\[info\]/, log, "")
log = Regex.named_captures(~r/\[(?<params>.+)\]/, log)
log_params = String.split(log["params"], ",")
# Compute the expected parameters in the right order.
# This involves recreating the headers in the same order
# as `insert_all`. The user values come first and then
# the autogenerated id
headers =
row1
|> Enum.reduce(%{}, fn {field, _}, headers -> Map.put(headers, field, true) end)
|> Map.put(:bid, true)
|> Map.keys()
row1_regex = [
int: "#{int}",
uuid: ["#{int}", uuid],
inserted_at: inspect(datetime),
updated_at: inspect(datetime),
bid: @uuid_regex
]
row2_regex = [
int: ["#{int2}", uuid2],
uuid: uuid2,
inserted_at: inspect(datetime2),
updated_at: inspect(datetime2),
bid: @uuid_regex
]
expected_param_regex =
Enum.flat_map([row1_regex, row2_regex], fn row ->
Enum.flat_map(headers, fn field ->
case Keyword.get(row, field) do
params when is_list(params) -> params
param -> [param]
end
end)
end)
assert length(log_params) == length(expected_param_regex)
Enum.zip(log_params, expected_param_regex)
|> Enum.each(fn {log, expected_regex} ->
assert log =~ expected_regex
end)
end
@tag :insert_select
@tag :placeholders
test "for insert_all with entries and placeholders" do
# Placeholders
datetime = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
datetime2 = NaiveDateTime.add(datetime, 1)
placeholder_map = %{datetime: datetime, datetime2: datetime2}
# Row 1
int = 1
uuid = Ecto.UUID.generate()
uuid_query = from l in Logging, where: l.int == ^int and l.uuid == ^uuid, select: l.uuid
row1 = [
int: int,
uuid: uuid_query,
inserted_at: {:placeholder, :datetime},
updated_at: {:placeholder, :datetime}
]
# Row 2
int2 = 2
uuid2 = Ecto.UUID.generate()
int_query = from l in Logging, where: l.int == ^int2 and l.uuid == ^uuid2, select: l.int
row2 = [
int: int_query,
uuid: uuid2,
inserted_at: {:placeholder, :datetime2},
updated_at: {:placeholder, :datetime2}
]
# Extract the parameters from the log:
# 1. Remove the colour codes
# 2. Remove the log level
# 3. Capture everything inside of the square brackets
log =
capture_log(fn ->
TestRepo.insert_all(Logging, [row1, row2],
placeholders: placeholder_map,
log: :info
)
end)
log = Regex.replace(~r/\e\[[0-9]+m/, log, "")
log = Regex.replace(~r/\[info\]/, log, "")
log = Regex.named_captures(~r/\[(?<params>.+)\]/, log)
log_params = String.split(log["params"], ",")
# Compute the expected parameters in the right order.
# This involves recreating the headers in the same order
# as `insert_all`. The placeholders come first and then
# the user values and then the autogenerated id
headers =
row1
|> Enum.reduce(%{}, fn {field, _}, headers -> Map.put(headers, field, true) end)
|> Map.put(:bid, true)
|> Map.drop([:inserted_at, :updated_at])
|> Map.keys()
row1_regex = [
int: "#{int}",
uuid: ["#{int}", uuid],
bid: @uuid_regex
]
row2_regex = [
int: ["#{int2}", uuid2],
uuid: uuid2,
bid: @uuid_regex
]
row_param_regex =
Enum.flat_map([row1_regex, row2_regex], fn row ->
Enum.flat_map(headers, fn field ->
case Keyword.get(row, field) do
params when is_list(params) -> params
param -> [param]
end
end)
end)
placeholder_regex = [inspect(datetime), inspect(datetime2)]
expected_param_regex = placeholder_regex ++ row_param_regex
assert length(log_params) == length(expected_param_regex)
Enum.zip(log_params, expected_param_regex)
|> Enum.each(fn {log, expected_regex} ->
assert log =~ expected_regex
end)
end
@tag :with_conflict_target
test "for insert_all with query with conflict query" do
# Source query
int = 1
uuid = Ecto.UUID.generate()
source_query =
from l in Logging,
where: l.int == ^int and l.uuid == ^uuid,
select: %{uuid: l.uuid, int: l.int}
# Conflict query
conflict_int = 0
conflict_uuid = Ecto.UUID.generate()
conflict_update = 2
conflict_query =
from l in Logging,
where: l.int == ^conflict_int and l.uuid == ^conflict_uuid,
update: [set: [int: ^conflict_update]]
# Ensure parameters are complete and in correct order
log =
capture_log(fn ->
TestRepo.insert_all(Logging, source_query,
on_conflict: conflict_query,
conflict_target: :bid,
log: :info
)
end)
param_regex =
~r/\[(?<int>.+), \"(?<uuid>.+)\", (?<conflict_update>.+), (?<conflict_int>.+), \"(?<conflict_uuid>.+)\"\]/
param_logs = Regex.named_captures(param_regex, log)
# Query parameters
assert param_logs["int"] == Integer.to_string(int)
assert param_logs["uuid"] == uuid
# Conflict query parameters
assert param_logs["conflict_update"] == Integer.to_string(conflict_update)
assert param_logs["conflict_int"] == Integer.to_string(conflict_int)
assert param_logs["conflict_uuid"] == conflict_uuid
end
@tag :insert_select
@tag :with_conflict_target
test "for insert_all with entries conflict query" do
# Row 1
int = 1
uuid = Ecto.UUID.generate()
uuid_query = from l in Logging, where: l.int == ^int and l.uuid == ^uuid, select: l.uuid
datetime = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
row1 = [
int: int,
uuid: uuid_query,
inserted_at: datetime,
updated_at: datetime
]
# Row 2
int2 = 2
uuid2 = Ecto.UUID.generate()
int_query = from l in Logging, where: l.int == ^int2 and l.uuid == ^uuid2, select: l.int
datetime2 = NaiveDateTime.add(datetime, 1)
row2 = [
int: int_query,
uuid: uuid2,
inserted_at: datetime2,
updated_at: datetime2
]
# Conflict query
conflict_int = 0
conflict_uuid = Ecto.UUID.generate()
conflict_update = 2
conflict_query =
from l in Logging,
where: l.int == ^conflict_int and l.uuid == ^conflict_uuid,
update: [set: [int: ^conflict_update]]
# Extract the parameters from the log:
# 1. Remove the colour codes
# 2. Remove the log level
# 3. Capture everything inside of the square brackets
log =
capture_log(fn ->
TestRepo.insert_all(Logging, [row1, row2],
on_conflict: conflict_query,
conflict_target: :bid,
log: :info
)
end)
log = Regex.replace(~r/\e\[[0-9]+m/, log, "")
log = Regex.replace(~r/\[info\]/, log, "")
log = Regex.named_captures(~r/\[(?<params>.+)\]/, log)
log_params = String.split(log["params"], ",")
# Compute the expected parameters in the right order.
# This involves recreating the headers in the same order
# as `insert_all`. The user values come first, then
# the autogenerated id and then the conflict params
headers =
row1
|> Enum.reduce(%{}, fn {field, _}, headers -> Map.put(headers, field, true) end)
|> Map.put(:bid, true)
|> Map.keys()
row1_regex = [
int: "#{int}",
uuid: ["#{int}", uuid],
bid: @uuid_regex,
inserted_at: inspect(datetime),
updated_at: inspect(datetime)
]
row2_regex = [
int: ["#{int2}", uuid2],
uuid: uuid2,
bid: @uuid_regex,
inserted_at: inspect(datetime2),
updated_at: inspect(datetime2)
]
row_param_regex =
Enum.flat_map([row1_regex, row2_regex], fn row ->
Enum.flat_map(headers, fn field ->
case Keyword.get(row, field) do
row_params when is_list(row_params) -> row_params
row_param -> [row_param]
end
end)
end)
conflict_param_regex = ["#{conflict_update}", "#{conflict_int}", conflict_uuid]
expected_param_regex = row_param_regex ++ conflict_param_regex
assert length(log_params) == length(expected_param_regex)
Enum.zip(log_params, expected_param_regex)
|> Enum.each(fn {log, expected_regex} ->
assert log =~ expected_regex
end)
end
@tag :insert_select
@tag :placeholders
@tag :with_conflict_target
test "for insert_all with entries, placeholders and conflict query" do
# Row 1
int = 1
uuid = Ecto.UUID.generate()
uuid_query = from l in Logging, where: l.int == ^int and l.uuid == ^uuid, select: l.uuid
row1 = [
int: int,
uuid: uuid_query,
inserted_at: {:placeholder, :datetime},
updated_at: {:placeholder, :datetime2}
]
# Row 2
int2 = 2
uuid2 = Ecto.UUID.generate()
int_query = from l in Logging, where: l.int == ^int2 and l.uuid == ^uuid2, select: l.int
row2 = [
int: int_query,
uuid: uuid2,
inserted_at: {:placeholder, :datetime},
updated_at: {:placeholder, :datetime2}
]
# Placeholders
datetime = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
datetime2 = NaiveDateTime.add(datetime, 1)
placeholder_map = %{datetime: datetime, datetime2: datetime2}
# Conflict query
conflict_int = 0
conflict_uuid = Ecto.UUID.generate()
conflict_update = 2
conflict_query =
from l in Logging,
where: l.int == ^conflict_int and l.uuid == ^conflict_uuid,
update: [set: [int: ^conflict_update]]
# Extract the parameters from the log:
# 1. Remove the colour codes
# 2. Remove the log level
# 3. Capture everything inside of the square brackets
log =
capture_log(fn ->
TestRepo.insert_all(Logging, [row1, row2],
placeholders: placeholder_map,
on_conflict: conflict_query,
conflict_target: :bid,
log: :info
)
end)
log = Regex.replace(~r/\e\[[0-9]+m/, log, "")
log = Regex.replace(~r/\[info\]/, log, "")
log = Regex.named_captures(~r/\[(?<params>.+)\]/, log)
log_params = String.split(log["params"], ",")
# Compute the expected parameters in the right order.
# This involves recreating the headers in the same order
# as `insert_all`. The placeholders come first, then the
# user value, then the autogenerated id and then the conflict
# params
headers =
row1
|> Enum.reduce(%{}, fn {field, _}, headers -> Map.put(headers, field, true) end)
|> Map.put(:bid, true)
|> Map.drop([:inserted_at, :updated_at])
|> Map.keys()
row1_regex = [
int: "#{int}",
uuid: ["#{int}", uuid],
bid: @uuid_regex
]
row2_regex = [
int: ["#{int2}", uuid2],
uuid: uuid2,
bid: @uuid_regex
]
row_param_regex =
Enum.flat_map([row1_regex, row2_regex], fn row ->
Enum.flat_map(headers, fn field ->
case Keyword.get(row, field) do
row_params when is_list(row_params) -> row_params
row_param -> [row_param]
end
end)
end)
placeholder_regex = [inspect(datetime), inspect(datetime2)]
conflict_param_regex = ["#{conflict_update}", "#{conflict_int}", conflict_uuid]
expected_param_regex = placeholder_regex ++ row_param_regex ++ conflict_param_regex
assert length(log_params) == length(expected_param_regex)
Enum.zip(log_params, expected_param_regex)
|> Enum.each(fn {log, expected_regex} ->
assert log =~ expected_regex
end)
end
test "for insert" do
# Insert values
int = 1
uuid = Ecto.UUID.generate()
# Ensure parameters are complete and in correct order
log =
capture_log(fn ->
TestRepo.insert!(%Logging{uuid: uuid, int: int},
log: :info
)
end)
param_regex =
~r/\[(?<int>.+), \"(?<uuid>.+)\", (?<inserted_at>.+), (?<updated_at>.+), \"(?<bid>.+)\"\]/
param_logs = Regex.named_captures(param_regex, log)
# User changes
assert param_logs["int"] == Integer.to_string(int)
assert param_logs["uuid"] == uuid
# Autogenerated changes
assert param_logs["inserted_at"] =~ @naive_datetime_regex
assert param_logs["updated_at"] =~ @naive_datetime_regex
# Filters
assert param_logs["bid"] =~ @uuid_regex
end
@tag :with_conflict_target
test "for insert with conflict query" do
# Insert values
int = 1
uuid = Ecto.UUID.generate()
# Conflict query
conflict_int = 0
conflict_uuid = Ecto.UUID.generate()
conflict_update = 2
conflict_query =
from l in Logging,
where: l.int == ^conflict_int and l.uuid == ^conflict_uuid,
update: [set: [int: ^conflict_update]]
# Ensure parameters are complete and in correct order
log =
capture_log(fn ->
TestRepo.insert!(%Logging{uuid: uuid, int: int},
on_conflict: conflict_query,
conflict_target: :bid,
log: :info
)
end)
param_regex =
~r/\[(?<int>.+), \"(?<uuid>.+)\", (?<inserted_at>.+), (?<updated_at>.+), \"(?<bid>.+)\", (?<conflict_update>.+), (?<conflict_int>.+), \"(?<conflict_uuid>.+)\"\]/
param_logs = Regex.named_captures(param_regex, log)
# User changes
assert param_logs["int"] == Integer.to_string(int)
assert param_logs["uuid"] == uuid
# Autogenerated changes
assert param_logs["inserted_at"] =~ @naive_datetime_regex
assert param_logs["updated_at"] =~ @naive_datetime_regex
# Filters
assert param_logs["bid"] =~ @uuid_regex
# Conflict query parameters
assert param_logs["conflict_update"] == Integer.to_string(conflict_update)
assert param_logs["conflict_int"] == Integer.to_string(conflict_int)
assert param_logs["conflict_uuid"] == conflict_uuid
end
test "for update" do
# Update values
int = 1
uuid = Ecto.UUID.generate()
current = TestRepo.insert!(%Logging{})
# Ensure parameters are complete and in correct order
log =
capture_log(fn ->
TestRepo.update!(Ecto.Changeset.change(current, %{uuid: uuid, int: int}), log: :info)
end)
param_regex = ~r/\[(?<int>.+), \"(?<uuid>.+)\", (?<updated_at>.+), \"(?<bid>.+)\"\]/
param_logs = Regex.named_captures(param_regex, log)
# User changes
assert param_logs["int"] == Integer.to_string(int)
assert param_logs["uuid"] == uuid
# Autogenerated changes
assert param_logs["updated_at"] =~ @naive_datetime_regex
# Filters
assert param_logs["bid"] == current.bid
end
test "for delete" do
current = TestRepo.insert!(%Logging{})
# Ensure parameters are complete and in correct order
log =
capture_log(fn ->
TestRepo.delete!(current, log: :info)
end)
param_regex = ~r/\[\"(?<bid>.+)\"\]/
param_logs = Regex.named_captures(param_regex, log)
# Filters
assert param_logs["bid"] == current.bid
end
test "for queries" do
int = 1
uuid = Ecto.UUID.generate()
# all
log =
capture_log(fn ->
TestRepo.all(
from(l in Logging,
select: type(^"1", :integer),
where: l.int == ^int and l.uuid == ^uuid
),
log: :info
)
end)
param_regex = ~r/\[(?<tagged_int>.+), (?<int>.+), \"(?<uuid>.+)\"\]/
param_logs = Regex.named_captures(param_regex, log)
assert param_logs["tagged_int"] == Integer.to_string(int)
assert param_logs["int"] == Integer.to_string(int)
assert param_logs["uuid"] == uuid
# update_all
update = 2
log =
capture_log(fn ->
from(l in Logging,
where: l.int == ^int and l.uuid == ^uuid,
update: [set: [int: ^update]]
)
|> TestRepo.update_all([], log: :info)
end)
param_regex = ~r/\[(?<update>.+), (?<int>.+), \"(?<uuid>.+)\"\]/
param_logs = Regex.named_captures(param_regex, log)
assert param_logs["update"] == Integer.to_string(update)
assert param_logs["int"] == Integer.to_string(int)
assert param_logs["uuid"] == uuid
# delete_all
log =
capture_log(fn ->
TestRepo.delete_all(from(l in Logging, where: l.int == ^int and l.uuid == ^uuid),
log: :info
)
end)
param_regex = ~r/\[(?<int>.+), \"(?<uuid>.+)\"\]/
param_logs = Regex.named_captures(param_regex, log)
assert param_logs["int"] == Integer.to_string(int)
assert param_logs["uuid"] == uuid
end
@tag :stream
test "for queries with stream" do
int = 1
uuid = Ecto.UUID.generate()
log =
capture_log(fn ->
stream =
TestRepo.stream(from(l in Logging, where: l.int == ^int and l.uuid == ^uuid),
log: :info
)
TestRepo.transaction(fn -> Enum.to_list(stream) end)
end)
param_regex = ~r/\[(?<int>.+), \"(?<uuid>.+)\"\]/
param_logs = Regex.named_captures(param_regex, log)
assert param_logs["int"] == Integer.to_string(int)
assert param_logs["uuid"] == uuid
end
@tag :array_type
test "for queries with array type" do
uuid = Ecto.UUID.generate()
uuid2 = Ecto.UUID.generate()
log =
capture_log(fn ->
TestRepo.all(from(a in ArrayLogging, where: a.uuids == ^[uuid, uuid2]),
log: :info
)
end)
param_regex = ~r/\[(?<uuids>\[.+\])\]/
param_logs = Regex.named_captures(param_regex, log)
assert param_logs["uuids"] == "[\"#{uuid}\", \"#{uuid2}\"]"
end
end
end

View File

@@ -0,0 +1,775 @@
defmodule Ecto.Integration.MigrationTest do
use ExUnit.Case, async: true
alias Ecto.Integration.{TestRepo, PoolRepo}
defmodule CreateMigration do
use Ecto.Migration
@table table(:create_table_migration)
@index index(:create_table_migration, [:value], unique: true)
def up do
create @table do
add :value, :integer
end
create @index
end
def down do
drop @index
drop @table
end
end
defmodule AddColumnMigration do
use Ecto.Migration
def up do
create table(:add_col_migration) do
add :value, :integer
end
alter table(:add_col_migration) do
add :to_be_added, :integer
end
execute "INSERT INTO add_col_migration (value, to_be_added) VALUES (1, 2)"
end
def down do
drop table(:add_col_migration)
end
end
defmodule AlterColumnMigration do
use Ecto.Migration
def up do
create table(:alter_col_migration) do
add :from_null_to_not_null, :integer
add :from_not_null_to_null, :integer, null: false
add :from_default_to_no_default, :integer, default: 0
add :from_no_default_to_default, :integer
end
alter table(:alter_col_migration) do
modify :from_null_to_not_null, :string, null: false
modify :from_not_null_to_null, :string, null: true
modify :from_default_to_no_default, :integer, default: nil
modify :from_no_default_to_default, :integer, default: 0
end
execute "INSERT INTO alter_col_migration (from_null_to_not_null) VALUES ('foo')"
end
def down do
drop table(:alter_col_migration)
end
end
defmodule AlterColumnFromMigration do
use Ecto.Migration
def change do
create table(:modify_from_products) do
add :value, :integer
add :nullable, :integer, null: false
end
if direction() == :up do
flush()
PoolRepo.insert_all "modify_from_products", [[value: 1, nullable: 1]]
end
alter table(:modify_from_products) do
modify :value, :bigint, from: :integer
modify :nullable, :bigint, null: true, from: {:integer, null: false}
end
end
end
defmodule AlterColumnFromPkeyMigration do
use Ecto.Migration
def change do
create table(:modify_from_authors, primary_key: false) do
add :id, :integer, primary_key: true
end
create table(:modify_from_posts) do
add :author_id, references(:modify_from_authors, type: :integer)
end
if direction() == :up do
flush()
PoolRepo.insert_all "modify_from_authors", [[id: 1]]
PoolRepo.insert_all "modify_from_posts", [[author_id: 1]]
end
alter table(:modify_from_posts) do
# remove the constraints modify_from_posts_author_id_fkey
modify :author_id, :integer, from: references(:modify_from_authors, type: :integer)
end
alter table(:modify_from_authors) do
modify :id, :bigint, from: :integer
end
alter table(:modify_from_posts) do
# add the constraints modify_from_posts_author_id_fkey
modify :author_id, references(:modify_from_authors, type: :bigint), from: :integer
end
end
end
defmodule AlterForeignKeyOnDeleteMigration do
use Ecto.Migration
def up do
create table(:alter_fk_users)
create table(:alter_fk_posts) do
add :alter_fk_user_id, :id
end
alter table(:alter_fk_posts) do
modify :alter_fk_user_id, references(:alter_fk_users, on_delete: :nilify_all)
end
end
def down do
drop table(:alter_fk_posts)
drop table(:alter_fk_users)
end
end
defmodule AlterForeignKeyOnUpdateMigration do
use Ecto.Migration
def up do
create table(:alter_fk_users)
create table(:alter_fk_posts) do
add :alter_fk_user_id, :id
end
alter table(:alter_fk_posts) do
modify :alter_fk_user_id, references(:alter_fk_users, on_update: :update_all)
end
end
def down do
drop table(:alter_fk_posts)
drop table(:alter_fk_users)
end
end
defmodule DropColumnMigration do
use Ecto.Migration
def up do
create table(:drop_col_migration) do
add :value, :integer
add :to_be_removed, :integer
end
execute "INSERT INTO drop_col_migration (value, to_be_removed) VALUES (1, 2)"
alter table(:drop_col_migration) do
remove :to_be_removed
end
end
def down do
drop table(:drop_col_migration)
end
end
defmodule RenameColumnMigration do
use Ecto.Migration
def up do
create table(:rename_col_migration) do
add :to_be_renamed, :integer
end
rename table(:rename_col_migration), :to_be_renamed, to: :was_renamed
execute "INSERT INTO rename_col_migration (was_renamed) VALUES (1)"
end
def down do
drop table(:rename_col_migration)
end
end
defmodule OnDeleteMigration do
use Ecto.Migration
def up do
create table(:parent1)
create table(:parent2)
create table(:ref_migration) do
add :parent1, references(:parent1, on_delete: :nilify_all)
end
alter table(:ref_migration) do
add :parent2, references(:parent2, on_delete: :delete_all)
end
end
def down do
drop table(:ref_migration)
drop table(:parent1)
drop table(:parent2)
end
end
defmodule OnDeleteNilifyColumnsMigration do
use Ecto.Migration
def up do
create table(:parent) do
add :col1, :integer
add :col2, :integer
end
create unique_index(:parent, [:id, :col1, :col2])
create table(:ref) do
add :col1, :integer
add :col2, :integer
add :parent_id,
references(:parent,
with: [col1: :col1, col2: :col2],
on_delete: {:nilify, [:parent_id, :col2]}
)
end
end
def down do
drop table(:ref)
drop table(:parent)
end
end
defmodule OnDeleteDefaultAllMigration do
use Ecto.Migration
def up do
create table(:parent, primary_key: [type: :bigint]) do
add :col1, :integer
add :col2, :integer
end
create unique_index(:parent, [:id, :col1, :col2])
create table(:ref) do
add :col1, :integer, default: 2
add :col2, :integer, default: 3
add :parent_id,
references(:parent,
with: [col1: :col1, col2: :col2],
on_delete: :default_all
), default: 1
end
end
def down do
drop table(:ref)
drop table(:parent)
end
end
defmodule OnDeleteDefaultColumnsMigration do
use Ecto.Migration
def up do
create table(:parent, primary_key: [type: :bigint]) do
add :col1, :integer
add :col2, :integer
end
create unique_index(:parent, [:id, :col1, :col2])
create table(:ref) do
add :col1, :integer, default: 2
add :col2, :integer, default: 3
add :parent_id,
references(:parent,
with: [col1: :col1, col2: :col2],
on_delete: {:default, [:parent_id, :col2]}
), default: 1
end
end
def down do
drop table(:ref)
drop table(:parent)
end
end
defmodule CompositeForeignKeyMigration do
use Ecto.Migration
def change do
create table(:composite_parent) do
add :key_id, :integer
end
create unique_index(:composite_parent, [:id, :key_id])
create table(:composite_child) do
add :parent_key_id, :integer
add :parent_id, references(:composite_parent, with: [parent_key_id: :key_id])
end
end
end
defmodule RenameIndexMigration do
use Ecto.Migration
def change do
create table(:composite_parent) do
add :key_id, :integer
end
create unique_index(:composite_parent, [:id, :key_id], name: "old_index_name")
rename index(:composite_parent, [:id, :key_id], name: "old_index_name"), to: "new_index_name"
end
end
defmodule ReferencesRollbackMigration do
use Ecto.Migration
def change do
create table(:parent) do
add :name, :string
end
create table(:child) do
add :parent_id, references(:parent)
end
end
end
defmodule RenameMigration do
use Ecto.Migration
@table_current table(:posts_migration)
@table_new table(:new_posts_migration)
def up do
create @table_current
rename @table_current, to: @table_new
end
def down do
drop @table_new
end
end
defmodule PrefixMigration do
use Ecto.Migration
@prefix "ecto_prefix_test"
def up do
execute TestRepo.create_prefix(@prefix)
create table(:first, prefix: @prefix)
create table(:second, prefix: @prefix) do
add :first_id, references(:first)
end
end
def down do
drop table(:second, prefix: @prefix)
drop table(:first, prefix: @prefix)
execute TestRepo.drop_prefix(@prefix)
end
end
defmodule NoSQLMigration do
use Ecto.Migration
def up do
create table(:collection, options: [capped: true])
execute create: "collection"
end
end
defmodule Parent do
use Ecto.Schema
schema "parent" do
end
end
defmodule NoErrorTableMigration do
use Ecto.Migration
def change do
create_if_not_exists table(:existing) do
add :name, :string
end
create_if_not_exists table(:existing) do
add :name, :string
end
create_if_not_exists table(:existing)
drop_if_exists table(:existing)
drop_if_exists table(:existing)
end
end
defmodule NoErrorIndexMigration do
use Ecto.Migration
def change do
create_if_not_exists index(:posts, [:title])
create_if_not_exists index(:posts, [:title])
drop_if_exists index(:posts, [:title])
drop_if_exists index(:posts, [:title])
end
end
defmodule InferredDropIndexMigration do
use Ecto.Migration
def change do
create index(:posts, [:title])
end
end
defmodule AlterPrimaryKeyMigration do
use Ecto.Migration
def change do
create table(:no_pk, primary_key: false) do
add :dummy, :string
end
alter table(:no_pk) do
add :id, :serial, primary_key: true
end
end
end
defmodule AddColumnIfNotExistsMigration do
use Ecto.Migration
def up do
create table(:add_col_if_not_exists_migration)
alter table(:add_col_if_not_exists_migration) do
add_if_not_exists :value, :integer
add_if_not_exists :to_be_added, :integer
end
execute "INSERT INTO add_col_if_not_exists_migration (value, to_be_added) VALUES (1, 2)"
end
def down do
drop table(:add_col_if_not_exists_migration)
end
end
defmodule DropColumnIfExistsMigration do
use Ecto.Migration
def up do
create table(:drop_col_if_exists_migration) do
add :value, :integer
add :to_be_removed, :integer
end
execute "INSERT INTO drop_col_if_exists_migration (value, to_be_removed) VALUES (1, 2)"
alter table(:drop_col_if_exists_migration) do
remove_if_exists :to_be_removed, :integer
end
end
def down do
drop table(:drop_col_if_exists_migration)
end
end
defmodule NoErrorOnConditionalColumnMigration do
use Ecto.Migration
def up do
create table(:no_error_on_conditional_column_migration)
alter table(:no_error_on_conditional_column_migration) do
add_if_not_exists :value, :integer
add_if_not_exists :value, :integer
remove_if_exists :value, :integer
remove_if_exists :value, :integer
end
end
def down do
drop table(:no_error_on_conditional_column_migration)
end
end
import Ecto.Query, only: [from: 2]
import Ecto.Migrator, only: [up: 4, down: 4]
# Avoid migration out of order warnings
@moduletag :capture_log
@base_migration 1_000_000
setup do
{:ok, migration_number: System.unique_integer([:positive]) + @base_migration}
end
test "create and drop table and indexes", %{migration_number: num} do
assert :ok == up(PoolRepo, num, CreateMigration, log: false)
assert :ok == down(PoolRepo, num, CreateMigration, log: false)
end
test "correctly infers how to drop index", %{migration_number: num} do
assert :ok == up(PoolRepo, num, InferredDropIndexMigration, log: false)
assert :ok == down(PoolRepo, num, InferredDropIndexMigration, log: false)
end
test "supports on delete", %{migration_number: num} do
assert :ok == up(PoolRepo, num, OnDeleteMigration, log: false)
parent1 = PoolRepo.insert! Ecto.put_meta(%Parent{}, source: "parent1")
parent2 = PoolRepo.insert! Ecto.put_meta(%Parent{}, source: "parent2")
writer = "INSERT INTO ref_migration (parent1, parent2) VALUES (#{parent1.id}, #{parent2.id})"
PoolRepo.query!(writer)
reader = from r in "ref_migration", select: {r.parent1, r.parent2}
assert PoolRepo.all(reader) == [{parent1.id, parent2.id}]
PoolRepo.delete!(parent1)
assert PoolRepo.all(reader) == [{nil, parent2.id}]
PoolRepo.delete!(parent2)
assert PoolRepo.all(reader) == []
assert :ok == down(PoolRepo, num, OnDeleteMigration, log: false)
end
test "composite foreign keys", %{migration_number: num} do
assert :ok == up(PoolRepo, num, CompositeForeignKeyMigration, log: false)
PoolRepo.insert_all("composite_parent", [[key_id: 2]])
assert [id] = PoolRepo.all(from p in "composite_parent", select: p.id)
catch_error(PoolRepo.insert_all("composite_child", [[parent_id: id, parent_key_id: 1]]))
assert {1, nil} = PoolRepo.insert_all("composite_child", [[parent_id: id, parent_key_id: 2]])
assert :ok == down(PoolRepo, num, CompositeForeignKeyMigration, log: false)
end
test "rename index", %{migration_number: num} do
assert :ok == up(PoolRepo, num, RenameIndexMigration, log: false)
assert :ok == down(PoolRepo, num, RenameIndexMigration, log: false)
end
test "rolls back references in change/1", %{migration_number: num} do
assert :ok == up(PoolRepo, num, ReferencesRollbackMigration, log: false)
assert :ok == down(PoolRepo, num, ReferencesRollbackMigration, log: false)
end
test "create table if not exists and drop table if exists does not raise on failure", %{migration_number: num} do
assert :ok == up(PoolRepo, num, NoErrorTableMigration, log: false)
end
@tag :create_index_if_not_exists
test "create index if not exists and drop index if exists does not raise on failure", %{migration_number: num} do
assert :ok == up(PoolRepo, num, NoErrorIndexMigration, log: false)
end
test "raises on NoSQL migrations", %{migration_number: num} do
assert_raise ArgumentError, ~r"does not support keyword lists in :options", fn ->
up(PoolRepo, num, NoSQLMigration, log: false)
end
end
@tag :add_column
test "add column", %{migration_number: num} do
assert :ok == up(PoolRepo, num, AddColumnMigration, log: false)
assert [2] == PoolRepo.all from p in "add_col_migration", select: p.to_be_added
:ok = down(PoolRepo, num, AddColumnMigration, log: false)
end
@tag :modify_column
test "modify column", %{migration_number: num} do
assert :ok == up(PoolRepo, num, AlterColumnMigration, log: false)
assert ["foo"] ==
PoolRepo.all from p in "alter_col_migration", select: p.from_null_to_not_null
assert [nil] ==
PoolRepo.all from p in "alter_col_migration", select: p.from_not_null_to_null
assert [nil] ==
PoolRepo.all from p in "alter_col_migration", select: p.from_default_to_no_default
assert [0] ==
PoolRepo.all from p in "alter_col_migration", select: p.from_no_default_to_default
query = "INSERT INTO `alter_col_migration` (\"from_not_null_to_null\") VALUES ('foo')"
assert catch_error(PoolRepo.query!(query))
:ok = down(PoolRepo, num, AlterColumnMigration, log: false)
end
@tag :modify_column
test "modify column with from", %{migration_number: num} do
assert :ok == up(PoolRepo, num, AlterColumnFromMigration, log: false)
assert [1] ==
PoolRepo.all from p in "modify_from_products", select: p.value
:ok = down(PoolRepo, num, AlterColumnFromMigration, log: false)
end
@tag :alter_primary_key
test "modify column with from and pkey", %{migration_number: num} do
assert :ok == up(PoolRepo, num, AlterColumnFromPkeyMigration, log: false)
assert [1] ==
PoolRepo.all from p in "modify_from_posts", select: p.author_id
:ok = down(PoolRepo, num, AlterColumnFromPkeyMigration, log: false)
end
@tag :alter_foreign_key
test "modify foreign key's on_delete constraint", %{migration_number: num} do
assert :ok == up(PoolRepo, num, AlterForeignKeyOnDeleteMigration, log: false)
PoolRepo.insert_all("alter_fk_users", [[]])
assert [id] = PoolRepo.all from p in "alter_fk_users", select: p.id
PoolRepo.insert_all("alter_fk_posts", [[alter_fk_user_id: id]])
PoolRepo.delete_all("alter_fk_users")
assert [nil] == PoolRepo.all from p in "alter_fk_posts", select: p.alter_fk_user_id
:ok = down(PoolRepo, num, AlterForeignKeyOnDeleteMigration, log: false)
end
@tag :assigns_id_type
test "modify foreign key's on_update constraint", %{migration_number: num} do
assert :ok == up(PoolRepo, num, AlterForeignKeyOnUpdateMigration, log: false)
PoolRepo.insert_all("alter_fk_users", [[]])
assert [id] = PoolRepo.all from p in "alter_fk_users", select: p.id
PoolRepo.insert_all("alter_fk_posts", [[alter_fk_user_id: id]])
PoolRepo.update_all("alter_fk_users", set: [id: 12345])
assert [12345] == PoolRepo.all from p in "alter_fk_posts", select: p.alter_fk_user_id
PoolRepo.delete_all("alter_fk_posts")
:ok = down(PoolRepo, num, AlterForeignKeyOnUpdateMigration, log: false)
end
@tag :remove_column
test "remove column", %{migration_number: num} do
assert :ok == up(PoolRepo, num, DropColumnMigration, log: false)
assert catch_error(PoolRepo.all from p in "drop_col_migration", select: p.to_be_removed)
:ok = down(PoolRepo, num, DropColumnMigration, log: false)
end
@tag :rename_column
test "rename column", %{migration_number: num} do
assert :ok == up(PoolRepo, num, RenameColumnMigration, log: false)
assert [1] == PoolRepo.all from p in "rename_col_migration", select: p.was_renamed
:ok = down(PoolRepo, num, RenameColumnMigration, log: false)
end
@tag :rename_table
test "rename table", %{migration_number: num} do
assert :ok == up(PoolRepo, num, RenameMigration, log: false)
assert :ok == down(PoolRepo, num, RenameMigration, log: false)
end
@tag :prefix
test "prefix", %{migration_number: num} do
assert :ok == up(PoolRepo, num, PrefixMigration, log: false)
assert :ok == down(PoolRepo, num, PrefixMigration, log: false)
end
@tag :alter_primary_key
test "alter primary key", %{migration_number: num} do
assert :ok == up(PoolRepo, num, AlterPrimaryKeyMigration, log: false)
assert :ok == down(PoolRepo, num, AlterPrimaryKeyMigration, log: false)
end
@tag :add_column_if_not_exists
@tag :remove_column_if_exists
test "add if not exists and remove if exists does not raise on failure", %{migration_number: num} do
assert :ok == up(PoolRepo, num, NoErrorOnConditionalColumnMigration, log: false)
assert :ok == down(PoolRepo, num, NoErrorOnConditionalColumnMigration, log: false)
end
@tag :add_column_if_not_exists
test "add column if not exists", %{migration_number: num} do
assert :ok == up(PoolRepo, num, AddColumnIfNotExistsMigration, log: false)
assert [2] == PoolRepo.all from p in "add_col_if_not_exists_migration", select: p.to_be_added
:ok = down(PoolRepo, num, AddColumnIfNotExistsMigration, log: false)
end
@tag :remove_column_if_exists
test "remove column when exists", %{migration_number: num} do
assert :ok == up(PoolRepo, num, DropColumnIfExistsMigration, log: false)
assert catch_error(PoolRepo.all from p in "drop_col_if_exists_migration", select: p.to_be_removed)
:ok = down(PoolRepo, num, DropColumnIfExistsMigration, log: false)
end
@tag :on_delete_nilify_column_list
test "nilify list of columns on_delete constraint", %{migration_number: num} do
assert :ok == up(PoolRepo, num, OnDeleteNilifyColumnsMigration, log: false)
PoolRepo.insert_all("parent", [%{col1: 1, col2: 2}])
assert [{id, col1, col2}] = PoolRepo.all from p in "parent", select: {p.id, p.col1, p.col2}
PoolRepo.insert_all("ref", [[parent_id: id, col1: col1, col2: col2]])
PoolRepo.delete_all("parent")
assert [{nil, col1, nil}] == PoolRepo.all from r in "ref", select: {r.parent_id, r.col1, r.col2}
:ok = down(PoolRepo, num, OnDeleteNilifyColumnsMigration, log: false)
end
@tag :on_delete_default_all
test "default all on_delete constraint", %{migration_number: num} do
assert :ok == up(PoolRepo, num, OnDeleteDefaultAllMigration, log: false)
PoolRepo.insert_all("parent", [%{id: 1, col1: 2, col2: 3}])
{id, col1, col2} = {Enum.random(10..1000), Enum.random(10..1000), Enum.random(10..1000)}
PoolRepo.insert_all("parent", [%{id: id, col1: col1, col2: col2}])
PoolRepo.insert_all("ref", [%{parent_id: id, col1: col1, col2: col2}])
PoolRepo.delete_all(from p in "parent", where: p.id == ^id)
assert [{1, 2, 3}] == PoolRepo.all from r in "ref", select: {r.parent_id, r.col1, r.col2}
:ok = down(PoolRepo, num, OnDeleteDefaultAllMigration, log: false)
end
@tag :on_delete_default_column_list
test "default list of columns on_delete constraint", %{migration_number: num} do
assert :ok == up(PoolRepo, num, OnDeleteDefaultColumnsMigration, log: false)
PoolRepo.insert_all("parent", [%{id: 1, col1: 20, col2: 3}])
{id, col2} = {Enum.random(10..1000), Enum.random(10..1000)}
PoolRepo.insert_all("parent", [%{id: id, col1: 20, col2: col2}])
PoolRepo.insert_all("ref", [%{parent_id: id, col1: 20, col2: col2}])
PoolRepo.delete_all(from p in "parent", where: p.id == ^id)
assert [{1, 20, 3}] == PoolRepo.all from r in "ref", select: {r.parent_id, r.col1, r.col2}
:ok = down(PoolRepo, num, OnDeleteDefaultColumnsMigration, log: false)
end
end

View File

@@ -0,0 +1,264 @@
Code.require_file "../support/file_helpers.exs", __DIR__
defmodule Ecto.Integration.MigratorTest do
use Ecto.Integration.Case
import Support.FileHelpers
import ExUnit.CaptureLog
import Ecto.Migrator
alias Ecto.Integration.{TestRepo, PoolRepo}
alias Ecto.Migration.SchemaMigration
setup config do
Process.register(self(), config.test)
PoolRepo.delete_all(SchemaMigration)
:ok
end
defmodule AnotherSchemaMigration do
use Ecto.Migration
def change do
execute TestRepo.create_prefix("bad_schema_migrations"),
TestRepo.drop_prefix("bad_schema_migrations")
create table(:schema_migrations, prefix: "bad_schema_migrations") do
add :version, :string
add :inserted_at, :integer
end
end
end
defmodule BrokenLinkMigration do
use Ecto.Migration
def change do
Task.start_link(fn -> raise "oops" end)
Process.sleep(:infinity)
end
end
defmodule GoodMigration do
use Ecto.Migration
def up do
create table(:good_migration)
end
def down do
drop table(:good_migration)
end
end
defmodule BadMigration do
use Ecto.Migration
def change do
execute "CREATE WHAT"
end
end
test "migrations up and down" do
assert migrated_versions(PoolRepo) == []
assert up(PoolRepo, 31, GoodMigration, log: false) == :ok
[migration] = PoolRepo.all(SchemaMigration)
assert migration.version == 31
assert migration.inserted_at
assert migrated_versions(PoolRepo) == [31]
assert up(PoolRepo, 31, GoodMigration, log: false) == :already_up
assert migrated_versions(PoolRepo) == [31]
assert down(PoolRepo, 32, GoodMigration, log: false) == :already_down
assert migrated_versions(PoolRepo) == [31]
assert down(PoolRepo, 31, GoodMigration, log: false) == :ok
assert migrated_versions(PoolRepo) == []
end
@tag :prefix
test "does not commit migration if insert into schema migration fails" do
# First we create a new schema migration table in another prefix
assert up(PoolRepo, 33, AnotherSchemaMigration, log: false) == :ok
assert migrated_versions(PoolRepo) == [33]
catch_error(up(PoolRepo, 34, GoodMigration, log: false, prefix: "bad_schema_migrations"))
catch_error(PoolRepo.all("good_migration"))
catch_error(PoolRepo.all("good_migration", prefix: "bad_schema_migrations"))
assert down(PoolRepo, 33, AnotherSchemaMigration, log: false) == :ok
end
test "ecto-generated migration queries pass schema_migration in telemetry options" do
handler = fn _event_name, _measurements, metadata ->
send(self(), metadata)
end
# migration table creation
Process.put(:telemetry, handler)
migrated_versions(PoolRepo, log: false)
assert_received %{options: [schema_migration: true]}
# transaction begin statement
Process.put(:telemetry, handler)
migrated_versions(PoolRepo, skip_table_creation: true, log: false)
assert_received %{options: [schema_migration: true]}
# retrieving the migration versions
Process.put(:telemetry, handler)
migrated_versions(PoolRepo, migration_lock: false, skip_table_creation: true, log: false)
assert_received %{options: [schema_migration: true]}
end
test "bad execute migration" do
assert catch_error(up(PoolRepo, 31, BadMigration, log: false))
assert DynamicSupervisor.which_children(Ecto.MigratorSupervisor) == []
end
test "broken link migration" do
Process.flag(:trap_exit, true)
assert capture_log(fn ->
{:ok, pid} = Task.start_link(fn -> up(PoolRepo, 31, BrokenLinkMigration, log: false) end)
assert_receive {:EXIT, ^pid, _}
end) =~ "oops"
assert capture_log(fn ->
catch_exit(up(PoolRepo, 31, BrokenLinkMigration, log: false))
end) =~ "oops"
end
test "run up to/step migration", config do
in_tmp fn path ->
create_migration(47, config)
create_migration(48, config)
assert [47] = run(PoolRepo, path, :up, step: 1, log: false)
assert count_entries() == 1
assert [48] = run(PoolRepo, path, :up, to: 48, log: false)
end
end
test "run down to/step migration", config do
in_tmp fn path ->
migrations = [
create_migration(49, config),
create_migration(50, config),
]
assert [49, 50] = run(PoolRepo, path, :up, all: true, log: false)
purge migrations
assert [50] = run(PoolRepo, path, :down, step: 1, log: false)
purge migrations
assert count_entries() == 1
assert [50] = run(PoolRepo, path, :up, to: 50, log: false)
end
end
test "runs all migrations", config do
in_tmp fn path ->
migrations = [
create_migration(53, config),
create_migration(54, config),
]
assert [53, 54] = run(PoolRepo, path, :up, all: true, log: false)
assert [] = run(PoolRepo, path, :up, all: true, log: false)
purge migrations
assert [54, 53] = run(PoolRepo, path, :down, all: true, log: false)
purge migrations
assert count_entries() == 0
assert [53, 54] = run(PoolRepo, path, :up, all: true, log: false)
end
end
test "does not commit half transactions on bad syntax", config do
in_tmp fn path ->
migrations = [
create_migration(64, config),
create_migration("65_+", config)
]
assert_raise SyntaxError, fn ->
run(PoolRepo, path, :up, all: true, log: false)
end
refute_received {:up, _}
assert count_entries() == 0
purge migrations
end
end
@tag :lock_for_migrations
test "raises when connection pool is too small" do
config = Application.fetch_env!(:ecto_sql, PoolRepo)
config = Keyword.merge(config, pool_size: 1)
Application.put_env(:ecto_sql, __MODULE__.SingleConnectionRepo, config)
defmodule SingleConnectionRepo do
use Ecto.Repo, otp_app: :ecto_sql, adapter: PoolRepo.__adapter__()
end
{:ok, _pid} = SingleConnectionRepo.start_link()
in_tmp fn path ->
exception_message = ~r/Migrations failed to run because the connection pool size is less than 2/
assert_raise Ecto.MigrationError, exception_message, fn ->
run(SingleConnectionRepo, path, :up, all: true, log: false)
end
end
end
test "does not raise when connection pool is too small but there is no lock" do
config = Application.fetch_env!(:ecto_sql, PoolRepo)
config = Keyword.merge(config, pool_size: 1, migration_lock: nil)
Application.put_env(:ecto_sql, __MODULE__.SingleConnectionNoLockRepo, config)
defmodule SingleConnectionNoLockRepo do
use Ecto.Repo, otp_app: :ecto_sql, adapter: PoolRepo.__adapter__()
end
{:ok, _pid} = SingleConnectionNoLockRepo.start_link()
in_tmp fn path ->
run(SingleConnectionNoLockRepo, path, :up, all: true, log: false)
end
end
defp count_entries() do
PoolRepo.aggregate(SchemaMigration, :count, :version)
end
defp create_migration(num, config) do
module = Module.concat(__MODULE__, "Migration#{num}")
File.write! "#{num}_migration_#{num}.exs", """
defmodule #{module} do
use Ecto.Migration
def up do
send #{inspect config.test}, {:up, #{inspect num}}
end
def down do
send #{inspect config.test}, {:down, #{inspect num}}
end
end
"""
module
end
defp purge(modules) do
Enum.each(List.wrap(modules), fn m ->
:code.delete m
:code.purge m
end)
end
end

View File

@@ -0,0 +1,15 @@
defmodule Ecto.Integration.QueryManyTest do
use Ecto.Integration.Case, async: true
alias Ecto.Integration.TestRepo
test "query_many!/4" do
results = TestRepo.query_many!("SELECT 1; SELECT 2;")
assert [%{rows: [[1]], num_rows: 1}, %{rows: [[2]], num_rows: 1}] = results
end
test "query_many!/4 with iodata" do
results = TestRepo.query_many!(["SELECT", ?\s, ?1, ";", ?\s, "SELECT", ?\s, ?2, ";"])
assert [%{rows: [[1]], num_rows: 1}, %{rows: [[2]], num_rows: 1}] = results
end
end

View File

@@ -0,0 +1,316 @@
defmodule Ecto.Integration.SandboxTest do
use ExUnit.Case
alias Ecto.Adapters.SQL.Sandbox
alias Ecto.Integration.{PoolRepo, TestRepo}
alias Ecto.Integration.Post
import ExUnit.CaptureLog
Application.put_env(:ecto_sql, __MODULE__.DynamicRepo, Application.compile_env(:ecto_sql, TestRepo))
defmodule DynamicRepo do
use Ecto.Repo, otp_app: :ecto_sql, adapter: TestRepo.__adapter__()
end
describe "errors" do
test "raises if repo doesn't exist" do
assert_raise UndefinedFunctionError, ~r"function UnknownRepo.get_dynamic_repo/0 is undefined", fn ->
Sandbox.mode(UnknownRepo, :manual)
end
end
test "raises if repo is not started" do
assert_raise RuntimeError, ~r"could not lookup Ecto repo #{inspect DynamicRepo} because it was not started", fn ->
Sandbox.mode(DynamicRepo, :manual)
end
end
test "raises if repo is not using sandbox" do
assert_raise RuntimeError, ~r"cannot invoke sandbox operation with pool DBConnection", fn ->
Sandbox.mode(PoolRepo, :manual)
end
assert_raise RuntimeError, ~r"cannot invoke sandbox operation with pool DBConnection", fn ->
Sandbox.checkout(PoolRepo)
end
end
test "includes link to SQL sandbox on ownership errors" do
assert_raise DBConnection.OwnershipError,
~r"See Ecto.Adapters.SQL.Sandbox docs for more information.", fn ->
TestRepo.all(Post)
end
end
end
describe "mode" do
test "uses the repository when checked out" do
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
TestRepo.all(Post)
end
Sandbox.checkout(TestRepo)
assert TestRepo.all(Post) == []
Sandbox.checkin(TestRepo)
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
TestRepo.all(Post)
end
end
test "uses the repository when allowed from another process" do
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
TestRepo.all(Post)
end
parent = self()
Task.start_link fn ->
Sandbox.checkout(TestRepo)
Sandbox.allow(TestRepo, self(), parent)
send(parent, :allowed)
Process.sleep(:infinity)
end
assert_receive :allowed
assert TestRepo.all(Post) == []
end
test "uses the repository when allowed from another process by registered name" do
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
TestRepo.all(Post)
end
parent = self()
Process.register(parent, __MODULE__)
Task.start_link fn ->
Sandbox.checkout(TestRepo)
Sandbox.allow(TestRepo, self(), __MODULE__)
send(parent, :allowed)
Process.sleep(:infinity)
end
assert_receive :allowed
assert TestRepo.all(Post) == []
Process.unregister(__MODULE__)
end
test "uses the repository when shared from another process" do
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
TestRepo.all(Post)
end
parent = self()
Task.start_link(fn ->
Sandbox.checkout(TestRepo)
Sandbox.mode(TestRepo, {:shared, self()})
send(parent, :shared)
Process.sleep(:infinity)
end)
assert_receive :shared
assert Task.async(fn -> TestRepo.all(Post) end) |> Task.await == []
after
Sandbox.mode(TestRepo, :manual)
end
test "works with a dynamic repo" do
repo_pid = start_supervised!({DynamicRepo, name: nil})
DynamicRepo.put_dynamic_repo(repo_pid)
assert Sandbox.mode(DynamicRepo, :manual) == :ok
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
DynamicRepo.all(Post)
end
Sandbox.checkout(DynamicRepo)
assert DynamicRepo.all(Post) == []
end
test "works with a repo pid" do
repo_pid = start_supervised!({DynamicRepo, name: nil})
DynamicRepo.put_dynamic_repo(repo_pid)
assert Sandbox.mode(repo_pid, :manual) == :ok
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
DynamicRepo.all(Post)
end
Sandbox.checkout(repo_pid)
assert DynamicRepo.all(Post) == []
end
end
describe "savepoints" do
test "runs inside a sandbox that is rolled back on checkin" do
Sandbox.checkout(TestRepo)
assert TestRepo.insert(%Post{})
assert TestRepo.all(Post) != []
Sandbox.checkin(TestRepo)
Sandbox.checkout(TestRepo)
assert TestRepo.all(Post) == []
Sandbox.checkin(TestRepo)
end
test "runs inside a sandbox that may be disabled" do
Sandbox.checkout(TestRepo, sandbox: false)
assert TestRepo.insert(%Post{})
assert TestRepo.all(Post) != []
Sandbox.checkin(TestRepo)
Sandbox.checkout(TestRepo)
assert {1, _} = TestRepo.delete_all(Post)
Sandbox.checkin(TestRepo)
Sandbox.checkout(TestRepo, sandbox: false)
assert {1, _} = TestRepo.delete_all(Post)
Sandbox.checkin(TestRepo)
end
test "runs inside a sandbox with caller data when preloading associations" do
Sandbox.checkout(TestRepo)
assert TestRepo.insert(%Post{})
parent = self()
Task.start_link fn ->
Sandbox.allow(TestRepo, parent, self())
assert [_] = TestRepo.all(Post) |> TestRepo.preload([:author, :comments])
send parent, :success
end
assert_receive :success
end
test "runs inside a sidebox with custom ownership timeout" do
:ok = Sandbox.checkout(TestRepo, ownership_timeout: 200)
parent = self()
assert capture_log(fn ->
{:ok, pid} =
Task.start(fn ->
Sandbox.allow(TestRepo, parent, self())
TestRepo.transaction(fn -> Process.sleep(500) end)
end)
ref = Process.monitor(pid)
assert_receive {:DOWN, ^ref, _, ^pid, _}, 1000
end) =~ "it owned the connection for longer than 200ms"
end
test "does not taint the sandbox on query errors" do
Sandbox.checkout(TestRepo)
{:ok, _} = TestRepo.insert(%Post{}, skip_transaction: true)
{:error, _} = TestRepo.query("INVALID")
{:ok, _} = TestRepo.insert(%Post{}, skip_transaction: true)
Sandbox.checkin(TestRepo)
end
end
describe "transactions" do
@tag :transaction_isolation
test "with custom isolation level" do
Sandbox.checkout(TestRepo, isolation: "READ UNCOMMITTED")
# Setting it to the same level later on works
TestRepo.query!("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED")
# Even inside a transaction
TestRepo.transaction fn ->
TestRepo.query!("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED")
end
end
test "disconnects on transaction timeouts" do
Sandbox.checkout(TestRepo)
assert capture_log(fn ->
{:error, :rollback} =
TestRepo.transaction(fn -> Process.sleep(1000) end, timeout: 100)
end) =~ "timed out"
Sandbox.checkin(TestRepo)
end
end
describe "checkouts" do
test "with transaction inside checkout" do
Sandbox.checkout(TestRepo)
refute TestRepo.checked_out?()
refute TestRepo.in_transaction?()
TestRepo.checkout(fn ->
assert TestRepo.checked_out?()
refute TestRepo.in_transaction?()
TestRepo.transaction(fn ->
assert TestRepo.checked_out?()
assert TestRepo.in_transaction?()
end)
assert TestRepo.checked_out?()
refute TestRepo.in_transaction?()
end)
refute TestRepo.checked_out?()
refute TestRepo.in_transaction?()
end
test "with checkout inside transaction" do
Sandbox.checkout(TestRepo)
refute TestRepo.checked_out?()
refute TestRepo.in_transaction?()
TestRepo.transaction(fn ->
assert TestRepo.checked_out?()
assert TestRepo.in_transaction?()
TestRepo.checkout(fn ->
assert TestRepo.checked_out?()
assert TestRepo.in_transaction?()
end)
assert TestRepo.checked_out?()
assert TestRepo.in_transaction?()
end)
refute TestRepo.checked_out?()
refute TestRepo.in_transaction?()
end
end
describe "start_owner!/2" do
test "checks out the connection" do
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
TestRepo.all(Post)
end
owner = Sandbox.start_owner!(TestRepo)
assert TestRepo.all(Post) == []
:ok = Sandbox.stop_owner(owner)
refute Process.alive?(owner)
end
test "can set shared mode" do
assert_raise DBConnection.OwnershipError, ~r"cannot find ownership process", fn ->
TestRepo.all(Post)
end
parent = self()
Task.start_link(fn ->
owner = Sandbox.start_owner!(TestRepo, shared: true)
send(parent, {:owner, owner})
Process.sleep(:infinity)
end)
assert_receive {:owner, owner}
assert TestRepo.all(Post) == []
:ok = Sandbox.stop_owner(owner)
after
Sandbox.mode(TestRepo, :manual)
end
end
end

View File

@@ -0,0 +1,178 @@
defmodule Ecto.Integration.SQLTest do
use Ecto.Integration.Case, async: Application.compile_env(:ecto, :async_integration_tests, true)
alias Ecto.Integration.PoolRepo
alias Ecto.Integration.TestRepo
alias Ecto.Integration.Barebone
alias Ecto.Integration.Post
alias Ecto.Integration.CorruptedPk
alias Ecto.Integration.Tag
import Ecto.Query, only: [from: 2]
test "fragmented types" do
datetime = ~N[2014-01-16 20:26:51]
TestRepo.insert!(%Post{inserted_at: datetime})
query = from p in Post, where: fragment("? >= ?", p.inserted_at, ^datetime), select: p.inserted_at
assert [^datetime] = TestRepo.all(query)
end
test "fragmented schemaless types" do
TestRepo.insert!(%Post{visits: 123})
assert [123] = TestRepo.all(from p in "posts", select: type(fragment("visits"), :integer))
end
test "type casting negative integers" do
TestRepo.insert!(%Post{visits: -42})
assert [-42] = TestRepo.all(from(p in Post, select: type(p.visits, :integer)))
end
@tag :array_type
test "fragment array types" do
text1 = "foo"
text2 = "bar"
result = TestRepo.query!("SELECT $1::text[]", [[text1, text2]])
assert result.rows == [[[text1, text2]]]
end
@tag :array_type
test "Converts empty array correctly" do
result = TestRepo.query!("SELECT array[1,2,3] = $1", [[]])
assert result.rows == [[false]]
result = TestRepo.query!("SELECT array[]::integer[] = $1", [[]])
assert result.rows == [[true]]
%{id: tag_id} = TestRepo.insert!(%Tag{uuids: []})
query = from t in Tag, where: t.uuids == []
assert [%{id: ^tag_id}] = TestRepo.all(query)
end
test "query!/4 with dynamic repo" do
TestRepo.put_dynamic_repo(:unknown)
assert_raise RuntimeError, ~r/:unknown/, fn -> TestRepo.query!("SELECT 1") end
end
test "query!/4" do
result = TestRepo.query!("SELECT 1")
assert result.rows == [[1]]
end
test "query!/4 with iodata" do
result = TestRepo.query!(["SELECT", ?\s, ?1])
assert result.rows == [[1]]
end
test "disconnect_all/2" do
assert :ok = PoolRepo.disconnect_all(0)
end
test "to_sql/3" do
{sql, []} = TestRepo.to_sql(:all, Barebone)
assert sql =~ "SELECT"
assert sql =~ "barebones"
{sql, [0]} = TestRepo.to_sql(:update_all, from(b in Barebone, update: [set: [num: ^0]]))
assert sql =~ "UPDATE"
assert sql =~ "barebones"
assert sql =~ "SET"
{sql, []} = TestRepo.to_sql(:delete_all, Barebone)
assert sql =~ "DELETE"
assert sql =~ "barebones"
end
test "raises when primary key is not unique on struct operation" do
schema = %CorruptedPk{a: "abc"}
TestRepo.insert!(schema)
TestRepo.insert!(schema)
TestRepo.insert!(schema)
assert_raise Ecto.MultiplePrimaryKeyError,
~r|expected delete on corrupted_pk to return at most one entry but got 3 entries|,
fn -> TestRepo.delete!(schema) end
end
test "Repo.insert! escape" do
TestRepo.insert!(%Post{title: "'"})
query = from(p in Post, select: p.title)
assert ["'"] == TestRepo.all(query)
end
test "Repo.update! escape" do
p = TestRepo.insert!(%Post{title: "hello"})
TestRepo.update!(Ecto.Changeset.change(p, title: "'"))
query = from(p in Post, select: p.title)
assert ["'"] == TestRepo.all(query)
end
@tag :insert_cell_wise_defaults
test "Repo.insert_all escape" do
TestRepo.insert_all(Post, [%{title: "'"}])
query = from(p in Post, select: p.title)
assert ["'"] == TestRepo.all(query)
end
test "Repo.update_all escape" do
TestRepo.insert!(%Post{title: "hello"})
TestRepo.update_all(Post, set: [title: "'"])
reader = from(p in Post, select: p.title)
assert ["'"] == TestRepo.all(reader)
query = from(Post, where: "'" != "")
TestRepo.update_all(query, set: [title: "''"])
assert ["''"] == TestRepo.all(reader)
end
test "Repo.delete_all escape" do
TestRepo.insert!(%Post{title: "hello"})
assert [_] = TestRepo.all(Post)
TestRepo.delete_all(from(Post, where: "'" == "'"))
assert [] == TestRepo.all(Post)
end
test "load" do
inserted_at = ~N[2016-01-01 09:00:00]
TestRepo.insert!(%Post{title: "title1", inserted_at: inserted_at, public: false})
result = Ecto.Adapters.SQL.query!(TestRepo, "SELECT * FROM posts", [])
posts = Enum.map(result.rows, &TestRepo.load(Post, {result.columns, &1}))
assert [%Post{title: "title1", inserted_at: ^inserted_at, public: false}] = posts
end
test "returns true when table exists" do
assert Ecto.Adapters.SQL.table_exists?(TestRepo, "posts")
end
test "returns false table doesn't exists" do
refute Ecto.Adapters.SQL.table_exists?(TestRepo, "unknown")
end
test "returns result as a formatted table" do
TestRepo.insert_all(Post, [%{title: "my post title", counter: 1, public: nil}])
# resolve correct query for each adapter
query = from(p in Post, select: [p.title, p.counter, p.public])
{query, _} = Ecto.Adapters.SQL.to_sql(:all, TestRepo, query)
table =
query
|> TestRepo.query!()
|> Ecto.Adapters.SQL.format_table()
assert table == "+---------------+---------+--------+\n| title | counter | public |\n+---------------+---------+--------+\n| my post title | 1 | NULL |\n+---------------+---------+--------+"
end
test "format_table edge cases" do
assert Ecto.Adapters.SQL.format_table(nil) == ""
assert Ecto.Adapters.SQL.format_table(%{columns: nil, rows: nil}) == ""
assert Ecto.Adapters.SQL.format_table(%{columns: [], rows: []}) == ""
assert Ecto.Adapters.SQL.format_table(%{columns: [], rows: [["test"]]}) == ""
assert Ecto.Adapters.SQL.format_table(%{columns: ["test"], rows: []}) == "+------+\n| test |\n+------+\n+------+"
assert Ecto.Adapters.SQL.format_table(%{columns: ["test"], rows: nil}) == "+------+\n| test |\n+------+\n+------+"
end
end

View File

@@ -0,0 +1,44 @@
defmodule Ecto.Integration.StreamTest do
use Ecto.Integration.Case, async: Application.compile_env(:ecto, :async_integration_tests, true)
alias Ecto.Integration.TestRepo
alias Ecto.Integration.Post
alias Ecto.Integration.Comment
import Ecto.Query
test "stream empty" do
assert {:ok, []} = TestRepo.transaction(fn() ->
TestRepo.stream(Post)
|> Enum.to_list()
end)
assert {:ok, []} = TestRepo.transaction(fn() ->
TestRepo.stream(from p in Post)
|> Enum.to_list()
end)
end
test "stream without schema" do
%Post{} = TestRepo.insert!(%Post{title: "title1"})
%Post{} = TestRepo.insert!(%Post{title: "title2"})
assert {:ok, ["title1", "title2"]} = TestRepo.transaction(fn() ->
TestRepo.stream(from(p in "posts", order_by: p.title, select: p.title))
|> Enum.to_list()
end)
end
test "stream with assoc" do
p1 = TestRepo.insert!(%Post{title: "1"})
%Comment{id: cid1} = TestRepo.insert!(%Comment{text: "1", post_id: p1.id})
%Comment{id: cid2} = TestRepo.insert!(%Comment{text: "2", post_id: p1.id})
stream = TestRepo.stream(Ecto.assoc(p1, :comments))
assert {:ok, [c1, c2]} = TestRepo.transaction(fn() ->
Enum.to_list(stream)
end)
assert c1.id == cid1
assert c2.id == cid2
end
end

View File

@@ -0,0 +1,153 @@
defmodule Ecto.Integration.SubQueryTest 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.Comment
test "from: subqueries with select source" do
TestRepo.insert!(%Post{title: "hello", public: true})
query = from p in Post, select: p
assert ["hello"] =
TestRepo.all(from p in subquery(query), select: p.title)
assert [post] =
TestRepo.all(from p in subquery(query), select: p)
assert %NaiveDateTime{} = post.inserted_at
assert post.__meta__.state == :loaded
end
@tag :map_boolean_in_expression
test "from: subqueries with map and select expression" do
TestRepo.insert!(%Post{title: "hello", public: true})
query = from p in Post, select: %{title: p.title, pub: not p.public}
assert ["hello"] =
TestRepo.all(from p in subquery(query), select: p.title)
assert [%{title: "hello", pub: false}] =
TestRepo.all(from p in subquery(query), select: p)
assert [{"hello", %{title: "hello", pub: false}}] =
TestRepo.all(from p in subquery(query), select: {p.title, p})
assert [{%{title: "hello", pub: false}, false}] =
TestRepo.all(from p in subquery(query), select: {p, p.pub})
end
@tag :map_boolean_in_expression
test "from: subqueries with map update and select expression" do
TestRepo.insert!(%Post{title: "hello", public: true})
query = from p in Post, select: %{p | public: not p.public}
assert ["hello"] =
TestRepo.all(from p in subquery(query), select: p.title)
assert [%Post{title: "hello", public: false}] =
TestRepo.all(from p in subquery(query), select: p)
assert [{"hello", %Post{title: "hello", public: false}}] =
TestRepo.all(from p in subquery(query), select: {p.title, p})
assert [{%Post{title: "hello", public: false}, false}] =
TestRepo.all(from p in subquery(query), select: {p, p.public})
end
test "from: subqueries with map update on virtual field and select expression" do
TestRepo.insert!(%Post{title: "hello"})
query = from p in Post, select: %{p | temp: p.title}
assert ["hello"] =
TestRepo.all(from p in subquery(query), select: p.temp)
assert [%Post{title: "hello", temp: "hello"}] =
TestRepo.all(from p in subquery(query), select: p)
end
@tag :subquery_aggregates
test "from: subqueries with aggregates" do
TestRepo.insert!(%Post{visits: 10})
TestRepo.insert!(%Post{visits: 11})
TestRepo.insert!(%Post{visits: 13})
query = from p in Post, select: [:visits], order_by: [asc: :visits]
assert [13] = TestRepo.all(from p in subquery(query), select: max(p.visits))
query = from p in Post, select: [:visits], order_by: [asc: :visits], limit: 2
assert [11] = TestRepo.all(from p in subquery(query), select: max(p.visits))
query = from p in Post, order_by: [asc: :visits]
assert [13] = TestRepo.all(from p in subquery(query), select: max(p.visits))
query = from p in Post, order_by: [asc: :visits], limit: 2
assert [11] = TestRepo.all(from p in subquery(query), select: max(p.visits))
end
test "from: subqueries with parameters" do
TestRepo.insert!(%Post{visits: 10, title: "hello"})
TestRepo.insert!(%Post{visits: 11, title: "hello"})
TestRepo.insert!(%Post{visits: 13, title: "world"})
query = from p in Post, where: p.visits >= ^11 and p.visits <= ^13
query = from p in subquery(query), where: p.title == ^"hello", select: fragment("? + ?", p.visits, ^1)
assert [12] = TestRepo.all(query)
end
test "join: subqueries with select source" do
%{id: id} = TestRepo.insert!(%Post{title: "hello", public: true})
TestRepo.insert!(%Comment{post_id: id})
query = from p in Post, select: p
assert ["hello"] =
TestRepo.all(from c in Comment, join: p in subquery(query), on: c.post_id == p.id, select: p.title)
assert [%Post{inserted_at: %NaiveDateTime{}}] =
TestRepo.all(from c in Comment, join: p in subquery(query), on: c.post_id == p.id, select: p)
end
test "join: subqueries with parameters" do
TestRepo.insert!(%Post{visits: 10, title: "hello"})
TestRepo.insert!(%Post{visits: 11, title: "hello"})
TestRepo.insert!(%Post{visits: 13, title: "world"})
TestRepo.insert!(%Comment{})
TestRepo.insert!(%Comment{})
query = from p in Post, where: p.visits >= ^11 and p.visits <= ^13
query = from c in Comment,
join: p in subquery(query),
on: true,
where: p.title == ^"hello",
select: fragment("? + ?", p.visits, ^1)
assert [12, 12] = TestRepo.all(query)
end
@tag :subquery_in_order_by
test "subqueries in order by" do
TestRepo.insert!(%Post{visits: 10, title: "hello"})
TestRepo.insert!(%Post{visits: 11, title: "hello"})
query = from p in Post, as: :p, order_by: [asc: exists(from p in Post, where: p.visits > parent_as(:p).visits)]
assert [%{visits: 11}, %{visits: 10}] = TestRepo.all(query)
end
@tag :multicolumn_distinct
@tag :subquery_in_distinct
test "subqueries in distinct" do
TestRepo.insert!(%Post{visits: 10, title: "hello1"})
TestRepo.insert!(%Post{visits: 10, title: "hello2"})
TestRepo.insert!(%Post{visits: 11, title: "hello"})
query = from p in Post, as: :p, distinct: exists(from p in Post, where: p.visits > parent_as(:p).visits), order_by: [asc: :title]
assert [%{title: "hello"}, %{title: "hello1"}] = TestRepo.all(query)
end
@tag :subquery_in_group_by
test "subqueries in group by" do
TestRepo.insert!(%Post{visits: 10, title: "hello1"})
TestRepo.insert!(%Post{visits: 10, title: "hello2"})
TestRepo.insert!(%Post{visits: 11, title: "hello"})
query = from p in Post, as: :p, select: sum(p.visits), group_by: exists(from p in Post, where: p.visits > parent_as(:p).visits), order_by: [sum(p.visits)]
query
|> TestRepo.all()
|> Enum.map(&Decimal.new/1)
|> Enum.zip([Decimal.new(11), Decimal.new(20)])
|> Enum.all?(fn {a, b} -> Decimal.eq?(a, b) end)
|> assert()
end
end

View File

@@ -0,0 +1,277 @@
defmodule Ecto.Integration.TransactionTest do
# We can keep this test async as long as it
# is the only one access the transactions table
use Ecto.Integration.Case, async: true
import Ecto.Query
alias Ecto.Integration.PoolRepo # Used for writes
alias Ecto.Integration.TestRepo # Used for reads
@moduletag :capture_log
defmodule UniqueError do
defexception message: "unique error"
end
setup do
PoolRepo.delete_all "transactions"
:ok
end
defmodule Trans do
use Ecto.Schema
schema "transactions" do
field :num, :integer
end
end
test "transaction returns value" do
refute PoolRepo.in_transaction?()
{:ok, val} = PoolRepo.transaction(fn ->
assert PoolRepo.in_transaction?()
{:ok, val} =
PoolRepo.transaction(fn ->
assert PoolRepo.in_transaction?()
42
end)
assert PoolRepo.in_transaction?()
val
end)
refute PoolRepo.in_transaction?()
assert val == 42
end
test "transaction re-raises" do
assert_raise UniqueError, fn ->
PoolRepo.transaction(fn ->
PoolRepo.transaction(fn ->
raise UniqueError
end)
end)
end
end
# tag is required for TestRepo, since it is checkout in
# Ecto.Integration.Case setup
@tag isolation_level: :snapshot
test "transaction commits" do
# mssql requires that all transactions that use same shared lock are set
# to :snapshot isolation level
opts = [isolation_level: :snapshot]
PoolRepo.transaction(fn ->
e = PoolRepo.insert!(%Trans{num: 1})
assert [^e] = PoolRepo.all(Trans)
assert [] = TestRepo.all(Trans)
end, opts)
assert [%Trans{num: 1}] = PoolRepo.all(Trans)
end
@tag isolation_level: :snapshot
test "transaction rolls back" do
opts = [isolation_level: :snapshot]
try do
PoolRepo.transaction(fn ->
e = PoolRepo.insert!(%Trans{num: 2})
assert [^e] = PoolRepo.all(Trans)
assert [] = TestRepo.all(Trans)
raise UniqueError
end, opts)
rescue
UniqueError -> :ok
end
assert [] = TestRepo.all(Trans)
end
test "transaction rolls back per repository" do
message = "cannot call rollback outside of transaction"
assert_raise RuntimeError, message, fn ->
PoolRepo.rollback(:done)
end
assert_raise RuntimeError, message, fn ->
TestRepo.transaction fn ->
PoolRepo.rollback(:done)
end
end
end
@tag :assigns_id_type
test "transaction rolls back with reason on aborted transaction" do
e1 = PoolRepo.insert!(%Trans{num: 13})
assert_raise Ecto.ConstraintError, fn ->
TestRepo.transaction fn ->
PoolRepo.insert!(%Trans{id: e1.id, num: 14})
end
end
end
test "nested transaction partial rollback" do
assert PoolRepo.transaction(fn ->
e1 = PoolRepo.insert!(%Trans{num: 3})
assert [^e1] = PoolRepo.all(Trans)
try do
PoolRepo.transaction(fn ->
e2 = PoolRepo.insert!(%Trans{num: 4})
assert [^e1, ^e2] = PoolRepo.all(from(t in Trans, order_by: t.num))
raise UniqueError
end)
rescue
UniqueError -> :ok
end
assert_raise DBConnection.ConnectionError, "transaction rolling back",
fn() -> PoolRepo.insert!(%Trans{num: 5}) end
end) == {:error, :rollback}
assert TestRepo.all(Trans) == []
end
test "manual rollback doesn't bubble up" do
x = PoolRepo.transaction(fn ->
e = PoolRepo.insert!(%Trans{num: 6})
assert [^e] = PoolRepo.all(Trans)
PoolRepo.rollback(:oops)
end)
assert x == {:error, :oops}
assert [] = TestRepo.all(Trans)
end
test "manual rollback bubbles up on nested transaction" do
assert PoolRepo.transaction(fn ->
e = PoolRepo.insert!(%Trans{num: 7})
assert [^e] = PoolRepo.all(Trans)
assert {:error, :oops} = PoolRepo.transaction(fn ->
PoolRepo.rollback(:oops)
end)
assert_raise DBConnection.ConnectionError, "transaction rolling back",
fn() -> PoolRepo.insert!(%Trans{num: 8}) end
end) == {:error, :rollback}
assert [] = TestRepo.all(Trans)
end
test "transactions are not shared in repo" do
pid = self()
opts = [isolation_level: :snapshot]
new_pid = spawn_link fn ->
PoolRepo.transaction(fn ->
e = PoolRepo.insert!(%Trans{num: 9})
assert [^e] = PoolRepo.all(Trans)
send(pid, :in_transaction)
receive do
:commit -> :ok
after
5000 -> raise "timeout"
end
end, opts)
send(pid, :committed)
end
receive do
:in_transaction -> :ok
after
5000 -> raise "timeout"
end
# mssql requires that all transactions that use same shared lock
# set transaction isolation level to "snapshot" so this must be wrapped into
# explicit transaction
PoolRepo.transaction(fn ->
assert [] = PoolRepo.all(Trans)
end, opts)
send(new_pid, :commit)
receive do
:committed -> :ok
after
5000 -> raise "timeout"
end
assert [%Trans{num: 9}] = PoolRepo.all(Trans)
end
## Checkout
describe "with checkouts" do
test "transaction inside checkout" do
PoolRepo.checkout(fn ->
refute PoolRepo.in_transaction?()
PoolRepo.transaction(fn ->
assert PoolRepo.in_transaction?()
end)
refute PoolRepo.in_transaction?()
end)
end
test "checkout inside transaction" do
PoolRepo.transaction(fn ->
assert PoolRepo.in_transaction?()
PoolRepo.checkout(fn ->
assert PoolRepo.in_transaction?()
end)
assert PoolRepo.in_transaction?()
end)
end
@tag :transaction_checkout_raises
test "checkout raises on transaction attempt" do
assert_raise DBConnection.ConnectionError, ~r"connection was checked out with status", fn ->
PoolRepo.checkout(fn -> PoolRepo.query!("BEGIN") end)
end
end
end
## Logging
defp register_telemetry() do
Process.put(:telemetry, fn _, measurements, event -> send(self(), {measurements, event}) end)
end
test "log begin, commit and rollback" do
register_telemetry()
PoolRepo.transaction(fn ->
assert_received {measurements, %{params: [], result: {:ok, _res}}}
assert is_integer(measurements.query_time) and measurements.query_time >= 0
assert is_integer(measurements.queue_time) and measurements.queue_time >= 0
refute_received %{}
register_telemetry()
end)
assert_received {measurements, %{params: [], result: {:ok, _res}}}
assert is_integer(measurements.query_time) and measurements.query_time >= 0
refute Map.has_key?(measurements, :queue_time)
assert PoolRepo.transaction(fn ->
refute_received %{}
register_telemetry()
PoolRepo.rollback(:log_rollback)
end) == {:error, :log_rollback}
assert_received {measurements, %{params: [], result: {:ok, _res}}}
assert is_integer(measurements.query_time) and measurements.query_time >= 0
refute Map.has_key?(measurements, :queue_time)
end
test "log queries inside transactions" do
PoolRepo.transaction(fn ->
register_telemetry()
assert [] = PoolRepo.all(Trans)
assert_received {measurements, %{params: [], result: {:ok, _res}}}
assert is_integer(measurements.query_time) and measurements.query_time >= 0
assert is_integer(measurements.decode_time) and measurements.query_time >= 0
refute Map.has_key?(measurements, :queue_time)
end)
end
end