rails / rails

Ruby on Rails
https://rubyonrails.org
MIT License
55.81k stars 21.6k forks source link

`ActiveRecord#attributes` is affected immediately after migration #50748

Closed sinsoku closed 8 months ago

sinsoku commented 8 months ago

Summary

columns and column_names cache the schema definition, so the return value will not change after a migration. However, the attributes method is always affected by the latest DB schema.

This behavior can cause problems when adding new columns in rolling deployments. This is because older revisions of the Rails server do not support the new columns.

Steps to reproduce

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem "rails"
  gem "sqlite3"
end

require "active_record"
require "minitest/autorun"
require "logger"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :posts, force: true do |t|
  end
end

class AddTitleToPost < ActiveRecord::Migration[7.1]
  def self.up
    add_column :posts, :title, :string
  end
end

class Post < ActiveRecord::Base
end

class BugTest < Minitest::Test
  def test_post_attributes
    Post.create!

    post1 = Post.first
    assert_equal({ "id" => post1.id }, post1.attributes)

    AddTitleToPost.up

    # `.columns` and `.column_names` returns column information before migration.
    assert_equal 1, Post.columns.size
    assert_equal ["id"], Post.column_names

    # Caching is effective if the same SQL is specified, so add SQL comments to avoid it.
    post2 = Post.annotate("after migration").first
    assert_equal({ "id" => post2.id }, post2.attributes)
    # Failure:
    # BugTest#test_post_attributes [issue.rb:51]:
    # Expected: {"id"=>1}
    #   Actual: {"id"=>1, "title"=>nil}
  end
end

Expected behavior

I think the attributes method should return a Hash that matches the information before migration, similar to columns and column_names.

Actual behavior

The attributes method includes the same columns as the latest DB schema.

System configuration

Rails version: v7.1.2

Ruby version: v3.3.0

Additional information

Using column_names allows you to use attributes safely in rolling deployments.

post.attributes.slice(*Post.column_names)

But this code is not cool.

kamipo commented 8 months ago

This is a side effect for SELECT * queries.

To respect staled before-migration columns, use ActiveRecord::Base.enumerate_columns_in_select_statements = true

Ref #41718.