Skip to content

Commit e8ac2da

Browse files
committed
change :constraint_handler to accept a function
`:constraint_handler` now accepts a `(exception, opts -> keyword)` function instead of an MFA tuple. It can be passed per-operation or set globally via `default_options`/`prepare_options` in the repo. Removes `Ecto.Adapters.SQL.Constraint` behaviour and the config-level `:constraint_handler` option. The `to_constraints/2` callback stays on `Ecto.Adapters.SQL.Connection`. Adds `Ecto.Adapters.SQL.to_constraints/4` and a corresponding `MyRepo.to_constraints/3` convenience function.
1 parent 0f6db45 commit e8ac2da

13 files changed

Lines changed: 142 additions & 415 deletions

File tree

integration_test/myxql/constraints_test.exs

Lines changed: 19 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ defmodule Ecto.Integration.ConstraintsTest do
55
alias Ecto.Integration.PoolRepo
66

77
defmodule CustomConstraintHandler do
8-
@behaviour Ecto.Adapters.SQL.Constraint
9-
108
@quotes ~w(" ' `)
119

12-
@impl Ecto.Adapters.SQL.Constraint
13-
# An example of a custom handler a user might write
10+
# An example of a custom handler a user might write.
11+
# Handles custom MySQL signal exceptions from triggers,
12+
# falling back to the default handler.
1413
def to_constraints(%MyXQL.Error{mysql: %{name: :ER_SIGNAL_EXCEPTION}, message: message}, opts) do
1514
# Assumes this is the only use-case of `ER_SIGNAL_EXCEPTION` the user has implemented custom errors for
1615
with [_, quoted] <- :binary.split(message, "Overlapping values for key "),
@@ -196,6 +195,8 @@ defmodule Ecto.Integration.ConstraintsTest do
196195
:ok = up(PoolRepo, num, TriggerEmulatingConstraintMigration, log: false)
197196
end)
198197

198+
constraint_handler = &CustomConstraintHandler.to_constraints/2
199+
199200
changeset = Ecto.Changeset.change(%Constraint{}, from: 0, to: 10)
200201
{:ok, item} = PoolRepo.insert(changeset)
201202

@@ -204,32 +205,11 @@ defmodule Ecto.Integration.ConstraintsTest do
204205

205206
overlapping_changeset = Ecto.Changeset.change(%Constraint{}, from: 9, to: 12)
206207

207-
msg_re = ~r/constraint error when attempting to insert struct/
208-
209-
# When the changeset doesn't expect the db error
210-
exception =
211-
assert_raise Ecto.ConstraintError, msg_re, fn -> PoolRepo.insert(overlapping_changeset) end
212-
213-
assert exception.message =~ "\"cannot_overlap\" (exclusion_constraint)"
214-
assert exception.message =~ "The changeset has not defined any constraint."
215-
assert exception.message =~ "call `exclusion_constraint/3`"
216-
217-
# When the changeset does expect the db error
218-
# but the key does not match the default generated by `exclusion_constraint`
219-
exception =
220-
assert_raise Ecto.ConstraintError, msg_re, fn ->
221-
overlapping_changeset
222-
|> Ecto.Changeset.exclusion_constraint(:from)
223-
|> PoolRepo.insert()
224-
end
225-
226-
assert exception.message =~ "\"cannot_overlap\" (exclusion_constraint)"
227-
228-
# When the changeset does expect the db error, but doesn't give a custom message
208+
# Custom handler converts the trigger error into a constraint
229209
{:error, changeset} =
230210
overlapping_changeset
231211
|> Ecto.Changeset.exclusion_constraint(:from, name: :cannot_overlap)
232-
|> PoolRepo.insert()
212+
|> PoolRepo.insert(constraint_handler: constraint_handler)
233213

234214
assert changeset.errors == [
235215
from:
@@ -239,50 +219,26 @@ defmodule Ecto.Integration.ConstraintsTest do
239219

240220
assert changeset.data.__meta__.state == :built
241221

242-
# When the changeset does expect the db error and gives a custom message
243-
{:error, changeset} =
222+
# Without the custom handler, the default handler doesn't recognize
223+
# the custom signal, so the error is raised as-is
224+
assert_raise MyXQL.Error, fn ->
244225
overlapping_changeset
245-
|> Ecto.Changeset.exclusion_constraint(:from,
246-
name: :cannot_overlap,
247-
message: "must not overlap"
248-
)
226+
|> Ecto.Changeset.exclusion_constraint(:from, name: :cannot_overlap)
249227
|> PoolRepo.insert()
228+
end
250229

251-
assert changeset.errors == [
252-
from:
253-
{"must not overlap", [constraint: :exclusion, constraint_name: "cannot_overlap"]}
254-
]
255-
256-
assert changeset.data.__meta__.state == :built
257-
258-
# When the changeset does expect the db error, but a different handler is used
259-
exception =
260-
assert_raise MyXQL.Error, fn ->
261-
overlapping_changeset
262-
|> Ecto.Changeset.exclusion_constraint(:from, name: :cannot_overlap)
263-
|> PoolRepo.insert(
264-
constraint_handler: {Ecto.Adapters.MyXQL.Connection, :to_constraints, []}
265-
)
266-
end
267-
268-
assert exception.message =~ "Overlapping values for key 'constraints_test.cannot_overlap'"
269-
270-
# When custom error is coming from an UPDATE
271-
overlapping_update_changeset = Ecto.Changeset.change(item, from: 0, to: 9)
272-
230+
# Custom handler also works on UPDATE
273231
{:error, changeset} =
274-
overlapping_update_changeset
275-
|> Ecto.Changeset.exclusion_constraint(:from,
276-
name: :cannot_overlap,
277-
message: "must not overlap"
278-
)
279-
|> PoolRepo.insert()
232+
Ecto.Changeset.change(item, from: 0, to: 9)
233+
|> Ecto.Changeset.exclusion_constraint(:from, name: :cannot_overlap)
234+
|> PoolRepo.update(constraint_handler: constraint_handler)
280235

281236
assert changeset.errors == [
282237
from:
283-
{"must not overlap", [constraint: :exclusion, constraint_name: "cannot_overlap"]}
238+
{"violates an exclusion constraint",
239+
[constraint: :exclusion, constraint_name: "cannot_overlap"]}
284240
]
285241

286242
assert changeset.data.__meta__.state == :loaded
287243
end
288-
end
244+
end

integration_test/myxql/test_helper.exs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,7 @@ Application.put_env(:ecto_sql, PoolRepo,
5757
url: Application.get_env(:ecto_sql, :mysql_test_url) <> "/ecto_test",
5858
pool_size: 5,
5959
pool_count: String.to_integer(System.get_env("POOL_COUNT", "1")),
60-
show_sensitive_data_on_connection_error: true,
61-
# Passes through into adapter_meta
62-
constraint_handler: {Ecto.Integration.ConstraintsTest.CustomConstraintHandler, :to_constraints, []}
60+
show_sensitive_data_on_connection_error: true
6361
)
6462

6563
defmodule Ecto.Integration.PoolRepo do
@@ -87,8 +85,6 @@ _ = Ecto.Adapters.MyXQL.storage_down(TestRepo.config())
8785

8886
{:ok, _pid} = TestRepo.start_link()
8987

90-
# Passes through into adapter_meta, overrides Application config
91-
# {:ok, _pid} = PoolRepo.start_link([constraint_handler: {Ecto.Integration.ConstraintsTest.CustomConstraintHandler, :to_constraints, []}])
9288
{:ok, _pid} = PoolRepo.start_link()
9389

9490
%{rows: [[version]]} = TestRepo.query!("SELECT @@version", [])

integration_test/pg/constraints_test.exs

Lines changed: 19 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@ defmodule Ecto.Integration.ConstraintsTest do
55
alias Ecto.Integration.PoolRepo
66

77
defmodule CustomConstraintHandler do
8-
@behaviour Ecto.Adapters.SQL.Constraint
9-
10-
@impl Ecto.Adapters.SQL.Constraint
11-
# An example of a custom handler a user might write
8+
# An example of a custom handler a user might write.
9+
# Handles custom PG error codes from triggers, falling back to the default handler.
1210
def to_constraints(
13-
%Postgrex.Error{postgres: %{pg_code: "ZZ001", constraint: constraint}} = _err,
11+
%Postgrex.Error{postgres: %{pg_code: "ZZ001", constraint: constraint}},
1412
_opts
1513
) do
16-
# Assumes that all pg_codes of ZZ001 are check constraint,
14+
# Assumes that all pg_codes of ZZ001 are check constraints,
1715
# which may or may not be realistic
1816
[check: constraint]
1917
end
@@ -254,80 +252,43 @@ defmodule Ecto.Integration.ConstraintsTest do
254252
:ok = up(PoolRepo, num, TriggerEmulatingConstraintMigration, log: false)
255253
end)
256254

255+
constraint_handler = &CustomConstraintHandler.to_constraints/2
256+
257257
changeset = Ecto.Changeset.change(%Constraint{}, price: 99, from: 201, to: 202)
258258
{:ok, item} = PoolRepo.insert(changeset)
259259

260260
above_max_changeset = Ecto.Changeset.change(%Constraint{}, price: 100)
261261

262-
msg_re = ~r/constraint error when attempting to insert struct/
263-
264-
# When the changeset doesn't expect the db error
265-
exception =
266-
assert_raise Ecto.ConstraintError, msg_re, fn -> PoolRepo.insert(above_max_changeset) end
267-
268-
assert exception.message =~ "\"price_above_max\" (check_constraint)"
269-
assert exception.message =~ "The changeset has not defined any constraint."
270-
assert exception.message =~ "call `check_constraint/3`"
271-
272-
# When the changeset does expect the db error, but doesn't give a custom message
262+
# Custom handler converts the trigger error into a constraint
273263
{:error, changeset} =
274264
above_max_changeset
275265
|> Ecto.Changeset.check_constraint(:price, name: :price_above_max)
276-
|> PoolRepo.insert()
266+
|> PoolRepo.insert(constraint_handler: constraint_handler)
277267

278268
assert changeset.errors == [
279269
price: {"is invalid", [constraint: :check, constraint_name: "price_above_max"]}
280270
]
281271

282272
assert changeset.data.__meta__.state == :built
283273

284-
# When the changeset does expect the db error and gives a custom message
285-
{:error, changeset} =
274+
# Without the custom handler, the default handler doesn't recognize
275+
# the custom error code, so the error is raised as-is
276+
assert_raise Postgrex.Error, fn ->
286277
above_max_changeset
287-
|> Ecto.Changeset.check_constraint(:price,
288-
name: :price_above_max,
289-
message: "must be less than the max price"
290-
)
278+
|> Ecto.Changeset.check_constraint(:price, name: :price_above_max)
291279
|> PoolRepo.insert()
280+
end
292281

293-
assert changeset.errors == [
294-
price:
295-
{"must be less than the max price",
296-
[constraint: :check, constraint_name: "price_above_max"]}
297-
]
298-
299-
assert changeset.data.__meta__.state == :built
300-
301-
# When the changeset does expect the db error, but a different handler is used
302-
exception =
303-
assert_raise Postgrex.Error, fn ->
304-
above_max_changeset
305-
|> Ecto.Changeset.check_constraint(:price, name: :price_above_max)
306-
|> PoolRepo.insert(
307-
constraint_handler: {Ecto.Adapters.Postgres.Connection, :to_constraints, []}
308-
)
309-
end
310-
311-
# Just raises as-is
312-
assert exception.postgres.message == "price must be less than 100, got 100"
313-
314-
# When custom error is coming from an UPDATE
315-
above_max_update_changeset = Ecto.Changeset.change(item, price: 100)
316-
282+
# Custom handler also works on UPDATE
317283
{:error, changeset} =
318-
above_max_update_changeset
319-
|> Ecto.Changeset.check_constraint(:price,
320-
name: :price_above_max,
321-
message: "must be less than the max price"
322-
)
323-
|> PoolRepo.insert()
284+
Ecto.Changeset.change(item, price: 100)
285+
|> Ecto.Changeset.check_constraint(:price, name: :price_above_max)
286+
|> PoolRepo.update(constraint_handler: constraint_handler)
324287

325288
assert changeset.errors == [
326-
price:
327-
{"must be less than the max price",
328-
[constraint: :check, constraint_name: "price_above_max"]}
289+
price: {"is invalid", [constraint: :check, constraint_name: "price_above_max"]}
329290
]
330291

331292
assert changeset.data.__meta__.state == :loaded
332293
end
333-
end
294+
end

integration_test/pg/test_helper.exs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,7 @@ pool_repo_config = [
6767
max_seconds: 10
6868
]
6969

70-
Application.put_env(
71-
:ecto_sql,
72-
PoolRepo,
73-
pool_repo_config ++
74-
[
75-
# Passes through into adapter_meta
76-
constraint_handler:
77-
{Ecto.Integration.ConstraintsTest.CustomConstraintHandler, :to_constraints, []}
78-
]
79-
)
70+
Application.put_env(:ecto_sql, PoolRepo, pool_repo_config)
8071

8172
Application.put_env(
8273
:ecto_sql,
@@ -109,8 +100,6 @@ _ = Ecto.Adapters.Postgres.storage_down(TestRepo.config())
109100

110101
{:ok, _pid} = TestRepo.start_link()
111102

112-
# Passes through into adapter_meta, overrides Application config
113-
# {:ok, _pid} = PoolRepo.start_link([constraint_handler: {Ecto.Integration.ConstraintsTest.CustomConstraintHandler, :to_constraints, []}])
114103
{:ok, _pid} = PoolRepo.start_link()
115104

116105
{:ok, _pid} = AdvisoryLockPoolRepo.start_link()

0 commit comments

Comments
 (0)