require 'cases/helper'
require 'support/connection_helper'

class HotCompatibilityTest < ActiveRecord::TestCase
  self.use_transactional_tests = false
  include ConnectionHelper

  setup do
    @klass = Class.new(ActiveRecord::Base) do
      connection.create_table :hot_compatibilities, force: true do |t|
        t.string :foo
        t.string :bar
      end

      def self.name; 'HotCompatibility'; end
    end
  end

  teardown do
    ActiveRecord::Base.connection.drop_table :hot_compatibilities
  end

  test "insert after remove_column" do
    # warm cache
    @klass.create!

    # we have 3 columns
    assert_equal 3, @klass.columns.length

    # remove one of them
    @klass.connection.remove_column :hot_compatibilities, :bar

    # we still have 3 columns in the cache
    assert_equal 3, @klass.columns.length

    # but we can successfully create a record so long as we don't
    # reference the removed column
    record = @klass.create! foo: 'foo'
    record.reload
    assert_equal 'foo', record.foo
  end

  test "update after remove_column" do
    record = @klass.create! foo: 'foo'
    assert_equal 3, @klass.columns.length
    @klass.connection.remove_column :hot_compatibilities, :bar
    assert_equal 3, @klass.columns.length

    record.reload
    assert_equal 'foo', record.foo
    record.foo = 'bar'
    record.save!
    record.reload
    assert_equal 'bar', record.foo
  end

  if current_adapter?(:PostgreSQLAdapter)
    test "cleans up after prepared statement failure in a transaction" do
      with_two_connections do |original_connection, ddl_connection|
        record = @klass.create! bar: 'bar'

        # prepare the reload statement in a transaction
        @klass.transaction do
          record.reload
        end

        assert get_prepared_statement_cache(@klass.connection).any?,
          "expected prepared statement cache to have something in it"

        # add a new column
        ddl_connection.add_column :hot_compatibilities, :baz, :string

        assert_raise(ActiveRecord::PreparedStatementCacheExpired) do
          @klass.transaction do
            record.reload
          end
        end

        assert_empty get_prepared_statement_cache(@klass.connection),
          "expected prepared statement cache to be empty but it wasn't"
      end
    end

    test "cleans up after prepared statement failure in nested transactions" do
      with_two_connections do |original_connection, ddl_connection|
        record = @klass.create! bar: 'bar'

        # prepare the reload statement in a transaction
        @klass.transaction do
          record.reload
        end

        assert get_prepared_statement_cache(@klass.connection).any?,
          "expected prepared statement cache to have something in it"

        # add a new column
        ddl_connection.add_column :hot_compatibilities, :baz, :string

        assert_raise(ActiveRecord::PreparedStatementCacheExpired) do
          @klass.transaction do
            @klass.transaction do
              @klass.transaction do
                record.reload
              end
            end
          end
        end

        assert_empty get_prepared_statement_cache(@klass.connection),
          "expected prepared statement cache to be empty but it wasn't"
      end
    end
  end

  private

  def get_prepared_statement_cache(connection)
    connection.instance_variable_get(:@statements)
      .instance_variable_get(:@cache)[Process.pid]
  end

  # Rails will automatically clear the prepared statements on the connection
  # that runs the migration, so we use two connections to simulate what would
  # actually happen on a production system; we'd have one connection running the
  # migration from the rake task ("ddl_connection" here), and we'd have another
  # connection in a web worker.
  def with_two_connections
    run_without_connection do |original_connection|
      ActiveRecord::Base.establish_connection(original_connection.merge(pool_size: 2))
      begin
        ddl_connection = ActiveRecord::Base.connection_pool.checkout
        begin
          yield original_connection, ddl_connection
        ensure
          ActiveRecord::Base.connection_pool.checkin ddl_connection
        end
      ensure
        ActiveRecord::Base.clear_all_connections!
      end
    end
  end
end
