2017-07-09 13:41:28 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
require "thread"
|
2011-06-06 14:17:44 -04:00
|
|
|
require "cases/helper"
|
2016-08-06 12:26:20 -04:00
|
|
|
require "models/person"
|
|
|
|
require "models/job"
|
|
|
|
require "models/reader"
|
|
|
|
require "models/ship"
|
|
|
|
require "models/legacy_thing"
|
|
|
|
require "models/personal_legacy_thing"
|
|
|
|
require "models/reference"
|
|
|
|
require "models/string_key_object"
|
|
|
|
require "models/car"
|
|
|
|
require "models/bulb"
|
|
|
|
require "models/engine"
|
|
|
|
require "models/wheel"
|
|
|
|
require "models/treasure"
|
2017-10-23 13:10:51 -04:00
|
|
|
require "models/frog"
|
2004-12-31 14:38:04 -05:00
|
|
|
|
2006-09-03 20:02:38 -04:00
|
|
|
class LockWithoutDefault < ActiveRecord::Base; end
|
|
|
|
|
|
|
|
class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
|
2011-11-29 07:28:04 -05:00
|
|
|
self.table_name = :lock_without_defaults_cust
|
2017-01-05 03:20:57 -05:00
|
|
|
column_defaults # to test @column_defaults caching.
|
2011-11-29 14:40:29 -05:00
|
|
|
self.locking_column = :custom_lock_version
|
2006-09-03 20:02:38 -04:00
|
|
|
end
|
|
|
|
|
2012-08-21 14:46:37 -04:00
|
|
|
class ReadonlyNameShip < Ship
|
|
|
|
attr_readonly :name
|
2007-11-16 15:31:24 -05:00
|
|
|
end
|
|
|
|
|
2008-01-21 12:20:51 -05:00
|
|
|
class OptimisticLockingTest < ActiveRecord::TestCase
|
2012-03-07 23:56:23 -05:00
|
|
|
fixtures :people, :legacy_things, :references, :string_key_objects, :peoples_treasures
|
2011-03-14 22:19:47 -04:00
|
|
|
|
2012-06-17 11:43:31 -04:00
|
|
|
def test_quote_value_passed_lock_col
|
|
|
|
p1 = Person.find(1)
|
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
p1.first_name = "anika2"
|
2012-06-17 11:43:31 -04:00
|
|
|
p1.save!
|
|
|
|
|
|
|
|
assert_equal 1, p1.lock_version
|
|
|
|
end
|
|
|
|
|
2011-03-14 22:19:47 -04:00
|
|
|
def test_non_integer_lock_existing
|
|
|
|
s1 = StringKeyObject.find("record1")
|
|
|
|
s2 = StringKeyObject.find("record1")
|
|
|
|
assert_equal 0, s1.lock_version
|
|
|
|
assert_equal 0, s2.lock_version
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
s1.name = "updated record"
|
2011-03-14 22:19:47 -04:00
|
|
|
s1.save!
|
|
|
|
assert_equal 1, s1.lock_version
|
|
|
|
assert_equal 0, s2.lock_version
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
s2.name = "doubly updated record"
|
2011-03-14 22:19:47 -04:00
|
|
|
assert_raise(ActiveRecord::StaleObjectError) { s2.save! }
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_non_integer_lock_destroy
|
|
|
|
s1 = StringKeyObject.find("record1")
|
|
|
|
s2 = StringKeyObject.find("record1")
|
|
|
|
assert_equal 0, s1.lock_version
|
|
|
|
assert_equal 0, s2.lock_version
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
s1.name = "updated record"
|
2011-03-14 22:19:47 -04:00
|
|
|
s1.save!
|
|
|
|
assert_equal 1, s1.lock_version
|
|
|
|
assert_equal 0, s2.lock_version
|
|
|
|
assert_raise(ActiveRecord::StaleObjectError) { s2.destroy }
|
|
|
|
|
|
|
|
assert s1.destroy
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_predicate s1, :frozen?
|
|
|
|
assert_predicate s1, :destroyed?
|
2011-03-14 22:19:47 -04:00
|
|
|
assert_raises(ActiveRecord::RecordNotFound) { StringKeyObject.find("record1") }
|
|
|
|
end
|
2004-12-31 14:38:04 -05:00
|
|
|
|
|
|
|
def test_lock_existing
|
|
|
|
p1 = Person.find(1)
|
|
|
|
p2 = Person.find(1)
|
2006-06-20 15:54:35 -04:00
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
2006-06-19 18:48:51 -04:00
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
p1.first_name = "stu"
|
2006-06-20 15:54:35 -04:00
|
|
|
p1.save!
|
|
|
|
assert_equal 1, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
2006-06-19 18:48:51 -04:00
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
p2.first_name = "sue"
|
2009-03-08 16:11:58 -04:00
|
|
|
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
|
2004-12-31 14:38:04 -05:00
|
|
|
end
|
2008-01-18 02:30:42 -05:00
|
|
|
|
2010-02-21 22:47:30 -05:00
|
|
|
# See Lighthouse ticket #1966
|
|
|
|
def test_lock_destroy
|
|
|
|
p1 = Person.find(1)
|
|
|
|
p2 = Person.find(1)
|
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
p1.first_name = "stu"
|
2010-02-21 22:47:30 -05:00
|
|
|
p1.save!
|
|
|
|
assert_equal 1, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
|
|
|
|
|
|
|
assert_raises(ActiveRecord::StaleObjectError) { p2.destroy }
|
|
|
|
|
|
|
|
assert p1.destroy
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_predicate p1, :frozen?
|
|
|
|
assert_predicate p1, :destroyed?
|
2010-02-21 22:47:30 -05:00
|
|
|
assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) }
|
|
|
|
end
|
|
|
|
|
2007-05-25 17:31:44 -04:00
|
|
|
def test_lock_repeating
|
|
|
|
p1 = Person.find(1)
|
|
|
|
p2 = Person.find(1)
|
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
p1.first_name = "stu"
|
2007-05-25 17:31:44 -04:00
|
|
|
p1.save!
|
|
|
|
assert_equal 1, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
p2.first_name = "sue"
|
2009-03-08 16:11:58 -04:00
|
|
|
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
|
2016-08-06 12:26:20 -04:00
|
|
|
p2.first_name = "sue2"
|
2009-03-08 16:11:58 -04:00
|
|
|
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
|
2007-05-25 17:31:44 -04:00
|
|
|
end
|
2004-12-31 14:38:04 -05:00
|
|
|
|
|
|
|
def test_lock_new
|
2016-08-06 13:37:57 -04:00
|
|
|
p1 = Person.new(first_name: "anika")
|
2006-06-20 15:54:35 -04:00
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
p1.first_name = "anika2"
|
2006-06-20 15:54:35 -04:00
|
|
|
p1.save!
|
2004-12-31 14:38:04 -05:00
|
|
|
p2 = Person.find(p1.id)
|
2006-06-20 15:54:35 -04:00
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
p1.first_name = "anika3"
|
2006-06-20 15:54:35 -04:00
|
|
|
p1.save!
|
|
|
|
assert_equal 1, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
p2.first_name = "sue"
|
2009-03-08 16:11:58 -04:00
|
|
|
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
|
2006-01-14 04:36:52 -05:00
|
|
|
end
|
2008-01-18 02:30:42 -05:00
|
|
|
|
2011-10-14 10:28:02 -04:00
|
|
|
def test_lock_exception_record
|
2016-08-06 13:37:57 -04:00
|
|
|
p1 = Person.new(first_name: "mira")
|
2011-10-14 10:28:02 -04:00
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
p1.first_name = "mira2"
|
2011-10-14 10:28:02 -04:00
|
|
|
p1.save!
|
|
|
|
p2 = Person.find(p1.id)
|
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
p1.first_name = "mira3"
|
2011-10-14 10:28:02 -04:00
|
|
|
p1.save!
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
p2.first_name = "sue"
|
2011-10-14 10:28:02 -04:00
|
|
|
error = assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
|
|
|
|
assert_equal(error.record.object_id, p2.object_id)
|
|
|
|
end
|
|
|
|
|
2016-04-23 18:31:18 -04:00
|
|
|
def test_lock_new_when_explicitly_passing_nil
|
2016-08-06 13:37:57 -04:00
|
|
|
p1 = Person.new(first_name: "anika", lock_version: nil)
|
2016-04-23 18:31:18 -04:00
|
|
|
p1.save!
|
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
end
|
|
|
|
|
2017-04-22 07:01:59 -04:00
|
|
|
def test_lock_new_when_explicitly_passing_value
|
|
|
|
p1 = Person.new(first_name: "Douglas Adams", lock_version: 42)
|
|
|
|
p1.save!
|
|
|
|
assert_equal 42, p1.lock_version
|
|
|
|
end
|
|
|
|
|
2010-11-27 05:14:26 -05:00
|
|
|
def test_touch_existing_lock
|
|
|
|
p1 = Person.find(1)
|
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
|
|
|
|
p1.touch
|
|
|
|
assert_equal 1, p1.lock_version
|
2016-09-14 11:07:15 -04:00
|
|
|
assert_not p1.changed?, "Changes should have been cleared"
|
2010-11-27 05:14:26 -05:00
|
|
|
end
|
|
|
|
|
2015-04-16 10:40:52 -04:00
|
|
|
def test_touch_stale_object
|
2016-08-06 12:26:20 -04:00
|
|
|
person = Person.create!(first_name: "Mehmet Emin")
|
2015-04-16 10:40:52 -04:00
|
|
|
stale_person = Person.find(person.id)
|
2016-08-06 12:26:20 -04:00
|
|
|
person.update_attribute(:gender, "M")
|
2015-04-16 10:40:52 -04:00
|
|
|
|
|
|
|
assert_raises(ActiveRecord::StaleObjectError) do
|
|
|
|
stale_person.touch
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-03-04 01:32:46 -05:00
|
|
|
def test_update_with_dirty_primary_key
|
|
|
|
assert_raises(ActiveRecord::RecordNotUnique) do
|
|
|
|
person = Person.find(1)
|
|
|
|
person.id = 2
|
|
|
|
person.save!
|
|
|
|
end
|
|
|
|
|
|
|
|
person = Person.find(1)
|
|
|
|
person.id = 42
|
|
|
|
person.save!
|
|
|
|
|
|
|
|
assert Person.find(42)
|
|
|
|
assert_raises(ActiveRecord::RecordNotFound) do
|
|
|
|
Person.find(1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_delete_with_dirty_primary_key
|
|
|
|
person = Person.find(1)
|
|
|
|
person.id = 2
|
|
|
|
person.delete
|
|
|
|
|
|
|
|
assert Person.find(2)
|
|
|
|
assert_raises(ActiveRecord::RecordNotFound) do
|
|
|
|
Person.find(1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_destroy_with_dirty_primary_key
|
|
|
|
person = Person.find(1)
|
|
|
|
person.id = 2
|
|
|
|
person.destroy
|
|
|
|
|
|
|
|
assert Person.find(2)
|
|
|
|
assert_raises(ActiveRecord::RecordNotFound) do
|
|
|
|
Person.find(1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-04-22 07:01:59 -04:00
|
|
|
def test_explicit_update_lock_column_raise_error
|
|
|
|
person = Person.find(1)
|
|
|
|
|
|
|
|
assert_raises(ActiveRecord::StaleObjectError) do
|
|
|
|
person.first_name = "Douglas Adams"
|
|
|
|
person.lock_version = 42
|
|
|
|
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_predicate person, :lock_version_changed?
|
2017-04-22 07:01:59 -04:00
|
|
|
|
|
|
|
person.save
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2006-01-14 04:36:52 -05:00
|
|
|
def test_lock_column_name_existing
|
|
|
|
t1 = LegacyThing.find(1)
|
|
|
|
t2 = LegacyThing.find(1)
|
2006-06-20 15:54:35 -04:00
|
|
|
assert_equal 0, t1.version
|
|
|
|
assert_equal 0, t2.version
|
|
|
|
|
2008-06-15 16:25:59 -04:00
|
|
|
t1.tps_report_number = 700
|
2006-06-20 15:54:35 -04:00
|
|
|
t1.save!
|
|
|
|
assert_equal 1, t1.version
|
|
|
|
assert_equal 0, t2.version
|
|
|
|
|
2008-06-15 16:25:59 -04:00
|
|
|
t2.tps_report_number = 800
|
2009-03-08 16:11:58 -04:00
|
|
|
assert_raise(ActiveRecord::StaleObjectError) { t2.save! }
|
2006-06-20 15:54:35 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_lock_column_is_mass_assignable
|
2016-08-06 13:37:57 -04:00
|
|
|
p1 = Person.create(first_name: "bianca")
|
2006-06-20 15:54:35 -04:00
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
|
2006-01-14 04:36:52 -05:00
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
p1.first_name = "bianca2"
|
2006-06-20 15:54:35 -04:00
|
|
|
p1.save!
|
|
|
|
assert_equal 1, p1.lock_version
|
|
|
|
assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
|
2006-06-19 18:48:51 -04:00
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2006-09-03 20:02:38 -04:00
|
|
|
def test_lock_without_default_sets_version_to_zero
|
|
|
|
t1 = LockWithoutDefault.new
|
2016-08-03 20:36:30 -04:00
|
|
|
|
2006-09-03 20:02:38 -04:00
|
|
|
assert_equal 0, t1.lock_version
|
2016-08-03 20:36:30 -04:00
|
|
|
assert_nil t1.lock_version_before_type_cast
|
|
|
|
|
|
|
|
t1.save!
|
|
|
|
t1.reload
|
2013-02-26 09:35:03 -05:00
|
|
|
|
|
|
|
assert_equal 0, t1.lock_version
|
2016-08-03 20:36:30 -04:00
|
|
|
assert_equal 0, t1.lock_version_before_type_cast
|
|
|
|
end
|
|
|
|
|
2017-04-27 17:23:50 -04:00
|
|
|
def test_touch_existing_lock_without_default_should_work_with_null_in_the_database
|
|
|
|
ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')")
|
|
|
|
t1 = LockWithoutDefault.last
|
|
|
|
|
|
|
|
assert_equal 0, t1.lock_version
|
|
|
|
assert_nil t1.lock_version_before_type_cast
|
|
|
|
|
|
|
|
t1.touch
|
|
|
|
|
|
|
|
assert_equal 1, t1.lock_version
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_touch_stale_object_with_lock_without_default
|
|
|
|
t1 = LockWithoutDefault.create!(title: "title1")
|
|
|
|
stale_object = LockWithoutDefault.find(t1.id)
|
|
|
|
|
|
|
|
t1.update!(title: "title2")
|
|
|
|
|
|
|
|
assert_raises(ActiveRecord::StaleObjectError) do
|
|
|
|
stale_object.touch
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-08-03 20:36:30 -04:00
|
|
|
def test_lock_without_default_should_work_with_null_in_the_database
|
|
|
|
ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')")
|
|
|
|
t1 = LockWithoutDefault.last
|
2017-04-27 17:23:50 -04:00
|
|
|
t2 = LockWithoutDefault.find(t1.id)
|
2016-08-03 20:36:30 -04:00
|
|
|
|
|
|
|
assert_equal 0, t1.lock_version
|
|
|
|
assert_nil t1.lock_version_before_type_cast
|
|
|
|
assert_equal 0, t2.lock_version
|
|
|
|
assert_nil t2.lock_version_before_type_cast
|
|
|
|
|
|
|
|
t1.title = "new title1"
|
|
|
|
t2.title = "new title2"
|
|
|
|
|
|
|
|
assert_nothing_raised { t1.save! }
|
|
|
|
assert_equal 1, t1.lock_version
|
|
|
|
assert_equal "new title1", t1.title
|
|
|
|
|
|
|
|
assert_raise(ActiveRecord::StaleObjectError) { t2.save! }
|
|
|
|
assert_equal 0, t2.lock_version
|
|
|
|
assert_equal "new title2", t2.title
|
2006-09-03 20:02:38 -04:00
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2016-08-22 14:16:08 -04:00
|
|
|
def test_lock_without_default_queries_count
|
|
|
|
t1 = LockWithoutDefault.create(title: "title1")
|
|
|
|
|
|
|
|
assert_equal "title1", t1.title
|
|
|
|
assert_equal 0, t1.lock_version
|
|
|
|
|
|
|
|
assert_queries(1) { t1.update(title: "title2") }
|
|
|
|
|
|
|
|
t1.reload
|
|
|
|
assert_equal "title2", t1.title
|
|
|
|
assert_equal 1, t1.lock_version
|
|
|
|
|
|
|
|
t2 = LockWithoutDefault.new(title: "title1")
|
|
|
|
|
|
|
|
assert_queries(1) { t2.save! }
|
|
|
|
|
|
|
|
t2.reload
|
|
|
|
assert_equal "title1", t2.title
|
|
|
|
assert_equal 0, t2.lock_version
|
|
|
|
end
|
|
|
|
|
2006-09-03 20:02:38 -04:00
|
|
|
def test_lock_with_custom_column_without_default_sets_version_to_zero
|
|
|
|
t1 = LockWithCustomColumnWithoutDefault.new
|
2016-08-03 20:36:30 -04:00
|
|
|
|
2006-09-03 20:02:38 -04:00
|
|
|
assert_equal 0, t1.custom_lock_version
|
Properly persist `lock_version` as 0 if the DB has no default
The reason this bug occured is that we never actually check to see if
this column has changed from it's default, since it was never assigned
and is not mutable.
It appears I was wrong in b301c40224c6d15b539dbcc7485adb44d810f88c, with
my statement of "there is no longer a case where a given value would
differ from the default, but would not already be marked as changed."
However, I chose not to revert the deletion of
`initialize_internals_callback` from that commit, as I think a solution
closer to where the problem lies is less likely to get erroneously
removed. I'm not super happy with this solution, but it mirrors what is
being done in `_update_record`, and a fix for one should work for the
other.
I toyed with the idea of changing the definition of `changed?` on the
type to `changed_in_place?`. If we type cast the raw value, it'll break
a test about updating not modifying the lock column if nothing else was
changed. We could have the definition check if `raw_old_value` is `nil`,
but this feels fragile and less intention revealing. It would, however,
have the benefit of cleaning up old data that incorrectly persisted as
`nil`.
Fixes #18422
2015-01-09 11:49:21 -05:00
|
|
|
assert_nil t1.custom_lock_version_before_type_cast
|
2013-02-26 09:35:03 -05:00
|
|
|
|
Properly persist `lock_version` as 0 if the DB has no default
The reason this bug occured is that we never actually check to see if
this column has changed from it's default, since it was never assigned
and is not mutable.
It appears I was wrong in b301c40224c6d15b539dbcc7485adb44d810f88c, with
my statement of "there is no longer a case where a given value would
differ from the default, but would not already be marked as changed."
However, I chose not to revert the deletion of
`initialize_internals_callback` from that commit, as I think a solution
closer to where the problem lies is less likely to get erroneously
removed. I'm not super happy with this solution, but it mirrors what is
being done in `_update_record`, and a fix for one should work for the
other.
I toyed with the idea of changing the definition of `changed?` on the
type to `changed_in_place?`. If we type cast the raw value, it'll break
a test about updating not modifying the lock column if nothing else was
changed. We could have the definition check if `raw_old_value` is `nil`,
but this feels fragile and less intention revealing. It would, however,
have the benefit of cleaning up old data that incorrectly persisted as
`nil`.
Fixes #18422
2015-01-09 11:49:21 -05:00
|
|
|
t1.save!
|
|
|
|
t1.reload
|
2016-08-03 20:36:30 -04:00
|
|
|
|
2013-02-26 09:35:03 -05:00
|
|
|
assert_equal 0, t1.custom_lock_version
|
2016-08-03 20:36:30 -04:00
|
|
|
assert_equal 0, t1.custom_lock_version_before_type_cast
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_lock_with_custom_column_without_default_should_work_with_null_in_the_database
|
|
|
|
ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults_cust(title) VALUES('title1')")
|
|
|
|
|
|
|
|
t1 = LockWithCustomColumnWithoutDefault.last
|
2017-04-27 17:23:50 -04:00
|
|
|
t2 = LockWithCustomColumnWithoutDefault.find(t1.id)
|
2016-08-03 20:36:30 -04:00
|
|
|
|
|
|
|
assert_equal 0, t1.custom_lock_version
|
|
|
|
assert_nil t1.custom_lock_version_before_type_cast
|
|
|
|
assert_equal 0, t2.custom_lock_version
|
|
|
|
assert_nil t2.custom_lock_version_before_type_cast
|
|
|
|
|
|
|
|
t1.title = "new title1"
|
|
|
|
t2.title = "new title2"
|
|
|
|
|
|
|
|
assert_nothing_raised { t1.save! }
|
|
|
|
assert_equal 1, t1.custom_lock_version
|
|
|
|
assert_equal "new title1", t1.title
|
|
|
|
|
|
|
|
assert_raise(ActiveRecord::StaleObjectError) { t2.save! }
|
|
|
|
assert_equal 0, t2.custom_lock_version
|
|
|
|
assert_equal "new title2", t2.title
|
2006-09-03 20:02:38 -04:00
|
|
|
end
|
2007-02-07 11:10:40 -05:00
|
|
|
|
2016-08-22 14:16:08 -04:00
|
|
|
def test_lock_with_custom_column_without_default_queries_count
|
|
|
|
t1 = LockWithCustomColumnWithoutDefault.create(title: "title1")
|
|
|
|
|
|
|
|
assert_equal "title1", t1.title
|
|
|
|
assert_equal 0, t1.custom_lock_version
|
|
|
|
|
|
|
|
assert_queries(1) { t1.update(title: "title2") }
|
|
|
|
|
|
|
|
t1.reload
|
|
|
|
assert_equal "title2", t1.title
|
|
|
|
assert_equal 1, t1.custom_lock_version
|
|
|
|
|
|
|
|
t2 = LockWithCustomColumnWithoutDefault.new(title: "title1")
|
|
|
|
|
|
|
|
assert_queries(1) { t2.save! }
|
|
|
|
|
|
|
|
t2.reload
|
|
|
|
assert_equal "title1", t2.title
|
|
|
|
assert_equal 0, t2.custom_lock_version
|
|
|
|
end
|
|
|
|
|
2007-11-16 15:31:24 -05:00
|
|
|
def test_readonly_attributes
|
2016-08-06 12:26:20 -04:00
|
|
|
assert_equal Set.new([ "name" ]), ReadonlyNameShip.readonly_attributes
|
2007-11-16 15:31:24 -05:00
|
|
|
|
2016-08-06 13:37:57 -04:00
|
|
|
s = ReadonlyNameShip.create(name: "unchangeable name")
|
2012-08-21 14:46:37 -04:00
|
|
|
s.reload
|
|
|
|
assert_equal "unchangeable name", s.name
|
2007-11-16 15:31:24 -05:00
|
|
|
|
2013-01-02 11:46:58 -05:00
|
|
|
s.update(name: "changed name")
|
2012-08-21 14:46:37 -04:00
|
|
|
s.reload
|
|
|
|
assert_equal "unchangeable name", s.name
|
2007-11-16 15:31:24 -05:00
|
|
|
end
|
|
|
|
|
2011-01-09 10:52:41 -05:00
|
|
|
def test_quote_table_name
|
|
|
|
ref = references(:michael_magician)
|
|
|
|
ref.favourite = !ref.favourite
|
|
|
|
assert ref.save
|
|
|
|
end
|
|
|
|
|
|
|
|
# Useful for partial updates, don't only update the lock_version if there
|
|
|
|
# is nothing else being updated.
|
|
|
|
def test_update_without_attributes_does_not_only_update_lock_version
|
|
|
|
assert_nothing_raised do
|
2016-08-06 13:37:57 -04:00
|
|
|
p1 = Person.create!(first_name: "anika")
|
2011-01-09 10:52:41 -05:00
|
|
|
lock_version = p1.lock_version
|
|
|
|
p1.save
|
|
|
|
p1.reload
|
|
|
|
assert_equal lock_version, p1.lock_version
|
|
|
|
end
|
|
|
|
end
|
2011-09-09 03:31:54 -04:00
|
|
|
|
Fix conflicts `counter_cache` with `touch: true` by optimistic locking.
```
# create_table :posts do |t|
# t.integer :comments_count, default: 0
# t.integer :lock_version
# t.timestamps
# end
class Post < ApplicationRecord
end
# create_table :comments do |t|
# t.belongs_to :post
# end
class Comment < ApplicationRecord
belongs_to :post, touch: true, counter_cache: true
end
```
Before:
```
post = Post.create!
# => begin transaction
INSERT INTO "posts" ("created_at", "updated_at", "lock_version")
VALUES ("2017-12-11 21:27:11.387397", "2017-12-11 21:27:11.387397", 0)
commit transaction
comment = Comment.create!(post: post)
# => begin transaction
INSERT INTO "comments" ("post_id") VALUES (1)
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) + 1,
"lock_version" = COALESCE("lock_version", 0) + 1 WHERE "posts"."id" = 1
UPDATE "posts" SET "updated_at" = '2017-12-11 21:27:11.398330',
"lock_version" = 1 WHERE "posts"."id" = 1 AND "posts"."lock_version" = 0
rollback transaction
# => ActiveRecord::StaleObjectError: Attempted to touch a stale object: Post.
Comment.take.destroy!
# => begin transaction
DELETE FROM "comments" WHERE "comments"."id" = 1
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) - 1,
"lock_version" = COALESCE("lock_version", 0) + 1 WHERE "posts"."id" = 1
UPDATE "posts" SET "updated_at" = '2017-12-11 21:42:47.785901',
"lock_version" = 1 WHERE "posts"."id" = 1 AND "posts"."lock_version" = 0
rollback transaction
# => ActiveRecord::StaleObjectError: Attempted to touch a stale object: Post.
```
After:
```
post = Post.create!
# => begin transaction
INSERT INTO "posts" ("created_at", "updated_at", "lock_version")
VALUES ("2017-12-11 21:27:11.387397", "2017-12-11 21:27:11.387397", 0)
commit transaction
comment = Comment.create!(post: post)
# => begin transaction
INSERT INTO "comments" ("post_id") VALUES (1)
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) + 1,
"lock_version" = COALESCE("lock_version", 0) + 1,
"updated_at" = '2017-12-11 21:37:09.802642' WHERE "posts"."id" = 1
commit transaction
comment.destroy!
# => begin transaction
DELETE FROM "comments" WHERE "comments"."id" = 1
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) - 1,
"lock_version" = COALESCE("lock_version", 0) + 1,
"updated_at" = '2017-12-11 21:39:02.685520' WHERE "posts"."id" = 1
commit transaction
```
Fixes #31199.
2017-12-11 16:56:07 -05:00
|
|
|
def test_counter_cache_with_touch_and_lock_version
|
|
|
|
car = Car.create!
|
|
|
|
|
|
|
|
assert_equal 0, car.wheels_count
|
|
|
|
assert_equal 0, car.lock_version
|
|
|
|
|
|
|
|
previously_car_updated_at = car.updated_at
|
2017-12-17 14:30:50 -05:00
|
|
|
travel(2.second) do
|
Fix conflicts `counter_cache` with `touch: true` by optimistic locking.
```
# create_table :posts do |t|
# t.integer :comments_count, default: 0
# t.integer :lock_version
# t.timestamps
# end
class Post < ApplicationRecord
end
# create_table :comments do |t|
# t.belongs_to :post
# end
class Comment < ApplicationRecord
belongs_to :post, touch: true, counter_cache: true
end
```
Before:
```
post = Post.create!
# => begin transaction
INSERT INTO "posts" ("created_at", "updated_at", "lock_version")
VALUES ("2017-12-11 21:27:11.387397", "2017-12-11 21:27:11.387397", 0)
commit transaction
comment = Comment.create!(post: post)
# => begin transaction
INSERT INTO "comments" ("post_id") VALUES (1)
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) + 1,
"lock_version" = COALESCE("lock_version", 0) + 1 WHERE "posts"."id" = 1
UPDATE "posts" SET "updated_at" = '2017-12-11 21:27:11.398330',
"lock_version" = 1 WHERE "posts"."id" = 1 AND "posts"."lock_version" = 0
rollback transaction
# => ActiveRecord::StaleObjectError: Attempted to touch a stale object: Post.
Comment.take.destroy!
# => begin transaction
DELETE FROM "comments" WHERE "comments"."id" = 1
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) - 1,
"lock_version" = COALESCE("lock_version", 0) + 1 WHERE "posts"."id" = 1
UPDATE "posts" SET "updated_at" = '2017-12-11 21:42:47.785901',
"lock_version" = 1 WHERE "posts"."id" = 1 AND "posts"."lock_version" = 0
rollback transaction
# => ActiveRecord::StaleObjectError: Attempted to touch a stale object: Post.
```
After:
```
post = Post.create!
# => begin transaction
INSERT INTO "posts" ("created_at", "updated_at", "lock_version")
VALUES ("2017-12-11 21:27:11.387397", "2017-12-11 21:27:11.387397", 0)
commit transaction
comment = Comment.create!(post: post)
# => begin transaction
INSERT INTO "comments" ("post_id") VALUES (1)
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) + 1,
"lock_version" = COALESCE("lock_version", 0) + 1,
"updated_at" = '2017-12-11 21:37:09.802642' WHERE "posts"."id" = 1
commit transaction
comment.destroy!
# => begin transaction
DELETE FROM "comments" WHERE "comments"."id" = 1
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) - 1,
"lock_version" = COALESCE("lock_version", 0) + 1,
"updated_at" = '2017-12-11 21:39:02.685520' WHERE "posts"."id" = 1
commit transaction
```
Fixes #31199.
2017-12-11 16:56:07 -05:00
|
|
|
Wheel.create!(wheelable: car)
|
|
|
|
end
|
|
|
|
|
|
|
|
assert_equal 1, car.reload.wheels_count
|
|
|
|
assert_not_equal previously_car_updated_at, car.updated_at
|
|
|
|
assert_equal 1, car.lock_version
|
|
|
|
|
|
|
|
previously_car_updated_at = car.updated_at
|
2018-02-07 04:32:02 -05:00
|
|
|
travel(1.day) do
|
|
|
|
car.wheels.first.update(size: 42)
|
|
|
|
end
|
Fix conflicts `counter_cache` with `touch: true` by optimistic locking.
```
# create_table :posts do |t|
# t.integer :comments_count, default: 0
# t.integer :lock_version
# t.timestamps
# end
class Post < ApplicationRecord
end
# create_table :comments do |t|
# t.belongs_to :post
# end
class Comment < ApplicationRecord
belongs_to :post, touch: true, counter_cache: true
end
```
Before:
```
post = Post.create!
# => begin transaction
INSERT INTO "posts" ("created_at", "updated_at", "lock_version")
VALUES ("2017-12-11 21:27:11.387397", "2017-12-11 21:27:11.387397", 0)
commit transaction
comment = Comment.create!(post: post)
# => begin transaction
INSERT INTO "comments" ("post_id") VALUES (1)
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) + 1,
"lock_version" = COALESCE("lock_version", 0) + 1 WHERE "posts"."id" = 1
UPDATE "posts" SET "updated_at" = '2017-12-11 21:27:11.398330',
"lock_version" = 1 WHERE "posts"."id" = 1 AND "posts"."lock_version" = 0
rollback transaction
# => ActiveRecord::StaleObjectError: Attempted to touch a stale object: Post.
Comment.take.destroy!
# => begin transaction
DELETE FROM "comments" WHERE "comments"."id" = 1
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) - 1,
"lock_version" = COALESCE("lock_version", 0) + 1 WHERE "posts"."id" = 1
UPDATE "posts" SET "updated_at" = '2017-12-11 21:42:47.785901',
"lock_version" = 1 WHERE "posts"."id" = 1 AND "posts"."lock_version" = 0
rollback transaction
# => ActiveRecord::StaleObjectError: Attempted to touch a stale object: Post.
```
After:
```
post = Post.create!
# => begin transaction
INSERT INTO "posts" ("created_at", "updated_at", "lock_version")
VALUES ("2017-12-11 21:27:11.387397", "2017-12-11 21:27:11.387397", 0)
commit transaction
comment = Comment.create!(post: post)
# => begin transaction
INSERT INTO "comments" ("post_id") VALUES (1)
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) + 1,
"lock_version" = COALESCE("lock_version", 0) + 1,
"updated_at" = '2017-12-11 21:37:09.802642' WHERE "posts"."id" = 1
commit transaction
comment.destroy!
# => begin transaction
DELETE FROM "comments" WHERE "comments"."id" = 1
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) - 1,
"lock_version" = COALESCE("lock_version", 0) + 1,
"updated_at" = '2017-12-11 21:39:02.685520' WHERE "posts"."id" = 1
commit transaction
```
Fixes #31199.
2017-12-11 16:56:07 -05:00
|
|
|
|
|
|
|
assert_equal 1, car.reload.wheels_count
|
|
|
|
assert_not_equal previously_car_updated_at, car.updated_at
|
|
|
|
assert_equal 2, car.lock_version
|
|
|
|
|
|
|
|
previously_car_updated_at = car.updated_at
|
2017-12-17 14:30:50 -05:00
|
|
|
travel(2.second) do
|
Fix conflicts `counter_cache` with `touch: true` by optimistic locking.
```
# create_table :posts do |t|
# t.integer :comments_count, default: 0
# t.integer :lock_version
# t.timestamps
# end
class Post < ApplicationRecord
end
# create_table :comments do |t|
# t.belongs_to :post
# end
class Comment < ApplicationRecord
belongs_to :post, touch: true, counter_cache: true
end
```
Before:
```
post = Post.create!
# => begin transaction
INSERT INTO "posts" ("created_at", "updated_at", "lock_version")
VALUES ("2017-12-11 21:27:11.387397", "2017-12-11 21:27:11.387397", 0)
commit transaction
comment = Comment.create!(post: post)
# => begin transaction
INSERT INTO "comments" ("post_id") VALUES (1)
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) + 1,
"lock_version" = COALESCE("lock_version", 0) + 1 WHERE "posts"."id" = 1
UPDATE "posts" SET "updated_at" = '2017-12-11 21:27:11.398330',
"lock_version" = 1 WHERE "posts"."id" = 1 AND "posts"."lock_version" = 0
rollback transaction
# => ActiveRecord::StaleObjectError: Attempted to touch a stale object: Post.
Comment.take.destroy!
# => begin transaction
DELETE FROM "comments" WHERE "comments"."id" = 1
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) - 1,
"lock_version" = COALESCE("lock_version", 0) + 1 WHERE "posts"."id" = 1
UPDATE "posts" SET "updated_at" = '2017-12-11 21:42:47.785901',
"lock_version" = 1 WHERE "posts"."id" = 1 AND "posts"."lock_version" = 0
rollback transaction
# => ActiveRecord::StaleObjectError: Attempted to touch a stale object: Post.
```
After:
```
post = Post.create!
# => begin transaction
INSERT INTO "posts" ("created_at", "updated_at", "lock_version")
VALUES ("2017-12-11 21:27:11.387397", "2017-12-11 21:27:11.387397", 0)
commit transaction
comment = Comment.create!(post: post)
# => begin transaction
INSERT INTO "comments" ("post_id") VALUES (1)
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) + 1,
"lock_version" = COALESCE("lock_version", 0) + 1,
"updated_at" = '2017-12-11 21:37:09.802642' WHERE "posts"."id" = 1
commit transaction
comment.destroy!
# => begin transaction
DELETE FROM "comments" WHERE "comments"."id" = 1
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) - 1,
"lock_version" = COALESCE("lock_version", 0) + 1,
"updated_at" = '2017-12-11 21:39:02.685520' WHERE "posts"."id" = 1
commit transaction
```
Fixes #31199.
2017-12-11 16:56:07 -05:00
|
|
|
car.wheels.first.destroy!
|
|
|
|
end
|
|
|
|
|
|
|
|
assert_equal 0, car.reload.wheels_count
|
|
|
|
assert_not_equal previously_car_updated_at, car.updated_at
|
|
|
|
assert_equal 3, car.lock_version
|
|
|
|
end
|
|
|
|
|
2015-07-19 17:50:12 -04:00
|
|
|
def test_polymorphic_destroy_with_dependencies_and_lock_version
|
|
|
|
car = Car.create!
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
assert_difference "car.wheels.count" do
|
Fix conflicts `counter_cache` with `touch: true` by optimistic locking.
```
# create_table :posts do |t|
# t.integer :comments_count, default: 0
# t.integer :lock_version
# t.timestamps
# end
class Post < ApplicationRecord
end
# create_table :comments do |t|
# t.belongs_to :post
# end
class Comment < ApplicationRecord
belongs_to :post, touch: true, counter_cache: true
end
```
Before:
```
post = Post.create!
# => begin transaction
INSERT INTO "posts" ("created_at", "updated_at", "lock_version")
VALUES ("2017-12-11 21:27:11.387397", "2017-12-11 21:27:11.387397", 0)
commit transaction
comment = Comment.create!(post: post)
# => begin transaction
INSERT INTO "comments" ("post_id") VALUES (1)
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) + 1,
"lock_version" = COALESCE("lock_version", 0) + 1 WHERE "posts"."id" = 1
UPDATE "posts" SET "updated_at" = '2017-12-11 21:27:11.398330',
"lock_version" = 1 WHERE "posts"."id" = 1 AND "posts"."lock_version" = 0
rollback transaction
# => ActiveRecord::StaleObjectError: Attempted to touch a stale object: Post.
Comment.take.destroy!
# => begin transaction
DELETE FROM "comments" WHERE "comments"."id" = 1
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) - 1,
"lock_version" = COALESCE("lock_version", 0) + 1 WHERE "posts"."id" = 1
UPDATE "posts" SET "updated_at" = '2017-12-11 21:42:47.785901',
"lock_version" = 1 WHERE "posts"."id" = 1 AND "posts"."lock_version" = 0
rollback transaction
# => ActiveRecord::StaleObjectError: Attempted to touch a stale object: Post.
```
After:
```
post = Post.create!
# => begin transaction
INSERT INTO "posts" ("created_at", "updated_at", "lock_version")
VALUES ("2017-12-11 21:27:11.387397", "2017-12-11 21:27:11.387397", 0)
commit transaction
comment = Comment.create!(post: post)
# => begin transaction
INSERT INTO "comments" ("post_id") VALUES (1)
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) + 1,
"lock_version" = COALESCE("lock_version", 0) + 1,
"updated_at" = '2017-12-11 21:37:09.802642' WHERE "posts"."id" = 1
commit transaction
comment.destroy!
# => begin transaction
DELETE FROM "comments" WHERE "comments"."id" = 1
UPDATE "posts" SET "comments_count" = COALESCE("comments_count", 0) - 1,
"lock_version" = COALESCE("lock_version", 0) + 1,
"updated_at" = '2017-12-11 21:39:02.685520' WHERE "posts"."id" = 1
commit transaction
```
Fixes #31199.
2017-12-11 16:56:07 -05:00
|
|
|
car.wheels.create
|
2015-07-19 17:50:12 -04:00
|
|
|
end
|
2016-08-06 12:26:20 -04:00
|
|
|
assert_difference "car.wheels.count", -1 do
|
2015-07-19 17:50:12 -04:00
|
|
|
car.reload.destroy
|
|
|
|
end
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_predicate car, :destroyed?
|
2015-07-19 17:50:12 -04:00
|
|
|
end
|
|
|
|
|
2012-03-07 23:56:23 -05:00
|
|
|
def test_removing_has_and_belongs_to_many_associations_upon_destroy
|
2016-08-06 12:26:20 -04:00
|
|
|
p = RichPerson.create! first_name: "Jon"
|
2012-03-07 23:56:23 -05:00
|
|
|
p.treasures.create!
|
2018-01-25 18:16:57 -05:00
|
|
|
assert_not_empty p.treasures
|
2012-03-07 23:56:23 -05:00
|
|
|
p.destroy
|
2018-01-25 18:16:57 -05:00
|
|
|
assert_empty p.treasures
|
|
|
|
assert_empty RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1")
|
2012-03-07 23:56:23 -05:00
|
|
|
end
|
2014-06-17 11:41:06 -04:00
|
|
|
|
|
|
|
def test_yaml_dumping_with_lock_column
|
|
|
|
t1 = LockWithoutDefault.new
|
|
|
|
t2 = YAML.load(YAML.dump(t1))
|
|
|
|
|
|
|
|
assert_equal t1.attributes, t2.attributes
|
|
|
|
end
|
2011-01-09 10:52:41 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
|
|
|
|
fixtures :people, :legacy_things, :references
|
|
|
|
|
2015-03-10 22:21:19 -04:00
|
|
|
# need to disable transactional tests, because otherwise the sqlite3
|
2011-01-09 10:52:41 -05:00
|
|
|
# adapter (at least) chokes when we try and change the schema in the middle
|
|
|
|
# of a test (see test_increment_counter_*).
|
2015-03-10 22:21:19 -04:00
|
|
|
self.use_transactional_tests = false
|
2011-01-09 10:52:41 -05:00
|
|
|
|
2016-08-06 13:37:57 -04:00
|
|
|
{ lock_version: Person, custom_lock_version: LegacyThing }.each do |name, model|
|
2007-02-07 11:10:40 -05:00
|
|
|
define_method("test_increment_counter_updates_#{name}") do
|
|
|
|
counter_test model, 1 do |id|
|
|
|
|
model.increment_counter :test_count, id
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
define_method("test_decrement_counter_updates_#{name}") do
|
|
|
|
counter_test model, -1 do |id|
|
|
|
|
model.decrement_counter :test_count, id
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
define_method("test_update_counters_updates_#{name}") do
|
|
|
|
counter_test model, 1 do |id|
|
2016-08-06 13:37:57 -04:00
|
|
|
model.update_counters id, test_count: 1
|
2007-02-07 11:10:40 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2010-02-21 22:47:30 -05:00
|
|
|
|
|
|
|
# See Lighthouse ticket #1966
|
|
|
|
def test_destroy_dependents
|
2014-09-06 07:06:40 -04:00
|
|
|
# Establish dependent relationship between Person and PersonalLegacyThing
|
2016-08-06 12:26:20 -04:00
|
|
|
add_counter_column_to(Person, "personal_legacy_things_count")
|
2014-09-06 07:06:40 -04:00
|
|
|
PersonalLegacyThing.reset_column_information
|
2010-02-21 22:47:30 -05:00
|
|
|
|
|
|
|
# Make sure that counter incrementing doesn't cause problems
|
2016-08-06 13:37:57 -04:00
|
|
|
p1 = Person.new(first_name: "fjord")
|
2010-02-21 22:47:30 -05:00
|
|
|
p1.save!
|
2016-08-06 13:37:57 -04:00
|
|
|
t = PersonalLegacyThing.new(person: p1)
|
2010-02-21 22:47:30 -05:00
|
|
|
t.save!
|
|
|
|
p1.reload
|
2014-09-06 07:06:40 -04:00
|
|
|
assert_equal 1, p1.personal_legacy_things_count
|
2010-02-21 22:47:30 -05:00
|
|
|
assert p1.destroy
|
|
|
|
assert_equal true, p1.frozen?
|
|
|
|
assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) }
|
2014-09-06 07:06:40 -04:00
|
|
|
assert_raises(ActiveRecord::RecordNotFound) { PersonalLegacyThing.find(t.id) }
|
2011-12-01 12:36:03 -05:00
|
|
|
ensure
|
2016-08-06 12:26:20 -04:00
|
|
|
remove_counter_column_from(Person, "personal_legacy_things_count")
|
2014-09-06 07:06:40 -04:00
|
|
|
PersonalLegacyThing.reset_column_information
|
2010-02-21 22:47:30 -05:00
|
|
|
end
|
2010-05-08 19:06:05 -04:00
|
|
|
|
2017-04-29 04:21:05 -04:00
|
|
|
def test_destroy_existing_object_with_locking_column_value_null_in_the_database
|
|
|
|
ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')")
|
|
|
|
t1 = LockWithoutDefault.last
|
|
|
|
|
|
|
|
assert_equal 0, t1.lock_version
|
|
|
|
assert_nil t1.lock_version_before_type_cast
|
|
|
|
|
|
|
|
t1.destroy
|
|
|
|
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_predicate t1, :destroyed?
|
2017-04-29 04:21:05 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_destroy_stale_object
|
|
|
|
t1 = LockWithoutDefault.create!(title: "title1")
|
|
|
|
stale_object = LockWithoutDefault.find(t1.id)
|
|
|
|
|
|
|
|
t1.update!(title: "title2")
|
|
|
|
|
|
|
|
assert_raises(ActiveRecord::StaleObjectError) do
|
|
|
|
stale_object.destroy!
|
|
|
|
end
|
|
|
|
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_not_predicate stale_object, :destroyed?
|
2017-04-29 04:21:05 -04:00
|
|
|
end
|
|
|
|
|
2007-02-07 11:10:40 -05:00
|
|
|
private
|
|
|
|
|
2016-10-28 23:05:58 -04:00
|
|
|
def add_counter_column_to(model, col = "test_count")
|
2016-08-06 13:37:57 -04:00
|
|
|
model.connection.add_column model.table_name, col, :integer, null: false, default: 0
|
2007-02-07 11:10:40 -05:00
|
|
|
model.reset_column_information
|
|
|
|
end
|
|
|
|
|
2011-12-01 12:36:03 -05:00
|
|
|
def remove_counter_column_from(model, col = :test_count)
|
|
|
|
model.connection.remove_column model.table_name, col
|
2007-02-07 11:10:40 -05:00
|
|
|
model.reset_column_information
|
|
|
|
end
|
|
|
|
|
|
|
|
def counter_test(model, expected_count)
|
|
|
|
add_counter_column_to(model)
|
2012-04-26 13:32:55 -04:00
|
|
|
object = model.first
|
2007-02-07 11:10:40 -05:00
|
|
|
assert_equal 0, object.test_count
|
|
|
|
assert_equal 0, object.send(model.locking_column)
|
|
|
|
yield object.id
|
|
|
|
object.reload
|
|
|
|
assert_equal expected_count, object.test_count
|
|
|
|
assert_equal 1, object.send(model.locking_column)
|
|
|
|
ensure
|
|
|
|
remove_counter_column_from(model)
|
|
|
|
end
|
2006-06-19 18:48:51 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# TODO: test against the generated SQL since testing locking behavior itself
|
|
|
|
# is so cumbersome. Will deadlock Ruby threads if the underlying db.execute
|
|
|
|
# blocks, so separate script called by Kernel#system is needed.
|
|
|
|
# (See exec vs. async_exec in the PostgreSQL adapter.)
|
2014-05-16 14:05:45 -04:00
|
|
|
unless in_memory_db?
|
2008-01-21 12:20:51 -05:00
|
|
|
class PessimisticLockingTest < ActiveRecord::TestCase
|
2015-03-10 22:21:19 -04:00
|
|
|
self.use_transactional_tests = false
|
2006-12-06 15:28:26 -05:00
|
|
|
fixtures :people, :readers
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2006-07-10 14:24:35 -04:00
|
|
|
def setup
|
2011-01-09 10:52:41 -05:00
|
|
|
Person.connection_pool.clear_reloadable_connections!
|
2006-12-06 15:28:26 -05:00
|
|
|
# Avoid introspection queries during tests.
|
|
|
|
Person.columns; Reader.columns
|
2006-07-10 14:24:35 -04:00
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
|
|
|
# Test typical find.
|
2006-07-10 14:24:35 -04:00
|
|
|
def test_sane_find_with_lock
|
|
|
|
assert_nothing_raised do
|
|
|
|
Person.transaction do
|
2012-04-27 23:55:11 -04:00
|
|
|
Person.lock.find(1)
|
2006-06-19 18:48:51 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
|
|
|
# PostgreSQL protests SELECT ... FOR UPDATE on an outer join.
|
|
|
|
unless current_adapter?(:PostgreSQLAdapter)
|
|
|
|
# Test locked eager find.
|
|
|
|
def test_eager_find_with_lock
|
|
|
|
assert_nothing_raised do
|
|
|
|
Person.transaction do
|
2012-04-27 23:55:11 -04:00
|
|
|
Person.includes(:readers).lock.find(1)
|
2006-12-05 17:07:55 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-08-23 16:08:34 -04:00
|
|
|
def test_lock_does_not_raise_when_the_object_is_not_dirty
|
|
|
|
person = Person.find 1
|
2006-07-10 14:24:35 -04:00
|
|
|
assert_nothing_raised do
|
2017-08-23 16:08:34 -04:00
|
|
|
person.lock!
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_lock_raises_when_the_record_is_dirty
|
|
|
|
person = Person.find 1
|
|
|
|
person.first_name = "fooman"
|
|
|
|
assert_raises(RuntimeError) do
|
|
|
|
person.lock!
|
2006-07-10 14:24:35 -04:00
|
|
|
end
|
2006-06-19 18:48:51 -04:00
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2017-10-23 13:10:51 -04:00
|
|
|
def test_locking_in_after_save_callback
|
|
|
|
assert_nothing_raised do
|
|
|
|
frog = ::Frog.create(name: "Old Frog")
|
|
|
|
frog.name = "New Frog"
|
|
|
|
assert_not_deprecated do
|
|
|
|
frog.save!
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-01-18 17:03:55 -05:00
|
|
|
def test_with_lock_commits_transaction
|
|
|
|
person = Person.find 1
|
|
|
|
person.with_lock do
|
2016-08-06 12:26:20 -04:00
|
|
|
person.first_name = "fooman"
|
2012-01-18 17:03:55 -05:00
|
|
|
person.save!
|
|
|
|
end
|
2016-08-06 12:26:20 -04:00
|
|
|
assert_equal "fooman", person.reload.first_name
|
2012-01-18 17:03:55 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def test_with_lock_rolls_back_transaction
|
|
|
|
person = Person.find 1
|
|
|
|
old = person.first_name
|
|
|
|
person.with_lock do
|
2016-08-06 12:26:20 -04:00
|
|
|
person.first_name = "fooman"
|
2012-01-18 17:03:55 -05:00
|
|
|
person.save!
|
2016-08-06 12:26:20 -04:00
|
|
|
raise "oops"
|
2012-01-18 17:03:55 -05:00
|
|
|
end rescue nil
|
|
|
|
assert_equal old, person.reload.first_name
|
|
|
|
end
|
|
|
|
|
2014-01-28 23:56:20 -05:00
|
|
|
if current_adapter?(:PostgreSQLAdapter)
|
|
|
|
def test_lock_sending_custom_lock_statement
|
|
|
|
Person.transaction do
|
|
|
|
person = Person.find(1)
|
2016-02-28 22:22:08 -05:00
|
|
|
assert_sql(/LIMIT \$?\d FOR SHARE NOWAIT/) do
|
2016-08-06 12:26:20 -04:00
|
|
|
person.lock!("FOR SHARE NOWAIT")
|
2014-01-28 23:56:20 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-11-09 02:53:55 -05:00
|
|
|
def test_no_locks_no_wait
|
|
|
|
first, second = duel { Person.find 1 }
|
|
|
|
assert first.end > second.end
|
|
|
|
end
|
2016-08-03 20:36:30 -04:00
|
|
|
|
2017-11-09 02:53:55 -05:00
|
|
|
private
|
2016-08-06 13:55:02 -04:00
|
|
|
def duel(zzz = 5)
|
|
|
|
t0, t1, t2, t3 = nil, nil, nil, nil
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
a = Thread.new do
|
|
|
|
t0 = Time.now
|
|
|
|
Person.transaction do
|
|
|
|
yield
|
|
|
|
sleep zzz # block thread 2 for zzz seconds
|
2006-07-10 14:24:35 -04:00
|
|
|
end
|
2016-08-06 13:55:02 -04:00
|
|
|
t1 = Time.now
|
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2016-08-06 13:55:02 -04:00
|
|
|
b = Thread.new do
|
|
|
|
sleep zzz / 2.0 # ensure thread 1 tx starts first
|
|
|
|
t2 = Time.now
|
|
|
|
Person.transaction { yield }
|
|
|
|
t3 = Time.now
|
2006-06-19 18:48:51 -04:00
|
|
|
end
|
2016-08-06 13:55:02 -04:00
|
|
|
|
|
|
|
a.join
|
|
|
|
b.join
|
|
|
|
|
|
|
|
assert t1 > t0 + zzz
|
|
|
|
assert t2 > t0
|
|
|
|
assert t3 > t2
|
|
|
|
[t0.to_f..t1.to_f, t2.to_f..t3.to_f]
|
|
|
|
end
|
2006-06-19 18:48:51 -04:00
|
|
|
end
|
2005-06-10 10:58:02 -04:00
|
|
|
end
|