Tuesday, June 3, 2008

Asymmetrical database structures between production and non-production environments

So long time ago, before I was born as a consultant, there was some specific needs for the database tables. What we need was a table to mock external databases that we don't have access to in the development environment but the production does. What we did was creating a table right in the test cases and deal with it there.

Problems followed.

For one, MySQL and Rails doesn't have support for nested transaction.
For two, All tests written are in transaction that is rolled back every time the method ends.
For three, we need to ensure that the implementation employs the transaction mechanism somehow.

Whenever we create a new table, the transaction stops working. Whatever we did to the database, it was showing in the later test methods. Therefore there is no way we can ensure that when the transaction was supposed to be rolled back, it was indeed rolled back.

Solution:

The best we could do at that time was clearing out the tables ourselves. Andy Kotlinski wrote an assertion called assert_in_transaction to work around the problem. Talk to him if you would like more information on it.

Several months passed. The project was shelved for whatever reason.
Yet other months passed. The client decided to dig the zombie out of the grave.

All hell broke lose.

Somehow, Rails 2.0 doesn't allow us to work around it the way we did any more. The reason was something I couldn't remember any more because I forgot to blog about it at the time of digging that grave. What do we do?

Solution:

Instead of creating the tables in the test methods, I thought of a different way.

I want to have the transaction working.
I hate putting Data Description Language into the tests.
I like the "Elegance: Simple and Powerful", quoting Dr. David Matuszek from the University of Pennsylvania.

It turns out that we completely overlooked the power of Ruby. Database migrations are Ruby files. It means we can do pretty much anything a Ruby file can do. So we employ a simple condition to the migration

if RAILS_ENV == 'test'
create_your_fancy_test_tables
else
do_some_funny_stuff
end

Not bad, but still, not good. Why? Because we also want it in the development database too. And what happens when you have 8 different environments and you have to hand pick them?

Take two:

unless ['production'].include? RAILS_ENV do
create_your_fancy_test_tables
else
do_some_funny_stuff
end

Better. But still, what happens when you have 20 different migrations ahead?

Take three:

Define a module like so:

module SkipEnv
def skip_env
unless ['production'].include? RAILS_ENV do
yield
end
end
end

Put the module in the lib directory. Then, in the migration:

class << self do; include SkipEnv; end

def self.up
skip_env do
create_your_fancy_test_tables
end
end

def self.down
skip_env do
drop_your_fancy_test_tables
end
end

Hmm.. something's missing: the do_your_fancy_stuff. It turns out, our do_your_fancy_stuff does absolutely nothing, so we just cut it out.

It has to be a class method because the migration up and down methods are class methods.

Alright, looks good. We can create test tables anywhere except production, or any other environments you put into the array, and we can choose to skip environments in anything having access to the lib folder of the rails app, not only migrations. Awesome.

Further refinement would be to put it as a plugin, and install it in any Rails app you happen to have the need for such mechanism.

We didn't do that, because the zombie was buried back in the ground... again. This time, the reason is that to fix it and make it works for Rails 2 and have ActiveResource support, we would need to take a month and work on it full time.

No comments: