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