update
This commit is contained in:
90
phoenix/deps/ecto_sql/integration_test/sql/alter.exs
Normal file
90
phoenix/deps/ecto_sql/integration_test/sql/alter.exs
Normal 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
|
||||
59
phoenix/deps/ecto_sql/integration_test/sql/lock.exs
Normal file
59
phoenix/deps/ecto_sql/integration_test/sql/lock.exs
Normal 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
|
||||
857
phoenix/deps/ecto_sql/integration_test/sql/logging.exs
Normal file
857
phoenix/deps/ecto_sql/integration_test/sql/logging.exs
Normal 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
|
||||
775
phoenix/deps/ecto_sql/integration_test/sql/migration.exs
Normal file
775
phoenix/deps/ecto_sql/integration_test/sql/migration.exs
Normal 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
|
||||
264
phoenix/deps/ecto_sql/integration_test/sql/migrator.exs
Normal file
264
phoenix/deps/ecto_sql/integration_test/sql/migrator.exs
Normal 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
|
||||
15
phoenix/deps/ecto_sql/integration_test/sql/query_many.exs
Normal file
15
phoenix/deps/ecto_sql/integration_test/sql/query_many.exs
Normal 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
|
||||
316
phoenix/deps/ecto_sql/integration_test/sql/sandbox.exs
Normal file
316
phoenix/deps/ecto_sql/integration_test/sql/sandbox.exs
Normal 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
|
||||
178
phoenix/deps/ecto_sql/integration_test/sql/sql.exs
Normal file
178
phoenix/deps/ecto_sql/integration_test/sql/sql.exs
Normal 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
|
||||
44
phoenix/deps/ecto_sql/integration_test/sql/stream.exs
Normal file
44
phoenix/deps/ecto_sql/integration_test/sql/stream.exs
Normal 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
|
||||
153
phoenix/deps/ecto_sql/integration_test/sql/subquery.exs
Normal file
153
phoenix/deps/ecto_sql/integration_test/sql/subquery.exs
Normal 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
|
||||
277
phoenix/deps/ecto_sql/integration_test/sql/transaction.exs
Normal file
277
phoenix/deps/ecto_sql/integration_test/sql/transaction.exs
Normal 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
|
||||
@@ -0,0 +1,45 @@
|
||||
defmodule Support.FileHelpers do
|
||||
import ExUnit.Assertions
|
||||
|
||||
@doc """
|
||||
Returns the `tmp_path` for tests.
|
||||
"""
|
||||
def tmp_path do
|
||||
Path.expand("../../tmp", __DIR__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Executes the given function in a temp directory
|
||||
tailored for this test case and test.
|
||||
"""
|
||||
defmacro in_tmp(fun) do
|
||||
{name, _arity} = __CALLER__.function || raise "in_tmp must be called inside a function"
|
||||
path = Path.join([tmp_path(), "#{__CALLER__.module}", "#{name}"])
|
||||
|
||||
quote do
|
||||
path = unquote(path)
|
||||
File.rm_rf!(path)
|
||||
File.mkdir_p!(path)
|
||||
File.cd!(path, fn -> unquote(fun).(path) end)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts a file was generated.
|
||||
"""
|
||||
def assert_file(file) do
|
||||
assert File.regular?(file), "Expected #{file} to exist, but does not"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Asserts a file was generated and that it matches a given pattern.
|
||||
"""
|
||||
def assert_file(file, callback) when is_function(callback, 1) do
|
||||
assert_file(file)
|
||||
callback.(File.read!(file))
|
||||
end
|
||||
|
||||
def assert_file(file, match) do
|
||||
assert_file(file, &assert(&1 =~ match))
|
||||
end
|
||||
end
|
||||
166
phoenix/deps/ecto_sql/integration_test/support/migration.exs
Normal file
166
phoenix/deps/ecto_sql/integration_test/support/migration.exs
Normal file
@@ -0,0 +1,166 @@
|
||||
defmodule Ecto.Integration.Migration do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
# IO.puts "TESTING MIGRATION LOCK"
|
||||
# Process.sleep(10000)
|
||||
|
||||
create table(:users, comment: "users table") do
|
||||
add :name, :string, comment: "name column"
|
||||
add :custom_id, :uuid
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create table(:posts) do
|
||||
add :title, :string, size: 100
|
||||
add :counter, :integer
|
||||
add :blob, :binary
|
||||
add :bid, :binary_id
|
||||
add :uuid, :uuid
|
||||
add :meta, :map
|
||||
add :links, {:map, :string}
|
||||
add :intensities, {:map, :float}
|
||||
add :public, :boolean
|
||||
add :cost, :decimal, precision: 2, scale: 1
|
||||
add :visits, :integer
|
||||
add :wrapped_visits, :integer
|
||||
add :intensity, :float
|
||||
add :author_id, :integer
|
||||
add :posted, :date
|
||||
add :read_only, :string
|
||||
timestamps(null: true)
|
||||
end
|
||||
|
||||
create table(:posts_users, primary_key: false) do
|
||||
add :post_id, references(:posts)
|
||||
add :user_id, references(:users)
|
||||
end
|
||||
|
||||
create table(:posts_users_pk) do
|
||||
add :post_id, references(:posts)
|
||||
add :user_id, references(:users)
|
||||
timestamps()
|
||||
end
|
||||
|
||||
# Add a unique index on uuid. We use this
|
||||
# to verify the behaviour that the index
|
||||
# only matters if the UUID column is not NULL.
|
||||
create unique_index(:posts, [:uuid], comment: "posts index")
|
||||
|
||||
create table(:permalinks) do
|
||||
add :uniform_resource_locator, :string
|
||||
add :title, :string
|
||||
add :post_id, references(:posts)
|
||||
add :user_id, references(:users)
|
||||
end
|
||||
|
||||
create unique_index(:permalinks, [:post_id])
|
||||
create unique_index(:permalinks, [:uniform_resource_locator])
|
||||
|
||||
create table(:comments) do
|
||||
add :text, :string, size: 100
|
||||
add :lock_version, :integer, default: 1
|
||||
add :post_id, references(:posts)
|
||||
add :author_id, references(:users)
|
||||
end
|
||||
|
||||
create table(:customs, primary_key: false) do
|
||||
add :bid, :binary_id, primary_key: true
|
||||
add :uuid, :uuid
|
||||
end
|
||||
|
||||
create unique_index(:customs, [:uuid])
|
||||
|
||||
create table(:customs_customs, primary_key: false) do
|
||||
add :custom_id1, references(:customs, column: :bid, type: :binary_id)
|
||||
add :custom_id2, references(:customs, column: :bid, type: :binary_id)
|
||||
end
|
||||
|
||||
create table(:barebones) do
|
||||
add :num, :integer
|
||||
end
|
||||
|
||||
create table(:transactions) do
|
||||
add :num, :integer
|
||||
end
|
||||
|
||||
create table(:lock_counters) do
|
||||
add :count, :integer
|
||||
end
|
||||
|
||||
create table(:orders) do
|
||||
add :label, :string
|
||||
add :item, :map
|
||||
add :items, :map
|
||||
add :meta, :map
|
||||
add :permalink_id, references(:permalinks)
|
||||
end
|
||||
|
||||
unless :array_type in ExUnit.configuration()[:exclude] do
|
||||
create table(:tags) do
|
||||
add :ints, {:array, :integer}
|
||||
add :uuids, {:array, :uuid}, default: []
|
||||
add :items, {:array, :map}
|
||||
end
|
||||
|
||||
create table(:array_loggings) do
|
||||
add :uuids, {:array, :uuid}, default: []
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
|
||||
unless :bitstring_type in ExUnit.configuration()[:exclude] do
|
||||
create table(:bitstrings) do
|
||||
add :bs, :bitstring
|
||||
add :bs_with_default, :bitstring, default: <<42::6>>
|
||||
add :bs_with_size, :bitstring, size: 10
|
||||
end
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Duration) do
|
||||
unless :duration_type in ExUnit.configuration()[:exclude] do
|
||||
create table(:durations) do
|
||||
add :dur, :duration
|
||||
add :dur_with_fields, :duration, fields: "MONTH"
|
||||
add :dur_with_precision, :duration, precision: 4
|
||||
add :dur_with_fields_and_precision, :duration, fields: "HOUR TO SECOND", precision: 1
|
||||
add :dur_with_default, :duration, default: "10 MONTH"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
create table(:composite_pk, primary_key: false) do
|
||||
add :a, :integer, primary_key: true
|
||||
add :b, :integer, primary_key: true
|
||||
add :name, :string
|
||||
end
|
||||
|
||||
create table(:corrupted_pk, primary_key: false) do
|
||||
add :a, :string
|
||||
end
|
||||
|
||||
create table(:posts_users_composite_pk) do
|
||||
add :post_id, references(:posts), primary_key: true
|
||||
add :user_id, references(:users), primary_key: true
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:posts_users_composite_pk, [:post_id, :user_id])
|
||||
|
||||
create table(:usecs) do
|
||||
add :naive_datetime_usec, :naive_datetime_usec
|
||||
add :utc_datetime_usec, :utc_datetime_usec
|
||||
end
|
||||
|
||||
create table(:bits) do
|
||||
add :bit, :bit
|
||||
end
|
||||
|
||||
create table(:loggings, primary_key: false) do
|
||||
add :bid, :binary_id, primary_key: true
|
||||
add :int, :integer
|
||||
add :uuid, :uuid
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
end
|
||||
23
phoenix/deps/ecto_sql/integration_test/support/repo.exs
Normal file
23
phoenix/deps/ecto_sql/integration_test/support/repo.exs
Normal file
@@ -0,0 +1,23 @@
|
||||
defmodule Ecto.Integration.Repo do
|
||||
defmacro __using__(opts) do
|
||||
quote do
|
||||
use Ecto.Repo, unquote(opts)
|
||||
|
||||
@query_event __MODULE__
|
||||
|> Module.split()
|
||||
|> Enum.map(& &1 |> Macro.underscore() |> String.to_atom())
|
||||
|> Kernel.++([:query])
|
||||
|
||||
def init(_, opts) do
|
||||
fun = &Ecto.Integration.Repo.handle_event/4
|
||||
:telemetry.attach_many(__MODULE__, [[:custom], @query_event], fun, :ok)
|
||||
{:ok, opts}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event(event, latency, metadata, _config) do
|
||||
handler = Process.delete(:telemetry) || fn _, _, _ -> :ok end
|
||||
handler.(event, latency, metadata)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user