Skip to content

CommandRecorder doesn't invoke recorded commands correctly on Ruby 3 #42653

@JoshCheek

Description

@JoshCheek

The ActiveRecord::Migration::CommandRecorder records commands and then replays them later. However it does not invoke the original commands correctly on Ruby 3, because it does not consider keyword arguments.

Steps to reproduce

require 'active_record'
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'

class ActiveRecord::Migration::CommandRecorder
  def update_view(*args) = record(:update_view, args)   # https://github.com/scenic-views/scenic/blob/v1.5.4/lib/scenic/command_recorder.rb#L14
  def invert_update_view(*args) = [:update_view, *args] # https://github.com/scenic-views/scenic/blob/v1.5.4/lib/scenic/command_recorder.rb#L31
end

# https://github.com/scenic-views/scenic/blob/v1.5.4/lib/scenic/statements.rb#L25
class ActiveRecord::ConnectionAdapters::SQLite3Adapter
  def update_view(name, version:, revert_to_version:) =
    puts("UPDATE VIEW: #{name}, version: #{version}, revert_to_version: #{revert_to_version}")
end

class SomeMigration < ActiveRecord::Migration[6.1]
  def change() = update_view(:my_view, version: 2, revert_to_version: 1)
end

pp RUBY_VERSION: RUBY_VERSION, 'ActiveRecord::VERSION': ActiveRecord::VERSION::STRING
puts
SomeMigration.migrate :down

Expected behavior

$ ruby example.rb
{:RUBY_VERSION=>"3.0.1", :"ActiveRecord::VERSION"=>"6.1.3.2"}

==  SomeMigration: reverting ==================================================
-- update_view(:my_view, {:version=>2, :revert_to_version=>1})
UPDATE VIEW: my_view, version: 2, revert_to_version: 1
   -> 0.0000s
==  SomeMigration: reverted (0.0001s) =========================================

Actual behavior

$ ruby example.rb
{:RUBY_VERSION=>"3.0.1", :"ActiveRecord::VERSION"=>"6.1.3.2"}

==  SomeMigration: reverting ==================================================
-- update_view(:my_view, {:version=>2, :revert_to_version=>1})
ar.example.rb:11:in `update_view': wrong number of arguments (given 2, expected 1; required keywords: version, revert_to_version) (ArgumentError)
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/migration.rb:931:in `block in method_missing'
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/migration.rb:899:in `block in say_with_time'
	from /Users/josh/.rubies/ruby-3.0.1/lib/ruby/3.0.0/benchmark.rb:293:in `measure'
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/migration.rb:899:in `say_with_time'
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/migration.rb:920:in `method_missing'
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/migration/command_recorder.rb:126:in `block in replay'
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/migration/command_recorder.rb:125:in `each'
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/migration/command_recorder.rb:125:in `replay'
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/migration.rb:743:in `revert'
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/migration.rb:867:in `exec_migration'
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/migration.rb:853:in `block (2 levels) in migrate'
	from /Users/josh/.rubies/ruby-3.0.1/lib/ruby/3.0.0/benchmark.rb:293:in `measure'
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/migration.rb:852:in `block in migrate'
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/connection_adapters/abstract/connection_pool.rb:462:in `with_connection'
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/migration.rb:851:in `migrate'
	from /Users/josh/.gem/ruby/3.0.1/gems/activerecord-6.1.3.2/lib/active_record/migration.rb:665:in `migrate'
	from ar.example.rb:24:in `<main>'

System configuration

Rails version: 6.1.3.2

Ruby version: ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [arm64-darwin20]

Fixing it

For the "expected behaviour" step, I got it to work by changing the replay method to this (I'm not asserting this is correct, I find this keyword change very confusing, TBH):

class ActiveRecord::Migration::CommandRecorder
  def replay(migration)
    commands.each do |cmd, args, block|
      if args.last.respond_to?(:to_hash) && args.last.to_hash.all? { |k, _| k.is_a? Symbol }
        *ordinals, keywords = args
      else
        ordinals, keywords = args, {}
      end
      migration.send(cmd, *ordinals, **keywords, &block)
    end
  end
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions