# HoboFields - Migration Generator

Our test requires to prepare the testapp:
{.hidden}

    doctest_require: 'prepare_testapp'

{.hidden}

## 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

    $ script/generator 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"

Cleanup
{.hidden}

    >> Advert.field_specs.delete :price
{.hidden}


### 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"

Cleanup:
{.hidden}

        >> Advert.field_specs.delete(:category_id)
        >> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
{.hidden}

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]"

Cleanup:
{.hidden}

        >> Advert.field_specs.delete(:c_id)
        >> Advert.index_specs.delete_if {|spec| spec.fields==["c_id"]}
{.hidden}

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"

Cleanup:
{.hidden}

        >> Advert.field_specs.delete(:category_id)
        >> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
{.hidden}

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'"

Cleanup:
{.hidden}

        >> Advert.field_specs.delete(:category_id)
        >> Advert.index_specs.delete_if {|spec| spec.fields==["category_id"]}
{.hidden}

### 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"
        >>

Cleanup:
{.hidden}

        >> Advert.field_specs.delete(:updated_at)
        >> Advert.field_specs.delete(:created_at)
{.hidden}

### 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]'

Cleanup:
{.hidden}

        >> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
{.hidden}

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'

Cleanup:
{.hidden}

        >> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
{.hidden}

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'"

Cleanup:
{.hidden}

        >> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
{.hidden}

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]"

Cleanup:
{.hidden}

        >> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
{.hidden}

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'"

Cleanup:
{.hidden}

        >> Advert.index_specs.delete_if {|spec| spec.fields==["title"]}
{.hidden}

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]"

Cleanup:
{.hidden}

        >> Advert.index_specs.delete_if {|spec| spec.fields==["title", "category_id"]}
{.hidden}

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

As with renaming columns, we have to tell the migration generator about the rename. Here we create a new class 'Advertisement', and tell ActiveRecord to forget about the Advert class. This requires code that shouldn't be shown to impressionable children.
{.hidden}

    >>
     def nuke_model_class(klass)
       ActiveSupport::DescendantsTracker.instance_eval do
         class_variable_get('@@direct_descendants')[ActiveRecord::Base].delete(klass)
       end
      Object.instance_eval { remove_const klass.name.to_sym }
     end
    >> nuke_model_class(Advert)
{.hidden}

    >>
     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

    >> nuke_model_class(Advertisement)
{.hidden}

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"

Cleanup
{.hidden}

        >> Advert.field_specs.delete(:type)
        >> nuke_model_class(SuperFancyAdvert)
        >> nuke_model_class(FancyAdvert)
        >> Advert.index_specs.delete_if {|spec| spec.fields==["type"]}
{.hidden}


## 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

    >> nuke_model_class(Advert)
{.hidden}

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

     add_column :ads, :created_at, :datetime"

    >>
     class Advert < ActiveRecord::Base
       fields do
         body :text
         title :string, :default => "Untitled"
       end
     end
{.hidden}

## Legacy Keys

HoboFields has some support for legacy keys.

    >> Advert.field_specs.clear
    >>
     class Advert
       fields do
         name :string, :default => "No Name"
         body :text
       end
       set_primary_key "advert_id"
     end
    >> up, down = Generators::Hobo::Migration::Migrator.run(:adverts => {:id => :advert_id})
    >> up
    =>
     "rename_column :adverts, :id, :advert_id

    >> nuke_model_class(Advert)
    >> nuke_model_class(Ad)
    >> ActiveRecord::Base.connection.execute "drop table `adverts`;"
{.hidden}