# 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 $ 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" >> 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\"" >> 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 self.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; self.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 self.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}