activerecord_constraints 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,433 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2007-2011 Ease Software, Inc. and Perry Smith
4
+ # All Rights Reserved
5
+ #
6
+
7
+ # Copyright (c) 2009 Perry Smith
8
+
9
+ # This file is part of activerecord_constraints.
10
+
11
+ # activerecord_constraints is free software: you can redistribute it
12
+ # and/or modify it under the terms of the GNU General Public License as
13
+ # published by the Free Software Foundation, either version 3 of the
14
+ # License, or (at your option) any later version.
15
+
16
+ # activerecord_constraints is distributed in the hope that it will be
17
+ # useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19
+ # General Public License for more details.
20
+
21
+ # You should have received a copy of the GNU General Public License
22
+ # along with activerecord_constraints. If not, see
23
+ # <http://www.gnu.org/licenses/>.
24
+
25
+ require 'active_record'
26
+ require 'active_record/base'
27
+ require 'active_record/connection_adapters/abstract_adapter'
28
+
29
+ # PostgresConstraints
30
+ module ActiveRecord
31
+ module ConnectionAdapters
32
+ # Module that will be included in other modules to reduce
33
+ # duplication. It implements the conversion from a list of
34
+ # options that specify a constraint to the SQL statement that
35
+ # implements the constraint.
36
+ #
37
+ # A constraint is broken into parts:
38
+ # 1. An optional constraint name
39
+ # 2. One of:
40
+ # 1. unique constraint
41
+ # 2. foreign key constraint
42
+ # 3. check constraint
43
+ # 3. An optional deferrable clause
44
+ # 4. An optional initially clause
45
+ #
46
+ # There are two places to declare a constraint. PostgreSQL calls
47
+ # these column constraints and table constraints. A column
48
+ # constraint can have a few things that a table constraint can not
49
+ # such as not null or null constraints. Those are handled
50
+ # elsewhere in Active Record although they might get moved in to
51
+ # here since this code allows those constraints to be named which
52
+ # may have some advantages.
53
+ #
54
+ # The SQL syntax for the column and table constraints are very
55
+ # similar so the routines handle both with a flag that specifies
56
+ # if the routine is being called to create a column constraint or
57
+ # a table constraint.
58
+ #
59
+ # The name of a constraint can be specified with either a
60
+ # <tt>:name => "constraint_name"</tt> option or, for example,
61
+ # <tt>:unique => "constraint_name"</tt>. The main advantage to
62
+ # this is to allow a multiple named constraints in the same column
63
+ # specification.
64
+ #
65
+ module Constraints
66
+ # Generic constraint code
67
+ module Abstract
68
+ end
69
+
70
+ # Generallized SQL constraint code.
71
+ module Sql
72
+ # Utility routine to return true if the options has the
73
+ # indicated constraint type.
74
+ def has_constraint(options, constraint_type)
75
+ options.has_key?(constraint_type) &&
76
+ options[constraint_type] != false
77
+ end
78
+
79
+ # Utility routine to produce the named part of a named
80
+ # constraint: passed the full set of options along with the type
81
+ # of the constraint, e.g. :unique.
82
+ def name_str(options, constraint_type)
83
+ if options[constraint_type] == true
84
+ n = options[:name]
85
+ else
86
+ n = options[constraint_type]
87
+ end
88
+ n.blank? ? "" : " CONSTRAINT #{n}"
89
+ end
90
+
91
+ # Utility routine to produce the additional string needed in a
92
+ # table constraint that is not needed in a column constraint.
93
+ # Passed the column name (which may be an array of names), the
94
+ # column_constraint flag, and the an optional prefix string.
95
+ def table_str(column_name, column_constraint, prefix_string = "")
96
+ # No space is needed around the parens but it looks nicer with
97
+ # spaces.
98
+ column_constraint ? "" :
99
+ " #{prefix_string}( #{column_to_str(column_name)} )"
100
+ end
101
+
102
+ # Creates the DEFERRABLE string
103
+ def deferrable_str(options)
104
+ if options.has_key?(:deferrable)
105
+ ((options[:deferrable] == false) ? " NOT" : "") +
106
+ " DEFERRABLE"
107
+ else
108
+ ""
109
+ end
110
+ end
111
+
112
+ # Creates the INITIALLY string
113
+ def initially_str(options)
114
+ if options.has_key?(:initially)
115
+ ref_str << " INITIALLY #{to_db_string(options[:initially])}"
116
+ else
117
+ ""
118
+ end
119
+ end
120
+
121
+ # Creates the suffix options deferrable and initially
122
+ def suffix_str(options)
123
+ deferrable_str(options) + initially_str(options)
124
+ end
125
+
126
+ # Utility routine to produce the string for the UNIQUE
127
+ # constraint. For a column constraint, the syntax may be
128
+ # either <tt>:unique => "constraint_name"</tt> or it can be
129
+ # <tt>:unique => true</tt> followed by an optional
130
+ # <tt>:name => "constraint_name"</tt>. If constraint_name is
131
+ # a symbol, it is simply converted to a string.
132
+ def unique_str(column_name, options, column_constraint)
133
+ ActiveRecord::Base.logger.debug("IN: Constraints#unique_str")
134
+ return "" unless has_constraint(options, :unique)
135
+ constraint_name = name_str(options, :unique)
136
+ column_spec = table_str(column_name, column_constraint)
137
+ constraint_name + " UNIQUE" + column_spec
138
+ end
139
+
140
+ # Utility routine to produce the string for a CHECK constraint.
141
+ # The alternatives here are: (the first two are named, the last
142
+ # two are unnamed)
143
+ # 1) :check => "constraint_name", :expr => "check expression"
144
+ # 2) :check => true, :name => "constraint_name",
145
+ # :expr => "check expression"
146
+ # 3) :check => true, :expr => "check expression"
147
+ # 4) :check => "check expression"
148
+ def check_str(column_name, options, column_constraint)
149
+ ActiveRecord::Base.logger.debug("IN: Constraints#check_str")
150
+ return "" unless has_constraint(options, :check)
151
+
152
+ # Have to dance a little bit here...
153
+ if options[:check] == true
154
+ expr = options[:expr]
155
+ name = options[:name]
156
+ elsif options.has_key?(:expr)
157
+ expr = options[:expr]
158
+ name = options[:check]
159
+ else
160
+ expr = options[:check]
161
+ name = nil
162
+ end
163
+ constraint_name = name_str({ :name => name, :check => true }, :check)
164
+ # column string is not part of CHECK constraints
165
+ constraint_name + " CHECK ( #{expr} )"
166
+ end
167
+
168
+ # Simple function to convert symbols and strings to what SQL
169
+ # wants.
170
+ # +:no_action+:: goes to "NO ACTION"
171
+ # +:cascade+:: goes to "CASCADE"
172
+ # etc
173
+ def to_db_string(f)
174
+ f.to_s.upcase.gsub(/_/, ' ')
175
+ end
176
+
177
+ # Utility routine to produce the string for a FOREIGN KEY
178
+ # constraint. Like a UNIQUE constraint, the optional name of
179
+ # the constraint can either the string assigned to the
180
+ # :reference option or a separate :name option.
181
+ def reference_str(column_name, options, column_constraint)
182
+ ActiveRecord::Base.logger.debug("IN: Constraints#reference_str")
183
+ return "" unless has_constraint(options, :reference)
184
+ constraint_name = name_str(options, :reference)
185
+ column_spec = table_str(column_name, column_constraint,
186
+ "FOREIGN KEY ")
187
+ local_options = { }
188
+ if md = /(.*)_id$/.match(column_name.to_s)
189
+ local_options[:table_name] = md[1].pluralize
190
+ local_options[:foreign_key] = "id"
191
+ end
192
+ local_options.merge!(options)
193
+ ref_column_str = column_to_str(local_options[:foreign_key])
194
+ ref_str = " REFERENCES #{local_options[:table_name]} (#{ref_column_str})"
195
+
196
+ if local_options.has_key?(:delete)
197
+ ref_str << " ON DELETE #{to_db_string(local_options[:delete])}"
198
+ end
199
+
200
+ if local_options.has_key?(:update)
201
+ ref_str << " ON UPDATE #{to_db_string(local_options[:update])}"
202
+ end
203
+
204
+ constraint_name + column_spec + ref_str
205
+ end
206
+
207
+ # Utility routine to return the column or the array of columns
208
+ # as a string.
209
+ def column_to_str(column)
210
+ ActiveRecord::Base.logger.debug("IN: Constraints#column_to_str")
211
+ if column.is_a? Array
212
+ column.map { |c| "\"#{c.to_s}\""}.join(", ")
213
+ else
214
+ "\"#{column.to_s}\""
215
+ end
216
+ end
217
+ end
218
+
219
+ # Postgresql specific constraint code
220
+ module Postgresql
221
+ include Sql
222
+ end
223
+ end
224
+
225
+ module SchemaStatements
226
+ # When base.add_column_options_with_constraints! is called by
227
+ # ColumnDefinition#add_column_options_with_constraints! it ends
228
+ # up calling
229
+ # SchemaStatements#add_column_options_with_constraints!. We
230
+ # capture that call as well so that we can append the constraint
231
+ # clauses to the sql statement.
232
+ def add_column_options_with_constraints!(sql, options)
233
+ ActiveRecord::Base.logger.debug("IN: SchemaStatements#add_column_options_with_constraints!")
234
+
235
+ # TODO:
236
+ # This needs to dig out the database type of the connection
237
+ # and then extend the database specific set of Constraints
238
+ extend Constraints::Postgresql
239
+
240
+ add_column_options_without_constraints!(sql, options)
241
+ column_name = options[:column].name
242
+ sql << unique_str(column_name, options, true)
243
+ sql << reference_str(column_name, options, true)
244
+ sql << check_str(column_name, options, true)
245
+ sql << suffix_str(options)
246
+ end
247
+ alias_method_chain :add_column_options!, :constraints
248
+ end
249
+
250
+ class ColumnDefinition
251
+ # Will contain all the options used on a column definition
252
+ def options
253
+ ActiveRecord::Base.logger.debug("IN: ColumnDefinition#options")
254
+ @options
255
+ end
256
+
257
+ # Called from TableDefinition#column_with_constraints so we
258
+ # remember the options for each column being defined.
259
+ def options=(arg)
260
+ ActiveRecord::Base.logger.debug("IN: ColumnDefinition#options=")
261
+ @options = arg
262
+ end
263
+
264
+ # ColumnDefinition@add_column_options! is called which calls
265
+ # base.add_column_options! by to_sql of ColumnDefinition. We
266
+ # capture this call so that we can merge in the options we are
267
+ # interested in (namely, all of the original options used in to
268
+ # create the column
269
+ def add_column_options_with_constraints!(sql, options)
270
+ ActiveRecord::Base.logger.debug("IN: ColumnDefinition#add_column_options_with_constraints!")
271
+ add_column_options_without_constraints!(sql, options.merge(@options))
272
+ end
273
+ alias_method_chain :add_column_options!, :constraints
274
+ end
275
+
276
+ class TableDefinition
277
+ # TODO:
278
+ # This include needs to be an extend executed perhaps when the
279
+ # connection is created.
280
+ include Constraints::Postgresql
281
+
282
+ # As the table is being defined, we capture the call to column.
283
+ # column (now called column_with_constraints returns self which
284
+ # is a TableDefinition. TableDefinition#[] returns the column
285
+ # for the name passed. We add an @options attribute for later
286
+ # use (see ColumnDefinition#options=).
287
+ def column_with_constraints(name, type, options = { })
288
+ ActiveRecord::Base.logger.debug("IN: TableDefinition#column_with_constraints for #{name}")
289
+ ret = column_without_constraints(name, type, options)
290
+ ret[name].options = options
291
+ ret
292
+ end
293
+ alias_method_chain :column, :constraints
294
+
295
+ # to_sql is called to transform the table definition into an sql
296
+ # statement. We insert ourselves into that so that we can
297
+ # append the extra string needed for the constraints added by
298
+ # the +unique+ and +references+ table definition methods.
299
+ def to_sql_with_constraints
300
+ ActiveRecord::Base.logger.debug("IN: TableDefinition#to_sql_with_constraints")
301
+ to_sql_without_constraints + extra_str
302
+ end
303
+ alias_method_chain :to_sql, :constraints
304
+
305
+ # _fk_ stands for <em>foreign key</em>. This is more like a macro that
306
+ # defines a column that is a foreign key.
307
+ # This:
308
+ #
309
+ # create_table :foos do |t|
310
+ # # Make a foreign key to the id column in the bars table
311
+ # t.fk :bar_id
312
+ # end
313
+ #
314
+ # is the equivalent of this:
315
+ #
316
+ # create_table :foos do |t|
317
+ # # Make a foreign key to the id column in the bars table
318
+ # t.integer :bar_id, :null => false, :reference => true,
319
+ # :delete => :cascade, :deferrable => true
320
+ # end
321
+ #
322
+ # which is the same as this:
323
+ #
324
+ # create_table :foos do |t|
325
+ # # Make a foreign key to the id column in the bars table
326
+ # t.integer :bar_id, :null => false, :reference => true,
327
+ # :delete => :cascade, :deferrable => true,
328
+ # :table_name => :bars, :foreign_key => :id
329
+ # end
330
+ #
331
+ # These defaults were chosen because despite common practice,
332
+ # nulls in databases should be avoided, the constraint needs to
333
+ # be deferrable to get fixtures to work, and cascade on delete
334
+ # keeps things simple.
335
+ #
336
+ # But this should work also:
337
+ #
338
+ # create_table :foos do |t|
339
+ # # Make a foreign key to the id column in the bars table
340
+ # t.fk :bar_id, :toad_id, :banana_id, :delete => :no_action
341
+ # end
342
+ #
343
+ def fk(*names)
344
+ options = {
345
+ :null => false,
346
+ :reference => true,
347
+ :delete => :cascade,
348
+ :deferrable => true
349
+ }.merge(names.last.is_a?(Hash) ? names.pop : { })
350
+ self.integer(*names, options)
351
+ end
352
+
353
+ # Add a "unique" method to TableDefinition. e.g.
354
+ # create_table :users do |t|
355
+ # t.string :name, :null => false
356
+ # t.boolean :admin, :default => false
357
+ # t.timestamps
358
+ # t.unique :name
359
+ # end
360
+ #
361
+ # A list of UNIQUE can be specified by simply listing the
362
+ # columns:
363
+ # t.unique :name1, :name2, :name3
364
+ # This produces separate constraints. To produce a specification
365
+ # where a set of columns needs to be unique, put the column
366
+ # names inside an array. Both can be done at the same time:
367
+ # t.unique [ :name1, :name2 ], :name3
368
+ # produces where the tulple (name1, name2) must be unique and
369
+ # the name3 column must also be unique.
370
+ def unique(*args)
371
+ ActiveRecord::Base.logger.debug("IN: TableDefinition#unique")
372
+ options = { :unique => true }.merge(args.last.is_a?(Hash) ? args.pop : { })
373
+ args.each { |arg| extra_str << ", #{unique_str(arg, options, false)}" }
374
+ end
375
+
376
+ # Pass a column and options (which may be empty). The column
377
+ # name of foo_id, by default, creates a reference to table foos,
378
+ # column id. :table_name may be passed in options to specify
379
+ # the foreign table name. :foreign_key may be passed to specify the
380
+ # foreign column.
381
+ # Both the passed in column (first argument) as well as thee
382
+ # :foreign_key option may be an array.
383
+ # :delete option may be passed in with the appropriate value
384
+ # such as :restrict, :cascade, etc.
385
+ #
386
+ # This might be broken because "deferrable" is not available
387
+ # where it use to be before.
388
+ #
389
+ def reference(column, options = { })
390
+ ActiveRecord::Base.logger.debug("IN: TableDefinition#reference")
391
+ extra_str << ", #{reference_str(column, options, false)}"
392
+ end
393
+
394
+ # Specify a check table constrant. In the simple case, this can
395
+ # be done as:
396
+ # create_table :users do |t|
397
+ # t.string :name, :null => false
398
+ # t.string :password, :null => false
399
+ # t.boolean :admin, :default => false
400
+ # t.timestamps
401
+ # t.check "password_check(password)"
402
+ # end
403
+ #
404
+ # Alternate forms for the above are:
405
+ # 1. To give the constraint a name of "password_constraint":
406
+ # t.check "password_check(password)", :name => "password_constraint"
407
+ # 2. Flip the above around:
408
+ # t.check "password_constraint", expr => "password_check(password)"
409
+ # 3. Same but perhaps more obvious:
410
+ # t.check name => "password_constraint", expr => "password_check(password)"
411
+ #
412
+ # The expression must be specified, the name of the constraint
413
+ # is always optional
414
+ #
415
+ def check(*args)
416
+ ActiveRecord::Base.logger.debug("IN: TableDefinition#check")
417
+ extra_str << ", #{check_str(column, options, false)}"
418
+ end
419
+
420
+ private
421
+
422
+ def extra_str
423
+ ActiveRecord::Base.logger.debug("IN: TableDefinition#extra_str")
424
+ @extra_str ||= ""
425
+ end
426
+
427
+ def extra_str=(arg)
428
+ ActiveRecord::Base.logger.debug("IN: TableDefinition#extra_str=")
429
+ @extra_str = arg
430
+ end
431
+ end
432
+ end
433
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :activerecord_constraints do
3
+ # # Task goes here
4
+ # end
data/test/database.yml ADDED
@@ -0,0 +1,13 @@
1
+
2
+ postgres:
3
+ adapter: postgresql
4
+ username: postgres
5
+ database: activerecord_constraints_plugin_test
6
+ min_messages: ERROR
7
+
8
+
9
+ postgres2:
10
+ adapter: postgresql
11
+ username: postgres
12
+ database: activerecord_constraints_plugin_test2
13
+ min_messages: ERROR
@@ -0,0 +1,122 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2007-2011 Ease Software, Inc. and Perry Smith
4
+ # All Rights Reserved
5
+ #
6
+
7
+ # Copyright (c) 2009 Perry Smith
8
+
9
+ # This file is part of activerecord_constraints.
10
+
11
+ # activerecord_constraints is free software: you can redistribute it
12
+ # and/or modify it under the terms of the GNU General Public License as
13
+ # published by the Free Software Foundation, either version 3 of the
14
+ # License, or (at your option) any later version.
15
+
16
+ # activerecord_constraints is distributed in the hope that it will be
17
+ # useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19
+ # General Public License for more details.
20
+
21
+ # You should have received a copy of the GNU General Public License
22
+ # along with activerecord_constraints. If not, see
23
+ # <http://www.gnu.org/licenses/>.
24
+
25
+ require 'test_helper'
26
+
27
+ # This test makes sure that opening a second connection keeps things working.
28
+
29
+ # The matching models
30
+ class Foo < ActiveRecord::Base
31
+ end
32
+
33
+ class Xyz < ActiveRecord::Base
34
+ end
35
+
36
+ class DoubleConnectionTest < ActiveSupport::TestCase
37
+ self.pre_loaded_fixtures = true
38
+ def setup
39
+ create_db(ActiveRecord::Base, db_config1)
40
+ ActiveRecord::Base.connection.create_table :foos do |t|
41
+ t.string :name, :unique => true
42
+ end
43
+
44
+ create_db(Xyz, db_config2)
45
+ Xyz.connection.create_table :xyzs do |t|
46
+ t.string :name, :unique => true
47
+ end
48
+ end
49
+
50
+ def teardown
51
+ ActiveRecord::Base.connection.drop_table :foos
52
+ Xyz.connection.drop_table :xyzs
53
+ end
54
+
55
+ def test_dc_can_save_valid_null_foo
56
+ f = Foo.new()
57
+ assert(f.save, "Can not save valid model with null name")
58
+ end
59
+
60
+ def test_dc_can_save_valid_null_xyz
61
+ f = Xyz.new()
62
+ assert(f.save, "Can not save valid model with null name")
63
+ end
64
+
65
+ def test_dc_can_save_valid_foo
66
+ f = Foo.new(:name => "dog")
67
+ assert(f.save, "Can not save valid model with name")
68
+ end
69
+
70
+ def test_dc_can_save_valid_xyz
71
+ f = Xyz.new(:name => "dog")
72
+ assert(f.save, "Can not save valid model with name")
73
+ end
74
+
75
+ def test_dc_cannot_save_duplicate_foo
76
+ f = Foo.new(:name => "myname")
77
+ g = Foo.new(:name => "myname")
78
+ f.save
79
+ assert_equal(false, g.save, "Should not be able to save duplicate")
80
+ end
81
+
82
+ def test_dc_cannot_save_duplicate_xyz
83
+ f = Xyz.new(:name => "myname")
84
+ g = Xyz.new(:name => "myname")
85
+ f.save
86
+ assert_equal(false, g.save, "Should not be able to save duplicate")
87
+ end
88
+
89
+ def test_dc_save_bang_throws_foo
90
+ f = Foo.new(:name => "myname")
91
+ g = Foo.new(:name => "myname")
92
+ f.save
93
+ assert_raise(ActiveRecord::RecordNotSaved) do
94
+ g.save!
95
+ end
96
+ end
97
+
98
+ def test_dc_save_bang_throws_xyz
99
+ f = Xyz.new(:name => "myname")
100
+ g = Xyz.new(:name => "myname")
101
+ f.save
102
+ assert_raise(ActiveRecord::RecordNotSaved) do
103
+ g.save!
104
+ end
105
+ end
106
+
107
+ def test_dc_error_message_foo
108
+ f = Foo.new(:name => "myname")
109
+ g = Foo.new(:name => "myname")
110
+ f.save
111
+ g.save
112
+ assert_equal("has already been taken", g.errors.on(:name))
113
+ end
114
+
115
+ def test_dc_error_message_xyz
116
+ f = Xyz.new(:name => "myname")
117
+ g = Xyz.new(:name => "myname")
118
+ f.save
119
+ g.save
120
+ assert_equal("has already been taken", g.errors.on(:name))
121
+ end
122
+ end
@@ -0,0 +1,95 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2007-2011 Ease Software, Inc. and Perry Smith
4
+ # All Rights Reserved
5
+ #
6
+
7
+ # Copyright (c) 2009 Perry Smith
8
+
9
+ # This file is part of activerecord_constraints.
10
+
11
+ # activerecord_constraints is free software: you can redistribute it
12
+ # and/or modify it under the terms of the GNU General Public License as
13
+ # published by the Free Software Foundation, either version 3 of the
14
+ # License, or (at your option) any later version.
15
+
16
+ # activerecord_constraints is distributed in the hope that it will be
17
+ # useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19
+ # General Public License for more details.
20
+
21
+ # You should have received a copy of the GNU General Public License
22
+ # along with activerecord_constraints. If not, see
23
+ # <http://www.gnu.org/licenses/>.
24
+
25
+ require 'test_helper'
26
+
27
+ # table with unique clauses
28
+ # Note that null does not equal null. So, testing with columns equal
29
+ # to null is sometimes not intuitively obvious. I'm going to avoid
30
+ # it.
31
+ class CreateBlahs < ActiveRecord::Migration
32
+ def self.up
33
+ create_table :blahs do |t|
34
+ t.string :name1, :null => false
35
+ t.string :name2, :null => false
36
+ t.unique [ :name1, :name2 ]
37
+ end
38
+ end
39
+
40
+ def self.down
41
+ drop_table :blahs
42
+ end
43
+ end
44
+
45
+ # The matching model
46
+ class Blah < ActiveRecord::Base
47
+ end
48
+
49
+ class UniqueConstraintsMultiTest < ActiveSupport::TestCase
50
+ def setup
51
+ create_db(ActiveRecord::Base, db_config1)
52
+ CreateBlahs.up
53
+ end
54
+
55
+ def teardown
56
+ CreateBlahs.down
57
+ end
58
+
59
+ def test_can_save_valid
60
+ f = Blah.new(:name1 => "happy", :name2=> "doggy")
61
+ assert(f.save, "Can not save valid model")
62
+ end
63
+
64
+ def test_duplicate_in_one_column_ok
65
+ f = Blah.new(:name1 => "happy", :name2=> "doggy")
66
+ g = Blah.new(:name1 => "happy", :name2=> "kitten")
67
+ f.save
68
+ assert(g.save, "Can not save valid model")
69
+ end
70
+
71
+ def test_duplicates_are_rejected
72
+ f = Blah.new(:name1 => "happy", :name2=> "doggy")
73
+ g = Blah.new(:name1 => "happy", :name2=> "doggy")
74
+ f.save
75
+ assert_equal(false, g.save, "Duplicate should have been rejected")
76
+ end
77
+
78
+ def test_errors_in_both_columns
79
+ f = Blah.new(:name1 => "happy", :name2=> "doggy")
80
+ g = Blah.new(:name1 => "happy", :name2=> "doggy")
81
+ f.save
82
+ g.save
83
+ assert_equal("has already been taken", g.errors.on(:name1))
84
+ assert_equal("has already been taken", g.errors.on(:name2))
85
+ end
86
+
87
+ def test_save_bang_throws
88
+ f = Blah.new(:name1 => "happy", :name2=> "doggy")
89
+ g = Blah.new(:name1 => "happy", :name2=> "doggy")
90
+ f.save
91
+ assert_raise(ActiveRecord::RecordNotSaved) do
92
+ g.save!
93
+ end
94
+ end
95
+ end