Plip's Weblog

Phil Winstanley - British .NET chap based in Lancashire. Enjoys tea and tech. Working for Microsoft.

Migrations - they're lovely

We're not talking about swallows (African, European or otherwise) but Database Migrations that ship with Ruby on Rails.

There's a cool bit of functionality which lets you stores database schema and incrimental changes in code files as well as a framework which offers the ability to roll the database forwards or backwards through versions.

The code is really simple, here's some code which creates a Users table: -

class AddUsersTable < ActiveRecord::Migration
  def self.up
    create_table :users, :id => false do |table|
      table.column :UserId, :integer
      table.column :Username, :string
      table.column :Password, :string
      table.column :EmailAddress, :string
    end
  end
  def self.down
    drop_table :users
  end
end

So, what does this do? Well this code is stored in a file inside my migrations folder called "001_add_users_table.rb". Take note of the "001" that indicates that this file is the first inside my Migration, it's the first step to creating my schema.

Notice there's a self.down towards the bottom of the file, this is executed when the database schema is rolled backwards to version 0 (remember that file is version 1 so version 0 is the database before version 1 ran, thus we need to drop the table).

I can tell the framework to change my database to a particular version by executing the following command line: -

rake migration VERSION=1

The framework stores the current database "version number" inside the database in a special single column single row table.

So, when I run that command line the following SQL get's executed against my SQL Server.

set implicit_transactions on 
go
CREATE TABLE schema_info (version int)
go
select * from CREATE TABLE schema_info (version int)
go
CREATE TABLE schema_info (version int)
go
select * from CREATE TABLE schema_info (version int)
go
CREATE TABLE schema_info (version int)
go
select * from CREATE TABLE schema_info (version int)
go
SELECT COLUMN_NAME as ColName, COLUMN_DEFAULT as DefaultValue, 
	DATA_TYPE as ColType, IS_NULLABLE As IsNullable, 
	COL_LENGTH('schema_info', COLUMN_NAME) as Length, 
	COLUMNPROPERTY(OBJECT_ID('schema_info'), COLUMN_NAME, 'IsIdentity') as IsIdentity, 
	NUMERIC_SCALE as Scale 
FROM INFORMATION_SCHEMA.COLUMNS 
WHERE TABLE_NAME = 'schema_info'
go
SELECT TOP 1 version FROM schema_info
go
CREATE TABLE schema_info (version int)
go
select * from CREATE TABLE schema_info (version int)
go
CREATE TABLE schema_info (version int)
go
select * from CREATE TABLE schema_info (version int)
go
CREATE TABLE schema_info (version int)
go
select * from CREATE TABLE schema_info (version int)
go
SELECT TOP 1 version FROM schema_info
go
CREATE TABLE users ([UserId] int, 
			[Username] varchar(255), 
			[Password] varchar(255), 
			[EmailAddress] varchar(255)) 
go
IF @@TRANCOUNT > 0 COMMIT TRAN
go
UPDATE schema_info SET version = 1
go
IF @@TRANCOUNT > 0 COMMIT TRAN
go
SELECT @@ROWCOUNT AS AffectedRows
go
SELECT TOP 1 * FROM schema_info
go
SELECT table_name from information_schema.tables WHERE table_type = 'BASE TABLE'
go
SELECT COLUMN_NAME as ColName, COLUMN_DEFAULT as DefaultValue, 
	DATA_TYPE as ColType, IS_NULLABLE As IsNullable, 
	COL_LENGTH('users', COLUMN_NAME) as Length, 
	COLUMNPROPERTY(OBJECT_ID('users'), COLUMN_NAME, 'IsIdentity') as IsIdentity, 
	NUMERIC_SCALE as Scale 
FROM INFORMATION_SCHEMA.COLUMNS 
WHERE TABLE_NAME = 'users'
go
EXEC sp_helpindex users
go
IF @@TRANCOUNT > 0 COMMIT TRAN
go

Now, when I run the reverse command and roll the system back I run this: -

rake migration VERSION=0

Which in turn pumps the following SQL into the database: -

set implicit_transactions on 
go
CREATE TABLE schema_info (version int)
go
select * from CREATE TABLE schema_info (version int)
go
CREATE TABLE schema_info (version int)
go
select * from CREATE TABLE schema_info (version int)
go
CREATE TABLE schema_info (version int)
go
select * from CREATE TABLE schema_info (version int)
go
SELECT COLUMN_NAME as ColName, COLUMN_DEFAULT as DefaultValue, 
	DATA_TYPE as ColType, IS_NULLABLE As IsNullable, 
	COL_LENGTH('schema_info', COLUMN_NAME) as Length, 
	COLUMNPROPERTY(OBJECT_ID('schema_info'), COLUMN_NAME, 'IsIdentity') as IsIdentity, 
	NUMERIC_SCALE as Scale 
FROM INFORMATION_SCHEMA.COLUMNS 
WHERE TABLE_NAME = 'schema_info'
go
SELECT TOP 1 version FROM schema_info
go
SELECT TOP 1 version FROM schema_info
go
CREATE TABLE schema_info (version int)
go
select * from CREATE TABLE schema_info (version int)
go
CREATE TABLE schema_info (version int)
go
select * from CREATE TABLE schema_info (version int)
go
CREATE TABLE schema_info (version int)
go
select * from CREATE TABLE schema_info (version int)
go
SELECT TOP 1 version FROM schema_info
go
SELECT TOP 1 version FROM schema_info
go
SELECT TOP 1 version FROM schema_info
go
SELECT TOP 1 version FROM schema_info
go
SELECT TOP 1 version FROM schema_info
go
DROP TABLE users
go
IF @@TRANCOUNT > 0 COMMIT TRAN
go
UPDATE schema_info SET version = 0
go
IF @@TRANCOUNT > 0 COMMIT TRAN
go
SELECT @@ROWCOUNT AS AffectedRows
go
SELECT TOP 1 * FROM schema_info
go
SELECT table_name from information_schema.tables WHERE table_type = 'BASE TABLE'
go
IF @@TRANCOUNT > 0 ROLLBACK TRAN
go

Pretty cool isn't it? :-)

Well - why does this matter? Just think source control and you'll have the same.

Comments

James said:

What if there's data in the tables?

# October 12, 2006 11:40 PM

Scott said:

James: my experience is the data is saved. If you are changing a data type in the table the normal errors apply if you try converting to an incompatible type. admittedly, I've only worked with ActiveRecord migratins with MySQL. Using SQL Server the experience may vary.

I too love migrations, Django has something similar. I'm always amazed at how many Rails clones I see my ASP.NET, JSF, etc... But no one ever tries to clone migrations. Between that and the built in unit testing, I can deal with all the aligators in my HTML templates. Even if it does remind me of ASP development(shudder)

# October 13, 2006 11:56 AM

Graham Pengelly said:

I've been noodling with the .Net attempts at implementing some of the Rails ideas. Have a look at Subsonic on Codeplex (http://www.codeplex.com/Wiki/View.aspx?ProjectName=actionpack) It has got a simple but apparently effective DB migration tool that emits a sql file for the schema and one for the data that can then go into source control.

# October 13, 2006 1:21 PM

Shane said:

For .NET people considering Rails, this might be an interesting read:

http://www.infoq.com/articles/Netter-on-Rails

Have fun :)

# October 13, 2006 4:52 PM

Andrew said:

Why did you write this? Seriously there are already a million intro to Rails Migrations pages on the internet that say exactly the same thing. This page is just noise.

# November 1, 2006 11:45 PM

Daniel said:

Andrew >>

Why did you write that comment? Seriously there are already a million comments on the internet that say exactly the same thing. That comment is just noise.

# November 13, 2006 4:56 AM

Giva said:

Could you help me. Humor is the only test of gravity, and gravity of humor; for a subject which will not bear raillery is suspicious, and a jest which will not bear serious examination is false wit. Help me! Please help find sites for: Coupon for turbo tax. I found only this - <a href="turbo-tax.biz/.../">turbo tax form 3506</a>. Turbo tax, it's a picture that decides a natural search or impending buck and a 6-speed software to broadband. Turbo tax, in chart to this you far need quark guitar spots, devices and arrangement boat negotiations. With best wishes :-(, Giva from Benin.

# March 26, 2010 2:27 PM