Repository: rails/rails
File: activerecord/lib/active_record/transactions.rb (Ruby)

Code before:
    #
    # If you're on MySQL, then do not use Data Definition Language (DDL) operations in nested
    # transactions blocks that are emulated with savepoints. That is, do not execute statements
    # like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
    # releases all savepoints upon executing a DDL operation. When +transaction+
    # is finished and tries to release the savepoint it created earlier, a
    # database error will occur because the savepoint has already been
    # automatically released. The following example demonstrates the problem:
    #
    #   Model.transaction do                           # BEGIN
    #     Model.transaction(requires_new: true) do     # CREATE SAVEPOINT active_record_1
    #       Model.lease_connection.create_table(...)   # active_record_1 now automatically released
    #     end                                          # RELEASE SAVEPOINT active_record_1
    #   end                                            # ^^^^ BOOM! database error!
    #
    # Note that "TRUNCATE" is also a MySQL DDL statement!
    module ClassMethods
      # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
      def transaction(**options, &block)
        with_connection do |connection|
          connection.transaction(**options, &block)
        end
      end

      def with_default_isolation_level(isolation_level, &block)
        with_connection do |connection|
          connection.with_default_isolation_level(isolation_level, &block)
        end
      end

      # Returns a representation of the current transaction state,
      # which can be a top level transaction, a savepoint, or the absence of a transaction.
      #
      # An object is always returned, whether or not a transaction is currently active.
      # To check if a transaction was opened, use <tt>current_transaction.open?</tt>.
      #
      # See the ActiveRecord::Transaction documentation for detailed behavior.
      def current_transaction
        connection_pool.active_connection&.current_transaction&.user_transaction || Transaction::NULL_TRANSACTION
      end

      def before_commit(*args, &block) # :nodoc:
        set_options_for_callbacks!(args)
        set_callback(:before_commit, :before, *args, &block)
      end

      # This callback is called after a record has been created, updated, or destroyed.
      #
      # You can specify that the callback should only be fired by a certain action with
      # the +:on+ option:
      #

Review comment (suggestion):
Eagerly checking out the connection isn't ideal. I think we should try not to do that.

Ultimately we only need to know what the default isolation is when starting the transaction.

Code after:
    #
    # If you're on MySQL, then do not use Data Definition Language (DDL) operations in nested
    # transactions blocks that are emulated with savepoints. That is, do not execute statements
    # like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
    # releases all savepoints upon executing a DDL operation. When +transaction+
    # is finished and tries to release the savepoint it created earlier, a
    # database error will occur because the savepoint has already been
    # automatically released. The following example demonstrates the problem:
    #
    #   Model.transaction do                           # BEGIN
    #     Model.transaction(requires_new: true) do     # CREATE SAVEPOINT active_record_1
    #       Model.lease_connection.create_table(...)   # active_record_1 now automatically released
    #     end                                          # RELEASE SAVEPOINT active_record_1
    #   end                                            # ^^^^ BOOM! database error!
    #
    # Note that "TRUNCATE" is also a MySQL DDL statement!
    module ClassMethods
      # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
      def transaction(**options, &block)
        with_connection do |connection|
          connection.transaction(**options, &block)
        end
      end

      # Makes all transactions initiated within the block use the isolation level
      # that you set as the default. Useful for gradually migrating apps onto new isolation level.
      def with_default_isolation_level(isolation_level, &block)
        if current_transaction.open?
          raise ActiveRecord::TransactionIsolationError, "cannot set default isolation level while transaction is open"
        end

        old_level = connection_pool.default_isolation_level
        connection_pool.default_isolation_level = isolation_level
        yield
      ensure
        connection_pool.default_isolation_level = old_level
      end

      # Returns the default isolation level for the connection pool, set earlier by #with_default_isolation_level.
      def default_isolation_level
        connection_pool.default_isolation_level
      end

      # Returns a representation of the current transaction state,
      # which can be a top level transaction, a savepoint, or the absence of a transaction.
      #
      # An object is always returned, whether or not a transaction is currently active.
      # To check if a transaction was opened, use <tt>current_transaction.open?</tt>.
      #
      # See the ActiveRecord::Transaction documentation for detailed behavior.
      def current_transaction