Skip to content

Commit

Permalink
Introduce transaction_joinable flag to mark that the fixtures transac…
Browse files Browse the repository at this point in the history
…tion can't joined, a new savepoint is required even if :requires_new is not set. Use :requires_new option instead of :nest. Update changelog.

[rails#383 state:committed]
  • Loading branch information
jeremy committed Jan 10, 2009
1 parent 223a1d9 commit ab0ce05
Show file tree
Hide file tree
Showing 6 changed files with 45 additions and 47 deletions.
2 changes: 2 additions & 0 deletions activerecord/CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
*2.3.0/3.0*

* Support nested transactions using database savepoints. #383 [Jonathan Viney, Hongli Lai]

* Added dynamic scopes ala dynamic finders #1648 [Yaroslav Markin]

* Fixed that ActiveRecord::Base#new_record? should return false (not nil) for existing records #1219 [Yaroslav Markin]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,8 @@ def outside_transaction?
# - The block will be run without doing anything. All database statements
# that happen within the block are effectively appended to the already
# open database transaction.
# - However, if +start_db_transaction+ is set to true, then the block will
# be run inside a new database savepoint, effectively making the block
# a sub-transaction.
# - If the #transactional_fixtures attribute is set to true, then the first
# nested call to #transaction will create a new savepoint instead of
# doing nothing. This makes it possible for toplevel transactions in unit
# tests to behave like real transactions, even though a database
# transaction has already been opened.
# - However, if +requires_new+ is set, the block will be wrapped in a
# database savepoint acting as a sub-transaction.
#
# === Caveats
#
Expand All @@ -111,20 +105,25 @@ def outside_transaction?
# already-automatically-released savepoints:
#
# Model.connection.transaction do # BEGIN
# Model.connection.transaction(true) do # CREATE SAVEPOINT rails_savepoint_1
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...)
# # rails_savepoint_1 now automatically released
# end # RELEASE savepoint rails_savepoint_1 <--- BOOM! database error!
# # active_record_1 now automatically released
# end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error!
# end
def transaction(start_db_transaction = false)
start_db_transaction ||= open_transactions == 0 || (open_transactions == 1 && transactional_fixtures)
def transaction(options = {})
options.assert_valid_keys :requires_new, :joinable

last_transaction_joinable, @transaction_joinable =
@transaction_joinable, options[:joinable] || true
requires_new = options[:requires_new] || !last_transaction_joinable

transaction_open = false
begin
if block_given?
if start_db_transaction
if requires_new || open_transactions == 0
if open_transactions == 0
begin_db_transaction
else
elsif requires_new
create_savepoint
end
increment_open_transactions
Expand All @@ -145,6 +144,8 @@ def transaction(start_db_transaction = false)
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
end
ensure
@transaction_joinable = last_transaction_joinable

if outside_transaction?
@open_transactions = 0
elsif transaction_open
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,24 +165,23 @@ def increment_open_transactions
def decrement_open_transactions
@open_transactions -= 1
end


def transaction_joinable=(joinable)
@transaction_joinable = joinable
end

def create_savepoint
end

def rollback_to_savepoint
end

def release_savepoint
end

def current_savepoint_name
"rails_savepoint_#{open_transactions}"
"active_record_#{open_transactions}"
end

# Whether this AbstractAdapter is currently being used inside a unit test
# with transactional fixtures turned on. See DatabaseStatements#transaction
# for more information about the effect of this option.
attr_accessor :transactional_fixtures

def log_info(sql, name, ms)
if @logger && @logger.debug?
Expand Down
5 changes: 2 additions & 3 deletions activerecord/lib/active_record/fixtures.rb
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ def self.create_fixtures(fixtures_directory, table_names, class_names = {})

all_loaded_fixtures.update(fixtures_map)

connection.transaction(connection.open_transactions.zero?) do
connection.transaction(:requires_new => true) do
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
fixtures.each { |fixture| fixture.insert_fixtures }

Expand Down Expand Up @@ -937,8 +937,8 @@ def setup_fixtures
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
ActiveRecord::Base.connection.increment_open_transactions
ActiveRecord::Base.connection.transaction_joinable = false
ActiveRecord::Base.connection.begin_db_transaction
ActiveRecord::Base.connection.transactional_fixtures = true
# Load fixtures for every test.
else
Fixtures.reset_cache
Expand All @@ -961,7 +961,6 @@ def teardown_fixtures
if run_in_transaction? && ActiveRecord::Base.connection.open_transactions != 0
ActiveRecord::Base.connection.rollback_db_transaction
ActiveRecord::Base.connection.decrement_open_transactions
ActiveRecord::Base.connection.transactional_fixtures = false
end
ActiveRecord::Base.clear_active_connections!
end
Expand Down
27 changes: 12 additions & 15 deletions activerecord/lib/active_record/transactions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,14 @@ def self.included(base)
#
# User.find(:all) # => empty
#
# It is also possible to treat a certain #transaction call as its own
# sub-transaction, by passing <tt>:nest => true</tt> to #transaction. If
# anything goes wrong inside that transaction block, then the parent
# transaction will remain unaffected. For example:
# It is also possible to requires a sub-transaction by passing
# <tt>:requires_new => true</tt>. If anything goes wrong, the
# database rolls back to the beginning of the sub-transaction
# without rolling back the parent transaction. For example:
#
# User.transaction do
# User.create(:username => 'Kotori')
# User.transaction(:nest => true) do
# User.transaction(:requires_new => true) do
# User.create(:username => 'Nemu')
# raise ActiveRecord::Rollback
# end
Expand All @@ -169,20 +169,17 @@ def self.included(base)
# database error will occur because the savepoint has already been
# automatically released. The following example demonstrates the problem:
#
# Model.connection.transaction do # BEGIN
# Model.connection.transaction(true) do # CREATE SAVEPOINT rails_savepoint_1
# Model.connection.create_table(...) # rails_savepoint_1 now automatically released
# end # RELEASE savepoint rails_savepoint_1
# # ^^^^ BOOM! database error!
# Model.connection.transaction do # BEGIN
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...) # active_record_1 now automatically released
# end # RELEASE savepoint active_record_1
# # ^^^^ BOOM! database error!
# end
module ClassMethods
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
def transaction(options = {}, &block)
options.assert_valid_keys :nest

# See the API documentation for ConnectionAdapters::DatabaseStatements#transaction
# for useful information.
connection.transaction(options[:nest], &block)
# See the ConnectionAdapters::DatabaseStatements#transaction API docs.
connection.transaction(options, &block)
end
end

Expand Down
8 changes: 4 additions & 4 deletions activerecord/test/cases/transactions_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def test_force_savepoint_in_nested_transaction
@second.save!

begin
Topic.transaction :nest => true do
Topic.transaction :requires_new => true do
@first.happy = false
@first.save!
raise
Expand Down Expand Up @@ -268,17 +268,17 @@ def test_many_savepoints
@first.save!

begin
Topic.transaction :nest => true do
Topic.transaction :requires_new => true do
@first.content = "Two"
@first.save!

begin
Topic.transaction :nest => true do
Topic.transaction :requires_new => true do
@first.content = "Three"
@first.save!

begin
Topic.transaction :nest => true do
Topic.transaction :requires_new => true do
@first.content = "Four"
@first.save!
raise
Expand Down

0 comments on commit ab0ce05

Please sign in to comment.