Skip to content

Commit 5edd494

Browse files
authored
Tds query improvements (elixir-ecto#208)
1 parent 8788f99 commit 5edd494

File tree

10 files changed

+1125
-207
lines changed

10 files changed

+1125
-207
lines changed
+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
defmodule Ecto.Integration.ConstraintsTest do
2+
use ExUnit.Case, async: true
3+
4+
import Ecto.Migrator, only: [up: 4]
5+
alias Ecto.Integration.PoolRepo
6+
7+
defmodule ConstraintMigration do
8+
use Ecto.Migration
9+
10+
@table table(:constraints_test)
11+
12+
def change do
13+
create @table do
14+
add :price, :integer
15+
add :from, :integer
16+
add :to, :integer
17+
end
18+
create constraint(@table.name, :cannot_overlap, check: "[from] < [to]")
19+
create constraint(@table.name, "positive_price", check: "[price] > 0")
20+
end
21+
end
22+
23+
defmodule ConstraintMigration2 do
24+
use Ecto.Migration
25+
26+
def change do
27+
opts = [with: "NOCHECK", check: "[from] < 200"]
28+
create constraint(:constraints_test, "from_max", opts)
29+
end
30+
end
31+
32+
defmodule ConstraintMigration3 do
33+
use Ecto.Migration
34+
35+
def change do
36+
drop constraint(:constraints_test, "from_max")
37+
end
38+
end
39+
40+
defmodule Constraint do
41+
use Ecto.Integration.Schema
42+
43+
schema "constraints_test" do
44+
field :price, :integer
45+
field :from, :integer
46+
field :to, :integer
47+
end
48+
end
49+
50+
@base_migration 2_000_000
51+
52+
setup_all do
53+
ExUnit.CaptureLog.capture_log(fn ->
54+
num = @base_migration + System.unique_integer([:positive])
55+
up(PoolRepo, num, ConstraintMigration, log: false)
56+
end)
57+
58+
:ok
59+
end
60+
61+
test "creating, using, and dropping an exclusion constraint" do
62+
changeset = Ecto.Changeset.change(%Constraint{}, from: 0, to: 10)
63+
{:ok, _} = PoolRepo.insert(changeset)
64+
65+
non_overlapping_changeset = Ecto.Changeset.change(%Constraint{}, from: 11, to: 12)
66+
{:ok, _} = PoolRepo.insert(non_overlapping_changeset)
67+
68+
overlapping_changeset = Ecto.Changeset.change(%Constraint{}, from: 1900, to: 12)
69+
70+
exception =
71+
assert_raise Ecto.ConstraintError, ~r/constraint error when attempting to insert struct/, fn ->
72+
PoolRepo.insert(overlapping_changeset)
73+
end
74+
assert exception.message =~ "cannot_overlap (check_constraint)"
75+
assert exception.message =~ "The changeset has not defined any constraint."
76+
assert exception.message =~ "call `check_constraint/3`"
77+
78+
# Seems like below `Ecto.Changeset.check_constraint(:from)` is not valid for some reason,
79+
# constrint name is mandatory and ecto raises ArgumentError
80+
81+
# message = ~r/constraint error when attempting to insert struct/
82+
# exception =
83+
# assert_raise Ecto.ConstraintError, message, fn ->
84+
# overlapping_changeset
85+
# |> Ecto.Changeset.check_constraint(:from)
86+
# |> PoolRepo.insert()
87+
# end
88+
# assert exception.message =~ "cannot_overlap (check_constraint)"
89+
90+
{:error, changeset} =
91+
overlapping_changeset
92+
|> Ecto.Changeset.check_constraint(:from, name: :cannot_overlap)
93+
|> PoolRepo.insert()
94+
assert changeset.errors == [from: {"is invalid", [constraint: :check, constraint_name: "cannot_overlap"]}]
95+
assert changeset.data.__meta__.state == :built
96+
97+
ExUnit.CaptureLog.capture_log(fn ->
98+
# migrate over existing data, it should pass since `with: NOCHECK` is set
99+
num = @base_migration + System.unique_integer([:positive])
100+
assert :ok == up(PoolRepo, num, ConstraintMigration2, log: false)
101+
end)
102+
103+
# from is greated than max allowed by database, so check constrint should
104+
# forbid insert
105+
from_max_changeset = Ecto.Changeset.change(%Constraint{}, from: 300, to: 400)
106+
107+
exception =
108+
assert_raise Ecto.ConstraintError, ~r/constraint error when attempting to insert struct/, fn ->
109+
PoolRepo.insert(from_max_changeset)
110+
end
111+
assert exception.message =~ "from_max (check_constraint)"
112+
assert exception.message =~ "The changeset has not defined any constraint."
113+
assert exception.message =~ "call `check_constraint/3`"
114+
115+
ExUnit.CaptureLog.capture_log(fn ->
116+
num = @base_migration + System.unique_integer([:positive])
117+
assert :ok == up(PoolRepo, num, ConstraintMigration3, log: false)
118+
end)
119+
end
120+
121+
test "creating, using, and dropping a check constraint" do
122+
# When the changeset doesn't expect the db error
123+
changeset = Ecto.Changeset.change(%Constraint{}, price: -10)
124+
exception =
125+
assert_raise Ecto.ConstraintError, ~r/constraint error when attempting to insert struct/, fn ->
126+
PoolRepo.insert(changeset)
127+
end
128+
129+
assert exception.message =~ "positive_price (check_constraint)"
130+
assert exception.message =~ "The changeset has not defined any constraint."
131+
assert exception.message =~ "call `check_constraint/3`"
132+
133+
# When the changeset does expect the db error, but doesn't give a custom message
134+
{:error, changeset} =
135+
changeset
136+
|> Ecto.Changeset.check_constraint(:price, name: :positive_price)
137+
|> PoolRepo.insert()
138+
assert changeset.errors == [price: {"is invalid", [constraint: :check, constraint_name: "positive_price"]}]
139+
assert changeset.data.__meta__.state == :built
140+
141+
# When the changeset does expect the db error and gives a custom message
142+
changeset = Ecto.Changeset.change(%Constraint{}, price: -10)
143+
{:error, changeset} =
144+
changeset
145+
|> Ecto.Changeset.check_constraint(:price, name: :positive_price, message: "price must be greater than 0")
146+
|> PoolRepo.insert()
147+
assert changeset.errors == [price: {"price must be greater than 0", [constraint: :check, constraint_name: "positive_price"]}]
148+
assert changeset.data.__meta__.state == :built
149+
150+
# When the change does not violate the check constraint
151+
changeset = Ecto.Changeset.change(%Constraint{}, price: 10, from: 100, to: 200)
152+
{:ok, changeset} =
153+
changeset
154+
|> Ecto.Changeset.check_constraint(:price, name: :positive_price, message: "price must be greater than 0")
155+
|> PoolRepo.insert()
156+
assert is_integer(changeset.id)
157+
end
158+
end
+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
defmodule Ecto.Integration.MigrationsTest do
2+
use ExUnit.Case, async: true
3+
4+
alias Ecto.Integration.PoolRepo
5+
6+
# Avoid migration out of order warnings
7+
@moduletag :capture_log
8+
@base_migration 3_000_000
9+
10+
setup do
11+
{:ok, migration_number: System.unique_integer([:positive]) + @base_migration}
12+
end
13+
14+
defmodule AddColumnIfNotExistsMigration do
15+
use Ecto.Migration
16+
17+
def up do
18+
create table(:add_col_if_not_exists_migration)
19+
20+
alter table(:add_col_if_not_exists_migration) do
21+
add_if_not_exists :value, :integer
22+
add_if_not_exists :to_be_added, :integer
23+
end
24+
25+
execute "INSERT INTO add_col_if_not_exists_migration (value, to_be_added) VALUES (1, 2)"
26+
end
27+
28+
def down do
29+
drop table(:add_col_if_not_exists_migration)
30+
end
31+
end
32+
33+
defmodule DropColumnIfExistsMigration do
34+
use Ecto.Migration
35+
36+
def up do
37+
create table(:drop_col_if_exists_migration) do
38+
add :value, :integer
39+
add :to_be_removed, :integer
40+
end
41+
42+
execute "INSERT INTO drop_col_if_exists_migration (value, to_be_removed) VALUES (1, 2)"
43+
44+
alter table(:drop_col_if_exists_migration) do
45+
remove_if_exists :to_be_removed, :integer
46+
end
47+
end
48+
49+
def down do
50+
drop table(:drop_col_if_exists_migration)
51+
end
52+
end
53+
54+
defmodule DuplicateTableMigration do
55+
use Ecto.Migration
56+
57+
def change do
58+
create_if_not_exists table(:duplicate_table)
59+
create_if_not_exists table(:duplicate_table)
60+
end
61+
end
62+
63+
defmodule NoErrorOnConditionalColumnMigration do
64+
use Ecto.Migration
65+
66+
def up do
67+
create table(:no_error_on_conditional_column_migration)
68+
69+
alter table(:no_error_on_conditional_column_migration) do
70+
add_if_not_exists :value, :integer
71+
add_if_not_exists :value, :integer
72+
73+
remove_if_exists :value, :integer
74+
remove_if_exists :value, :integer
75+
end
76+
end
77+
78+
def down do
79+
drop table(:no_error_on_conditional_column_migration)
80+
end
81+
end
82+
83+
import Ecto.Query, only: [from: 2]
84+
import Ecto.Migrator, only: [up: 4, down: 4]
85+
86+
test "logs MSSQL notice messages" do
87+
# comparing to other db engines, in MSSQL there is no option in CREATE statement
88+
# if_not_existis, instead we need to use if statement
89+
# due this limitation, nothing will be logged in console
90+
num = @base_migration + System.unique_integer([:positive])
91+
up(PoolRepo, num, DuplicateTableMigration, log: false)
92+
93+
# check if first attempt in migration created table since seond yields nothing
94+
# about skipping in logs
95+
q = "SELECT COUNT(*) FROM [INFORMATION_SCHEMA].[TABLES] WHERE [TABLE_NAME] = @1"
96+
p = ["duplicate_table"]
97+
assert {:ok, %{rows: [[1]]}} = Ecto.Adapters.SQL.query(PoolRepo, q, p)
98+
end
99+
100+
@tag :no_error_on_conditional_column_migration
101+
test "add if not exists and drop if exists does not raise on failure", %{migration_number: num} do
102+
assert :ok == up(PoolRepo, num, NoErrorOnConditionalColumnMigration, log: false)
103+
assert :ok == down(PoolRepo, num, NoErrorOnConditionalColumnMigration, log: false)
104+
end
105+
106+
@tag :add_column_if_not_exists
107+
test "add column if not exists", %{migration_number: num} do
108+
assert :ok == up(PoolRepo, num, AddColumnIfNotExistsMigration, log: false)
109+
assert [2] == PoolRepo.all from p in "add_col_if_not_exists_migration", select: p.to_be_added
110+
:ok = down(PoolRepo, num, AddColumnIfNotExistsMigration, log: false)
111+
end
112+
113+
@tag :remove_column_if_exists
114+
test "remove column when exists", %{migration_number: num} do
115+
assert :ok == up(PoolRepo, num, DropColumnIfExistsMigration, log: false)
116+
assert catch_error(PoolRepo.all from p in "drop_col_if_exists_migration", select: p.to_be_removed)
117+
:ok = down(PoolRepo, num, DropColumnIfExistsMigration, log: false)
118+
end
119+
end
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
defmodule Ecto.Integration.TdsTypeTest do
2+
use ExUnit.Case, async: true
3+
4+
import Ecto.Type
5+
alias Tds.Ecto.VarChar
6+
alias Ecto.Adapters.Tds
7+
8+
@varchar_string "some string"
9+
10+
test "dumps through the adapter" do
11+
assert adapter_dump(Tds, {:map, VarChar}, %{"a" => @varchar_string}) ==
12+
{:ok, %{"a" => @varchar_string}}
13+
end
14+
15+
test "loads through the adapter" do
16+
assert adapter_load(Tds, {:map, VarChar}, %{"a" => {@varchar_string, :varchar}}) ==
17+
{:ok, %{"a" => @varchar_string}}
18+
19+
assert adapter_load(Tds, {:map, VarChar}, %{"a" => @varchar_string}) ==
20+
{:ok, %{"a" => @varchar_string}}
21+
end
22+
end

integration_test/tds/test_helper.exs

+13-4
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ ExUnit.start(
2020
:uses_msec,
2121
# Unique index compares even NULL values for post_id, so below fails inserting permalinks without setting valid post_id
2222
:insert_cell_wise_defaults,
23-
# SELECT NOT(t.bool_column) not supported
24-
:select_not,
2523
# MSSQL does not support strings on text fields
2624
:text_type_as_string,
2725
# IDENTITY_INSERT ON/OFF must be manually executed
@@ -38,8 +36,10 @@ ExUnit.start(
3836
:union_with_literals,
3937
# inline queries can't use order by
4038
:inline_order_by,
41-
# running destruction of PK columns requires that constraint is dropped first
39+
# running destruction of PK columns requires that PK constraint is dropped first
4240
:alter_primary_key,
41+
# running destruction of PK columns requires that PK constraint is dropped first
42+
# accessing column. This is same issue as :alter_primary_key
4343
:modify_column_with_from,
4444
# below 2 exclusions (in theory) requires filtered unique index on permalinks table post_id column e.g.
4545
# CREATE UNIQUE NONCLUSTERED INDEX idx_tbl_TestUnique_ID
@@ -90,7 +90,7 @@ defmodule Ecto.Integration.TestRepo do
9090
otp_app: :ecto_sql,
9191
adapter: Ecto.Adapters.Tds
9292

93-
def uuid, do: Tds.Types.UUID
93+
def uuid, do: Tds.Ecto.UUID
9494

9595
def create_prefix(prefix) do
9696
"""
@@ -141,6 +141,15 @@ defmodule Ecto.Integration.Case do
141141
end
142142
end
143143

144+
# :dbg.start()
145+
# :dbg.tracer()
146+
# :dbg.p(:all,:c)
147+
# :dbg.tpl(Ecto.Adapters.Tds.Connection, :column_change, :x)
148+
# :dbg.tpl(Ecto.Adapters.Tds.Connection, :execute_ddl, :x)
149+
# :dbg.tpl(Ecto.Adapters.Tds.Connection, :all, :x)
150+
# :dbg.tpl(Tds.Parameter, :prepare_params, :x)
151+
# :dbg.tpl(Tds.Parameter, :prepared_params, :x)
152+
144153
{:ok, _} = Ecto.Adapters.Tds.ensure_all_started(TestRepo.config(), :temporary)
145154

146155
# Load up the repository, start it, and run migrations

0 commit comments

Comments
 (0)