Skip to content

Commit 1a91099

Browse files
committed
Locking now done.
Default lock is now "WITH (UPDLOCK)". Fixes #368
1 parent 59e3e06 commit 1a91099

File tree

3 files changed

+76
-24
lines changed

3 files changed

+76
-24
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
#### Fixed
3939

40-
* n/a
40+
* Default lock is now "WITH (UPDLOCK)". Fixes #368
4141

4242
#### Security
4343

lib/arel/visitors/sqlserver.rb

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ def visit_Arel_Nodes_BindParam o, collector
1616
collector.add_bind(o) { |i| "@#{i-1}" }
1717
end
1818

19+
def visit_Arel_Nodes_Lock o, collector
20+
o.expr = Arel.sql('WITH (UPDLOCK)') if o.expr.to_s =~ /FOR UPDATE/
21+
collector << SPACE
22+
visit o.expr, collector
23+
end
24+
1925
def visit_Arel_Nodes_Offset o, collector
2026
collector << OFFSET
2127
visit o.expr, collector
@@ -29,6 +35,7 @@ def visit_Arel_Nodes_Limit o, collector
2935
end
3036

3137
def visit_Arel_Nodes_SelectStatement o, collector
38+
@select_statement = o
3239
if o.with
3340
collector = visit o.with, collector
3441
collector << SPACE
@@ -38,12 +45,41 @@ def visit_Arel_Nodes_SelectStatement o, collector
3845
}
3946
collector = visit_Orders_And_Let_Fetch_Happen o, collector
4047
collector = visit_Make_Fetch_Happen o, collector
41-
collector = visit o.lock, collector if o.lock
48+
collector
49+
ensure
50+
@select_statement = nil
51+
end
52+
53+
def visit_Arel_Nodes_JoinSource o, collector
54+
if o.left
55+
collector = visit o.left, collector
56+
collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector
57+
end
58+
if o.right.any?
59+
collector << " " if o.left
60+
collector = inject_join o.right, collector, ' '
61+
end
4262
collector
4363
end
4464

65+
def visit_Arel_Nodes_OuterJoin o, collector
66+
collector << "LEFT OUTER JOIN "
67+
collector = visit o.left, collector
68+
collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
69+
collector << " "
70+
visit o.right, collector
71+
end
72+
4573
# SQLServer ToSql/Visitor (Additions)
4674

75+
def visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, options = {}
76+
if select_statement_lock?
77+
collector = visit @select_statement.lock, collector
78+
collector << SPACE if options[:space]
79+
end
80+
collector
81+
end
82+
4783
def visit_Orders_And_Let_Fetch_Happen o, collector
4884
if (o.limit || o.offset) && o.orders.empty?
4985
table = table_From_Statement o
@@ -71,6 +107,10 @@ def visit_Make_Fetch_Happen o, collector
71107

72108
# SQLServer Helpers
73109

110+
def select_statement_lock?
111+
@select_statement && @select_statement.lock
112+
end
113+
74114
def table_From_Statement o
75115
core = o.cores.first
76116
if Arel::Table === core.from

test/cases/pessimistic_locking_test_sqlserver.rb

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,49 @@
44

55
class PessimisticLockingTestSQLServer < ActiveRecord::TestCase
66

7-
self.use_transactional_fixtures = false
87
fixtures :people, :readers
98

10-
setup do
11-
Person.columns; Reader.columns # Avoid introspection queries during tests.
9+
before do
10+
Person.columns
11+
Reader.columns
1212
end
1313

14-
context 'For simple finds with default lock option' do
14+
it 'uses with updlock by default' do
15+
assert_sql %r|SELECT \[people\]\.\* FROM \[people\] WITH \(UPDLOCK\)| do
16+
Person.lock(true).to_a.must_equal Person.all.to_a
17+
end
18+
end
1519

16-
should 'lock with simple find' do
20+
describe 'For simple finds with default lock option' do
21+
22+
it 'lock with simple find' do
1723
assert_nothing_raised do
1824
Person.transaction do
19-
Person.lock(true).find(1)
25+
Person.lock(true).find(1).must_equal Person.find(1)
2026
end
2127
end
2228
end
2329

24-
should 'lock with scoped find' do
30+
it 'lock with scoped find' do
2531
assert_nothing_raised do
2632
Person.transaction do
2733
Person.lock(true).scoping do
28-
Person.find(1)
34+
Person.find(1).must_equal Person.find(1)
2935
end
3036
end
3137
end
3238
end
3339

34-
should 'lock with eager find' do
40+
it 'lock with eager find' do
3541
assert_nothing_raised do
3642
Person.transaction do
37-
Person.lock(true).includes(:readers).find(1)
43+
person = Person.lock(true).includes(:readers).find(1)
44+
person.must_equal Person.find(1)
3845
end
3946
end
4047
end
4148

42-
should 'reload with lock when #lock! called' do
49+
it 'reload with lock when #lock! called' do
4350
assert_nothing_raised do
4451
Person.transaction do
4552
person = Person.find 1
@@ -50,29 +57,34 @@ class PessimisticLockingTestSQLServer < ActiveRecord::TestCase
5057
end
5158
end
5259

53-
should 'simply add lock to find all' do
54-
assert_sql %r|SELECT \[people\]\.\* FROM \[people\] WITH \(NOLOCK\)| do
55-
Person.lock('WITH (NOLOCK)').load
60+
it 'can add a custom lock directive' do
61+
assert_sql %r|SELECT \[people\]\.\* FROM \[people\] WITH\(HOLDLOCK, ROWLOCK\)| do
62+
Person.lock('WITH(HOLDLOCK, ROWLOCK)').load
5663
end
5764
end
5865

5966
end
6067

61-
context 'For paginated finds' do
68+
describe 'For paginated finds' do
6269

63-
setup do
70+
before do
71+
Person.delete_all
6472
20.times { |n| Person.create!(first_name: "Thing_#{n}") }
6573
end
6674

67-
should 'cope with eager loading un-locked paginated' do
68-
eager_ids_sql = /SELECT TOP \(5\).*FROM \[people\] WITH \(NOLOCK\)/
69-
loader_sql = /FROM \[people\] WITH \(NOLOCK\).*WHERE \[people\]\.\[id\] IN/
70-
assert_sql(eager_ids_sql,loader_sql) do
71-
Person.lock('WITH (NOLOCK)').limit(5).offset(10).includes(:readers).references(:readers).load
75+
it 'copes with eager loading un-locked paginated' do
76+
eager_ids_sql = /SELECT\s+DISTINCT \[people\].\[id\] FROM \[people\] WITH \(UPDLOCK\) LEFT OUTER JOIN \[readers\] WITH \(UPDLOCK\)\s+ON \[readers\].\[person_id\] = \[people\].\[id\]\s+ORDER BY \[people\].\[id\] ASC OFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY/
77+
loader_sql = /SELECT.*FROM \[people\] WITH \(UPDLOCK\).*WHERE \[people\]\.\[id\] IN/
78+
assert_sql(eager_ids_sql, loader_sql) do
79+
people = Person.lock(true).limit(5).offset(10).includes(:readers).references(:readers).to_a
80+
people[0].first_name.must_equal 'Thing_10'
81+
people[1].first_name.must_equal 'Thing_11'
82+
people[2].first_name.must_equal 'Thing_12'
83+
people[3].first_name.must_equal 'Thing_13'
84+
people[4].first_name.must_equal 'Thing_14'
7285
end
7386
end
7487

7588
end
7689

77-
7890
end

0 commit comments

Comments
 (0)