Skip to content

Commit 1fe4afd

Browse files
committed
Improve error handling on update optimistic lock version to nil
If attempting to update the lock version column to nil/NULL raise a more explicit message (which would otherwise raise a `NoMethodError` on the attempt to increment).
1 parent ee66185 commit 1fe4afd

File tree

3 files changed

+54
-0
lines changed

3 files changed

+54
-0
lines changed

activerecord/lib/active_record/locking/optimistic.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ def _update_row(attribute_names, attempted_action = "update")
101101
attribute_names = attribute_names.dup if attribute_names.frozen?
102102
attribute_names << locking_column
103103

104+
if self[locking_column].nil?
105+
raise(<<-MSG.squish)
106+
For optimistic locking, '#{locking_column}' should not be set to `nil`/`NULL`.
107+
Are you missing a default value or validation on '#{locking_column}'?
108+
MSG
109+
end
110+
104111
self[locking_column] += 1
105112

106113
affected_rows = self.class._update_record(

activerecord/test/cases/locking_test.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,43 @@ def test_touch_existing_lock_without_default_should_work_with_null_in_the_databa
305305
assert_equal ["lock_version", "updated_at"], t1.saved_changes.keys.sort
306306
end
307307

308+
def test_update_lock_version_to_nil_without_validation_or_constraint_raises_error
309+
t1 = LockWithoutDefault.create!(title: "title1")
310+
error = assert_raises(RuntimeError) { t1.update(lock_version: nil) }
311+
312+
assert_match(locking_column_nil_error_message, error.message)
313+
314+
error = assert_raises(RuntimeError) { t1.update!(lock_version: nil) }
315+
316+
assert_match(locking_column_nil_error_message, error.message)
317+
end
318+
319+
def test_update_lock_version_to_nil_without_validation_raises
320+
person = Person.find(1)
321+
error = assert_raises(RuntimeError) { person.update(lock_version: nil) }
322+
323+
assert_match(locking_column_nil_error_message, error.message)
324+
325+
error = assert_raises(RuntimeError) { person.update!(lock_version: nil) }
326+
327+
assert_match(locking_column_nil_error_message, error.message)
328+
end
329+
330+
def test_update_lock_version_to_nil_with_validation_does_not_raise_runtime_lock_version_error
331+
person = LockVersionValidatedPerson.find(1)
332+
assert_nothing_raised { person.update(lock_version: nil) }
333+
334+
assert_equal ["is not a number"], person.errors[:lock_version]
335+
end
336+
337+
def test_update_bang_lock_version_to_nil_with_validation_does_not_raise_runtime_lock_version_error
338+
error = assert_raises(ActiveRecord::RecordInvalid) do
339+
LockVersionValidatedPerson.find(1).update!(lock_version: nil)
340+
end
341+
342+
assert_equal ["is not a number"], error.record.errors[:lock_version]
343+
end
344+
308345
def test_touch_stale_object_with_lock_without_default
309346
t1 = LockWithoutDefault.create!(title: "title1")
310347
stale_object = LockWithoutDefault.find(t1.id)
@@ -557,6 +594,11 @@ def test_yaml_dumping_with_lock_column
557594

558595
assert_equal t1.attributes, t2.attributes
559596
end
597+
598+
private
599+
def locking_column_nil_error_message
600+
/'lock_version' should not be set to `nil`/
601+
end
560602
end
561603

562604
class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase

activerecord/test/models/person.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,8 @@ class SerializedPerson < ActiveRecord::Base
148148

149149
serialize :insures, coder: Insure
150150
end
151+
152+
class LockVersionValidatedPerson < ActiveRecord::Base
153+
self.table_name = "people"
154+
validates :lock_version, numericality: { only_integer: true }, allow_nil: false
155+
end

0 commit comments

Comments
 (0)