Skip to content

Commit 89bf043

Browse files
Add {:nilify, columns} to on_delete reference option. (#474)
1 parent ee06565 commit 89bf043

9 files changed

Lines changed: 106 additions & 17 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
elixirbase:
2727
- "1.11.4-erlang-21.3.8.24-alpine-3.13.3"
2828
postgres:
29+
- "15.0"
2930
- "11.11"
3031
- "9.6"
3132
- "9.5"

Earthfile

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ test:
2424
integration-test-all:
2525
ARG ELIXIR_BASE=1.13.4-erlang-24.3.4.2-alpine-3.16.0
2626
BUILD \
27+
--build-arg POSTGRES=15.0 \
2728
--build-arg POSTGRES=11.11 \
2829
--build-arg POSTGRES=9.6 \
2930
--build-arg POSTGRES=9.5 \
@@ -65,6 +66,14 @@ integration-test-postgres:
6566
# and in the 3.4 version, it is not included in postgresql-client but rather in postgresql
6667
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.4/main' >> /etc/apk/repositories
6768
RUN apk add postgresql=9.5.13-r0
69+
ELSE IF [ "$POSTGRES" = "15.0" ]
70+
# for 15.0 we need an upgraded version of pg_dump;
71+
# alpine 3.17 does not come with the postgres 15 client by default;
72+
# we must first update the public keys for the packages because they
73+
# might have been rotated since our image was built
74+
RUN apk add -X https://dl-cdn.alpinelinux.org/alpine/v3.17/main -u alpine-keys
75+
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/v3.17/main' >> /etc/apk/repositories
76+
RUN apk add postgresql15-client
6877
ELSE
6978
RUN apk add postgresql-client
7079
END
@@ -141,7 +150,7 @@ integration-test-mssql:
141150

142151

143152
setup-base:
144-
ARG ELIXIR_BASE=1.13.4-erlang-24.3.4.2-alpine-3.16.0
153+
ARG ELIXIR_BASE=1.13.4-erlang-24.3.4.2-alpine-3.17.0
145154
FROM hexpm/elixir:$ELIXIR_BASE
146155
RUN apk add --no-progress --update git build-base
147156
ENV ELIXIR_ASSERT_TIMEOUT=10000

integration_test/myxql/test_helper.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ excludes = [
100100
# MySQL doesn't have a boolean type, so this ends up returning 0/1
101101
:map_boolean_in_expression,
102102
# MySQL doesn't support indexed parameters
103-
:placeholders
103+
:placeholders,
104+
# MySQL doesn't support specifying columns for ON DELETE SET NULL
105+
:on_delete_nilify_column_list
104106
]
105107

106108
if Version.match?(version, ">= 8.0.0") do

integration_test/pg/test_helper.exs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,19 @@ version =
100100
excludes = [:selected_as_with_having, :selected_as_with_order_by_expression]
101101
excludes_above_9_5 = [:without_conflict_target]
102102
excludes_below_9_6 = [:add_column_if_not_exists, :no_error_on_conditional_column_migration]
103+
excludes_below_15_0 = [:on_delete_nilify_column_list]
103104

104-
if Version.match?(version, "< 9.6.0") do
105-
ExUnit.configure(exclude: excludes ++ excludes_above_9_5 ++ excludes_below_9_6)
106-
else
107-
ExUnit.configure(exclude: excludes ++ excludes_above_9_5)
105+
exclude_list = excludes ++ excludes_above_9_5
106+
107+
cond do
108+
Version.match?(version, "< 9.6.0") ->
109+
ExUnit.configure(exclude: exclude_list ++ excludes_below_9_6 ++ excludes_below_15_0)
110+
111+
Version.match?(version, "< 15.0.0") ->
112+
ExUnit.configure(exclude: exclude_list ++ excludes_below_15_0)
113+
114+
true ->
115+
ExUnit.configure(exclude: exclude_list)
108116
end
109117

110118
:ok = Ecto.Migrator.up(TestRepo, 0, Ecto.Integration.Migration, log: false)

integration_test/sql/migration.exs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,34 @@ defmodule Ecto.Integration.MigrationTest do
226226
end
227227
end
228228

229+
defmodule OnDeleteNilifyColumnsMigration do
230+
use Ecto.Migration
231+
232+
def up do
233+
create table(:parent) do
234+
add :col1, :integer
235+
add :col2, :integer
236+
end
237+
238+
create unique_index(:parent, [:id, :col1, :col2])
239+
240+
create table(:ref) do
241+
add :col1, :integer
242+
add :col2, :integer
243+
add :parent_id,
244+
references(:parent,
245+
with: [col1: :col1, col2: :col2],
246+
on_delete: {:nilify, [:parent_id, :col2]}
247+
)
248+
end
249+
end
250+
251+
def down do
252+
drop table(:ref)
253+
drop table(:parent)
254+
end
255+
end
256+
229257
defmodule CompositeForeignKeyMigration do
230258
use Ecto.Migration
231259

@@ -622,4 +650,18 @@ defmodule Ecto.Integration.MigrationTest do
622650
assert catch_error(PoolRepo.all from p in "drop_col_if_exists_migration", select: p.to_be_removed)
623651
:ok = down(PoolRepo, num, DropColumnIfExistsMigration, log: false)
624652
end
653+
654+
@tag :on_delete_nilify_column_list
655+
test "nilify list of columns on_delete constraint", %{migration_number: num} do
656+
assert :ok == up(PoolRepo, num, OnDeleteNilifyColumnsMigration, log: false)
657+
658+
PoolRepo.insert_all("parent", [%{col1: 1, col2: 2}])
659+
assert [{id, col1, col2}] = PoolRepo.all from p in "parent", select: {p.id, p.col1, p.col2}
660+
661+
PoolRepo.insert_all("ref", [[parent_id: id, col1: col1, col2: col2]])
662+
PoolRepo.delete_all("parent")
663+
assert [{nil, col1, nil}] == PoolRepo.all from r in "ref", select: {r.parent_id, r.col1, r.col2}
664+
665+
:ok = down(PoolRepo, num, OnDeleteNilifyColumnsMigration, log: false)
666+
end
625667
end

integration_test/tds/test_helper.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ ExUnit.start(
5454
# MSSQL can't reference aliased columns in HAVING
5555
:selected_as_with_having,
5656
# MSSQL can't reference aliased columns in ORDER BY expressions
57-
:selected_as_with_order_by_expression
57+
:selected_as_with_order_by_expression,
58+
# MSSQL doesn't support specifying columns for ON DELETE SET NULL
59+
:on_delete_nilify_column_list
5860
]
5961
)
6062

lib/ecto/adapters/postgres/connection.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,8 @@ if Code.ensure_loaded?(Postgrex) do
13051305
defp reference_column_type(type, opts), do: column_type(type, opts)
13061306

13071307
defp reference_on_delete(:nilify_all), do: " ON DELETE SET NULL"
1308+
defp reference_on_delete({:nilify, columns}),
1309+
do: [" ON DELETE SET NULL (", quote_names(columns), ")"]
13081310
defp reference_on_delete(:delete_all), do: " ON DELETE CASCADE"
13091311
defp reference_on_delete(:restrict), do: " ON DELETE RESTRICT"
13101312
defp reference_on_delete(_), do: []

lib/ecto/migration.ex

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,7 +1280,9 @@ defmodule Ecto.Migration do
12801280
the example above), or `nil`.
12811281
* `:type` - The foreign key type, which defaults to `:bigserial`.
12821282
* `:on_delete` - What to do if the referenced entry is deleted. May be
1283-
`:nothing` (default), `:delete_all`, `:nilify_all`, or `:restrict`.
1283+
`:nothing` (default), `:delete_all`, `:nilify_all`, `{:nilify, columns}`,
1284+
or `:restrict`. `{:nilify, columns}` expects a list of atoms for `columns`
1285+
and is not supported by all databases.
12841286
* `:on_update` - What to do if the referenced entry is updated. May be
12851287
`:nothing` (default), `:update_all`, `:nilify_all`, or `:restrict`.
12861288
* `:validate` - Whether or not to validate the foreign key constraint on
@@ -1302,14 +1304,8 @@ defmodule Ecto.Migration do
13021304
def references(table, opts) when is_binary(table) and is_list(opts) do
13031305
opts = Keyword.merge(foreign_key_repo_opts(), opts)
13041306
reference = struct(%Reference{table: table}, opts)
1305-
1306-
unless reference.on_delete in [:nothing, :delete_all, :nilify_all, :restrict] do
1307-
raise ArgumentError, "unknown :on_delete value: #{inspect reference.on_delete}"
1308-
end
1309-
1310-
unless reference.on_update in [:nothing, :update_all, :nilify_all, :restrict] do
1311-
raise ArgumentError, "unknown :on_update value: #{inspect reference.on_update}"
1312-
end
1307+
check_on_delete!(reference.on_delete)
1308+
check_on_update!(reference.on_update)
13131309

13141310
reference
13151311
end
@@ -1323,6 +1319,31 @@ defmodule Ecto.Migration do
13231319
|> Keyword.merge(Runner.repo_config(:migration_foreign_key, []))
13241320
end
13251321

1322+
defp check_on_delete!(on_delete)
1323+
when on_delete in [:nothing, :delete_all, :nilify_all, :restrict],
1324+
do: :ok
1325+
1326+
defp check_on_delete!({:nilify, columns}) when is_list(columns) do
1327+
unless Enum.all?(columns, &is_atom/1) do
1328+
raise ArgumentError,
1329+
"expected `columns` in `{:nilify, columns}` to be a list of atoms, got: #{inspect columns}"
1330+
end
1331+
1332+
:ok
1333+
end
1334+
1335+
defp check_on_delete!(on_delete) do
1336+
raise ArgumentError, "unknown :on_delete value: #{inspect(on_delete)}"
1337+
end
1338+
1339+
defp check_on_update!(on_update)
1340+
when on_update in [:nothing, :update_all, :nilify_all, :restrict],
1341+
do: :ok
1342+
1343+
defp check_on_update!(on_update) do
1344+
raise ArgumentError, "unknown :on_update value: #{inspect(on_update)}"
1345+
end
1346+
13261347
@doc ~S"""
13271348
Defines a constraint (either a check constraint or an exclusion constraint)
13281349
to be evaluated by the database when a row is inserted or updated.

test/ecto/adapters/postgres_test.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1501,7 +1501,8 @@ defmodule Ecto.Adapters.PostgresTest do
15011501
{:add, :category_10, %Reference{table: :categories, on_update: :restrict}, []},
15021502
{:add, :category_11, %Reference{table: :categories, prefix: "foo", on_update: :restrict}, []},
15031503
{:add, :category_12, %Reference{table: :categories, with: [here: :there]}, []},
1504-
{:add, :category_13, %Reference{table: :categories, on_update: :restrict, with: [here: :there], match: :full}, []},]}
1504+
{:add, :category_13, %Reference{table: :categories, on_update: :restrict, with: [here: :there], match: :full}, []},
1505+
{:add, :category_14, %Reference{table: :categories, with: [here: :there, here2: :there2], on_delete: {:nilify, [:here, :here2]}}, []},]}
15051506

15061507
assert execute_ddl(create) == ["""
15071508
CREATE TABLE "posts" ("id" serial,
@@ -1519,6 +1520,7 @@ defmodule Ecto.Adapters.PostgresTest do
15191520
"category_11" bigint, CONSTRAINT "posts_category_11_fkey" FOREIGN KEY ("category_11") REFERENCES "foo"."categories"("id") ON UPDATE RESTRICT,
15201521
"category_12" bigint, CONSTRAINT "posts_category_12_fkey" FOREIGN KEY ("category_12","here") REFERENCES "categories"("id","there"),
15211522
"category_13" bigint, CONSTRAINT "posts_category_13_fkey" FOREIGN KEY ("category_13","here") REFERENCES "categories"("id","there") MATCH FULL ON UPDATE RESTRICT,
1523+
"category_14" bigint, CONSTRAINT "posts_category_14_fkey" FOREIGN KEY ("category_14","here","here2") REFERENCES "categories"("id","there","there2") ON DELETE SET NULL ("here","here2"),
15221524
PRIMARY KEY ("id"))
15231525
""" |> remove_newlines]
15241526
end

0 commit comments

Comments
 (0)