activerecord_constraints 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/GNU-LICENSE +674 -0
- data/README +193 -0
- data/Rakefile +42 -0
- data/init.rb +45 -0
- data/install.rb +6 -0
- data/lib/activerecord_constraint_handlers.rb +307 -0
- data/lib/activerecord_constraints.rb +433 -0
- data/lib/tasks/activerecord_constraints_tasks.rake +4 -0
- data/test/database.yml +13 -0
- data/test/migration/double_connection_test.rb +122 -0
- data/test/migration/unique_constraints_multi_test.rb +95 -0
- data/test/migration/unique_constraints_null_test.rb +93 -0
- data/test/migration/unique_constraints_test.rb +87 -0
- data/test/test_helper.rb +76 -0
- data/uninstall.rb +6 -0
- metadata +70 -0
@@ -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
|
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
|