@@ -4,32 +4,7 @@ defmodule Ecto.Integration.ConstraintsTest do
44 import Ecto.Migrator , only: [ up: 4 ]
55 alias Ecto.Integration.PoolRepo
66
7- defmodule CustomConstraintHandler do
8- @ quotes ~w( " ' `)
9-
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.
13- def to_constraints ( % MyXQL.Error { mysql: % { name: :ER_SIGNAL_EXCEPTION } , message: message } , opts ) do
14- # Assumes this is the only use-case of `ER_SIGNAL_EXCEPTION` the user has implemented custom errors for
15- with [ _ , quoted ] <- :binary . split ( message , "Overlapping values for key " ) ,
16- [ _ , index | _ ] <- :binary . split ( quoted , @ quotes , [ :global ] ) do
17- [ exclusion: strip_source ( index , opts [ :source ] ) ]
18- else
19- _ -> [ ]
20- end
21- end
22-
23- def to_constraints ( err , opts ) do
24- # Falls back to default `ecto_sql` handler for all others
25- Ecto.Adapters.MyXQL.Connection . to_constraints ( err , opts )
26- end
27-
28- defp strip_source ( name , nil ) , do: name
29- defp strip_source ( name , source ) , do: String . trim_leading ( name , "#{ source } ." )
30- end
31-
32- defmodule ConstraintTableMigration do
7+ defmodule ConstraintMigration do
338 use Ecto.Migration
349
3510 @ table table ( :constraints_test )
@@ -40,67 +15,12 @@ defmodule Ecto.Integration.ConstraintsTest do
4015 add :from , :integer
4116 add :to , :integer
4217 end
43- end
44- end
45-
46- defmodule CheckConstraintMigration do
47- use Ecto.Migration
4818
49- @ table table ( :constraints_test )
50-
51- def change do
5219 # Only valid after MySQL 8.0.19
5320 create constraint ( @ table . name , :positive_price , check: "price > 0" )
5421 end
5522 end
5623
57- defmodule TriggerEmulatingConstraintMigration do
58- use Ecto.Migration
59-
60- @ table_name :constraints_test
61-
62- def up do
63- insert_trigger_sql = trigger_sql ( @ table_name , "INSERT" )
64- update_trigger_sql = trigger_sql ( @ table_name , "UPDATE" )
65-
66- drop_triggers ( @ table_name )
67- repo ( ) . query! ( insert_trigger_sql )
68- repo ( ) . query! ( update_trigger_sql )
69- end
70-
71- def down do
72- drop_triggers ( @ table_name )
73- end
74-
75- # FOR EACH ROW, not a great example performance-wise,
76- # but demonstrates the feature
77- defp trigger_sql ( table_name , before_type ) do
78- ~s"""
79- CREATE TRIGGER #{ table_name } _#{ String . downcase ( before_type ) } _overlap
80- BEFORE #{ String . upcase ( before_type ) }
81- ON #{ table_name }
82- FOR EACH ROW
83- BEGIN
84- DECLARE v_rowcount INT;
85- DECLARE v_msg VARCHAR(200);
86-
87- SELECT COUNT(*) INTO v_rowcount FROM #{ table_name }
88- WHERE (NEW.from <= `to` AND NEW.to >= `from`);
89-
90- IF v_rowcount > 0 THEN
91- SET v_msg = CONCAT('Overlapping values for key \\ '#{ table_name } .cannot_overlap\\ '');
92- SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_msg, MYSQL_ERRNO = 1644;
93- END IF;
94- END;
95- """
96- end
97-
98- defp drop_triggers ( table_name ) do
99- repo ( ) . query! ( "DROP TRIGGER IF EXISTS #{ table_name } _insert_overlap" )
100- repo ( ) . query! ( "DROP TRIGGER IF EXISTS #{ table_name } _update_overlap" )
101- end
102- end
103-
10424 defmodule Constraint do
10525 use Ecto.Integration.Schema
10626
@@ -116,27 +36,20 @@ defmodule Ecto.Integration.ConstraintsTest do
11636 setup_all do
11737 ExUnit.CaptureLog . capture_log ( fn ->
11838 num = @ base_migration + System . unique_integer ( [ :positive ] )
119- up ( PoolRepo , num , ConstraintTableMigration , log: false )
39+ up ( PoolRepo , num , ConstraintMigration , log: false )
12040 end )
12141
12242 :ok
12343 end
12444
12545 @ tag :create_constraint
12646 test "check constraint" do
127- num = @ base_migration + System . unique_integer ( [ :positive ] )
128-
129- ExUnit.CaptureLog . capture_log ( fn ->
130- :ok = up ( PoolRepo , num , CheckConstraintMigration , log: false )
131- end )
132-
13347 # When the changeset doesn't expect the db error
13448 changeset = Ecto.Changeset . change ( % Constraint { } , price: - 10 )
135-
13649 exception =
137- assert_raise Ecto.ConstraintError ,
138- ~r / constraint error when attempting to insert struct / ,
139- fn -> PoolRepo . insert ( changeset ) end
50+ assert_raise Ecto.ConstraintError , ~r / constraint error when attempting to insert struct / , fn ->
51+ PoolRepo . insert ( changeset )
52+ end
14053
14154 assert exception . message =~ "\" positive_price\" (check_constraint)"
14255 assert exception . message =~ "The changeset has not defined any constraint."
@@ -147,98 +60,43 @@ defmodule Ecto.Integration.ConstraintsTest do
14760 changeset
14861 |> Ecto.Changeset . check_constraint ( :price , name: :positive_price )
14962 |> PoolRepo . insert ( )
150-
151- assert changeset . errors == [
152- price: { "is invalid" , [ constraint: :check , constraint_name: "positive_price" ] }
153- ]
154-
63+ assert changeset . errors == [ price: { "is invalid" , [ constraint: :check , constraint_name: "positive_price" ] } ]
15564 assert changeset . data . __meta__ . state == :built
15665
15766 # When the changeset does expect the db error and gives a custom message
15867 changeset = Ecto.Changeset . change ( % Constraint { } , price: - 10 )
159-
16068 { :error , changeset } =
16169 changeset
162- |> Ecto.Changeset . check_constraint ( :price ,
163- name: :positive_price ,
164- message: "price must be greater than 0"
165- )
70+ |> Ecto.Changeset . check_constraint ( :price , name: :positive_price , message: "price must be greater than 0" )
16671 |> PoolRepo . insert ( )
167-
168- assert changeset . errors == [
169- price:
170- { "price must be greater than 0" ,
171- [ constraint: :check , constraint_name: "positive_price" ] }
172- ]
173-
72+ assert changeset . errors == [ price: { "price must be greater than 0" , [ constraint: :check , constraint_name: "positive_price" ] } ]
17473 assert changeset . data . __meta__ . state == :built
17574
17675 # When the change does not violate the check constraint
17776 changeset = Ecto.Changeset . change ( % Constraint { } , price: 10 , from: 100 , to: 200 )
178-
179- { :ok , result } =
77+ { :ok , changeset } =
18078 changeset
181- |> Ecto.Changeset . check_constraint ( :price ,
182- name: :positive_price ,
183- message: "price must be greater than 0"
184- )
79+ |> Ecto.Changeset . check_constraint ( :price , name: :positive_price , message: "price must be greater than 0" )
18580 |> PoolRepo . insert ( )
186-
187- assert is_integer ( result . id )
81+ assert is_integer ( changeset . id )
18882 end
18983
190- @ tag :constraint_handler
191- test "custom handled constraint" do
192- num = @ base_migration + System . unique_integer ( [ :positive ] )
193-
194- ExUnit.CaptureLog . capture_log ( fn ->
195- :ok = up ( PoolRepo , num , TriggerEmulatingConstraintMigration , log: false )
196- end )
197-
198- constraint_handler = & CustomConstraintHandler . to_constraints / 2
199-
200- changeset = Ecto.Changeset . change ( % Constraint { } , from: 0 , to: 10 )
201- { :ok , item } = PoolRepo . insert ( changeset )
202-
203- non_overlapping_changeset = Ecto.Changeset . change ( % Constraint { } , from: 11 , to: 12 )
204- { :ok , _ } = PoolRepo . insert ( non_overlapping_changeset )
205-
206- overlapping_changeset = Ecto.Changeset . change ( % Constraint { } , from: 9 , to: 12 )
207-
208- # Custom handler converts the trigger error into a constraint
209- { :error , changeset } =
210- overlapping_changeset
211- |> Ecto.Changeset . exclusion_constraint ( :from , name: :cannot_overlap )
212- |> PoolRepo . insert ( constraint_handler: constraint_handler )
213-
214- assert changeset . errors == [
215- from:
216- { "violates an exclusion constraint" ,
217- [ constraint: :exclusion , constraint_name: "cannot_overlap" ] }
218- ]
219-
220- assert changeset . data . __meta__ . state == :built
221-
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 ->
225- overlapping_changeset
226- |> Ecto.Changeset . exclusion_constraint ( :from , name: :cannot_overlap )
227- |> PoolRepo . insert ( )
84+ @ tag :create_constraint
85+ test "custom :constraint_handler option" do
86+ parent = self ( )
87+ custom_handler = fn _err , _opts ->
88+ send ( parent , :custom_handler_called )
89+ [ exclusion: "positive_price" ]
22890 end
22991
230- # Custom handler also works on UPDATE
231- { :error , changeset } =
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 )
235-
236- assert changeset . errors == [
237- from:
238- { "violates an exclusion constraint" ,
239- [ constraint: :exclusion , constraint_name: "cannot_overlap" ] }
240- ]
92+ changeset =
93+ % Constraint { }
94+ |> Ecto.Changeset . change ( price: - 10 )
95+ |> Ecto.Changeset . exclusion_constraint ( :price , name: :positive_price )
24196
242- assert changeset . data . __meta__ . state == :loaded
97+ { :error , changeset } = PoolRepo . insert ( changeset , constraint_handler: custom_handler )
98+ assert_received :custom_handler_called
99+ assert changeset . errors == [ price: { "violates an exclusion constraint" , [ constraint: :exclusion , constraint_name: "positive_price" ] } ]
100+ assert changeset . data . __meta__ . state == :built
243101 end
244- end
102+ end
0 commit comments