Skip to content

Commit 2ab7ca6

Browse files
committed
Provide support for running queries at different isolation levels using #run_with_isolation_level method that can take a block or not. Also implement a #user_options method that reflects on the current user session values. Resolves #20 [Murray Steele]
1 parent f165a9d commit 2ab7ca6

File tree

3 files changed

+65
-58
lines changed

3 files changed

+65
-58
lines changed

CHANGELOG

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11

22
MASTER
33

4-
*
4+
* Provide support for running queries at different isolation levels using #run_with_isolation_level method
5+
that can take a block or not. Also implement a #user_options method that reflects on the current user
6+
session values. Resolves #20 [Murray Steele]
57

68

79
* 2.2.15 * (March 23rd, 2009)

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -325,35 +325,28 @@ def finish_statement_handle(handle)
325325

326326
# DATABASE STATEMENTS ======================================#
327327

328-
# Returns the SET options active for the current connection.
329328
def user_options
330-
values = {}
331-
select_rows("dbcc useroptions").each {|field| values.merge!(field[0].to_sym => field[1])}
332-
values
333-
end
334-
335-
VALID_ISOLATION_LEVELS = ["READ UNCOMMITTED", "READ COMMITTED", "REPEATABLE READ", "SNAPSHOT", "SERIALIZABLE"]
336-
337-
# Runs a block with a given isolation level.
338-
# Supported isolation levels include
339-
# * <tt>"READ UNCOMMITTED"</tt>
340-
# * <tt>"READ COMMITTED"</tt>
341-
# * <tt>"REPEATABLE READ"</tt>
342-
# * <tt>"SERIALIZABLE"</tt>
343-
# * <tt>"SNAPSHOT"</tt>
344-
def run_with_isolation_level(isolation_level, &block)
345-
if !VALID_ISOLATION_LEVELS.include?(isolation_level.upcase)
346-
raise ArgumentError, "#{isolation_level} not a supported isolation level. Supported isolation levels are #{VALID_ISOLATION_LEVELS.to_sentence}"
329+
info_schema_query do
330+
select_rows("dbcc useroptions").inject(HashWithIndifferentAccess.new) do |values,row|
331+
set_option = row[0].gsub(/\s+/,'_')
332+
user_value = row[1]
333+
values[set_option] = user_value
334+
values
335+
end
347336
end
348-
349-
initial_isolation_level = user_options[:"isolation level"] || "READ COMMITTED"
350-
execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}"
337+
end
338+
339+
VALID_ISOLATION_LEVELS = ["READ COMMITTED", "READ UNCOMMITTED", "REPEATABLE READ", "SERIALIZABLE", "SNAPSHOT"]
340+
341+
def run_with_isolation_level(isolation_level)
342+
raise ArgumentError, "Invalid isolation level, #{isolation_level}. Supported levels include #{VALID_ISOLATION_LEVELS.to_sentence}." if !VALID_ISOLATION_LEVELS.include?(isolation_level.upcase)
343+
initial_isolation_level = user_options[:isolation_level] || "READ COMMITTED"
344+
do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}"
351345
begin
352-
result = yield
353-
return result
346+
yield
354347
ensure
355-
execute "SET TRANSACTION ISOLATION LEVEL #{initial_isolation_level}"
356-
end
348+
do_execute "SET TRANSACTION ISOLATION LEVEL #{initial_isolation_level}"
349+
end if block_given?
357350
end
358351

359352
def select_rows(sql, name = nil)

test/cases/adapter_test_sqlserver.rb

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
require 'models/subscriber'
66

77
class AdapterTestSqlserver < ActiveRecord::TestCase
8+
9+
fixtures :tasks
810

911
def setup
1012
@connection = ActiveRecord::Base.connection
@@ -377,65 +379,75 @@ def setup
377379
context 'For DatabaseStatements' do
378380

379381
context "finding out what user_options are available" do
382+
380383
should "run the database consistency checker useroptions command" do
381384
@connection.expects(:select_rows).with(regexp_matches(/^dbcc\s+useroptions$/i)).returns []
382385
@connection.user_options
383386
end
384387

385-
should "return a symbolized hash of the results" do
386-
@connection.expects(:select_rows).with(regexp_matches(/^dbcc\s+useroptions$/i)).returns [['some', 'thing'], ['an', 'other thing']]
387-
res = @connection.user_options
388-
assert_equal 'thing', res[:some]
389-
assert_equal 'other thing', res[:an]
390-
assert_equal 2, res.keys.size
388+
should "return a underscored key hash with indifferent access of the results" do
389+
@connection.expects(:select_rows).with(regexp_matches(/^dbcc\s+useroptions$/i)).returns [['some', 'thing'], ['isolation level', 'read uncommitted']]
390+
uo = @connection.user_options
391+
assert_equal 2, uo.keys.size
392+
assert_equal 'thing', uo['some']
393+
assert_equal 'thing', uo[:some]
394+
assert_equal 'read uncommitted', uo['isolation_level']
395+
assert_equal 'read uncommitted', uo[:isolation_level]
391396
end
397+
392398
end
393399

394400
context "altering isolation levels" do
401+
395402
should "barf if the requested isolation level is not valid" do
396-
@connection.class::VALID_ISOLATION_LEVELS.expects(:include?).returns false
397403
assert_raise(ArgumentError) do
398-
@connection.run_with_isolation_level 'something' do; end
404+
@connection.run_with_isolation_level 'INVALID ISOLATION LEVEL' do; end
399405
end
400406
end
401407

402408
context "with a valid isolation level" do
409+
403410
setup do
404-
@connection.class::VALID_ISOLATION_LEVELS.expects(:include?).returns true
405-
@connection.stubs(:user_options).returns({:"isolation level" => "something"})
406-
@yieldy = states('yield').starts_as(:not_yielded)
411+
@t1 = tasks(:first_task)
412+
@t2 = tasks(:another_task)
413+
assert @t1, 'Tasks :first_task should be in AR fixtures'
414+
assert @t2, 'Tasks :another_task should be in AR fixtures'
415+
good_isolation_level = @connection.user_options[:isolation_level].blank? || @connection.user_options[:isolation_level] =~ /read committed/i
416+
assert good_isolation_level, "User isolation level is not at a happy starting place: #{@connection.user_options[:isolation_level].inspect}"
407417
end
408418

409-
should "set the isolation level to that supplied before calling the supplied block" do
410-
@connection.expects(:execute).with(regexp_matches(/set transaction isolation level new_isolation_level/i)).when(@yieldy.is(:not_yielded))
411-
@connection.stubs(:execute).when(@yieldy.is(:yielded))
412-
413-
@connection.run_with_isolation_level 'new_isolation_level' do
414-
@yieldy.become(:yielded)
419+
should 'allow #run_with_isolation_level to not take a block to set it' do
420+
begin
421+
@connection.run_with_isolation_level 'READ UNCOMMITTED'
422+
assert_match %r|read uncommitted|i, @connection.user_options[:isolation_level]
423+
ensure
424+
@connection.run_with_isolation_level 'READ COMMITTED'
415425
end
416426
end
417-
418-
should "set the isolation level back to the original after calling the supplied block" do
419-
@connection.expects(:execute).with(regexp_matches(/set transaction isolation level something/i)).when(@yieldy.is(:yielded))
420-
@connection.stubs(:execute).when(@yieldy.is(:not_yielded))
421427

422-
@connection.run_with_isolation_level 'new_isolation_level' do
423-
@yieldy.become(:yielded)
424-
end
428+
should 'return block value using #run_with_isolation_level' do
429+
assert_same_elements Task.find(:all), @connection.run_with_isolation_level('READ UNCOMMITTED') { Task.find(:all) }
425430
end
426-
427-
should "set the isolation level back to the original after calling the supplied block even when the block raises an exception" do
428-
@connection.expects(:execute).with(regexp_matches(/set transaction isolation level something/i)).when(@yieldy.is(:yielded))
429-
@connection.stubs(:execute).when(@yieldy.is(:not_yielded))
430431

431-
assert_raise(RuntimeError) do
432-
@connection.run_with_isolation_level 'new_isolation_level' do
433-
@yieldy.become(:yielded)
434-
raise "a problem"
432+
should 'pass a read uncommitted isolation level test' do
433+
assert_nil @t2.starting, 'Fixture should have this empty.'
434+
begin
435+
Task.transaction do
436+
@t2.starting = Time.now
437+
@t2.save
438+
@dirty_t2 = @connection.run_with_isolation_level('READ UNCOMMITTED') { Task.find(@t2.id) }
439+
raise ActiveRecord::ActiveRecordError
435440
end
441+
rescue
442+
'Do Nothing'
436443
end
444+
assert @dirty_t2, 'Should have a Task record from within block above.'
445+
assert @dirty_t2.starting, 'Should have a dirty date.'
446+
assert_nil Task.find(@t2.id).starting, 'Should be nil again from botched transaction above.'
437447
end
448+
438449
end
450+
439451
end
440452

441453
end

0 commit comments

Comments
 (0)