activerecord_constraints 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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