Tuesday, November 06, 2007

Enhanced Migrations v1.2.0

The original release of this highly useful plugin marked a turning point in collaborative Rails development by freeing the developer to commit their database migration without fear of having it ignored because of a higher placed migration number. This latest release includes some minor bug fixes plus a useful method for stepping through migrations one at a time without the need for copying and pasting long version numbers.

  1. Fixed bug where an empty migrations_info table would create a non-parseable schema.rb.
  2. Made plugin database independent.
  3. Added capability to step through migrations using VERSION=[previous, next, first and last].
  4. dump_schema_information now returns all migrations, not just latest (credit to François Beausolei).
  5. Added tests which use SQLite and a task (enhanced_migrations:clean_sqlite_db) to help with testing.
As an example of item number three, consider the following situation. Using the enhanced migrations plugin, you've just created a migration that adds a new table to your database. Upon running it, you discover that you used the wrong data type for a column. Rather than having to copy and paste the previous migration's version number, simply using 'previous' in the VERSION number will now suffice.

rake db:migrate VERSION=previous
This also works using prev along with next, first and last for their respective operations. Keep in mind that first will go to the first migration and is not the same as VERSION=0.

Migrate as usual

shell> rake db:migrate
== CreateRecipesTable: migrating ==============================================
-- create_table(:recipes)
-> 0.0902s
== CreateRecipesTable: migrated (0.0904s) =====================================

== AddRecipesForUser1: migrating ==============================================
-- execute("INSERT INTO recipes (name, owner) VALUES ('Lemon Meringue Pie', 'user1')")
-> 0.3684s
== AddRecipesForUser1: migrated (0.5302s) =====================================

== AddRecipesForUser2: migrating ==============================================
-- execute("INSERT INTO recipes (name, owner) VALUES ('Steak and Kidney Pie', 'user2')")
-> 0.2574s
== AddRecipesForUser2: migrated (0.3962s) =====================================

Migrate to the previous version

shell> rake db:migrate VERSION=previous
== AddRecipesForUser2: reverting ==============================================
-- execute("DELETE FROM recipes WHERE owner = 'user2'")
-> 0.4512s
== AddRecipesForUser2: reverted (0.4516s) =====================================

Migrate to the first version

shell> rake db:migrate VERSION=first
== AddRecipesForUser1: reverting ==============================================
-- execute("DELETE FROM recipes WHERE owner = 'user1'")
-> 0.1676s
== AddRecipesForUser1: reverted (0.1678s) =====================================

Migrate another previous version, essentially the same as VERSION=0 since we are already at the first migration.

shell> rake db:migrate VERSION=previous
== CreateRecipesTable: reverting ==============================================
-- drop_table(:recipes)
-> 0.1680s
== CreateRecipesTable: reverted (0.1683s) =====================================

How to get
Download the gem from Rubyforge and install it like so:
sudo gem install enhanced_migrations-1.2.1.gem
Update - 11/15/2007
The previous version had a small bug that was discovered during use here that sometimes caused an app's database yaml file to be overwritten by the gem's database config which is used only for testing. The chances of this happening outside of our particular setup are small but we felt it warranted an immediate fix. We've also added a gzip'd source file for the plugin fans out there.

Links in the post have been updated to reflect the location of this new release.

13 comments:

Aaron Batalion said...

Sean,

Take a look at this recent commit on edge.
http://dev.rubyonrails.org/changeset/8039

I think the syntax db:rollback or db:rollback STEP=3 is pretty clean.

Thoughts?
Aaron

Sean said...

That is pretty clean actually. I'll put that on the TODO for the next release.

Thanks for the heads up.

-sean

Brian said...

Do you have to install it as a Gem or can you install it as a plugin instead?

Sean said...

Brian,
The latest release includes a source tgz file which you should be able to put in your vendor/plugins dir. Let us know if you run into any issues using it!

Unknown said...

Question: How do your enhanced migrations handle version dependencies? Is it the timestamp in version control? I.e. if I'm starting from scratch with a new db and run all the migrations, how does it determine order?

Thanks in advance.

Sean said...

Ken,
The timestamp is generated from GMT microtime so that the chance of a collision between team members is extremely low. Order of the migrations is determined by the increasing value of the timestamp.

Not sure what you mean by version dependencies, care to clarify?

-sean

Unknown said...

Hi Sean, thanks for the clarification.

When I say version dependencies, I mean that a migration that renames a column is dependent on a previous migration that created the column, which is in turn dependent on the migration that created the table.

It sounds like you're just extending the default increasing-integer scheme with a more granular counter, but still assuming that all previous migrations are necessary. That's fine and answers my question. Thanks!

Do you find you get many "real" collisions, i.e. two teams try to rename the same column? Do you have policies or communication protocols in place to prevent them?


K

Sean said...

Ken,
Now I understand what you're asking. In fact, Enhanced Migrations runs all previous migrations that have not been run regardless of time stamp.

Between our team of about 20 people, where a half-dozen people are working on the same project at any given time, we have yet to run into any collisions.

-sean

Anonymous said...

I found an issue with the dump_schema_information. We use foreign keys and the sql schema format, but for some reason our specs running on Rails 1.2 with MySQL 5 were erroring on the syntax for the migration insertion statements.

We fixed it by editing line 62 in the latest gem code, adding an additional newline in the print statement:

"INSERT INTO #{ActiveRecord::Migrator.schema_info_table_name} VALUES(#{migration["id"]}, '#{migration["created_at"]}');\n\n"


Hope this helps

-Zach

Sean said...

Zach,
Thanks for the code fix, we'll make the change and push it out in the next version.

Kris Nuttycombe said...

I also ran into some problems using the redhillonrails_core plugin in combination with the enhanced_migrations gem. The redhillonrails_core plugin uses alias_method_chain to chain some additional functionality onto SchemaDumper#tables, and this gets overwritten when the gem loads (because you use SchemaDumper.send(:define_method, :tables)...

I fixed the problem by changing the enhanced_migrations gem to use alias_method_chain as well, so it doesn't stomp on redhillonrails (or whatever else might be chained in there). Here's the code (can provide as a patch if needed)


ActiveRecord::SchemaDumper.class_eval do
def tables_with_enhanced_migrations(stream)
ignore_tables << "migrations_info"
tables_without_enhanced_migrations(stream)
end
alias_method_chain :tables, :enhanced_migrations
end

Sergio said...

Hi,
when using this plugin a had an error doing a migration, because the SQL query is using the NOW() function to get the current timestamp and that's not supported by sqlite. The solution I found is to replace the NOW()'s with the ruby equivalent Time.now.to_i.
Maybe this issue is resolved in Rails 2.1, but just in case... hope this can be useful to somebody.

Sean said...

Kris,
We're no longer supporting the Enhanced Migrations plugin due to Rails 2.1 native support of timestamp-based migrations. Please check out this most excellent post about migrating your app to use the new migrations.