HoboFields - Migration Generator

doctest_require: 'prepare_testapp'

The migration generator – introduction

The migration generator works by:

  • Loading all of the models in your Rails app
  • Using the Rails schema-dumper to extract information about the current state of the database.
  • Calculating the changes that are required to bring the database into sync with your application.

Normally you would run the migration generator as a regular Rails generator. You would type

$ rails generate hobo:migration

in your Rails app, and the migration file would be created in db/migrate.

In order to demonstrate the generator in this doctest script however, we’ll be using the Ruby API instead. The method Generators::Hobo::Migration::Migrator.run returns a pair of strings – the up migration and the down migration.

At the moment the database is empty and no ActiveRecord models exist, so the generator is going to tell us there is nothing to do.

>> Generators::Hobo::Migration::Migrator.run
=> ["", ""]

Models without fields do are ignored

The migration generator only takes into account classes that use HoboFields, i.e. classes with a fields do declaration. Models without this are ignored:

>> class Advert < ActiveRecord::Base; end
>> Generators::Hobo::Migration::Migrator.run
=> ["", ""]

You can also tell HoboFields to ignore additional tables. You can place this command in your environment.rb or elsewhere:

>> Generators::Hobo::Migration::Migrator.ignore_tables = ["green_fishes"]

Create the table

Here we see a simple create_table migration along with the drop_table down migration

>>
 class Advert < ActiveRecord::Base
   fields do
     name :string
   end
 end
>> up, down = Generators::Hobo::Migration::Migrator.run
>> up
=>
 "create_table :adverts do |t|
   t.string :name
 end"
>> down
=> "drop_table :adverts"

Normally we would run the generated migration with rake db:create. We can achieve the same effect directly in Ruby like this:

>> ActiveRecord::Migration.class_eval up
>> Advert.columns.*.name
=> ["id", "name"]

We’ll define a method to make that easier next time

>>
 def migrate(renames={})
   up, down = Generators::Hobo::Migration::Migrator.run(renames)
   ActiveRecord::Migration.class_eval(up)
   ActiveRecord::Base.send(:descendants).each { |model| model.reset_column_information }
   [up, down]
 end

We’ll have a look at the migration generator in more detail later, first we’ll have a look at the extra features HoboFields has added to the model.

Add fields

If we add a new field to the model, the migration generator will add it to the database.

>>
 class Advert
   fields do
     name :string
     body :text
     published_at :datetime
   end
 end
>> up, down = migrate
>> up
=>
 "add_column :adverts, :body, :text
 add_column :adverts, :published_at, :datetime"
>> down
=>
 "remove_column :adverts, :body
 remove_column :adverts, :published_at"
>>

Remove fields

If we remove a field from the model, the migration generator removes the database column. Note that we have to explicitly clear the known fields to achieve this in rdoctest – in a Rails context you would simply edit the file

>> Advert.field_specs.clear # not normally needed
 class Advert < ActiveRecord::Base
   fields do
     name :string
     body :text
   end
 end
>> up, down = migrate
>> up
=> "remove_column :adverts, :published_at"
>> down
=> "add_column :adverts, :published_at, :datetime"

Rename a field

Here we rename the name field to title. By default the generator sees this as removing name and adding title.

>> Advert.field_specs.clear # not normally needed
 class Advert < ActiveRecord::Base
   fields do
     title :string
     body :text
   end
 end
>> # Just generate - don't run the migration:
>> up, down = Generators::Hobo::Migration::Migrator.run
>> up
=>
 "add_column :adverts, :title, :string
 remove_column :adverts, :name"
>> down
=>""
remove_column :adverts, :title
add_column :adverts, :name, :string
>>

When run as a generator, the migration-generator won’t make this assumption. Instead it will prompt for user input to resolve the ambiguity. When using the Ruby API, we can ask for a rename instead of an add + drop by passing in a hash:

>> up, down = Generators::Hobo::Migration::Migrator.run(:adverts => { :name => :title })
>> up
=> "rename_column :adverts, :name, :title"
>> down
=> "rename_column :adverts, :title, :name"

Let’s apply that change to the database

>> migrate

Change a type

>>
 class Advert
   fields do
     title :text
     body :text
   end
 end
>> up, down = Generators::Hobo::Migration::Migrator.run
>> up
=> "change_column :adverts, :title, :text, :limit => nil"
>> down
=> "change_column :adverts, :title, :string"

Add a default

>>
 class Advert
   fields do
     title :string, :default => "Untitled"
     body :text
   end
 end
>> up, down = migrate
>> up.split(',').slice(0,3).join(',')
=> 'change_column :adverts, :title, :string'
>> up.split(',').slice(3,2).sort.join(',')
=> ' :default => "Untitled", :limit => 255'
>> down
=> "change_column :adverts, :title, :string"

Limits

>>
 class Advert
   fields do
     price :integer, :limit => 2
   end
 end
>> up, down = Generators::Hobo::Migration::Migrator.run
>> up
=> "add_column :adverts, :price, :integer, :limit => 2"

Note that limit on a decimal column is ignored (use :scale and :precision)

>>
 class Advert
   fields do
     price :decimal, :limit => 4
   end
 end
>> up, down = Generators::Hobo::Migration::Migrator.run
>> up
=> "add_column :adverts, :price, :decimal"

Foreign Keys

HoboFields extends the belongs_to macro so that it also declares the foreign-key field. It also generates an index on the field.

    >>
     class Advert
       belongs_to :category
     end
    >> up, down = Generators::Hobo::Migration::Migrator.run
    >> up
    =>
     "add_column :adverts, :category_id, :integer

     add_index :adverts, [:category_id]"
    >> down
    =>
     "remove_column :adverts, :category_id

     remove_index :adverts, :name => :index_adverts_on_category_id rescue ActiveRecord::StatementInvalid"

If you specify a custom foreign key, the migration generator observes that:

    >>
     class Advert
       belongs_to :category, :foreign_key => "c_id"
     end
    >> up, down = Generators::Hobo::Migration::Migrator.run
    >> up
    =>
     "add_column :adverts, :c_id, :integer

     add_index :adverts, [:c_id]"

You can avoid generating the index by specifying :index => false

    >>
     class Advert
       belongs_to :category, :index => false
     end
    >> up, down = Generators::Hobo::Migration::Migrator.run
    >> up
    => "add_column :adverts, :category_id, :integer"

You can specify the index name with :index

    >>
     class Advert
       belongs_to :category, :index => 'my_index'
     end
    >> up, down = Generators::Hobo::Migration::Migrator.run
    >> up
    =>
     "add_column :adverts, :category_id, :integer

     add_index :adverts, [:category_id], :name => 'my_index'"

Timestamps

updated_at and created_at can be declared with the shorthand timestamps

    >>
     class Advert
       fields do
         timestamps
       end
     end
    >> up, down = Generators::Hobo::Migration::Migrator.run
    >> up
    =>
     "add_column :adverts, :created_at, :datetime
     add_column :adverts, :updated_at, :datetime"
    >> down
    =>
     "remove_column :adverts, :created_at
     remove_column :adverts, :updated_at"
    >>

Indices

You can add an index to a field definition

    >>
     class Advert
       fields do
         title :string, :index => true
       end
     end
    >> up, down = Generators::Hobo::Migration::Migrator.run
    >> up.split("\n")[2]
    => 'add_index :adverts, [:title]'

You can ask for a unique index

    >>
     class Advert
       fields do
         title :string, :index => true, :unique => true
       end
     end
    >> up, down = Generators::Hobo::Migration::Migrator.run
    >> up.split("\n")[2]
    => 'add_index :adverts, [:title], :unique => true'

You can specify the name for the index

    >>
     class Advert
       fields do
         title :string, :index => 'my_index'
       end
     end
    >> up, down = Generators::Hobo::Migration::Migrator.run
    >> up.split("\n")[2]
    => "add_index :adverts, [:title], :name => 'my_index'"

You can ask for an index outside of the fields block

    >>
     class Advert
       index :title
     end
    >> up, down = Generators::Hobo::Migration::Migrator.run
    >> up.split("\n")[2]
    => "add_index :adverts, [:title]"

The available options for the index function are :unique and :name

    >>
     class Advert
       index :title, :unique => true, :name => 'my_index'
     end
    >> up, down = Generators::Hobo::Migration::Migrator.run
    >> up.split("\n")[2]
    => "add_index :adverts, [:title], :unique => true, :name => 'my_index'"

You can create an index on more than one field

    >>
     class Advert
       index [:title, :category_id]
     end
    >> up, down = Generators::Hobo::Migration::Migrator.run
    >> up.split("\n")[2]
    => "add_index :adverts, [:title, :category_id]"

Finally, you can specify that the migration generator should completely ignore an index by passing its name to ignore_index in the model. This is helpful for preserving indices that can’t be automatically generated, such as prefix indices in MySQL.

Rename a table

The migration generator respects the set_table_name declaration, although as before, we need to explicitly tell the generator that we want a rename rather than a create and a drop.

>>
 class Advert
   set_table_name "ads"
   fields do
     title :string, :default => "Untitled"
     body :text
   end
 end
>> up, down = Generators::Hobo::Migration::Migrator.run(:adverts => :ads)
>> up
=> "rename_table :adverts, :ads"
>> down
=> "rename_table :ads, :adverts"

Set the table name back to what it should be and confirm we’re in sync:

>> class Advert; set_table_name "adverts"; end
>> Generators::Hobo::Migration::Migrator.run
=> ["", ""]

Rename a table

>>
 class Advertisement < ActiveRecord::Base
   fields do
     title :string, :default => "Untitled"
     body :text
   end
 end
>> up, down = Generators::Hobo::Migration::Migrator.run(:adverts => :advertisements)
>> up
=> "rename_table :adverts, :advertisements"
>> down
=> "rename_table :advertisements, :adverts"

Drop a table

If you delete a model, the migration generator will create a drop_table migration.

Dropping tables is where the automatic down-migration really comes in handy:

>> up, down = Generators::Hobo::Migration::Migrator.run
>> up
=> "drop_table :adverts"
>> down
=>
 "create_table "adverts", :force => true do |t|
   t.text   "body"
   t.string "title", :default => "Untitled"
 end"

STI

Adding an STI subclass

Adding a subclass or two should introduce the ‘type’ column and no other changes

    >>
     class Advert < ActiveRecord::Base
       fields do
         body :text
         title :string, :default => "Untitled"
       end
     end
     class FancyAdvert < Advert
     end
     class SuperFancyAdvert < FancyAdvert
     end
    >> up, down = Generators::Hobo::Migration::Migrator.run
    >> up
    =>
     "add_column :adverts, :type, :string

     add_index :adverts, [:type]"
    >> down
    =>
     "remove_column :adverts, :type

     remove_index :adverts, :name => :index_adverts_on_type rescue ActiveRecord::StatementInvalid"

Coping with multiple changes

The migration generator is designed to create complete migrations even if many changes to the models have taken place.

First let’s confirm we’re in a known state. One model, ‘Advert’, with a string ‘title’ and text ‘body’:

>> Advert.connection.schema_cache.clear!
>> Advert.reset_column_information
>> Advert.connection.tables
=> ["adverts"]
>> Advert.columns.*.name
=> ["id", "body", "title"]
>> Generators::Hobo::Migration::Migrator.run
=> ["", ""]

Rename a column and change the default

>> Advert.field_specs.clear
>>
 class Advert
   fields do
     name :string, :default => "No Name"
     body :text
   end
 end
>> up, down = Generators::Hobo::Migration::Migrator.run(:adverts => {:title => :name})
>> up
=>
 "rename_column :adverts, :title, :name
 change_column :adverts, :name, :string, :default => "No Name", :limit => 255"
>> down
=>
 'rename_column :adverts, :name, :title
 change_column :adverts, :title, :string, :default => "Untitled"'

Rename a table and add a column

Legacy Keys

HoboFields has some support for legacy keys.


Edit this page