Rubynating

Thursday, January 11, 2007

Rails Migrations aren't atomic

This note is applicable to Rails 1.1.6.5618 (which is really Rails 1.2 Release Candidate)

If you use the migrations feature to keep track of and control schema changes, you should be aware that a migration involves two steps and that these aren't atomic. This can come to bite you as we shall see.

First, what are the two steps?

Step 1: Make the changes to your application tables
Step 2: Update the schema_info table

Suppose that your schema version is currently 3 i.e. the value of the version field of the schema_info table is 3. Next say, you have developed the following schema change (004_add_create_date_and_usage.rb)

class AddCreateDateAndUsage < ActiveRecord::Migration      
def self.up
add_column :mytable, :created_at, :datetime
add_column :mytable, :usage_count, number
end
def self.down
remove_column :mytable, :created_at
remove_column :mytable, :usage_count
end
end


When you run this by issuing the command rake db:migrate it will obviously fail when it tries to add the field usage_count since number is not a valid type. The rake task quits at this stage. If you check the database you will see that the table mytable now has a created_at field, no usage_count field and that the schema_info table says the version is still 3. Oops!

No problem, you think. I'll just run rake db:migrate VERSION=3 and all will be back to the way it was. You can then fix the mistake and run the migration again. WRONG ANSWER!

The trouble is that since SCHEMA_INFO.VERSION=3 running rake db:migrate VERSION=3 does nothing!

The way around it to manually edit the schema_info table and set the value of version to 4 and then run rake db:migrate VERSION=3.

Tuesday, July 18, 2006

Rails views use GET requests to modify state

It is important to note that default views such as for a list action generated by Rails scaffolding breaks a cardinal rule of HTTP-based processing -- that a GET request shall not modify the state. Said differently, one should be able to issue the same GET request any number of times and get the same response.

If you look at the view generated for the list action, Rails uses a link_to tag on the RHTML page for the destroy action which results in a GET request being issued. This has repercussions that go beyond the pedantic finger-wagging sermon on violating "cardinal rules".

Consider a Rails application you have developed that includes the default view for listing Employees from the EMPLOYEES table. The view includes a link each to edit and delete a given row. Now if your web application URL is accessible by a web crawler, it will merrily crawl away your entire EMPLOYEES table!

And no -- using a JavaScript confirmation isn't a safeguard since the web-crawler is like a user with JavaScript disabled!

Saturday, August 20, 2005

Getting Rails and MySQL to play nice

Working on my first Rails application has been quite an experience -- and not the kind you think.

I've been following along with Dave Thomas' Rails book. It provides a gentle introduction to the various Rails idioms and has proven handy. Following the spirit of the instructions, I created the necessary database and tables in my MySQL instance. Chapter 6 explains that in order for Rails to communicate with a database one has to specify connection settings in the config/database.yml file. So, I provide the following

development:
adapter: mysql
database: cc_dev
host: localhost
username:
password:

I issued the command

proj_root> ruby script/generate scaffold Project Add

expecting it to create a Project model object (associated with the PROJECTS table that I had already created in MySQL) that would be accessed through the Add controller. However, after a few create messages on the console, I got the following message:

No connection could be made because the target machine actively refused it. - connect(2)

Huh? Wasn't exactly the response I was expecting. I spent a long time trying various things such as: providing a username and password, providing an IP address instead of localhost etc, etc, I reached out to Google. There I came across a post by Jared Richardson on the same topic. While my context was different it suggested a cure: comment out the line

skip-networking

in the my.cnf file. Never having played in the bowels of MySQL, I found that on WinXP, that translates to the MYSQL_INSTALL_DIR\my.ini file. Since it did not say anything about skipping networking, I added it for good measure and commented it out by prefixing the line with a # character. Still no dice.

Now it just so happens that Jared's a colleague and a friend (small world). I wrote to him about my impasse and he suggested specifying the port in the database.yml file. I was skeptical -- since the Rails crowd had been drilling in the virtues of convention over configuration; I was thinking -- surely Rails would use defaults like with most other things. Anyway, I had wasted several hours on this problem and I had nothing to lose. So I added the line

port: 3306

so, my database.yml now looks like

development:
adapter: mysql
database: cc_dev
host: localhost
port: 3306
username:
password:

Paydirt! Running the scaffold command again ran through to completion! Hope that helps you and saves you few hours.