Skip to content

Commit f0cdeda

Browse files
authored
Add support in migrations for collations in Postgres (#662)
1 parent 96415e3 commit f0cdeda

File tree

7 files changed

+228
-6
lines changed

7 files changed

+228
-6
lines changed

integration_test/myxql/migrations_test.exs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,41 @@ defmodule Ecto.Integration.MigrationsTest do
2929
end
3030
end
3131

32+
text_variants = ~w/tinytext text mediumtext longtext/a
33+
@text_variants text_variants
34+
35+
collation = "utf8mb4_bin"
36+
@collation collation
37+
38+
defmodule CollateMigration do
39+
use Ecto.Migration
40+
41+
@text_variants text_variants
42+
@collation collation
43+
44+
def change do
45+
create table(:collate_reference) do
46+
add :name, :string, collation: @collation
47+
end
48+
49+
create unique_index(:collate_reference, :name)
50+
51+
create table(:collate) do
52+
add :string, :string, collation: @collation
53+
add :varchar, :varchar, size: 255, collation: @collation
54+
add :name_string, references(:collate_reference, type: :string, column: :name), collation: @collation
55+
56+
for type <- @text_variants do
57+
add type, type, collation: @collation
58+
end
59+
end
60+
61+
alter table(:collate) do
62+
modify :string, :string, collation: "utf8mb4_general_ci"
63+
end
64+
end
65+
end
66+
3267
describe "Migrator" do
3368
@get_lock_command ~s[SELECT GET_LOCK('ecto_Ecto.Integration.PoolRepo', -1)]
3469
@release_lock_command ~s[SELECT RELEASE_LOCK('ecto_Ecto.Integration.PoolRepo')]
@@ -107,5 +142,26 @@ defmodule Ecto.Integration.MigrationsTest do
107142

108143
assert log =~ "ALTER TABLE `alter_table` ADD `column2` varchar(255) COMMENT 'second column' AFTER `column1`"
109144
end
145+
146+
test "collation can be set on a column" do
147+
num = @base_migration + System.unique_integer([:positive])
148+
assert :ok = Ecto.Migrator.up(PoolRepo, num, CollateMigration, log: false)
149+
query = fn column -> """
150+
SELECT collation_name
151+
FROM information_schema.columns
152+
WHERE table_name = 'collate' AND column_name = '#{column}';
153+
"""
154+
end
155+
156+
assert %{
157+
rows: [["utf8mb4_general_ci"]]
158+
} = Ecto.Adapters.SQL.query!(PoolRepo, query.("string"), [])
159+
160+
for type <- ~w/text name_string/ ++ @text_variants do
161+
assert %{
162+
rows: [[@collation]]
163+
} = Ecto.Adapters.SQL.query!(PoolRepo, query.(type), [])
164+
end
165+
end
110166
end
111167
end

integration_test/pg/migrations_test.exs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,40 @@ defmodule Ecto.Integration.MigrationsTest do
4040
end
4141
end
4242

43+
collation = "POSIX"
44+
@collation collation
45+
46+
text_types = ~w/char varchar text/a
47+
@text_types text_types
48+
49+
defmodule CollateMigration do
50+
use Ecto.Migration
51+
52+
@collation collation
53+
@text_types text_types
54+
55+
def change do
56+
create table(:collate_reference) do
57+
add :name, :string, primary_key: true, collation: @collation
58+
end
59+
60+
create unique_index(:collate_reference, :name)
61+
62+
create table(:collate) do
63+
add :string, :string, collation: @collation
64+
for type <- @text_types do
65+
add type, type, collation: @collation
66+
end
67+
68+
add :name_string, references(:collate_reference, type: :string, column: :name), collation: @collation
69+
end
70+
71+
alter table(:collate) do
72+
modify :string, :string, collation: "C"
73+
end
74+
end
75+
end
76+
4377
test "logs Postgres notice messages" do
4478
log =
4579
capture_log(fn ->
@@ -145,5 +179,28 @@ defmodule Ecto.Integration.MigrationsTest do
145179
refute down_log =~ @version_delete
146180
refute down_log =~ "commit []"
147181
end
182+
183+
test "collation can be set on a column" do
184+
num = @base_migration + System.unique_integer([:positive])
185+
186+
assert :ok = Ecto.Migrator.up(PoolRepo, num, CollateMigration, log: :info)
187+
188+
query = fn column -> """
189+
SELECT collation_name
190+
FROM information_schema.columns
191+
WHERE table_name = 'collate' AND column_name = '#{column}';
192+
"""
193+
end
194+
195+
assert %{
196+
rows: [["C"]]
197+
} = Ecto.Adapters.SQL.query!(PoolRepo, query.("string"), [])
198+
199+
for type <- @text_types do
200+
assert %{
201+
rows: [[@collation]]
202+
} = Ecto.Adapters.SQL.query!(PoolRepo, query.(type), [])
203+
end
204+
end
148205
end
149206
end

integration_test/tds/migrations_test.exs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,37 @@ defmodule Ecto.Integration.MigrationsTest do
1515
end
1616
end
1717

18+
collation = "Latin1_General_CS_AS"
19+
@collation collation
20+
21+
defmodule CollateMigration do
22+
use Ecto.Migration
23+
@collation collation
24+
25+
def change do
26+
create table(:collate_reference) do
27+
add :name, :string, collation: @collation
28+
end
29+
30+
create unique_index(:collate_reference, :name)
31+
32+
create table(:collate) do
33+
add :string, :string, collation: @collation
34+
add :char, :char, size: 255, collation: @collation
35+
add :nchar, :nchar, size: 255, collation: @collation
36+
add :varchar, :varchar, size: 255, collation: @collation
37+
add :nvarchar, :nvarchar, size: 255, collation: @collation
38+
add :text, :text, collation: @collation
39+
add :ntext, :ntext, collation: @collation
40+
add :name_string, references(:collate_reference, type: :string, column: :name), collation: @collation
41+
end
42+
43+
alter table(:collate) do
44+
modify :string, :string, collation: "Japanese_Bushu_Kakusu_100_CS_AS_KS_WS"
45+
end
46+
end
47+
end
48+
1849
describe "Migrator" do
1950
@get_lock_command ~s(sp_getapplock @Resource = 'ecto_Ecto.Integration.PoolRepo', @LockMode = 'Exclusive', @LockOwner = 'Transaction', @LockTimeout = -1)
2051
@create_table_sql ~s(CREATE TABLE [log_mode_table])
@@ -77,5 +108,27 @@ defmodule Ecto.Integration.MigrationsTest do
77108
refute down_log =~ @version_delete
78109
refute down_log =~ "commit []"
79110
end
111+
112+
test "collation can be set on a column" do
113+
num = @base_migration + System.unique_integer([:positive])
114+
assert :ok = Ecto.Migrator.up(PoolRepo, num, CollateMigration, log: :info)
115+
116+
query = fn column -> """
117+
SELECT collation_name
118+
FROM information_schema.columns
119+
WHERE table_name = 'collate' AND column_name = '#{column}';
120+
"""
121+
end
122+
123+
assert %{
124+
rows: [["Japanese_Bushu_Kakusu_100_CS_AS_KS_WS"]]
125+
} = Ecto.Adapters.SQL.query!(PoolRepo, query.("string"), [])
126+
127+
for type <- ~w/char varchar nchar nvarchar text ntext/ do
128+
assert %{
129+
rows: [[@collation]]
130+
} = Ecto.Adapters.SQL.query!(PoolRepo, query.(type), [])
131+
end
132+
end
80133
end
81134
end

lib/ecto/adapters/myxql/connection.ex

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,7 +1215,13 @@ if Code.ensure_loaded?(MyXQL) do
12151215
end
12161216

12171217
defp column_change(_table, {:add_if_not_exists, name, type, opts}) do
1218-
["ADD IF NOT EXISTS ", quote_name(name), ?\s, column_type(type, opts), column_options(opts)]
1218+
[
1219+
"ADD IF NOT EXISTS ",
1220+
quote_name(name),
1221+
?\s,
1222+
column_type(type, opts),
1223+
column_options(opts)
1224+
]
12191225
end
12201226

12211227
defp column_change(table, {:modify, name, %Reference{} = ref, opts}) do
@@ -1264,8 +1270,15 @@ if Code.ensure_loaded?(MyXQL) do
12641270
null = Keyword.get(opts, :null)
12651271
after_column = Keyword.get(opts, :after)
12661272
comment = Keyword.get(opts, :comment)
1273+
collation = Keyword.fetch(opts, :collation)
12671274

1268-
[default_expr(default), null_expr(null), comment_expr(comment), after_expr(after_column)]
1275+
[
1276+
default_expr(default),
1277+
collation_expr(collation),
1278+
null_expr(null),
1279+
comment_expr(comment),
1280+
after_expr(after_column)
1281+
]
12691282
end
12701283

12711284
defp comment_expr(comment, create_table? \\ false)
@@ -1286,6 +1299,9 @@ if Code.ensure_loaded?(MyXQL) do
12861299
defp null_expr(true), do: " NULL"
12871300
defp null_expr(_), do: []
12881301

1302+
defp collation_expr({:ok, collation_name}), do: " COLLATE \"#{collation_name}\""
1303+
defp collation_expr(_), do: []
1304+
12891305
defp new_constraint_expr(%Constraint{check: check} = constraint) when is_binary(check) do
12901306
[
12911307
"CONSTRAINT ",

lib/ecto/adapters/postgres/connection.ex

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,8 @@ if Code.ensure_loaded?(Postgrex) do
15551555
end
15561556

15571557
defp column_change(table, {:modify, name, %Reference{} = ref, opts}) do
1558+
collation = Keyword.fetch(opts, :collation)
1559+
15581560
[
15591561
drop_reference_expr(opts[:from], table, name),
15601562
"ALTER COLUMN ",
@@ -1564,19 +1566,23 @@ if Code.ensure_loaded?(Postgrex) do
15641566
", ADD ",
15651567
reference_expr(ref, table, name),
15661568
modify_null(name, opts),
1567-
modify_default(name, ref.type, opts)
1569+
modify_default(name, ref.type, opts),
1570+
collation_expr(collation)
15681571
]
15691572
end
15701573

15711574
defp column_change(table, {:modify, name, type, opts}) do
1575+
collation = Keyword.fetch(opts, :collation)
1576+
15721577
[
15731578
drop_reference_expr(opts[:from], table, name),
15741579
"ALTER COLUMN ",
15751580
quote_name(name),
15761581
" TYPE ",
15771582
column_type(type, opts),
15781583
modify_null(name, opts),
1579-
modify_default(name, type, opts)
1584+
modify_default(name, type, opts),
1585+
collation_expr(collation)
15801586
]
15811587
end
15821588

@@ -1624,14 +1630,18 @@ if Code.ensure_loaded?(Postgrex) do
16241630
defp column_options(type, opts) do
16251631
default = Keyword.fetch(opts, :default)
16261632
null = Keyword.get(opts, :null)
1633+
collation = Keyword.fetch(opts, :collation)
16271634

1628-
[default_expr(default, type), null_expr(null)]
1635+
[default_expr(default, type), null_expr(null), collation_expr(collation)]
16291636
end
16301637

16311638
defp null_expr(false), do: " NOT NULL"
16321639
defp null_expr(true), do: " NULL"
16331640
defp null_expr(_), do: []
16341641

1642+
defp collation_expr({:ok, collation_name}), do: " COLLATE \"#{collation_name}\""
1643+
defp collation_expr(_), do: []
1644+
16351645
defp new_constraint_expr(%Constraint{check: check} = constraint) when is_binary(check) do
16361646
[
16371647
"CONSTRAINT ",

lib/ecto/adapters/tds/connection.ex

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1455,6 +1455,8 @@ if Code.ensure_loaded?(Tds) do
14551455
end
14561456

14571457
defp column_change(statement_prefix, table, {:modify, name, type, opts}) do
1458+
collation = Keyword.fetch(opts, :collation)
1459+
14581460
[
14591461
drop_constraint_from_expr(opts[:from], table, name, statement_prefix),
14601462
maybe_drop_default_expr(statement_prefix, table, name, opts),
@@ -1465,6 +1467,7 @@ if Code.ensure_loaded?(Tds) do
14651467
" ",
14661468
column_type(type, opts),
14671469
null_expr(Keyword.get(opts, :null)),
1470+
collation_expr(collation),
14681471
"; "
14691472
],
14701473
[column_default_value(statement_prefix, table, name, opts)]
@@ -1500,7 +1503,9 @@ if Code.ensure_loaded?(Tds) do
15001503
defp column_options(table, name, opts) do
15011504
default = Keyword.fetch(opts, :default)
15021505
null = Keyword.get(opts, :null)
1503-
[null_expr(null), default_expr(table, name, default)]
1506+
collation = Keyword.fetch(opts, :collation)
1507+
1508+
[null_expr(null), default_expr(table, name, default), collation_expr(collation)]
15041509
end
15051510

15061511
defp column_default_value(statement_prefix, table, name, opts) do
@@ -1516,6 +1521,9 @@ if Code.ensure_loaded?(Tds) do
15161521
defp null_expr(true), do: [" NULL"]
15171522
defp null_expr(_), do: []
15181523

1524+
defp collation_expr({:ok, collation_name}), do: " COLLATE #{collation_name}"
1525+
defp collation_expr(_), do: []
1526+
15191527
defp default_expr(_table, _name, {:ok, nil}),
15201528
do: []
15211529

lib/ecto/migration.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,26 @@ defmodule Ecto.Migration do
274274
275275
config :app, App.Repo, migration_default_prefix: "my_prefix"
276276
277+
## Collations
278+
279+
Collations can be set on a column with the option `:collation`. This can be
280+
useful when relying on ASCII sorting of characters when using a fractional index
281+
for example. All supported collations and types that support setting a collocation
282+
are not known by `ecto_sql` and specifying an incorrect collation or a collation on
283+
an unsupported type might cause a migration to fail. Be sure to match the collation
284+
on any column that references another column.
285+
286+
def change do
287+
create table(:collate_reference) do
288+
add :name, :string, collation: "POSIX"
289+
end
290+
291+
create table(:collate) do
292+
add :string, :string, collation: "POSIX"
293+
add :name_ref, references(:collate_reference, type: :string, column: :name), collation: "POSIX"
294+
end
295+
end
296+
277297
## Comments
278298
279299
Migrations where you create or alter a table support specifying table
@@ -1166,6 +1186,7 @@ defmodule Ecto.Migration do
11661186
specified.
11671187
* `:scale` - the scale of a numeric type. Defaults to `0`.
11681188
* `:comment` - adds a comment to the added column.
1189+
* `:collation` - the collation of the text type.
11691190
* `:after` - positions field after the specified one. Only supported on MySQL,
11701191
it is ignored by other databases.
11711192
* `:generated` - a string representing the expression for a generated column. See
@@ -1345,6 +1366,7 @@ defmodule Ecto.Migration do
13451366
specified.
13461367
* `:scale` - the scale of a numeric type. Defaults to `0`.
13471368
* `:comment` - adds a comment to the modified column.
1369+
* `:collation` - the collation of the text type.
13481370
"""
13491371
def modify(column, type, opts \\ []) when is_atom(column) and is_list(opts) do
13501372
validate_precision_opts!(opts, column)

0 commit comments

Comments
 (0)