Skip to content

Commit d885a9e

Browse files
committed
Use ActiveRecord tranasaction interface vs our own run_with_isolation_level.
1 parent 75f7ef0 commit d885a9e

File tree

7 files changed

+112
-67
lines changed

7 files changed

+112
-67
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* Follow Rails convention and remove varying character default limits.
2020
* The `cs_equality_operator` is now s class configuration property only.
2121
* The `with_identity_insert_enabled(table_name)` is now public.
22+
* Use ActiveRecord tranasaction interface vs our own `run_with_isolation_level`.
2223

2324
#### Deprecated
2425

lib/active_record/connection_adapters/sqlserver/database_statements.rb

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ def begin_db_transaction
5252
do_execute 'BEGIN TRANSACTION'
5353
end
5454

55+
def transaction_isolation_levels
56+
super.merge snapshot: "SNAPSHOT"
57+
end
58+
59+
def begin_isolated_db_transaction(isolation)
60+
set_transaction_isolation_level transaction_isolation_levels.fetch(isolation)
61+
begin_db_transaction
62+
end
63+
64+
def set_transaction_isolation_level(isolation_level)
65+
do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}"
66+
begin_db_transaction
67+
end
68+
5569
def commit_db_transaction
5670
do_execute 'COMMIT TRANSACTION'
5771
end
@@ -175,19 +189,6 @@ def user_options_language
175189
end
176190
end
177191

178-
def run_with_isolation_level(isolation_level)
179-
unless valid_isolation_levels.include?(isolation_level.upcase)
180-
raise ArgumentError, "Invalid isolation level, #{isolation_level}. Supported levels include #{valid_isolation_levels.to_sentence}."
181-
end
182-
initial_isolation_level = user_options_isolation_level || 'READ COMMITTED'
183-
do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}"
184-
begin
185-
yield
186-
ensure
187-
do_execute "SET TRANSACTION ISOLATION LEVEL #{initial_isolation_level}"
188-
end if block_given?
189-
end
190-
191192
def newid_function
192193
select_value 'SELECT NEWID()'
193194
end
@@ -300,10 +301,6 @@ def set_identity_insert(table_name, enable = true)
300301
raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
301302
end
302303

303-
def valid_isolation_levels
304-
['READ COMMITTED', 'READ UNCOMMITTED', 'REPEATABLE READ', 'SERIALIZABLE', 'SNAPSHOT']
305-
end
306-
307304
# === SQLServer Specific (Executing) ============================ #
308305

309306
def do_execute(sql, name = 'SQL')
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
require 'active_record/connection_adapters/abstract/transaction'
2+
3+
module ActiveRecord
4+
module ConnectionAdapters
5+
6+
module SQLServerTransaction
7+
8+
private
9+
10+
def sqlserver?
11+
connection.respond_to?(:sqlserver?) && connection.sqlserver?
12+
end
13+
14+
def current_isolation_level
15+
return unless sqlserver?
16+
level = connection.user_options_isolation_level
17+
level.blank? ? 'READ COMMITTED' : level.upcase
18+
end
19+
20+
end
21+
22+
Transaction.include SQLServerTransaction
23+
24+
module SQLServerRealTransaction
25+
26+
attr_reader :starting_isolation_level
27+
28+
def initialize(connection, options)
29+
@connection = connection
30+
@starting_isolation_level = current_isolation_level if options[:isolation]
31+
super
32+
end
33+
34+
def commit
35+
super
36+
reset_starting_isolation_level
37+
end
38+
39+
private
40+
41+
def reset_starting_isolation_level
42+
if sqlserver? && starting_isolation_level
43+
connection.set_transaction_isolation_level(starting_isolation_level)
44+
end
45+
end
46+
47+
end
48+
49+
RealTransaction.include SQLServerRealTransaction
50+
51+
end
52+
end

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require 'active_record/connection_adapters/sqlserver/type'
1111
require 'active_record/connection_adapters/sqlserver/database_limits'
1212
require 'active_record/connection_adapters/sqlserver/database_statements'
13+
require 'active_record/connection_adapters/sqlserver/transaction'
1314
require 'active_record/connection_adapters/sqlserver/errors'
1415
require 'active_record/connection_adapters/sqlserver/schema_cache'
1516
require 'active_record/connection_adapters/sqlserver/schema_creation'
@@ -102,6 +103,10 @@ def supports_explain?
102103
true
103104
end
104105

106+
def supports_transaction_isolation?
107+
true
108+
end
109+
105110
def disable_referential_integrity
106111
do_execute "EXEC sp_MSforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'"
107112
yield

test/cases/adapter_test_sqlserver.rb

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -254,56 +254,6 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
254254
assert_equal 'read committed', user_options[:isolation_level]
255255
end
256256

257-
describe '#run_with_isolation_level' do
258-
259-
let(:task1) { tasks(:first_task) }
260-
let(:task2) { tasks(:another_task) }
261-
262-
before do
263-
assert task1, 'Tasks :first_task should be in AR fixtures'
264-
assert task2, 'Tasks :another_task should be in AR fixtures'
265-
good_isolation_level = connection.user_options_isolation_level.blank? || connection.user_options_isolation_level =~ /read committed/i
266-
assert good_isolation_level, "User isolation level is not at a happy starting place: #{connection.user_options_isolation_level.inspect}"
267-
end
268-
269-
it "barf if the requested isolation level is not valid" do
270-
assert_raise(ArgumentError) do
271-
connection.run_with_isolation_level 'INVALID ISOLATION LEVEL' do; end
272-
end
273-
end
274-
275-
it 'allow #run_with_isolation_level to not take a block to set it' do
276-
begin
277-
connection.run_with_isolation_level 'READ UNCOMMITTED'
278-
assert_match %r|read uncommitted|i, connection.user_options_isolation_level
279-
ensure
280-
connection.run_with_isolation_level 'READ COMMITTED'
281-
end
282-
end
283-
284-
it 'return block value using #run_with_isolation_level' do
285-
assert_equal Task.all.sort, connection.run_with_isolation_level('READ UNCOMMITTED') { Task.all.sort }
286-
end
287-
288-
it 'pass a read uncommitted isolation level test' do
289-
assert_nil task2.starting, 'Fixture should have this empty.'
290-
begin
291-
Task.transaction do
292-
task2.starting = Time.now
293-
task2.save
294-
@dirty_t2 = connection.run_with_isolation_level('READ UNCOMMITTED') { Task.find(task2.id) }
295-
raise ActiveRecord::ActiveRecordError
296-
end
297-
rescue
298-
'Do Nothing'
299-
end
300-
assert @dirty_t2, 'Should have a Task record from within block above.'
301-
assert @dirty_t2.starting, 'Should have a dirty date.'
302-
assert_nil Task.find(task2.id).starting, 'Should be nil again from botched transaction above.'
303-
end
304-
305-
end
306-
307257
end
308258

309259
describe 'schema statements' do

test/cases/coerced_tests.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,29 @@ def self.search(term)
331331

332332

333333

334+
335+
class TransactionIsolationTest < ActiveRecord::TestCase
336+
337+
# SQL Server will lock the table for counts even when both
338+
# connections are `READ COMMITTED`. So we bypass with `READPAST`.
339+
coerce_tests! %r{read committed}
340+
test "read committed coerced" do
341+
Tag.transaction(isolation: :read_committed) do
342+
assert_equal 0, Tag.count
343+
Tag2.transaction do
344+
Tag2.create
345+
assert_equal 0, Tag.lock('WITH(READPAST)').count
346+
end
347+
end
348+
assert_equal 1, Tag.count
349+
end
350+
351+
# I really need some help understanding this one.
352+
coerce_tests! %r{repeatable read}
353+
354+
end
355+
356+
334357
require 'models/post'
335358
module ActiveRecord
336359
class WhereChainTest < ActiveRecord::TestCase

test/cases/transaction_test_sqlserver.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,23 @@ class TransactionTestSQLServer < ActiveRecord::TestCase
3030
end
3131
end
3232

33+
it 'can use an isolation level and reverts back to starting isolation level' do
34+
begin
35+
in_level = nil
36+
begin_level = connection.user_options_isolation_level
37+
begin_level.must_match %r{read committed}
38+
Ship.transaction(isolation: :serializable) do
39+
Ship.create! name: 'Black Pearl'
40+
in_level = connection.user_options_isolation_level
41+
end
42+
after_level = connection.user_options_isolation_level
43+
in_level.must_match %r{serializable}i
44+
after_level.must_match %r{read committed}
45+
ensure
46+
connection.set_transaction_isolation_level 'READ COMMITTED'
47+
end
48+
end
49+
3350

3451
protected
3552

0 commit comments

Comments
 (0)