2 require 'models/person'
3 require 'models/reader'
4 require 'models/legacy_thing'
5 require 'models/reference'
7 class LockWithoutDefault
< ActiveRecord
::Base; end
9 class LockWithCustomColumnWithoutDefault
< ActiveRecord
::Base
10 set_table_name
:lock_without_defaults_cust
11 set_locking_column
:custom_lock_version
14 class ReadonlyFirstNamePerson
< Person
15 attr_readonly
:first_name
18 class OptimisticLockingTest
< ActiveRecord
::TestCase
19 fixtures
:people, :legacy_things, :references
21 # need to disable transactional fixtures, because otherwise the sqlite3
22 # adapter (at least) chokes when we try and change the schema in the middle
23 # of a test (see test_increment_counter_*).
24 self.use_transactional_fixtures
= false
26 def test_lock_existing
29 assert_equal
0, p1
.lock_version
30 assert_equal
0, p2
.lock_version
34 assert_equal
1, p1
.lock_version
35 assert_equal
0, p2
.lock_version
38 assert_raise(ActiveRecord
::StaleObjectError) { p2
.save
! }
44 assert_equal
0, p1
.lock_version
45 assert_equal
0, p2
.lock_version
49 assert_equal
1, p1
.lock_version
50 assert_equal
0, p2
.lock_version
52 assert_raises(ActiveRecord
::StaleObjectError) { p2
.destroy
}
55 assert_equal
true, p1
.frozen
?
56 assert_raises(ActiveRecord
::RecordNotFound) { Person
.find(1) }
59 def test_lock_repeating
62 assert_equal
0, p1
.lock_version
63 assert_equal
0, p2
.lock_version
67 assert_equal
1, p1
.lock_version
68 assert_equal
0, p2
.lock_version
71 assert_raise(ActiveRecord
::StaleObjectError) { p2
.save
! }
72 p2
.first_name
= 'sue2'
73 assert_raise(ActiveRecord
::StaleObjectError) { p2
.save
! }
77 p1
= Person
.new(:first_name => 'anika')
78 assert_equal
0, p1
.lock_version
80 p1
.first_name
= 'anika2'
82 p2
= Person
.find(p1
.id
)
83 assert_equal
0, p1
.lock_version
84 assert_equal
0, p2
.lock_version
86 p1
.first_name
= 'anika3'
88 assert_equal
1, p1
.lock_version
89 assert_equal
0, p2
.lock_version
92 assert_raise(ActiveRecord
::StaleObjectError) { p2
.save
! }
95 def test_lock_new_with_nil
96 p1
= Person
.new(:first_name => 'anika')
98 p1
.lock_version
= nil # simulate bad fixture or column with no default
100 assert_equal
1, p1
.lock_version
104 def test_lock_column_name_existing
105 t1
= LegacyThing
.find(1)
106 t2
= LegacyThing
.find(1)
107 assert_equal
0, t1
.version
108 assert_equal
0, t2
.version
110 t1
.tps_report_number
= 700
112 assert_equal
1, t1
.version
113 assert_equal
0, t2
.version
115 t2
.tps_report_number
= 800
116 assert_raise(ActiveRecord
::StaleObjectError) { t2
.save
! }
119 def test_lock_column_is_mass_assignable
120 p1
= Person
.create(:first_name => 'bianca')
121 assert_equal
0, p1
.lock_version
122 assert_equal p1
.lock_version
, Person
.new(p1
.attributes
).lock_version
124 p1
.first_name
= 'bianca2'
126 assert_equal
1, p1
.lock_version
127 assert_equal p1
.lock_version
, Person
.new(p1
.attributes
).lock_version
130 def test_lock_without_default_sets_version_to_zero
131 t1
= LockWithoutDefault
.new
132 assert_equal
0, t1
.lock_version
135 def test_lock_with_custom_column_without_default_sets_version_to_zero
136 t1
= LockWithCustomColumnWithoutDefault
.new
137 assert_equal
0, t1
.custom_lock_version
140 def test_readonly_attributes
141 assert_equal Set
.new([ 'first_name' ]), ReadonlyFirstNamePerson
.readonly_attributes
143 p
= ReadonlyFirstNamePerson
.create(:first_name => "unchangeable name")
145 assert_equal
"unchangeable name", p
.first_name
147 p
.update_attributes(:first_name => "changed name")
149 assert_equal
"unchangeable name", p
.first_name
152 { :lock_version => Person
, :custom_lock_version => LegacyThing
}.each
do |name
, model
|
153 define_method("test_increment_counter_updates_#{name}") do
154 counter_test model
, 1 do |id
|
155 model
.increment_counter
:test_count, id
159 define_method("test_decrement_counter_updates_#{name}") do
160 counter_test model
, -1 do |id
|
161 model
.decrement_counter
:test_count, id
165 define_method("test_update_counters_updates_#{name}") do
166 counter_test model
, 1 do |id
|
167 model
.update_counters id
, :test_count => 1
172 def test_quote_table_name
173 ref
= references(:michael_magician)
174 ref
.favourite
= !ref
.favourite
178 # Useful for partial updates, don't only update the lock_version if there
179 # is nothing else being updated.
180 def test_update_without_attributes_does_not_only_update_lock_version
181 assert_nothing_raised
do
182 p1
= Person
.new(:first_name => 'anika')
183 p1
.send(:update_with_lock, [])
189 def add_counter_column_to(model
)
190 model
.connection
.add_column model
.table_name
, :test_count, :integer, :null => false, :default => 0
191 model
.reset_column_information
192 # OpenBase does not set a value to existing rows when adding a not null default column
193 model
.update_all(:test_count => 0) if current_adapter
?(:OpenBaseAdapter)
196 def remove_counter_column_from(model
)
197 model
.connection
.remove_column model
.table_name
, :test_count
198 model
.reset_column_information
201 def counter_test(model
, expected_count
)
202 add_counter_column_to(model
)
203 object
= model
.find(:first)
204 assert_equal
0, object
.test_count
205 assert_equal
0, object
.send(model
.locking_column
)
208 assert_equal expected_count
, object
.test_count
209 assert_equal
1, object
.send(model
.locking_column
)
211 remove_counter_column_from(model
)
216 # TODO: test against the generated SQL since testing locking behavior itself
217 # is so cumbersome. Will deadlock Ruby threads if the underlying db.execute
218 # blocks, so separate script called by Kernel#system is needed.
219 # (See exec vs. async_exec in the PostgreSQL adapter.)
221 # TODO: The Sybase, and OpenBase adapters currently have no support for pessimistic locking
223 unless current_adapter
?(:SybaseAdapter, :OpenBaseAdapter)
224 class PessimisticLockingTest
< ActiveRecord
::TestCase
225 self.use_transactional_fixtures
= false
226 fixtures
:people, :readers
229 # Avoid introspection queries during tests.
230 Person
.columns
; Reader
.columns
234 def test_sane_find_with_lock
235 assert_nothing_raised
do
236 Person
.transaction
do
237 Person
.find
1, :lock => true
243 def test_sane_find_with_scoped_lock
244 assert_nothing_raised
do
245 Person
.transaction
do
246 Person
.with_scope(:find => { :lock => true }) do
253 # PostgreSQL protests SELECT ... FOR UPDATE on an outer join.
254 unless current_adapter
?(:PostgreSQLAdapter)
255 # Test locked eager find.
256 def test_eager_find_with_lock
257 assert_nothing_raised
do
258 Person
.transaction
do
259 Person
.find
1, :include => :readers, :lock => true
265 # Locking a record reloads it.
266 def test_sane_lock_method
267 assert_nothing_raised
do
268 Person
.transaction
do
269 person
= Person
.find
1
270 old
, person
.first_name
= person
.first_name
, 'fooman'
272 assert_equal old
, person
.first_name
277 if current_adapter
?(:PostgreSQLAdapter, :OracleAdapter)
278 use_concurrent_connections
280 def test_no_locks_no_wait
281 first
, second
= duel
{ Person
.find
1 }
282 assert first
.end > second
.end
285 def test_second_lock_waits
286 assert
[0.2, 1, 5].any
? { |zzz
|
287 first
, second
= duel(zzz
) { Person
.find
1, :lock => true }
288 second
.end > first
.end
294 t0
, t1
, t2
, t3
= nil, nil, nil, nil
298 Person
.transaction
do
300 sleep zzz
# block thread 2 for zzz seconds
306 sleep zzz
/ 2.0 # ensure thread 1 tx starts first
308 Person
.transaction
{ yield }
318 [t0
.to_f
..t1
.to_f
, t2
.to_f
..t3
.to_f
]