Ticket #5457: nested_transactions_with_implicit_transaction_config.3.patch
| File nested_transactions_with_implicit_transaction_config.3.patch, 16.5 kB (added by tarmo, 10 months ago) |
|---|
-
a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
old new 55 55 end 56 56 57 57 # Wrap a block in a transaction. Returns result of block. 58 def transaction(start_db_transaction = true )58 def transaction(start_db_transaction = true, savepoint_number = 1) 59 59 transaction_open = false 60 savepoint_open = false 60 61 begin 61 62 if block_given? 62 63 if start_db_transaction 63 begin_db_transaction 64 if savepoint_number == 1 65 begin_db_transaction 66 else 67 if create_savepoint(savepoint_number) 68 savepoint_open = true 69 end 70 end 64 71 transaction_open = true 65 72 end 66 73 yield … … 68 75 rescue Exception => database_transaction_rollback 69 76 if transaction_open 70 77 transaction_open = false 71 rollback_db_transaction 78 unless savepoint_open 79 rollback_db_transaction 80 else 81 savepoint_open = false 82 rollback_to_savepoint(savepoint_number) 83 end 72 84 end 73 85 raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback 74 86 end 75 87 ensure 76 88 if transaction_open 77 89 begin 78 commit_db_transaction 90 unless savepoint_open 91 commit_db_transaction 92 else 93 release_savepoint(savepoint_number) 94 end 79 95 rescue Exception => database_transaction_rollback 80 rollback_db_transaction 96 unless savepoint_open 97 rollback_db_transaction 98 else 99 rollback_to_savepoint(savepoint_number) 100 end 81 101 raise 82 102 end 83 103 end … … 93 113 # done if the transaction block raises an exception or returns false. 94 114 def rollback_db_transaction() end 95 115 116 # abstract create_savepoint method that does nothing 117 def create_savepoint(sp_number) 118 end 119 120 # abstract rollback_to_savepoint method that does nothing 121 def rollback_to_savepoint(sp_number) 122 end 123 124 # abstract release_savepoint method that does nothing 125 def release_savepoint(sp_number) 126 end 127 96 128 # Alias for #add_limit_offset!. 97 129 def add_limit!(sql, options) 98 130 add_limit_offset!(sql, options) if options -
a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
old new 315 315 # Transactions aren't supported 316 316 end 317 317 318 def create_savepoint(sp_number) 319 execute("SAVEPOINT rails_nested_transaction_#{sp_number}") 320 return true 321 rescue Exception 322 # savepoints are not supported 323 end 324 325 def rollback_to_savepoint(sp_number) 326 execute("ROLLBACK TO SAVEPOINT rails_nested_transaction_#{sp_number}") 327 rescue Exception 328 # savepoints are not supported 329 end 330 331 def release_savepoint(sp_number) 332 execute("RELEASE SAVEPOINT rails_nested_transaction_#{sp_number}") 333 rescue Exception 334 # savepoints are not supported 335 end 336 318 337 319 338 def add_limit_offset!(sql, options) #:nodoc: 320 339 if limit = options[:limit] -
a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
old new 433 433 execute "ROLLBACK" 434 434 end 435 435 436 def create_savepoint(sp_number) 437 execute("SAVEPOINT rails_nested_transaction_#{sp_number}") 438 return true 439 rescue Exception 440 # savepoints are not supported 441 end 442 443 def rollback_to_savepoint(sp_number) 444 execute("ROLLBACK TO SAVEPOINT rails_nested_transaction_#{sp_number}") 445 rescue Exception 446 # savepoints are not supported 447 end 448 449 def release_savepoint(sp_number) 450 execute("RELEASE SAVEPOINT rails_nested_transaction_#{sp_number}") 451 rescue Exception 452 # savepoints are not supported 453 end 454 436 455 # SCHEMA STATEMENTS ======================================== 437 456 438 457 # Returns the list of all tables in the schema search path or a specified schema. -
a/activerecord/lib/active_record/fixtures.rb
old new 927 927 load_fixtures 928 928 @@already_loaded_fixtures[self.class] = @loaded_fixtures 929 929 end 930 Thread.current['transactional_test'] = true 930 931 ActiveRecord::Base.send :increment_open_transactions 931 932 ActiveRecord::Base.connection.begin_db_transaction 932 933 # Load fixtures for every test. … … 949 950 end 950 951 951 952 # Rollback changes if a transaction is active. 952 if use_transactional_fixtures? && Thread.current['open_transactions'] != 0 953 ActiveRecord::Base.connection.rollback_db_transaction 954 Thread.current['open_transactions'] = 0 953 if use_transactional_fixtures? 954 if Thread.current['open_transactions'] != 0 955 ActiveRecord::Base.connection.rollback_db_transaction 956 Thread.current['open_transactions'] = 0 957 end 958 Thread.current['transactional_test'] = false 955 959 end 956 960 ActiveRecord::Base.verify_active_connections! 957 961 end -
a/activerecord/lib/active_record/transactions.rb
old new 72 72 # should be ready to catch those in your application code. One exception is the ActiveRecord::Rollback exception, which will 73 73 # trigger a ROLLBACK when raised, but not be re-raised by the transaction block. 74 74 module ClassMethods 75 def transaction( &block)75 def transaction(options={}, &block) 76 76 previous_handler = trap('TERM') { raise TransactionError, "Transaction aborted" } 77 77 increment_open_transactions 78 78 79 79 begin 80 connection.transaction( Thread.current['start_db_transaction'], &block)80 connection.transaction((options[:force] == true) || Thread.current['start_db_transaction'], Thread.current['open_transactions'], &block) 81 81 ensure 82 82 decrement_open_transactions 83 83 trap('TERM', previous_handler) 84 84 end 85 85 end 86 86 87 # Sets the options for implicit transactions. For different 88 # action types. 89 # 90 # The action types are: 91 # * <tt>:save</tt> - transaction type for creating or updating a record. 92 # * <tt>:destroy</tt> - transaction type for deleting a record. 93 # 94 # The transaction types are: 95 # * <tt>:none</tt> - no transaction is created. 96 # * <tt>:flat</tt> - transaction is only created if non exist. This is the default. 97 # * <tt>:nested</tt> - transaction is created even if one exists. This only works if the database supports nested transactions, if it does not then the behaviour is the same as for :flat. 98 # 99 # Option examples: 100 # set_transaction_types :save => :flat 101 # set_transaction_types :save => :none, :destroy => :nested 102 # set_transaction_types :nested 103 def set_transaction_types(options) 104 case options 105 when Symbol 106 options = { :save => options, :destroy => options } 107 when Hash 108 options[:save] ||= :flat 109 options[:destroy] ||= :flat 110 else 111 raise(ArgumentError, "Invalid transaction type(s): %s", options.inspect) 112 end 113 114 options.assert_valid_keys(:save, :destroy) 115 116 write_inheritable_attribute("transaction_types", options) 117 end 118 119 def get_transaction_type(action_type) 120 get_transaction_types[action_type] || :flat 121 end 122 123 def get_transaction_types 124 (read_inheritable_attribute("transaction_types") or write_inheritable_attribute("transaction_types", {})) 125 end 126 87 127 private 88 128 def increment_open_transactions #:nodoc: 89 129 open = Thread.current['open_transactions'] ||= 0 90 Thread.current['start_db_transaction'] = open.zero? 130 Thread.current['start_db_transaction'] = open.zero? || (open == 1 && Thread.current['transactional_test']) 91 131 Thread.current['open_transactions'] = open + 1 92 132 end 93 133 … … 96 136 end 97 137 end 98 138 99 def transaction( &block)100 self.class.transaction( &block)139 def transaction(options={}, &block) 140 self.class.transaction(options, &block) 101 141 end 102 142 103 143 def destroy_with_transactions #:nodoc: 104 transaction { destroy_without_transactions } 144 transaction_type = self.class.get_transaction_type(:destroy) 145 if transaction_type == :none 146 destroy_without_transactions 147 else 148 options = { :force => (transaction_type == :nested) } 149 transaction(options) { destroy_without_transactions } 150 end 105 151 end 106 152 107 153 def save_with_transactions(perform_validation = true) #:nodoc: 108 rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } } 154 rollback_active_record_state! do 155 transaction_type = self.class.get_transaction_type(:save) 156 if transaction_type == :none 157 save_without_transactions 158 else 159 options = { :force => (transaction_type == :nested) } 160 transaction(options) { save_without_transactions(perform_validation) } 161 end 162 end 109 163 end 110 164 111 165 def save_with_transactions! #:nodoc: 112 rollback_active_record_state! { transaction { save_without_transactions! } } 166 rollback_active_record_state! do 167 transaction_type = self.class.get_transaction_type(:save) 168 if transaction_type == :none 169 save_without_transactions! 170 else 171 options = { :force => (transaction_type == :nested) } 172 transaction(options) { save_without_transactions! } 173 end 174 end 113 175 end 114 176 115 177 # Reset id and @new_record if the transaction rolls back. -
a/activerecord/test/abstract_unit.rb
old new 63 63 64 64 ActiveRecord::Base.connection.class.class_eval do 65 65 unless defined? IGNORED_SQL 66 IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^S ELECT @@ROWCOUNT/]66 IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SHOW FIELDS/, /^SELECT @@ROWCOUNT/] 67 67 68 68 def execute_with_counting(sql, name = nil, &block) 69 69 $query_count ||= 0 -
a/activerecord/test/associations_test.rb
old new 411 411 412 412 def test_not_resaved_when_unchanged 413 413 firm = Firm.find(:first, :include => :account) 414 assert_queries( 1) { firm.save! }414 assert_queries(3) { firm.save! } 415 415 416 416 firm = Firm.find(:first) 417 417 firm.account = Account.find(:first) 418 assert_queries( 1) { firm.save! }418 assert_queries(3) { firm.save! } 419 419 420 420 firm = Firm.find(:first).clone 421 421 firm.account = Account.find(:first) 422 assert_queries( 2) { firm.save! }422 assert_queries(4) { firm.save! } 423 423 424 424 firm = Firm.find(:first).clone 425 425 firm.account = Account.find(:first).clone 426 assert_queries( 2) { firm.save! }426 assert_queries(4) { firm.save! } 427 427 end 428 428 429 429 def test_save_still_works_after_accessing_nil_has_one … … 788 788 assert_equal 1, first_firm.clients_of_firm.size 789 789 first_firm.clients_of_firm.reset 790 790 791 assert_queries( 1) do791 assert_queries(3) do 792 792 first_firm.clients_of_firm.create(:name => "Superstars") 793 793 end 794 794 -
a/activerecord/test/transactions_test.rb
old new 279 279 end 280 280 end 281 281 end 282 283 if current_adapter?(:PostgreSQLAdapter) or current_adapter?(:MysqlAdapter) 284 class NestedTransactionsTest < TransactionTest 285 286 def test_nested_explicit_transactions_with_forced_nesting 287 Topic.transaction do 288 @first.approved = true 289 @first.save 290 @second.approved = true 291 @second.save 292 begin 293 Topic.transaction :force => true do 294 @second.approved = false 295 @second.save 296 raise "Bad things!" 297 end 298 rescue 299 # ignore the exception 300 end 301 end 302 303 assert Topic.find(1).approved?, "First should have been approved" 304 assert Topic.find(2).approved?, "Second should have been approved" 305 end 306 307 def test_nested_explicit_transactions_without_forced_nesting 308 Topic.transaction do 309 @first.approved = true 310 @first.save 311 @second.approved = true 312 @second.save 313 begin 314 Topic.transaction do 315 @second.approved = false 316 @second.save 317 raise "Bad things!" 318 end 319 rescue 320 # ignore the exception 321 end 322 end 323 324 assert Topic.find(1).approved?, "First should have been approved" 325 assert !Topic.find(2).approved?, "Second should have been unapproved" 326 end 327 328 def test_transaction_type_save_none 329 with_topic_transaction_options :save => :none do 330 assert_queries(1) { @first.save! } 331 end 332 end 333 334 def test_transaction_type_save_flat 335 with_topic_transaction_options :save => :flat do 336 assert_queries(3) { @first.save! } 337 end 338 end 339 340 def test_transaction_type_save_flat_inside_transaction 341 with_topic_transaction_options :save => :flat do 342 Topic.transaction do 343 assert_queries(1) { @first.save! } 344 end 345 end 346 end 347 348 def test_transaction_type_save_nested 349 with_topic_transaction_options :save => :nested do 350 assert_queries(3) { @first.save! } 351 end 352 end 353 354 def test_transaction_type_save_nested_inside_transaction 355 with_topic_transaction_options :save => :nested do 356 Topic.transaction do 357 assert_queries(3) { @first.save! } 358 end 359 end 360 end 361 362 def test_transaction_type_destroy_none 363 with_topic_transaction_options :destroy => :none do 364 assert_queries(10) { assert @first.destroy } 365 end 366 end 367 368 def test_transaction_type_destroy_flat 369 with_topic_transaction_options :destroy => :flat do 370 assert_queries(12) { assert @first.destroy } 371 end 372 end 373 374 def test_transaction_type_destroy_nested 375 with_topic_transaction_options :destroy => :nested do 376 assert_queries(14) { assert @first.destroy } 377 end 378 end 379 380 private 381 def with_topic_transaction_options(options) 382 # NOTE Reply does not inherit this because the class 383 # has already been loaded 384 old_types_topic = Topic.send(:get_transaction_types) 385 old_types_reply = Topic.send(:get_transaction_types) 386 387 begin 388 Topic.send(:set_transaction_types, options) 389 Reply.send(:set_transaction_types, options) 390 yield 391 ensure 392 Topic.send(:set_transaction_types, old_types_topic) 393 Reply.send(:set_transaction_types, old_types_reply) 394 end 395 end 396 end 397 end