Skip to content

Commit def0397

Browse files
authored
Merge pull request rails#50371 from fractaledmind/ar-sqlite-immediate-transactions
Ensure SQLite transaction default to IMMEDIATE mode
2 parents ac0fa17 + 1e2c904 commit def0397

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)