diff --git a/lib/ecto/adapters/postgres/connection.ex b/lib/ecto/adapters/postgres/connection.ex index 3248f144..df9ccbaf 100644 --- a/lib/ecto/adapters/postgres/connection.ex +++ b/lib/ecto/adapters/postgres/connection.ex @@ -1602,10 +1602,11 @@ if Code.ensure_loaded?(Postgrex) do "ALTER COLUMN ", quote_name(name), " TYPE ", - column_type(type, opts), + modify_column_type(type, opts), modify_null(name, opts), modify_default(name, type, opts), - collation_expr(collation) + collation_expr(collation), + modify_identity(name, type, opts) ] end @@ -1632,6 +1633,19 @@ if Code.ensure_loaded?(Postgrex) do defp column_change(_table, {:remove_if_exists, name}), do: ["DROP COLUMN IF EXISTS ", quote_name(name)] + # `:identity`'s "GENERATED BY DEFAULT AS IDENTITY" is valid as part of a column + # definition (CREATE TABLE/ADD COLUMN), but PostgreSQL rejects it combined with + # ALTER COLUMN ... TYPE - it must be added via a separate ALTER COLUMN ... ADD + # GENERATED clause (see modify_identity/3 below). + defp modify_column_type(:identity, opts), do: column_type_name(:identity, opts) + defp modify_column_type(type, opts), do: column_type(type, opts) + + defp modify_identity(name, :identity, opts) do + [", ALTER COLUMN ", quote_name(name), " ADD", identity_generated_expr(opts)] + end + + defp modify_identity(_name, _type, _opts), do: [] + defp modify_null(name, opts) do case Keyword.get(opts, :null) do true -> [", ALTER COLUMN ", quote_name(name), " DROP NOT NULL"] @@ -1798,22 +1812,7 @@ if Code.ensure_loaded?(Postgrex) do case Keyword.get(opts, :generated) do nil when type == :identity -> - cleanup = fn v -> is_integer(v) and v > 0 end - - sequence = - [Keyword.get(opts, :start_value)] - |> Enum.filter(cleanup) - |> Enum.map(&"START WITH #{&1}") - |> Kernel.++( - [Keyword.get(opts, :increment)] - |> Enum.filter(cleanup) - |> Enum.map(&"INCREMENT BY #{&1}") - ) - - case sequence do - [] -> [type_name, " GENERATED BY DEFAULT AS IDENTITY"] - _ -> [type_name, " GENERATED BY DEFAULT AS IDENTITY(", Enum.join(sequence, " "), ") "] - end + [type_name, identity_generated_expr(opts)] nil -> type_name @@ -1827,6 +1826,25 @@ if Code.ensure_loaded?(Postgrex) do end end + defp identity_generated_expr(opts) do + cleanup = fn v -> is_integer(v) and v > 0 end + + sequence = + [Keyword.get(opts, :start_value)] + |> Enum.filter(cleanup) + |> Enum.map(&"START WITH #{&1}") + |> Kernel.++( + [Keyword.get(opts, :increment)] + |> Enum.filter(cleanup) + |> Enum.map(&"INCREMENT BY #{&1}") + ) + + case sequence do + [] -> " GENERATED BY DEFAULT AS IDENTITY" + _ -> [" GENERATED BY DEFAULT AS IDENTITY(", Enum.join(sequence, " "), ") "] + end + end + defp column_type_name({:array, type}, opts) do [column_type_name(type, opts), "[]"] end diff --git a/test/ecto/adapters/postgres_test.exs b/test/ecto/adapters/postgres_test.exs index e907da5e..25788087 100644 --- a/test/ecto/adapters/postgres_test.exs +++ b/test/ecto/adapters/postgres_test.exs @@ -2391,6 +2391,35 @@ defmodule Ecto.Adapters.PostgresTest do ] end + test "alter table modify to identity" do + alter = + {:alter, table(:posts), + [ + {:modify, :field, :identity, []} + ]} + + assert execute_ddl(alter) == [ + """ + ALTER TABLE "posts" + ALTER COLUMN "field" TYPE bigint, + ALTER COLUMN "field" ADD GENERATED BY DEFAULT AS IDENTITY + """ + |> remove_newlines + ] + end + + test "alter table modify to identity with options" do + alter = + {:alter, table(:posts), + [ + {:modify, :field, :identity, [start_value: 1_000, increment: 10]} + ]} + + assert execute_ddl(alter) == [ + ~s|ALTER TABLE "posts" ALTER COLUMN "field" TYPE bigint, ALTER COLUMN "field" ADD GENERATED BY DEFAULT AS IDENTITY(START WITH 1000 INCREMENT BY 10) | + ] + end + test "create table with generated column" do create = {:create, table(:posts),