Skip to content

Commit 1e2c904

Browse files
fractaledmindbyroot
authored andcommitted
Use SQLite IMMEDIATE transactions when possible.
Transactions run against the SQLite3 adapter default to IMMEDIATE mode to improve concurrency support and avoid busy exceptions. Fixture transactions use DEFERRED mode transactions as all `joinable` transactions become DEFERRED transactions.
1 parent ac0fa17 commit 1e2c904

File tree

7 files changed

+58
-20
lines changed

7 files changed

+58
-20
lines changed

activerecord/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
* Use SQLite `IMMEDIATE` transactions when possible.
2+
3+
Transactions run against the SQLite3 adapter default to IMMEDIATE mode to improve concurrency support and avoid busy exceptions.
4+
5+
*Stephen Margheim*
6+
17
* Raise specific exception when a connection is not defined.
28

39
The new `ConnectionNotDefined` exception provides connection name, shard and role accessors indicating the details of the connection that was requested.

activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,14 @@ def add_transaction_record(record, ensure_finalize = true)
411411
# Begins the transaction (and turns off auto-committing).
412412
def begin_db_transaction() end
413413

414+
def begin_deferred_transaction(isolation_level = nil) # :nodoc:
415+
if isolation_level
416+
begin_isolated_db_transaction(isolation_level)
417+
else
418+
begin_db_transaction
419+
end
420+
end
421+
414422
def transaction_isolation_levels
415423
{
416424
read_uncommitted: "READ UNCOMMITTED",

activerecord/lib/active_record/connection_adapters/abstract/transaction.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -448,10 +448,14 @@ def full_rollback?; false; end
448448
# = Active Record Real \Transaction
449449
class RealTransaction < Transaction
450450
def materialize!
451-
if isolation_level
452-
connection.begin_isolated_db_transaction(isolation_level)
451+
if joinable?
452+
if isolation_level
453+
connection.begin_isolated_db_transaction(isolation_level)
454+
else
455+
connection.begin_db_transaction
456+
end
453457
else
454-
connection.begin_db_transaction
458+
connection.begin_deferred_transaction(isolation_level)
455459
end
456460

457461
super

activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,25 +65,16 @@ def exec_delete(sql, name = "SQL", binds = []) # :nodoc:
6565
end
6666
alias :exec_update :exec_delete
6767

68-
def begin_isolated_db_transaction(isolation) # :nodoc:
69-
raise TransactionIsolationError, "SQLite3 only supports the `read_uncommitted` transaction isolation level" if isolation != :read_uncommitted
70-
raise StandardError, "You need to enable the shared-cache mode in SQLite mode before attempting to change the transaction isolation level" unless shared_cache?
68+
def begin_deferred_transaction(isolation = nil) # :nodoc:
69+
internal_begin_transaction(:deferred, isolation)
70+
end
7171

72-
with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
73-
ActiveSupport::IsolatedExecutionState[:active_record_read_uncommitted] = conn.get_first_value("PRAGMA read_uncommitted")
74-
conn.read_uncommitted = true
75-
begin_db_transaction
76-
end
72+
def begin_isolated_db_transaction(isolation) # :nodoc:
73+
internal_begin_transaction(:deferred, isolation)
7774
end
7875

7976
def begin_db_transaction # :nodoc:
80-
log("begin transaction", "TRANSACTION") do
81-
with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
82-
result = conn.transaction
83-
verified!
84-
result
85-
end
86-
end
77+
internal_begin_transaction(:immediate, nil)
8778
end
8879

8980
def commit_db_transaction # :nodoc:
@@ -114,6 +105,25 @@ def high_precision_current_timestamp
114105
end
115106

116107
private
108+
def internal_begin_transaction(mode, isolation)
109+
if isolation
110+
raise TransactionIsolationError, "SQLite3 only supports the `read_uncommitted` transaction isolation level" if isolation != :read_uncommitted
111+
raise StandardError, "You need to enable the shared-cache mode in SQLite mode before attempting to change the transaction isolation level" unless shared_cache?
112+
end
113+
114+
log("begin #{mode} transaction", "TRANSACTION") do
115+
with_raw_connection(allow_retry: true, materialize_transactions: false) do |conn|
116+
if isolation
117+
ActiveSupport::IsolatedExecutionState[:active_record_read_uncommitted] = conn.get_first_value("PRAGMA read_uncommitted")
118+
conn.read_uncommitted = true
119+
end
120+
result = conn.transaction(mode)
121+
verified!
122+
result
123+
end
124+
end
125+
end
126+
117127
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: false)
118128
log(sql, name, async: async) do |notification_payload|
119129
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|

activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,11 @@ def initialize(...)
120120
end
121121

122122
@config[:strict] = ConnectionAdapters::SQLite3Adapter.strict_strings_by_default unless @config.key?(:strict)
123-
@connection_parameters = @config.merge(database: @config[:database].to_s, results_as_hash: true)
123+
@connection_parameters = @config.merge(
124+
database: @config[:database].to_s,
125+
results_as_hash: true,
126+
default_transaction_mode: :immediate,
127+
)
124128
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
125129
end
126130

activerecord/test/cases/locking_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,7 @@ def duel(&block)
798798

799799
a = Thread.new do
800800
t0 = Time.now
801-
Person.transaction do
801+
Person.transaction(joinable: false) do
802802
yield
803803
b_wakeup.set
804804
a_wakeup.wait

activerecord/test/cases/transactions_test.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,6 +1386,12 @@ def test_sqlite_add_column_in_transaction
13861386
Topic.reset_column_information
13871387
end
13881388
end
1389+
1390+
def test_sqlite_default_transaction_mode_is_immediate
1391+
assert_queries_match(/BEGIN IMMEDIATE TRANSACTION/i, include_schema: false) do
1392+
Topic.transaction { Topic.lease_connection.materialize_transactions }
1393+
end
1394+
end
13891395
end
13901396

13911397
def test_transactions_state_from_rollback

0 commit comments

Comments
 (0)