Skip to content

Commit f165a9d

Browse files
h-lamemetaskills
authored andcommitted
Supply run_with_isolation_level method to allow running queries at various isolation levels (e.g. to go around other processes locking tables).
1 parent b91ec6f commit f165a9d

File tree

2 files changed

+93
-0
lines changed

2 files changed

+93
-0
lines changed

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,37 @@ def finish_statement_handle(handle)
325325

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

328+
# Returns the SET options active for the current connection.
329+
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}"
347+
end
348+
349+
initial_isolation_level = user_options[:"isolation level"] || "READ COMMITTED"
350+
execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}"
351+
begin
352+
result = yield
353+
return result
354+
ensure
355+
execute "SET TRANSACTION ISOLATION LEVEL #{initial_isolation_level}"
356+
end
357+
end
358+
328359
def select_rows(sql, name = nil)
329360
raw_select(sql,name).last
330361
end

test/cases/adapter_test_sqlserver.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,68 @@ def setup
376376

377377
context 'For DatabaseStatements' do
378378

379+
context "finding out what user_options are available" do
380+
should "run the database consistency checker useroptions command" do
381+
@connection.expects(:select_rows).with(regexp_matches(/^dbcc\s+useroptions$/i)).returns []
382+
@connection.user_options
383+
end
384+
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
391+
end
392+
end
393+
394+
context "altering isolation levels" do
395+
should "barf if the requested isolation level is not valid" do
396+
@connection.class::VALID_ISOLATION_LEVELS.expects(:include?).returns false
397+
assert_raise(ArgumentError) do
398+
@connection.run_with_isolation_level 'something' do; end
399+
end
400+
end
401+
402+
context "with a valid isolation level" do
403+
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)
407+
end
408+
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)
415+
end
416+
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))
421+
422+
@connection.run_with_isolation_level 'new_isolation_level' do
423+
@yieldy.become(:yielded)
424+
end
425+
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))
430+
431+
assert_raise(RuntimeError) do
432+
@connection.run_with_isolation_level 'new_isolation_level' do
433+
@yieldy.become(:yielded)
434+
raise "a problem"
435+
end
436+
end
437+
end
438+
end
439+
end
440+
379441
end
380442

381443
context 'For SchemaStatements' do

0 commit comments

Comments
 (0)