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.
- 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
|