# frozen_string_literal: true 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) && ActiveRecord::Base.connection.prepared_statements 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