Skip to content

Commit 678d397

Browse files
Add :generated option to migrations (#580)
1 parent 2236b6f commit 678d397

7 files changed

Lines changed: 151 additions & 36 deletions

File tree

lib/ecto/adapters/myxql/connection.ex

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,28 +1256,46 @@ if Code.ensure_loaded?(MyXQL) do
12561256
defp options_expr(options),
12571257
do: [?\s, to_string(options)]
12581258

1259-
defp column_type(type, _opts) when type in ~w(time utc_datetime naive_datetime)a,
1260-
do: ecto_to_db(type)
1259+
defp column_type(type, opts) when type in ~w(time utc_datetime naive_datetime)a do
1260+
generated = Keyword.get(opts, :generated)
1261+
[ecto_to_db(type), generated_expr(generated)]
1262+
end
12611263

12621264
defp column_type(type, opts)
12631265
when type in ~w(time_usec utc_datetime_usec naive_datetime_usec)a do
12641266
precision = Keyword.get(opts, :precision, 6)
1267+
generated = Keyword.get(opts, :generated)
12651268
type_name = ecto_to_db(type)
12661269

1267-
[type_name, ?(, to_string(precision), ?)]
1270+
[type_name, ?(, to_string(precision), ?), generated_expr(generated)]
12681271
end
12691272

12701273
defp column_type(type, opts) do
12711274
size = Keyword.get(opts, :size)
12721275
precision = Keyword.get(opts, :precision)
1276+
generated = Keyword.get(opts, :generated)
12731277
scale = Keyword.get(opts, :scale)
12741278

1275-
cond do
1276-
size -> [ecto_size_to_db(type), ?(, to_string(size), ?)]
1277-
precision -> [ecto_to_db(type), ?(, to_string(precision), ?,, to_string(scale || 0), ?)]
1278-
type == :string -> ["varchar(255)"]
1279-
true -> ecto_to_db(type)
1280-
end
1279+
type =
1280+
cond do
1281+
size -> [ecto_size_to_db(type), ?(, to_string(size), ?)]
1282+
precision -> [ecto_to_db(type), ?(, to_string(precision), ?,, to_string(scale || 0), ?)]
1283+
type == :string -> ["varchar(255)"]
1284+
true -> ecto_to_db(type)
1285+
end
1286+
1287+
[type, generated_expr(generated)]
1288+
end
1289+
1290+
defp generated_expr(nil), do: []
1291+
1292+
defp generated_expr(expr) when is_binary(expr) do
1293+
[" AS ", expr]
1294+
end
1295+
1296+
defp generated_expr(other) do
1297+
raise ArgumentError,
1298+
"the `:generated` option only accepts strings, received: #{inspect(other)}"
12811299
end
12821300

12831301
defp reference_expr(type, ref, table, name) do

lib/ecto/adapters/postgres/connection.ex

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1618,56 +1618,82 @@ if Code.ensure_loaded?(Postgrex) do
16181618
defp column_type({:array, type}, opts),
16191619
do: [column_type(type, opts), "[]"]
16201620

1621-
defp column_type(type, _opts) when type in ~w(time utc_datetime naive_datetime)a,
1622-
do: [ecto_to_db(type), "(0)"]
1621+
defp column_type(type, opts) when type in ~w(time utc_datetime naive_datetime)a do
1622+
generated = Keyword.get(opts, :generated)
1623+
[ecto_to_db(type), "(0)", generated_expr(generated)]
1624+
end
16231625

16241626
defp column_type(type, opts)
16251627
when type in ~w(time_usec utc_datetime_usec naive_datetime_usec)a do
16261628
precision = Keyword.get(opts, :precision)
1629+
generated = Keyword.get(opts, :generated)
16271630
type_name = ecto_to_db(type)
16281631

1629-
if precision do
1630-
[type_name, ?(, to_string(precision), ?)]
1631-
else
1632-
type_name
1633-
end
1632+
type =
1633+
if precision do
1634+
[type_name, ?(, to_string(precision), ?)]
1635+
else
1636+
type_name
1637+
end
1638+
1639+
[type, generated_expr(generated)]
16341640
end
16351641

16361642
defp column_type(:identity, opts) do
16371643
start_value = [Keyword.get(opts, :start_value)]
16381644
increment = [Keyword.get(opts, :increment)]
1645+
generated = Keyword.get(opts, :generated)
16391646
type_name = ecto_to_db(:identity)
16401647

1641-
cleanup = fn v -> is_integer(v) and v > 0 end
1648+
if generated do
1649+
[type_name, generated_expr(generated)]
1650+
else
1651+
cleanup = fn v -> is_integer(v) and v > 0 end
16421652

1643-
sequence =
1644-
start_value
1645-
|> Enum.filter(cleanup)
1646-
|> Enum.map(&"START WITH #{&1}")
1647-
|> Kernel.++(
1648-
increment
1653+
sequence =
1654+
start_value
16491655
|> Enum.filter(cleanup)
1650-
|> Enum.map(&"INCREMENT BY #{&1}")
1651-
)
1652-
1653-
case sequence do
1654-
[] -> [type_name, " GENERATED BY DEFAULT AS IDENTITY"]
1655-
_ -> [type_name, " GENERATED BY DEFAULT AS IDENTITY(", Enum.join(sequence, " "), ") "]
1656+
|> Enum.map(&"START WITH #{&1}")
1657+
|> Kernel.++(
1658+
increment
1659+
|> Enum.filter(cleanup)
1660+
|> Enum.map(&"INCREMENT BY #{&1}")
1661+
)
1662+
1663+
case sequence do
1664+
[] -> [type_name, " GENERATED BY DEFAULT AS IDENTITY"]
1665+
_ -> [type_name, " GENERATED BY DEFAULT AS IDENTITY(", Enum.join(sequence, " "), ") "]
1666+
end
16561667
end
16571668
end
16581669

16591670
defp column_type(type, opts) do
16601671
size = Keyword.get(opts, :size)
16611672
precision = Keyword.get(opts, :precision)
16621673
scale = Keyword.get(opts, :scale)
1674+
generated = Keyword.get(opts, :generated)
16631675
type_name = ecto_to_db(type)
16641676

1665-
cond do
1666-
size -> [type_name, ?(, to_string(size), ?)]
1667-
precision -> [type_name, ?(, to_string(precision), ?,, to_string(scale || 0), ?)]
1668-
type == :string -> [type_name, "(255)"]
1669-
true -> type_name
1670-
end
1677+
type =
1678+
cond do
1679+
size -> [type_name, ?(, to_string(size), ?)]
1680+
precision -> [type_name, ?(, to_string(precision), ?,, to_string(scale || 0), ?)]
1681+
type == :string -> [type_name, "(255)"]
1682+
true -> type_name
1683+
end
1684+
1685+
[type, generated_expr(generated)]
1686+
end
1687+
1688+
defp generated_expr(nil), do: []
1689+
1690+
defp generated_expr(expr) when is_binary(expr) do
1691+
[" GENERATED ", expr]
1692+
end
1693+
1694+
defp generated_expr(other) do
1695+
raise ArgumentError,
1696+
"the `:generated` option only accepts strings, received: #{inspect(other)}"
16711697
end
16721698

16731699
defp reference_expr(%Reference{} = ref, table, name) do

lib/ecto/adapters/tds/connection.ex

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1544,7 +1544,19 @@ if Code.ensure_loaded?(Tds) do
15441544
size = Keyword.get(opts, :size)
15451545
precision = Keyword.get(opts, :precision)
15461546
scale = Keyword.get(opts, :scale)
1547-
ecto_to_db(type, size, precision, scale)
1547+
generated = Keyword.get(opts, :generated)
1548+
[ecto_to_db(type, size, precision, scale), generated_expr(generated)]
1549+
end
1550+
1551+
defp generated_expr(nil), do: []
1552+
1553+
defp generated_expr(expr) when is_binary(expr) do
1554+
[" AS ", expr]
1555+
end
1556+
1557+
defp generated_expr(other) do
1558+
raise ArgumentError,
1559+
"the `:generated` option only accepts strings, received: #{inspect(other)}"
15481560
end
15491561

15501562
defp constraint_expr(%Reference{} = ref, table, name) do

lib/ecto/migration.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,9 @@ defmodule Ecto.Migration do
10921092
add :summary, :text # Database type
10931093
add :object, :map # Elixir type which is handled by the database
10941094
add :custom, :'"UserDefinedType"' # A case-sensitive, user-defined type name
1095+
add :identity, :integer, generated: "BY DEFAULT AS IDENTITY" # Postgres generated identity column
1096+
add :generated_psql, :string, generated: "ALWAYS AS (id::text) STORED" # Postgres calculated column
1097+
add :generated_other, :string, generated: "CAST(id AS char)" # MySQL and TDS calculated column
10951098
end
10961099
10971100
## Options
@@ -1112,6 +1115,9 @@ defmodule Ecto.Migration do
11121115
* `:comment` - adds a comment to the added column.
11131116
* `:after` - positions field after the specified one. Only supported on MySQL,
11141117
it is ignored by other databases.
1118+
* `:generated` - a string representing the expression for a generated column. See
1119+
above for a comprehensive set of examples for each of the built-in adapters. If
1120+
specified alongside `:start_value`/`:increment`, those options will be ignored.
11151121
* `:start_value` - option for `:identity` key, represents initial value in sequence
11161122
generation. Default is defined by the database.
11171123
* `:increment` - option for `:identity` key, represents increment value for

test/ecto/adapters/myxql_test.exs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,6 +1739,22 @@ defmodule Ecto.Adapters.MyXQLTest do
17391739
]
17401740
end
17411741

1742+
test "create table with generated column" do
1743+
create =
1744+
{:create, table(:posts),
1745+
[
1746+
{:add, :id, :integer, [primary_key: true]},
1747+
{:add, :id_float, :float, [generated: ~s|(CAST(id AS double))|]}
1748+
]}
1749+
1750+
assert execute_ddl(create) == [
1751+
"""
1752+
CREATE TABLE `posts` (`id` integer, `id_float` double AS (CAST(id AS double)), PRIMARY KEY (`id`)) ENGINE = INNODB
1753+
"""
1754+
|> remove_newlines
1755+
]
1756+
end
1757+
17421758
test "create table with a map column, and a map default with values" do
17431759
default = %{foo: "bar", baz: "boom"}
17441760
default_text = "'{" <> Enum.map_join(default, ",", fn {k, v} -> ~s{"#{k}":"#{v}"} end) <> "}'"

test/ecto/adapters/postgres_test.exs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2208,6 +2208,26 @@ defmodule Ecto.Adapters.PostgresTest do
22082208
]
22092209
end
22102210

2211+
test "create table with generated column" do
2212+
create =
2213+
{:create, table(:posts),
2214+
[
2215+
{:add, :id, :integer,
2216+
[
2217+
primary_key: true,
2218+
generated: "ALWAYS AS IDENTITY (MINVALUE 0 START WITH 0 INCREMENT BY 1)"
2219+
]},
2220+
{:add, :id_str, :string, [generated: ~s|ALWAYS AS (id) STORED|]}
2221+
]}
2222+
2223+
assert execute_ddl(create) == [
2224+
"""
2225+
CREATE TABLE "posts" ("id" integer GENERATED ALWAYS AS IDENTITY (MINVALUE 0 START WITH 0 INCREMENT BY 1), "id_str" varchar(255) GENERATED ALWAYS AS (id) STORED, PRIMARY KEY ("id"))
2226+
"""
2227+
|> remove_newlines
2228+
]
2229+
end
2230+
22112231
test "create table with binary column and null-byte default" do
22122232
create = {:create, table(:blobs), [{:add, :blob, :binary, [default: <<0>>]}]}
22132233

test/ecto/adapters/tds_test.exs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,23 @@ defmodule Ecto.Adapters.TdsTest do
15321532
]
15331533
end
15341534

1535+
test "create table with generated column" do
1536+
create =
1537+
{:create, table(:posts),
1538+
[
1539+
{:add, :id, :integer, [primary_key: true]},
1540+
{:add, :id_float, :float, [generated: ~s|(CAST(id AS float))|]}
1541+
]}
1542+
1543+
assert execute_ddl(create) == [
1544+
"""
1545+
CREATE TABLE [posts] ([id] integer, [id_float] float AS (CAST(id AS float)), CONSTRAINT [posts_pkey] PRIMARY KEY CLUSTERED ([id]));
1546+
"""
1547+
|> remove_newlines
1548+
|> Kernel.<>(" ")
1549+
]
1550+
end
1551+
15351552
test "create table with binary column and UTF-8 default" do
15361553
create = {:create, table(:blobs), [{:add, :blob, :binary, [default: "foo"]}]}
15371554

0 commit comments

Comments
 (0)