From d741e185bb07a5479887584cb8ab890787b2c214 Mon Sep 17 00:00:00 2001 From: Jim Lemmers Date: Mon, 28 Apr 2025 15:03:33 +0200 Subject: [PATCH 1/8] Add support for collations in Postgres --- integration_test/pg/migrations_test.exs | 62 ++++++++++++++++++++++++ integration_test/sql/migration.exs | 1 + lib/ecto/adapters/postgres/connection.ex | 20 ++++++-- 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/integration_test/pg/migrations_test.exs b/integration_test/pg/migrations_test.exs index a6c87cbd8..3e1a75f8a 100644 --- a/integration_test/pg/migrations_test.exs +++ b/integration_test/pg/migrations_test.exs @@ -40,6 +40,41 @@ defmodule Ecto.Integration.MigrationsTest do end end + collation = "POSIX" + @collation collation + + text_types = ~w/char varchar text/a + @text_types text_types + + defmodule CollateMigration do + use Ecto.Migration + + @collation collation + @text_types text_types + + def change do + create table(:collate_reference) do + add :name, :string, primary_key: true, collation: @collation + end + + create unique_index(:collate_reference, :name) + + create table(:collate) do + add :string, :string, collation: @collation + for type <- @text_types do + add type, type, collation: @collation + end + + add :integer, :integer, collation: @collation + add :name_string, references(:collate_reference, type: :string, column: :name), collation: @collation + end + + alter table(:collate) do + modify :string, :string, collation: "C" + end + end + end + test "logs Postgres notice messages" do log = capture_log(fn -> @@ -145,5 +180,32 @@ defmodule Ecto.Integration.MigrationsTest do refute down_log =~ @version_delete refute down_log =~ "commit []" end + + test "collation can be set on a column" do + num = @base_migration + System.unique_integer([:positive]) + + assert :ok = Ecto.Migrator.up(PoolRepo, num, CollateMigration, log: :info) + + query = fn column -> """ + SELECT collation_name + FROM information_schema.columns + WHERE table_name = 'collate' AND column_name = '#{column}'; + """ + end + + assert %{ + rows: [["C"]] + } = Ecto.Adapters.SQL.query!(PoolRepo, query.("string"), []) + + for type <- @text_types do + assert %{ + rows: [[@collation]] + } = Ecto.Adapters.SQL.query!(PoolRepo, query.(type), []) + end + + assert %{ + rows: [[nil]] + } = Ecto.Adapters.SQL.query!(PoolRepo, query.("integer"), []) + end end end diff --git a/integration_test/sql/migration.exs b/integration_test/sql/migration.exs index fe4429c58..430ad7cad 100644 --- a/integration_test/sql/migration.exs +++ b/integration_test/sql/migration.exs @@ -463,6 +463,7 @@ defmodule Ecto.Integration.MigrationTest do end end + import Ecto.Query, only: [from: 2] import Ecto.Migrator, only: [up: 4, down: 4] diff --git a/lib/ecto/adapters/postgres/connection.ex b/lib/ecto/adapters/postgres/connection.ex index f023bfb35..956c74727 100644 --- a/lib/ecto/adapters/postgres/connection.ex +++ b/lib/ecto/adapters/postgres/connection.ex @@ -1555,6 +1555,8 @@ if Code.ensure_loaded?(Postgrex) do end defp column_change(table, {:modify, name, %Reference{} = ref, opts}) do + collation = Keyword.fetch(opts, :collation) + [ drop_reference_expr(opts[:from], table, name), "ALTER COLUMN ", @@ -1564,11 +1566,14 @@ if Code.ensure_loaded?(Postgrex) do ", ADD ", reference_expr(ref, table, name), modify_null(name, opts), - modify_default(name, ref.type, opts) + modify_default(name, ref.type, opts), + collation_expr(collation, ref.type) ] end defp column_change(table, {:modify, name, type, opts}) do + collation = Keyword.fetch(opts, :collation) + [ drop_reference_expr(opts[:from], table, name), "ALTER COLUMN ", @@ -1576,7 +1581,8 @@ if Code.ensure_loaded?(Postgrex) do " TYPE ", column_type(type, opts), modify_null(name, opts), - modify_default(name, type, opts) + modify_default(name, type, opts), + collation_expr(collation, type) ] end @@ -1624,14 +1630,22 @@ if Code.ensure_loaded?(Postgrex) do defp column_options(type, opts) do default = Keyword.fetch(opts, :default) null = Keyword.get(opts, :null) + collation = Keyword.fetch(opts, :collation) - [default_expr(default, type), null_expr(null)] + [default_expr(default, type), null_expr(null), collation_expr(collation, type)] end defp null_expr(false), do: " NOT NULL" defp null_expr(true), do: " NULL" defp null_expr(_), do: [] + defp collation_expr({:ok, collation_name}, text_type) + when text_type in ~w/string text char varchar/a do + " COLLATE \"#{collation_name}\"" + end + + defp collation_expr(_, _), do: [] + defp new_constraint_expr(%Constraint{check: check} = constraint) when is_binary(check) do [ "CONSTRAINT ", From 55c31cfb717e4735950cc1a979792bb89577699d Mon Sep 17 00:00:00 2001 From: Jim Date: Tue, 29 Apr 2025 11:27:02 +0200 Subject: [PATCH 2/8] Add support for collation in MySQL --- integration_test/myxql/migrations_test.exs | 61 ++++++++++++++++++++++ lib/ecto/adapters/myxql/connection.ex | 40 ++++++++++---- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/integration_test/myxql/migrations_test.exs b/integration_test/myxql/migrations_test.exs index 34209bf46..920ba4f6e 100644 --- a/integration_test/myxql/migrations_test.exs +++ b/integration_test/myxql/migrations_test.exs @@ -29,6 +29,42 @@ defmodule Ecto.Integration.MigrationsTest do end end + text_variants = ~w/tinytext text mediumtext longtext/a + @text_variants text_variants + + collation = "utf8mb4_bin" + @collation collation + + defmodule CollateMigration do + use Ecto.Migration + + @text_variants text_variants + @collation collation + + def change do + create table(:collate_reference) do + add :name, :string, collation: @collation + end + + create unique_index(:collate_reference, :name) + + create table(:collate) do + add :string, :string, collation: @collation + add :varchar, :varchar, size: 255, collation: @collation + add :integer, :integer, collation: @collation + add :name_string, references(:collate_reference, type: :string, column: :name), collation: @collation + + for type <- @text_variants do + add type, type, collation: @collation + end + end + + alter table(:collate) do + modify :string, :string, collation: "utf8mb4_general_ci" + end + end + end + describe "Migrator" do @get_lock_command ~s[SELECT GET_LOCK('ecto_Ecto.Integration.PoolRepo', -1)] @release_lock_command ~s[SELECT RELEASE_LOCK('ecto_Ecto.Integration.PoolRepo')] @@ -107,5 +143,30 @@ defmodule Ecto.Integration.MigrationsTest do assert log =~ "ALTER TABLE `alter_table` ADD `column2` varchar(255) COMMENT 'second column' AFTER `column1`" end + + test "collation can be set on a column" do + num = @base_migration + System.unique_integer([:positive]) + assert :ok = Ecto.Migrator.up(PoolRepo, num, CollateMigration, log: false) + query = fn column -> """ + SELECT collation_name + FROM information_schema.columns + WHERE table_name = 'collate' AND column_name = '#{column}'; + """ + end + + assert %{ + rows: [["utf8mb4_general_ci"]] + } = Ecto.Adapters.SQL.query!(PoolRepo, query.("string"), []) + + for type <- ~w/text name_string/ ++ @text_variants do + assert %{ + rows: [[@collation]] + } = Ecto.Adapters.SQL.query!(PoolRepo, query.(type), []) + end + + assert %{ + rows: [[nil]] + } = Ecto.Adapters.SQL.query!(PoolRepo, query.("integer"), []) + end end end diff --git a/lib/ecto/adapters/myxql/connection.ex b/lib/ecto/adapters/myxql/connection.ex index 92d36d0c0..5aeb768db 100644 --- a/lib/ecto/adapters/myxql/connection.ex +++ b/lib/ecto/adapters/myxql/connection.ex @@ -1171,13 +1171,13 @@ if Code.ensure_loaded?(MyXQL) do quote_name(name), ?\s, reference_column_type(ref.type, opts), - column_options(opts), + column_options(ref.type, opts), reference_expr(ref, table, name) ] end defp column_definition(_table, {:add, name, type, opts}) do - [quote_name(name), ?\s, column_type(type, opts), column_options(opts)] + [quote_name(name), ?\s, column_type(type, opts), column_options(type, opts)] end defp column_changes(table, columns) do @@ -1194,13 +1194,13 @@ if Code.ensure_loaded?(MyXQL) do quote_name(name), ?\s, reference_column_type(ref.type, opts), - column_options(opts), + column_options(ref.type, opts), constraint_expr(ref, table, name) ] end defp column_change(_table, {:add, name, type, opts}) do - ["ADD ", quote_name(name), ?\s, column_type(type, opts), column_options(opts)] + ["ADD ", quote_name(name), ?\s, column_type(type, opts), column_options(type, opts)] end defp column_change(table, {:add_if_not_exists, name, %Reference{} = ref, opts}) do @@ -1209,13 +1209,19 @@ if Code.ensure_loaded?(MyXQL) do quote_name(name), ?\s, reference_column_type(ref.type, opts), - column_options(opts), + column_options(ref.type, opts), constraint_if_not_exists_expr(ref, table, name) ] end defp column_change(_table, {:add_if_not_exists, name, type, opts}) do - ["ADD IF NOT EXISTS ", quote_name(name), ?\s, column_type(type, opts), column_options(opts)] + [ + "ADD IF NOT EXISTS ", + quote_name(name), + ?\s, + column_type(type, opts), + column_options(type, opts) + ] end defp column_change(table, {:modify, name, %Reference{} = ref, opts}) do @@ -1225,7 +1231,7 @@ if Code.ensure_loaded?(MyXQL) do quote_name(name), ?\s, reference_column_type(ref.type, opts), - column_options(opts), + column_options(ref.type, opts), constraint_expr(ref, table, name) ] end @@ -1237,7 +1243,7 @@ if Code.ensure_loaded?(MyXQL) do quote_name(name), ?\s, column_type(type, opts), - column_options(opts) + column_options(type, opts) ] end @@ -1259,13 +1265,20 @@ if Code.ensure_loaded?(MyXQL) do defp column_change(_table, {:remove_if_exists, name}), do: ["DROP IF EXISTS ", quote_name(name)] - defp column_options(opts) do + defp column_options(type, opts) do default = Keyword.fetch(opts, :default) null = Keyword.get(opts, :null) after_column = Keyword.get(opts, :after) comment = Keyword.get(opts, :comment) + collation = Keyword.fetch(opts, :collation) - [default_expr(default), null_expr(null), comment_expr(comment), after_expr(after_column)] + [ + default_expr(default), + collation_expr(collation, type), + null_expr(null), + comment_expr(comment), + after_expr(after_column) + ] end defp comment_expr(comment, create_table? \\ false) @@ -1286,6 +1299,13 @@ if Code.ensure_loaded?(MyXQL) do defp null_expr(true), do: " NULL" defp null_expr(_), do: [] + defp collation_expr({:ok, collation_name}, text_type) + when text_type in ~w/string char varchar text tinytext mediumtext longtext/a do + " COLLATE \"#{collation_name}\"" + end + + defp collation_expr(_, _), do: [] + defp new_constraint_expr(%Constraint{check: check} = constraint) when is_binary(check) do [ "CONSTRAINT ", From 1727f2efb83e6f76452ba36ee01e29d9c9a9918f Mon Sep 17 00:00:00 2001 From: Jim Date: Tue, 29 Apr 2025 22:19:54 +0200 Subject: [PATCH 3/8] Add support for collation in MSSQL --- integration_test/tds/migrations_test.exs | 58 ++++++++++++++++++++++++ lib/ecto/adapters/tds/connection.ex | 30 ++++++++---- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/integration_test/tds/migrations_test.exs b/integration_test/tds/migrations_test.exs index 09ec07cfa..cf86e5bd4 100644 --- a/integration_test/tds/migrations_test.exs +++ b/integration_test/tds/migrations_test.exs @@ -15,6 +15,38 @@ defmodule Ecto.Integration.MigrationsTest do end end + collation = "Latin1_General_CS_AS" + @collation collation + + defmodule CollateMigration do + use Ecto.Migration + @collation collation + + def change do + create table(:collate_reference) do + add :name, :string, collation: @collation + end + + create unique_index(:collate_reference, :name) + + create table(:collate) do + add :string, :string, collation: @collation + add :char, :char, size: 255, collation: @collation + add :nchar, :nchar, size: 255, collation: @collation + add :varchar, :varchar, size: 255, collation: @collation + add :nvarchar, :nvarchar, size: 255, collation: @collation + add :text, :text, collation: @collation + add :ntext, :ntext, collation: @collation + add :integer, :integer, collation: @collation + add :name_string, references(:collate_reference, type: :string, column: :name), collation: @collation + end + + alter table(:collate) do + modify :string, :string, collation: "Japanese_Bushu_Kakusu_100_CS_AS_KS_WS" + end + end + end + describe "Migrator" do @get_lock_command ~s(sp_getapplock @Resource = 'ecto_Ecto.Integration.PoolRepo', @LockMode = 'Exclusive', @LockOwner = 'Transaction', @LockTimeout = -1) @create_table_sql ~s(CREATE TABLE [log_mode_table]) @@ -77,5 +109,31 @@ defmodule Ecto.Integration.MigrationsTest do refute down_log =~ @version_delete refute down_log =~ "commit []" end + + test "collation can be set on a column" do + num = @base_migration + System.unique_integer([:positive]) + assert :ok = Ecto.Migrator.up(PoolRepo, num, CollateMigration, log: :info) + + query = fn column -> """ + SELECT collation_name + FROM information_schema.columns + WHERE table_name = 'collate' AND column_name = '#{column}'; + """ + end + + assert %{ + rows: [["Japanese_Bushu_Kakusu_100_CS_AS_KS_WS"]] + } = Ecto.Adapters.SQL.query!(PoolRepo, query.("string"), []) + + for type <- ~w/char varchar nchar nvarchar text ntext/ do + assert %{ + rows: [[@collation]] + } = Ecto.Adapters.SQL.query!(PoolRepo, query.(type), []) + end + + assert %{ + rows: [[nil]] + } = Ecto.Adapters.SQL.query!(PoolRepo, query.("integer"), []) + end end end diff --git a/lib/ecto/adapters/tds/connection.ex b/lib/ecto/adapters/tds/connection.ex index 852e1ef37..b636b7f86 100644 --- a/lib/ecto/adapters/tds/connection.ex +++ b/lib/ecto/adapters/tds/connection.ex @@ -1369,13 +1369,13 @@ if Code.ensure_loaded?(Tds) do quote_name(name), " ", reference_column_type(ref.type, opts), - column_options(table, name, opts), + column_options(table, name, ref.type, opts), reference_expr(ref, table, name) ] end defp column_definition(table, {:add, name, type, opts}) do - [quote_name(name), " ", column_type(type, opts), column_options(table, name, opts)] + [quote_name(name), " ", column_type(type, opts), column_options(table, name, type, opts)] end defp column_changes(statement, table, columns) do @@ -1396,7 +1396,7 @@ if Code.ensure_loaded?(Tds) do quote_name(name), " ", reference_column_type(ref.type, opts), - column_options(table, name, opts), + column_options(table, name, ref.type, opts), "; " ], [statement_prefix, "ADD", constraint_expr(ref, table, name), "; "] @@ -1411,7 +1411,7 @@ if Code.ensure_loaded?(Tds) do quote_name(name), " ", column_type(type, opts), - column_options(table, name, opts), + column_options(table, name, type, opts), "; " ] ] @@ -1430,7 +1430,7 @@ if Code.ensure_loaded?(Tds) do quote_name(column_name), " ", column_type(type, opts), - column_options(table, column_name, opts), + column_options(table, column_name, type, opts), "; " ] ] @@ -1446,7 +1446,7 @@ if Code.ensure_loaded?(Tds) do quote_name(name), " ", reference_column_type(ref.type, opts), - column_options(table, name, opts), + column_options(table, name, ref.type, opts), "; " ], [statement_prefix, "ADD", constraint_expr(ref, table, name), "; "], @@ -1455,6 +1455,8 @@ if Code.ensure_loaded?(Tds) do end defp column_change(statement_prefix, table, {:modify, name, type, opts}) do + collation = Keyword.fetch(opts, :collation) + [ drop_constraint_from_expr(opts[:from], table, name, statement_prefix), maybe_drop_default_expr(statement_prefix, table, name, opts), @@ -1465,6 +1467,7 @@ if Code.ensure_loaded?(Tds) do " ", column_type(type, opts), null_expr(Keyword.get(opts, :null)), + collation_expr(collation, type), "; " ], [column_default_value(statement_prefix, table, name, opts)] @@ -1497,10 +1500,14 @@ if Code.ensure_loaded?(Tds) do ] end - defp column_options(table, name, opts) do + defp column_options(table, name, type, opts) do default = Keyword.fetch(opts, :default) null = Keyword.get(opts, :null) - [null_expr(null), default_expr(table, name, default)] + + collation = + Keyword.fetch(opts, :collation) + + [null_expr(null), default_expr(table, name, default), collation_expr(collation, type)] end defp column_default_value(statement_prefix, table, name, opts) do @@ -1516,6 +1523,13 @@ if Code.ensure_loaded?(Tds) do defp null_expr(true), do: [" NULL"] defp null_expr(_), do: [] + defp collation_expr({:ok, collation_name}, text_type) + when text_type in ~w/string char varchar nchar nvarchar text ntext/a do + " COLLATE #{collation_name}" + end + + defp collation_expr(_, _), do: [] + defp default_expr(_table, _name, {:ok, nil}), do: [] From c520dc50cc5852931bfc8da04023512283815b69 Mon Sep 17 00:00:00 2001 From: Jim Date: Tue, 29 Apr 2025 11:28:16 +0200 Subject: [PATCH 4/8] Add collation documentation --- lib/ecto/migration.ex | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/ecto/migration.ex b/lib/ecto/migration.ex index e26d8f495..1a5b8abd9 100644 --- a/lib/ecto/migration.ex +++ b/lib/ecto/migration.ex @@ -274,6 +274,27 @@ defmodule Ecto.Migration do config :app, App.Repo, migration_default_prefix: "my_prefix" + ## Collations + + For columns with a text type, the collation can be set on the column with the + option `:collation`. This can be useful when relying on ASCII sorting of + characters when using a fractional index for example. All supported collations + are not known by `ecto_sql` and specifying an incorrect collation might cause + a migration to fail. + + ### N.B. be sure to match the collation on any columns that reference other text columns. See example below + + def change do + create table(:collate_reference) do + add :name, :string, collation: "POSIX" + end + + create table(:collate) do + add :string, :string, collation: "POSIX" + add :name_ref, references(:collate_reference, type: :string, column: :name), collation: "POSIX" + end + end + ## Comments Migrations where you create or alter a table support specifying table @@ -1166,6 +1187,7 @@ defmodule Ecto.Migration do specified. * `:scale` - the scale of a numeric type. Defaults to `0`. * `:comment` - adds a comment to the added column. + * `:collation` - the collation of the text type. * `:after` - positions field after the specified one. Only supported on MySQL, it is ignored by other databases. * `:generated` - a string representing the expression for a generated column. See @@ -1345,6 +1367,7 @@ defmodule Ecto.Migration do specified. * `:scale` - the scale of a numeric type. Defaults to `0`. * `:comment` - adds a comment to the modified column. + * `:collation` - the collation of the text type. """ def modify(column, type, opts \\ []) when is_atom(column) and is_list(opts) do validate_precision_opts!(opts, column) From 586900b2faa85ccbfd37fdfce552bac00d895ddb Mon Sep 17 00:00:00 2001 From: Jim Date: Thu, 1 May 2025 20:14:42 +0200 Subject: [PATCH 5/8] Collation is set without checking column type --- integration_test/myxql/migrations_test.exs | 5 ---- integration_test/pg/migrations_test.exs | 5 ---- integration_test/tds/migrations_test.exs | 5 ---- lib/ecto/adapters/myxql/connection.ex | 28 +++++++++----------- lib/ecto/adapters/postgres/connection.ex | 14 ++++------ lib/ecto/adapters/tds/connection.ex | 30 +++++++++------------- 6 files changed, 29 insertions(+), 58 deletions(-) diff --git a/integration_test/myxql/migrations_test.exs b/integration_test/myxql/migrations_test.exs index 920ba4f6e..58d71b63a 100644 --- a/integration_test/myxql/migrations_test.exs +++ b/integration_test/myxql/migrations_test.exs @@ -51,7 +51,6 @@ defmodule Ecto.Integration.MigrationsTest do create table(:collate) do add :string, :string, collation: @collation add :varchar, :varchar, size: 255, collation: @collation - add :integer, :integer, collation: @collation add :name_string, references(:collate_reference, type: :string, column: :name), collation: @collation for type <- @text_variants do @@ -163,10 +162,6 @@ defmodule Ecto.Integration.MigrationsTest do rows: [[@collation]] } = Ecto.Adapters.SQL.query!(PoolRepo, query.(type), []) end - - assert %{ - rows: [[nil]] - } = Ecto.Adapters.SQL.query!(PoolRepo, query.("integer"), []) end end end diff --git a/integration_test/pg/migrations_test.exs b/integration_test/pg/migrations_test.exs index 3e1a75f8a..cde5434bc 100644 --- a/integration_test/pg/migrations_test.exs +++ b/integration_test/pg/migrations_test.exs @@ -65,7 +65,6 @@ defmodule Ecto.Integration.MigrationsTest do add type, type, collation: @collation end - add :integer, :integer, collation: @collation add :name_string, references(:collate_reference, type: :string, column: :name), collation: @collation end @@ -202,10 +201,6 @@ defmodule Ecto.Integration.MigrationsTest do rows: [[@collation]] } = Ecto.Adapters.SQL.query!(PoolRepo, query.(type), []) end - - assert %{ - rows: [[nil]] - } = Ecto.Adapters.SQL.query!(PoolRepo, query.("integer"), []) end end end diff --git a/integration_test/tds/migrations_test.exs b/integration_test/tds/migrations_test.exs index cf86e5bd4..9c10d4e72 100644 --- a/integration_test/tds/migrations_test.exs +++ b/integration_test/tds/migrations_test.exs @@ -37,7 +37,6 @@ defmodule Ecto.Integration.MigrationsTest do add :nvarchar, :nvarchar, size: 255, collation: @collation add :text, :text, collation: @collation add :ntext, :ntext, collation: @collation - add :integer, :integer, collation: @collation add :name_string, references(:collate_reference, type: :string, column: :name), collation: @collation end @@ -130,10 +129,6 @@ defmodule Ecto.Integration.MigrationsTest do rows: [[@collation]] } = Ecto.Adapters.SQL.query!(PoolRepo, query.(type), []) end - - assert %{ - rows: [[nil]] - } = Ecto.Adapters.SQL.query!(PoolRepo, query.("integer"), []) end end end diff --git a/lib/ecto/adapters/myxql/connection.ex b/lib/ecto/adapters/myxql/connection.ex index 5aeb768db..e1ff49e01 100644 --- a/lib/ecto/adapters/myxql/connection.ex +++ b/lib/ecto/adapters/myxql/connection.ex @@ -1171,13 +1171,13 @@ if Code.ensure_loaded?(MyXQL) do quote_name(name), ?\s, reference_column_type(ref.type, opts), - column_options(ref.type, opts), + column_options(opts), reference_expr(ref, table, name) ] end defp column_definition(_table, {:add, name, type, opts}) do - [quote_name(name), ?\s, column_type(type, opts), column_options(type, opts)] + [quote_name(name), ?\s, column_type(type, opts), column_options(opts)] end defp column_changes(table, columns) do @@ -1194,13 +1194,13 @@ if Code.ensure_loaded?(MyXQL) do quote_name(name), ?\s, reference_column_type(ref.type, opts), - column_options(ref.type, opts), + column_options(opts), constraint_expr(ref, table, name) ] end defp column_change(_table, {:add, name, type, opts}) do - ["ADD ", quote_name(name), ?\s, column_type(type, opts), column_options(type, opts)] + ["ADD ", quote_name(name), ?\s, column_type(type, opts), column_options(opts)] end defp column_change(table, {:add_if_not_exists, name, %Reference{} = ref, opts}) do @@ -1209,7 +1209,7 @@ if Code.ensure_loaded?(MyXQL) do quote_name(name), ?\s, reference_column_type(ref.type, opts), - column_options(ref.type, opts), + column_options(opts), constraint_if_not_exists_expr(ref, table, name) ] end @@ -1220,7 +1220,7 @@ if Code.ensure_loaded?(MyXQL) do quote_name(name), ?\s, column_type(type, opts), - column_options(type, opts) + column_options(opts) ] end @@ -1231,7 +1231,7 @@ if Code.ensure_loaded?(MyXQL) do quote_name(name), ?\s, reference_column_type(ref.type, opts), - column_options(ref.type, opts), + column_options(opts), constraint_expr(ref, table, name) ] end @@ -1243,7 +1243,7 @@ if Code.ensure_loaded?(MyXQL) do quote_name(name), ?\s, column_type(type, opts), - column_options(type, opts) + column_options(opts) ] end @@ -1265,7 +1265,7 @@ if Code.ensure_loaded?(MyXQL) do defp column_change(_table, {:remove_if_exists, name}), do: ["DROP IF EXISTS ", quote_name(name)] - defp column_options(type, opts) do + defp column_options(opts) do default = Keyword.fetch(opts, :default) null = Keyword.get(opts, :null) after_column = Keyword.get(opts, :after) @@ -1274,7 +1274,7 @@ if Code.ensure_loaded?(MyXQL) do [ default_expr(default), - collation_expr(collation, type), + collation_expr(collation), null_expr(null), comment_expr(comment), after_expr(after_column) @@ -1299,12 +1299,8 @@ if Code.ensure_loaded?(MyXQL) do defp null_expr(true), do: " NULL" defp null_expr(_), do: [] - defp collation_expr({:ok, collation_name}, text_type) - when text_type in ~w/string char varchar text tinytext mediumtext longtext/a do - " COLLATE \"#{collation_name}\"" - end - - defp collation_expr(_, _), do: [] + defp collation_expr({:ok, collation_name}), do: " COLLATE \"#{collation_name}\"" + defp collation_expr(_), do: [] defp new_constraint_expr(%Constraint{check: check} = constraint) when is_binary(check) do [ diff --git a/lib/ecto/adapters/postgres/connection.ex b/lib/ecto/adapters/postgres/connection.ex index 956c74727..7a284f73f 100644 --- a/lib/ecto/adapters/postgres/connection.ex +++ b/lib/ecto/adapters/postgres/connection.ex @@ -1567,7 +1567,7 @@ if Code.ensure_loaded?(Postgrex) do reference_expr(ref, table, name), modify_null(name, opts), modify_default(name, ref.type, opts), - collation_expr(collation, ref.type) + collation_expr(collation) ] end @@ -1582,7 +1582,7 @@ if Code.ensure_loaded?(Postgrex) do column_type(type, opts), modify_null(name, opts), modify_default(name, type, opts), - collation_expr(collation, type) + collation_expr(collation) ] end @@ -1632,19 +1632,15 @@ if Code.ensure_loaded?(Postgrex) do null = Keyword.get(opts, :null) collation = Keyword.fetch(opts, :collation) - [default_expr(default, type), null_expr(null), collation_expr(collation, type)] + [default_expr(default, type), null_expr(null), collation_expr(collation)] end defp null_expr(false), do: " NOT NULL" defp null_expr(true), do: " NULL" defp null_expr(_), do: [] - defp collation_expr({:ok, collation_name}, text_type) - when text_type in ~w/string text char varchar/a do - " COLLATE \"#{collation_name}\"" - end - - defp collation_expr(_, _), do: [] + defp collation_expr({:ok, collation_name}), do: " COLLATE \"#{collation_name}\"" + defp collation_expr(_), do: [] defp new_constraint_expr(%Constraint{check: check} = constraint) when is_binary(check) do [ diff --git a/lib/ecto/adapters/tds/connection.ex b/lib/ecto/adapters/tds/connection.ex index b636b7f86..8aaae1e9d 100644 --- a/lib/ecto/adapters/tds/connection.ex +++ b/lib/ecto/adapters/tds/connection.ex @@ -1369,13 +1369,13 @@ if Code.ensure_loaded?(Tds) do quote_name(name), " ", reference_column_type(ref.type, opts), - column_options(table, name, ref.type, opts), + column_options(table, name, opts), reference_expr(ref, table, name) ] end defp column_definition(table, {:add, name, type, opts}) do - [quote_name(name), " ", column_type(type, opts), column_options(table, name, type, opts)] + [quote_name(name), " ", column_type(type, opts), column_options(table, name, opts)] end defp column_changes(statement, table, columns) do @@ -1396,7 +1396,7 @@ if Code.ensure_loaded?(Tds) do quote_name(name), " ", reference_column_type(ref.type, opts), - column_options(table, name, ref.type, opts), + column_options(table, name, opts), "; " ], [statement_prefix, "ADD", constraint_expr(ref, table, name), "; "] @@ -1411,7 +1411,7 @@ if Code.ensure_loaded?(Tds) do quote_name(name), " ", column_type(type, opts), - column_options(table, name, type, opts), + column_options(table, name, opts), "; " ] ] @@ -1430,7 +1430,7 @@ if Code.ensure_loaded?(Tds) do quote_name(column_name), " ", column_type(type, opts), - column_options(table, column_name, type, opts), + column_options(table, column_name, opts), "; " ] ] @@ -1446,7 +1446,7 @@ if Code.ensure_loaded?(Tds) do quote_name(name), " ", reference_column_type(ref.type, opts), - column_options(table, name, ref.type, opts), + column_options(table, name, opts), "; " ], [statement_prefix, "ADD", constraint_expr(ref, table, name), "; "], @@ -1467,7 +1467,7 @@ if Code.ensure_loaded?(Tds) do " ", column_type(type, opts), null_expr(Keyword.get(opts, :null)), - collation_expr(collation, type), + collation_expr(collation), "; " ], [column_default_value(statement_prefix, table, name, opts)] @@ -1500,14 +1500,12 @@ if Code.ensure_loaded?(Tds) do ] end - defp column_options(table, name, type, opts) do + defp column_options(table, name, opts) do default = Keyword.fetch(opts, :default) null = Keyword.get(opts, :null) + collation = Keyword.fetch(opts, :collation) - collation = - Keyword.fetch(opts, :collation) - - [null_expr(null), default_expr(table, name, default), collation_expr(collation, type)] + [null_expr(null), default_expr(table, name, default), collation_expr(collation)] end defp column_default_value(statement_prefix, table, name, opts) do @@ -1523,12 +1521,8 @@ if Code.ensure_loaded?(Tds) do defp null_expr(true), do: [" NULL"] defp null_expr(_), do: [] - defp collation_expr({:ok, collation_name}, text_type) - when text_type in ~w/string char varchar nchar nvarchar text ntext/a do - " COLLATE #{collation_name}" - end - - defp collation_expr(_, _), do: [] + defp collation_expr({:ok, collation_name}), do: " COLLATE #{collation_name}" + defp collation_expr(_), do: [] defp default_expr(_table, _name, {:ok, nil}), do: [] From b94a1e3daca4992652483d542cfe93509223034e Mon Sep 17 00:00:00 2001 From: Jim Date: Thu, 1 May 2025 20:14:58 +0200 Subject: [PATCH 6/8] Update documentation --- lib/ecto/migration.ex | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/ecto/migration.ex b/lib/ecto/migration.ex index 1a5b8abd9..d3116bb00 100644 --- a/lib/ecto/migration.ex +++ b/lib/ecto/migration.ex @@ -276,13 +276,12 @@ defmodule Ecto.Migration do ## Collations - For columns with a text type, the collation can be set on the column with the - option `:collation`. This can be useful when relying on ASCII sorting of - characters when using a fractional index for example. All supported collations - are not known by `ecto_sql` and specifying an incorrect collation might cause - a migration to fail. - - ### N.B. be sure to match the collation on any columns that reference other text columns. See example below + Collations can be set on a column with the option `:collation`. This can be + useful when relying on ASCII sorting of characters when using a fractional index + for example. All supported collations and types that suuport setting a collocation + are not known by `ecto_sql` and specifying an incorrect collation or a collation on + an unsupported type might cause a migration to fail. Be sure to match the collation + on any column that references another column. def change do create table(:collate_reference) do From 1eb3957805784fe004c11bf79ae01070d1be08d3 Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Thu, 1 May 2025 20:10:45 -0400 Subject: [PATCH 7/8] Update integration_test/sql/migration.exs --- integration_test/sql/migration.exs | 1 - 1 file changed, 1 deletion(-) diff --git a/integration_test/sql/migration.exs b/integration_test/sql/migration.exs index 430ad7cad..fe4429c58 100644 --- a/integration_test/sql/migration.exs +++ b/integration_test/sql/migration.exs @@ -463,7 +463,6 @@ defmodule Ecto.Integration.MigrationTest do end end - import Ecto.Query, only: [from: 2] import Ecto.Migrator, only: [up: 4, down: 4] From 4fe02c4e22b652957de9b05cf4a3679a7b09cc98 Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Thu, 1 May 2025 20:10:55 -0400 Subject: [PATCH 8/8] Update lib/ecto/migration.ex --- lib/ecto/migration.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ecto/migration.ex b/lib/ecto/migration.ex index d3116bb00..d49a04544 100644 --- a/lib/ecto/migration.ex +++ b/lib/ecto/migration.ex @@ -278,7 +278,7 @@ defmodule Ecto.Migration do Collations can be set on a column with the option `:collation`. This can be useful when relying on ASCII sorting of characters when using a fractional index - for example. All supported collations and types that suuport setting a collocation + for example. All supported collations and types that support setting a collocation are not known by `ecto_sql` and specifying an incorrect collation or a collation on an unsupported type might cause a migration to fail. Be sure to match the collation on any column that references another column.