Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

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)

Restored the support for nested transactions in tests and fixed some assert_queries() which with the nested transactions support are getting two more queries for the transaction creation. The broken delete tests indeed appear two now have two more queries, unrelated to the nested transactions.

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

    old new  
    5555      end 
    5656 
    5757      # 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
    5959        transaction_open = false 
     60        savepoint_open = false 
    6061        begin 
    6162          if block_given? 
    6263            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 
    6471              transaction_open = true 
    6572            end 
    6673            yield 
     
    6875        rescue Exception => database_transaction_rollback 
    6976          if transaction_open 
    7077            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 
    7284          end 
    7385          raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback 
    7486        end 
    7587      ensure 
    7688        if transaction_open 
    7789          begin 
    78             commit_db_transaction 
     90            unless savepoint_open 
     91              commit_db_transaction 
     92            else 
     93              release_savepoint(savepoint_number) 
     94            end 
    7995          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 
    81101            raise 
    82102          end 
    83103        end 
     
    93113      # done if the transaction block raises an exception or returns false. 
    94114      def rollback_db_transaction() end 
    95115 
     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 
    96128      # Alias for #add_limit_offset!. 
    97129      def add_limit!(sql, options) 
    98130        add_limit_offset!(sql, options) if options 
  • a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb

    old new  
    315315        # Transactions aren't supported 
    316316      end 
    317317 
     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 
    318337 
    319338      def add_limit_offset!(sql, options) #:nodoc: 
    320339        if limit = options[:limit] 
  • a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

    old new  
    433433        execute "ROLLBACK" 
    434434      end 
    435435 
     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 
    436455      # SCHEMA STATEMENTS ======================================== 
    437456 
    438457      # Returns the list of all tables in the schema search path or a specified schema. 
  • a/activerecord/lib/active_record/fixtures.rb

    old new  
    927927            load_fixtures 
    928928            @@already_loaded_fixtures[self.class] = @loaded_fixtures 
    929929          end 
     930          Thread.current['transactional_test'] = true 
    930931          ActiveRecord::Base.send :increment_open_transactions 
    931932          ActiveRecord::Base.connection.begin_db_transaction 
    932933        # Load fixtures for every test. 
     
    949950        end 
    950951 
    951952        # 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 
    955959        end 
    956960        ActiveRecord::Base.verify_active_connections! 
    957961      end 
  • a/activerecord/lib/active_record/transactions.rb

    old new  
    7272    # should be ready to catch those in your application code. One exception is the ActiveRecord::Rollback exception, which will 
    7373    # trigger a ROLLBACK when raised, but not be re-raised by the transaction block. 
    7474    module ClassMethods 
    75       def transaction(&block) 
     75      def transaction(options={}, &block) 
    7676        previous_handler = trap('TERM') { raise TransactionError, "Transaction aborted" } 
    7777        increment_open_transactions 
    7878 
    7979        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) 
    8181        ensure 
    8282          decrement_open_transactions 
    8383          trap('TERM', previous_handler) 
    8484        end 
    8585      end 
    8686 
     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 
    87127      private 
    88128        def increment_open_transactions #:nodoc: 
    89129          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']) 
    91131          Thread.current['open_transactions'] = open + 1 
    92132        end 
    93133 
     
    96136        end 
    97137    end 
    98138 
    99     def transaction(&block) 
    100       self.class.transaction(&block) 
     139    def transaction(options={}, &block) 
     140      self.class.transaction(options, &block) 
    101141    end 
    102142 
    103143    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 
    105151    end 
    106152 
    107153    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 
    109163    end 
    110164 
    111165    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 
    113175    end 
    114176 
    115177    # Reset id and @new_record if the transaction rolls back. 
  • a/activerecord/test/abstract_unit.rb

    old new  
    6363 
    6464ActiveRecord::Base.connection.class.class_eval do 
    6565  unless defined? IGNORED_SQL 
    66     IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/] 
     66    IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SHOW FIELDS/, /^SELECT @@ROWCOUNT/] 
    6767 
    6868    def execute_with_counting(sql, name = nil, &block) 
    6969      $query_count ||= 0 
  • a/activerecord/test/associations_test.rb

    old new  
    411411 
    412412  def test_not_resaved_when_unchanged 
    413413    firm = Firm.find(:first, :include => :account) 
    414     assert_queries(1) { firm.save! } 
     414    assert_queries(3) { firm.save! } 
    415415 
    416416    firm = Firm.find(:first) 
    417417    firm.account = Account.find(:first) 
    418     assert_queries(1) { firm.save! } 
     418    assert_queries(3) { firm.save! } 
    419419 
    420420    firm = Firm.find(:first).clone 
    421421    firm.account = Account.find(:first) 
    422     assert_queries(2) { firm.save! } 
     422    assert_queries(4) { firm.save! } 
    423423 
    424424    firm = Firm.find(:first).clone 
    425425    firm.account = Account.find(:first).clone 
    426     assert_queries(2) { firm.save! } 
     426    assert_queries(4) { firm.save! } 
    427427  end 
    428428 
    429429  def test_save_still_works_after_accessing_nil_has_one 
     
    788788    assert_equal 1, first_firm.clients_of_firm.size 
    789789    first_firm.clients_of_firm.reset 
    790790 
    791     assert_queries(1) do 
     791    assert_queries(3) do 
    792792      first_firm.clients_of_firm.create(:name => "Superstars") 
    793793    end 
    794794 
  • a/activerecord/test/transactions_test.rb

    old new  
    279279    end 
    280280  end 
    281281end 
     282 
     283if 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 
     397end