Files
rails/activerecord/test/cases/locking_test.rb
Alfred Wong 33e1604b3e Specified column type for quote_value
When calling quote_value the underlying connection sometimes requires
more information about the column to properly return the correct quoted
value.

I ran into this issue when using optimistic locking in JRuby and the
activerecord-jdbcmssql-adapter. In SQLSever 2000, we aren't allowed to
insert a integer into a NVARCHAR column type so we need to format it as
N'3' if we want to insert into the NVARCHAR type. Unfortuantely, without
the column type being passed the connection adapter cannot properly return
the correct quote value because it doesn't know to return N'3' or '3'.

This patch is fairly straight forward where it just passes in the column
type into the quote_value, as it already has the ability to take in the column,
so it can properly handle at the connection level.

I've added the tests required to make sure that the quote_value method
is being passed the column type so that the underlying connection can
determine how to quote the value.

Conflicts:
	activerecord/CHANGELOG.md
	activerecord/lib/active_record/locking/optimistic.rb
2013-07-24 13:18:21 -03:00

509 lines
14 KiB
Ruby

require 'thread'
require 'mocha/setup'
require "cases/helper"
require 'models/person'
require 'models/job'
require 'models/reader'
require 'models/ship'
require 'models/legacy_thing'
require 'models/reference'
require 'models/string_key_object'
require 'models/treasure'
class LockWithoutDefault < ActiveRecord::Base; end
class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
self.table_name = :lock_without_defaults_cust
self.locking_column = :custom_lock_version
end
class ReadonlyNameShip < Ship
attr_readonly :name
end
class OptimisticLockingTest < ActiveRecord::TestCase
fixtures :people, :legacy_things, :references, :string_key_objects, :peoples_treasures
def test_quote_value_passed_lock_col
p1 = Person.find(1)
assert_equal 0, p1.lock_version
Person.expects(:quote_value).with(0, Person.columns_hash[Person.locking_column]).returns('0').once
p1.first_name = 'anika2'
p1.save!
assert_equal 1, p1.lock_version
end
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
s1.name = 'updated record'
s1.save!
assert_equal 1, s1.lock_version
assert_equal 0, s2.lock_version
s2.name = 'doubly updated record'
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
s1.name = 'updated record'
s1.save!
assert_equal 1, s1.lock_version
assert_equal 0, s2.lock_version
assert_raise(ActiveRecord::StaleObjectError) { s2.destroy }
assert s1.destroy
assert s1.frozen?
assert s1.destroyed?
assert_raises(ActiveRecord::RecordNotFound) { StringKeyObject.find("record1") }
end
def test_lock_existing
p1 = Person.find(1)
p2 = Person.find(1)
assert_equal 0, p1.lock_version
assert_equal 0, p2.lock_version
p1.first_name = 'stu'
p1.save!
assert_equal 1, p1.lock_version
assert_equal 0, p2.lock_version
p2.first_name = 'sue'
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
# 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
p1.first_name = 'stu'
p1.save!
assert_equal 1, p1.lock_version
assert_equal 0, p2.lock_version
assert_raises(ActiveRecord::StaleObjectError) { p2.destroy }
assert p1.destroy
assert p1.frozen?
assert p1.destroyed?
assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) }
end
def test_lock_repeating
p1 = Person.find(1)
p2 = Person.find(1)
assert_equal 0, p1.lock_version
assert_equal 0, p2.lock_version
p1.first_name = 'stu'
p1.save!
assert_equal 1, p1.lock_version
assert_equal 0, p2.lock_version
p2.first_name = 'sue'
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
p2.first_name = 'sue2'
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
def test_lock_new
p1 = Person.new(:first_name => 'anika')
assert_equal 0, p1.lock_version
p1.first_name = 'anika2'
p1.save!
p2 = Person.find(p1.id)
assert_equal 0, p1.lock_version
assert_equal 0, p2.lock_version
p1.first_name = 'anika3'
p1.save!
assert_equal 1, p1.lock_version
assert_equal 0, p2.lock_version
p2.first_name = 'sue'
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
def test_lock_exception_record
p1 = Person.new(:first_name => 'mira')
assert_equal 0, p1.lock_version
p1.first_name = 'mira2'
p1.save!
p2 = Person.find(p1.id)
assert_equal 0, p1.lock_version
assert_equal 0, p2.lock_version
p1.first_name = 'mira3'
p1.save!
p2.first_name = 'sue'
error = assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
assert_equal(error.record.object_id, p2.object_id)
end
def test_lock_new_with_nil
p1 = Person.new(:first_name => 'anika')
p1.save!
p1.lock_version = nil # simulate bad fixture or column with no default
p1.save!
assert_equal 1, p1.lock_version
end
def test_touch_existing_lock
p1 = Person.find(1)
assert_equal 0, p1.lock_version
p1.touch
assert_equal 1, p1.lock_version
end
def test_lock_column_name_existing
t1 = LegacyThing.find(1)
t2 = LegacyThing.find(1)
assert_equal 0, t1.version
assert_equal 0, t2.version
t1.tps_report_number = 700
t1.save!
assert_equal 1, t1.version
assert_equal 0, t2.version
t2.tps_report_number = 800
assert_raise(ActiveRecord::StaleObjectError) { t2.save! }
end
def test_lock_column_is_mass_assignable
p1 = Person.create(:first_name => 'bianca')
assert_equal 0, p1.lock_version
assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
p1.first_name = 'bianca2'
p1.save!
assert_equal 1, p1.lock_version
assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
end
def test_lock_without_default_sets_version_to_zero
t1 = LockWithoutDefault.new
assert_equal 0, t1.lock_version
end
def test_lock_with_custom_column_without_default_sets_version_to_zero
t1 = LockWithCustomColumnWithoutDefault.new
assert_equal 0, t1.custom_lock_version
end
def test_readonly_attributes
assert_equal Set.new([ 'name' ]), ReadonlyNameShip.readonly_attributes
s = ReadonlyNameShip.create(:name => "unchangeable name")
s.reload
assert_equal "unchangeable name", s.name
s.update_attributes(:name => "changed name")
s.reload
assert_equal "unchangeable name", s.name
end
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
p1 = Person.create!(:first_name => 'anika')
lock_version = p1.lock_version
p1.save
p1.reload
assert_equal lock_version, p1.lock_version
end
end
end
class SetLockingColumnTest < ActiveRecord::TestCase
def test_set_set_locking_column_with_value
k = Class.new( ActiveRecord::Base )
k.locking_column = "foo"
assert_equal "foo", k.locking_column
assert_deprecated do
k.set_locking_column "bar"
end
assert_equal "bar", k.locking_column
end
def test_set_locking_column_with_block
k = Class.new( ActiveRecord::Base )
k.locking_column = 'foo'
assert_deprecated do
k.set_locking_column do
"lock_" + ActiveSupport::Deprecation.silence { original_locking_column }
end
end
assert_equal "lock_foo", k.locking_column
end
def test_original_locking_column
k = Class.new(ActiveRecord::Base)
k.locking_column = "bar"
assert_deprecated do
assert_equal ActiveRecord::Locking::Optimistic::ClassMethods::DEFAULT_LOCKING_COLUMN, k.original_locking_column
end
k = Class.new(ActiveRecord::Base)
k.locking_column = "omg"
k.locking_column = "wtf"
assert_deprecated do
assert_equal "omg", k.original_locking_column
end
end
def test_removing_has_and_belongs_to_many_associations_upon_destroy
p = RichPerson.create! :first_name => 'Jon'
p.treasures.create!
assert !p.treasures.empty?
p.destroy
assert p.treasures.empty?
assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty?
end
end
class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
fixtures :people, :legacy_things, :references
# need to disable transactional fixtures, because otherwise the sqlite3
# adapter (at least) chokes when we try and change the schema in the middle
# of a test (see test_increment_counter_*).
self.use_transactional_fixtures = false
{ :lock_version => Person, :custom_lock_version => LegacyThing }.each do |name, model|
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|
model.update_counters id, :test_count => 1
end
end
end
# See Lighthouse ticket #1966
def test_destroy_dependents
# Establish dependent relationship between People and LegacyThing
add_counter_column_to(Person, 'legacy_things_count')
LegacyThing.connection.add_column LegacyThing.table_name, 'person_id', :integer
LegacyThing.reset_column_information
LegacyThing.class_eval do
belongs_to :person, :counter_cache => true
end
Person.class_eval do
has_many :legacy_things, :dependent => :destroy
end
# Make sure that counter incrementing doesn't cause problems
p1 = Person.new(:first_name => 'fjord')
p1.save!
t = LegacyThing.new(:person => p1)
t.save!
p1.reload
assert_equal 1, p1.legacy_things_count
assert p1.destroy
assert_equal true, p1.frozen?
assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) }
assert_raises(ActiveRecord::RecordNotFound) { LegacyThing.find(t.id) }
ensure
remove_counter_column_from(Person, 'legacy_things_count')
end
private
def add_counter_column_to(model, col='test_count')
model.connection.add_column model.table_name, col, :integer, :null => false, :default => 0
model.reset_column_information
# OpenBase does not set a value to existing rows when adding a not null default column
model.update_all(col => 0) if current_adapter?(:OpenBaseAdapter)
end
def remove_counter_column_from(model, col = :test_count)
model.connection.remove_column model.table_name, col
model.reset_column_information
end
def counter_test(model, expected_count)
add_counter_column_to(model)
object = model.find(:first)
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
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.)
# TODO: The Sybase, and OpenBase adapters currently have no support for pessimistic locking
unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db?
class PessimisticLockingTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
fixtures :people, :readers
def setup
Person.connection_pool.clear_reloadable_connections!
# Avoid introspection queries during tests.
Person.columns; Reader.columns
end
# Test typical find.
def test_sane_find_with_lock
assert_nothing_raised do
Person.transaction do
Person.find 1, :lock => true
end
end
end
# Test scoped lock.
def test_sane_find_with_scoped_lock
assert_nothing_raised do
Person.transaction do
Person.send(:with_scope, :find => { :lock => true }) do
Person.find 1
end
end
end
end
# 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
Person.find 1, :include => :readers, :lock => true
end
end
end
end
# Locking a record reloads it.
def test_sane_lock_method
assert_nothing_raised do
Person.transaction do
person = Person.find 1
old, person.first_name = person.first_name, 'fooman'
person.lock!
assert_equal old, person.first_name
end
end
end
def test_with_lock_commits_transaction
person = Person.find 1
person.with_lock do
person.first_name = 'fooman'
person.save!
end
assert_equal 'fooman', person.reload.first_name
end
def test_with_lock_rolls_back_transaction
person = Person.find 1
old = person.first_name
person.with_lock do
person.first_name = 'fooman'
person.save!
raise 'oops'
end rescue nil
assert_equal old, person.reload.first_name
end
if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
def test_no_locks_no_wait
first, second = duel { Person.find 1 }
assert first.end > second.end
end
# Hit by ruby deadlock detection since connection checkout is mutexed.
if RUBY_VERSION < '1.9.0'
def test_second_lock_waits
assert [0.2, 1, 5].any? { |zzz|
first, second = duel(zzz) { Person.find 1, :lock => true }
second.end > first.end
}
end
end
protected
def duel(zzz = 5)
t0, t1, t2, t3 = nil, nil, nil, nil
a = Thread.new do
t0 = Time.now
Person.transaction do
yield
sleep zzz # block thread 2 for zzz seconds
end
t1 = Time.now
end
b = Thread.new do
sleep zzz / 2.0 # ensure thread 1 tx starts first
t2 = Time.now
Person.transaction { yield }
t3 = Time.now
end
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
end
end
end