unless defined? SaneTransactions

  <<-README
    NAME

      sane_transactions

    SYNOPSIS

      the standard activerecord transactions are not sane.  consider the following

        ActiveRecord::Base.connection.transaction do
          Model.create!
          raise
        end

      in this case the Model creation is NOT rolled back.  the reason can be
      summarized thusly: ActiveRecord never starts a transaction when one is open,
      but commiting a transaction always occurs.  this means that, no matter how
      hard you try, one errant save|destroy|create from deep in the bowels of a
      plugin will unravel your carefully laid plans - throwing data integrity out
      the window, along with part of your salary.

      for the most part people simply ignore this fact in rails, and live with
      the possibility of inconsistent data after a failed complex controller
      action.

      sane_transactions address this issue, and more, in a small, furry, rails
      munging way.

    USAGE


      * POLS transactions

          require 'sane_transactions'

          ActiveRecord::Base.connection.transaction do
            Model.create!
            raise
          end

          # Model creation is rolled back


      * nested transactions work, only the top level transaction executes sql 

          require 'sane_transactions'

          ActiveRecord::Base.connection.transaction do
            Model.create!

            ActiveRecord::Base.connection.transaction do
              ActiveRecord::Base.connection.transaction do
                ActiveRecord::Base.connection.transaction do
                  raise
                end
              end
            end
          end

          # Model creation is rolled back


        * shortcuts are given, db code is important so certain methods are
          available at the top level

            require 'sane_transactions'

            transaction do
              Model.create!
              raise
            end

            # ahhhhhh


        * throw/catch based switches enable you to bail out and commit or
          rollback a transaction early and from any depth

            require 'sane_transactions'

            transaction do
              Model.create!
              rollback!
            end

            # Model creation is rolled back


        * a commit switch is provided too

            require 'sane_transactions'

            transaction do
              a = Model.create!
              commit!
              b = Model.create!
            end

            # a IS created, b IS NOT


        * savepoints are implemented, and can be individually rolled back

            require 'sane_transactions'

            transaction do
              savepoint do
                a = Model.create!
                rollback! :savepoint
              end

              b = Model.create!
            end

            # a IS NOT created, b IS

    INSTALL

      - put this file into RAILS_ROOT/lib

      - add "require 'sane_transactions'" to RAILS_ROOT/environment.rb

    DISCLAIMER

      this is pre-pre-pre-pre-alpha code

    AUTHOR
    
      ara.t.howard [[at]] gmail [[dot]] com
  README


  class Object
    def transaction *argv, &block
      ActiveRecord::Base.connection.transaction *argv, &block
    end
    def transaction? *argv, &block
      ActiveRecord::Base.connection.transaction? *argv, &block
    end
    def savepoint *argv, &block
      ActiveRecord::Base.connection.savepoint *argv, &block
    end
    def rollback! *argv, &block
      ActiveRecord::Base.connection.rollback! *argv, &block
    end
    def commit! *argv, &block
      ActiveRecord::Base.connection.commit! *argv, &block
    end
  end

  class Module
    def thread_safe_attribute name, options = {}
      options.to_options!
      prefix = options[:prefix]
      prefix = "#{ prefix.name.underscore }_" if Module === prefix
      key = "#{ prefix }#{ name }".inspect
      code = <<-code
        def #{ name } *argv, &block
          if argv.empty?
            Thread.current[#{ key }]
          else
            if block
              value = Thread.current[#{ key }]
              begin
                Thread.current[#{ key }] = argv.first
                block.call
              ensure
                Thread.current[#{ key }] = value 
              end
            else
              Thread.current[#{ key }] = argv.first
            end
          end
        end
        def #{ name }= value
          #{ name } value
        end
        def #{ name }?
          #{ name }
        end
      code
      module_eval code
    end
  end

  module ActiveRecord
    module Transactions
      class Savepoint < ::String
        class << self
          thread_safe_attribute 'initial', :prefix => ActiveRecord::Transactions::Savepoint
        end

        def initialize initial = self.class.initial
          reset! initial
        end

        def next
          succ!
        end

        def reset! initial = self.class.initial
          replace initial
        end

        initial "rails_savepoint_0"
      end

      class << self
        thread_safe_attribute 'using', :prefix => ActiveRecord::Transactions
        thread_safe_attribute 'are_active', :prefix => ActiveRecord::Transactions
        thread_safe_attribute 'savepoint', :prefix => ActiveRecord::Transactions
      end

      using :transaction
      are_active false
      savepoint Savepoint.new
    end

    module ConnectionAdapters
      module DatabaseStatements
        def transaction *argv, &block
          method = "transaction_using_#{ ActiveRecord::Transactions.using }"
          send method, *argv, &block
        end

        def transaction?
          ActiveRecord::Transactions.are_active?
        end

        def transaction_using_transaction *ignored
          return yield if ActiveRecord::Transactions.are_active?

          finish = nil
          returned = nil
          begin
            ActiveRecord::Transactions.are_active true
            ActiveRecord::Transactions.savepoint.reset!
            begin_db_transaction
            finish =
              catch(:transaction) do
                returned = yield ActiveRecord::Base.connection
                nil
              end
            returned
          ensure
            ActiveRecord::Transactions.are_active false
            if finish
              finish == :rollback ? rollback_db_transaction : commit_db_transaction
            else
              $! ? rollback_db_transaction : commit_db_transaction
            end
          end
        end

        def transaction_using_savepoint *ignored
          raise ActiveRecord::Transactions::TransactionError, 'no transaction in effect' unless
            ActiveRecord::Transactions.are_active?

          name = nil
          finish = nil
          returned = nil
          begin
            name = begin_db_savepoint
            finish =
              catch(:savepoint) do
                returned = yield ActiveRecord::Base.connection
                nil
              end
            returned
          ensure
            if finish
              rollback_db_savepoint name if finish == :rollback and name
            else
              rollback_db_savepoint name if $! and name
            end
          end
        end

        def savepoint
          raise ActiveRecord::Transactions::TransactionError, 'no transaction in effect' unless
            ActiveRecord::Transactions.are_active?

          ActiveRecord::Transactions.using :savepoint do
            transaction{ yield }
          end
        end

        def rollback! target = :transaction
          raise ActiveRecord::Transactions::TransactionError, 'no transaction in effect' unless
            ActiveRecord::Transactions.are_active?

          throw target, :rollback
        end

        def rollback_savepoint!
          rollback! :savepoint
        end

        def commit! target = :transaction
          raise ActiveRecord::Transactions::TransactionError, 'no transaction in effect' unless
            ActiveRecord::Transactions.are_active?

          throw target, :commit
        end

        def commit! target = :transaction
          raise ActiveRecord::Transactions::TransactionError, 'no transaction in effect' unless
            ActiveRecord::Transactions.are_active?

          throw target, :commit
        end

        def begin_db_savepoint name = ActiveRecord::Transactions.savepoint.next
          execute "SAVEPOINT #{ name }"
          name
        end

        def rollback_db_savepoint name
          execute "ROLLBACK TO SAVEPOINT #{ name }"
          name
        end

        def release_db_savepoint name
          execute "RELEASE SAVEPOINT #{ name }"
          name
        end
      end
    end
  end

  SaneTransactions = 42
end