Don’t YAML on, man (in Rails 4)

Last time, I made a simple model called Lightswitch so that I could play with mutable state:

class Lightswitch < ActiveRecord::Base
  attr_accessor :turned_on
  
  def turn_on
    self.turned_on = true
  end
end

When I first wrote the model, not wanting to suggest that my electrical fixtures were aroused, I named the attribute on instead of turned_on. I immediately saw some strange errors. rake test started complaining that there was no column named true. Today, I’m going to reproduce this error on purpose so that I will understand why, specifically, I can’t use on as a column name with active record.

So, to start with, we need a new model. Let’s call it Lightbulb:

will@willsbox testing_stuff (master)*$ rails g model Lightbulb on:boolean watts:integer
Running via Spring preloader in process 81441
      invoke  active_record
      create    db/migrate/20170523110420_create_lightbulbs.rb
      create    app/models/lightbulb.rb
      invoke    minitest
      create      test/models/lightbulb_test.rb
      create      test/fixtures/lightbulbs.yml
will@willsbox testing_stuff (master)*$ rake db:migrate RAILS_ENV=test      

So far so good. The migrate succeeds. Here’s an example of the output of rake test however:

13) Error:
LightbulbTest#test_valid:
ActiveRecord::Fixture::FixtureError: table "lightbulbs" has no column named "true".

The fixtures file lightbulb.yml looks like this:

one:
  on: false
  watts: 1

two:
  on: false
  watts: 1

Why the heck does it think there’s a column named true? To start digging, I went here to where ActiveRecord raises a FixtureError if there’s no column named true. The next thing I want to do is tell minitest to dump a whole stack trace when encountering this error, rather than just reporting that it failed.

Therefore, in test_helper.rb, I added this:

Minitest::Reporters.use!(
  Minitest::Reporters::DefaultReporter.new,
  ENV,
  Minitest::ExtensibleBacktraceFilter.new
)

This is not what you should do in a real project – it causes minitest to print an unfiltered stack trace for every test failure or error. This tells me that to take a look in active record’s activerecord-4.2.7.1/lib/active_record/fixtures.rb, where I changed this:

table_rows.each do |fixture_set_name, rows|
  rows.each do |row|
    conn.insert_fixture(row, fixture_set_name)
  end
end

to this:

table_rows.each do |fixture_set_name, rows|
  rows.each do |row|
    begin
      conn.insert_fixture(row, fixture_set_name)
    rescue ActiveRecord::Fixture::FixtureError => e
      puts "Failed to insert row"
      puts row.inspect
      puts "With fixture set name"
      puts fixture_set_name.inspect
    end
  end
end

and sure enough, in my test output, I start to see:

Failed to insert row
{true=>false, "watts"=>1, "created_at"=>"2017-05-23 11:47:52", 
"updated_at"=>"2017-05-23 11:47:52", "id"=>980190962}

It looks like whatever initializes table_rows is confused by having a column named on. I keep digging.

A little later, I went to the very helpful DC Ruby User Group, and fortunately someone there knew that on and off are reserved words in YAML for some reason, so when rails was parsing the YAML above, it would end up with {true => true} in the hash, so of course the insert would fail because there’s no column named true.

Note: this can also mess up translation files, as Julian Kniephoff pointed out.

I’m a little bit frustrated that rails g model ... will sometimes make a YAML file that rails itself can’t interpret.

I tried to reproduce this issue in rails 5.1, and it’s fixed! The new rails g model ... will emit yaml like this:

one:
  'on': false

Anyway, that was fun! Till next time, happy learning!

-Will

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s