abstract class Jennifer::Migration::Base

Overview

Migrations can manage the evolution of a schema used by a physical databases. It's a solution to the common problem of adding a field to make a new feature work in your local database, but being unsure of how to push that change to other developers and to the production server. With migrations, you can describe the transformations in self-contained classes that can be checked into version control systems and executed against another database that might be one, two, or five versions behind.

Example of a simple migration:

class AddMainFlagToContacts < Jennifer::Migration::Base
  def up
    create_table(::contacts) do |t|
      t.string :name
      t.string :number, {:null => false}
    end
  end

  def down
    drop_table(::contacts)
  end
end

This migration will add a boolean flag to the contacts table and remove it if you're backing out of the migration. It shows how all migrations have two methods #up and #down that describes the transformations required to implement or remove the migration. These methods can consist of both the migration specific methods like #create_table and #drop_table, but may also contain regular Crystal code for generating data needed for the transformations.

By default each migration invocation (both #up and #down) are wrapped into a transaction but not all RDBMS support transactional schema changes (like MySQL). To specify additional mechanism to rollback after failed invocation you can chose 2 option: run reverse method (#down for #up and vise verse) or invoke special callback.

Using reverse direction:

Jennifer::Config.configure do |conf|
  # ...
  conf.migration_failure_handler_method = "reverse_direction"
end

class AddMainFlagToContacts < Jennifer::Migration::Base
  def up
    change_table(::contacts) do |t|
      t.add_column :main, :bool, default: true
    end
  end

  def down
    change_table(::contacts) do |t|
      t.drop_column :main if column_exists?(:contacts, :main)
    end
  end
end

Using callbacks:

Jennifer::Config.configure do |conf|
  # ...
  conf.migration_failure_handler_method = "callback"
end

class AddMainFlagToContacts < Jennifer::Migration::Base
  def up
    change_table(::contacts) do |t|
      t.add_column :main, :bool, default: true
    end
  end

  def after_up_failure
    change_table(::contacts) do |t|
      t.drop_column :main if column_exists?(:contacts, :main)
    end
  end

  def down
    change_table(::contacts) do |t|
      t.drop_column :main
    end
  end

  def after_down_failure
    change_table(::contacts) do |t|
      t.add_column :main, :bool, default: true unless column_exists?(:contacts, :main)
    end
  end
end

Such sort of behavior is useful when you have complex migration with several separate changes.

Also you can disable automatic transaction passing false to .with_transaction in a migration class body:

class AddMainFlagToContacts < Jennifer::Migration::Base
  with_transaction false

  def up
    # ...
  end

  def down
    # ...
  end
end

Extended Modules

Defined in:

jennifer/migration/base.cr

Class Method Summary

Instance Method Summary

Class Method Detail

def self.migrations #

Returns all available migration classes.


[View source]
def self.version #

Returns migration timestamp.


[View source]
def self.versions #

Returns all existing migration timestamps based on available classes.


[View source]
def self.with_transaction(value : Bool) #

Specify whether #up, #down, #after_up_failure and #after_up_failure should be wrapped into a transaction.

true by default.


[View source]
def self.with_transaction? #

Returns whether #up, #down, #after_up_failure and #after_up_failure are wrapped into a transaction.


[View source]

Instance Method Detail

def adapter #

Database adapter connection instance.


[View source]
def add_foreign_key(from_table : String | Symbol, to_table : String | Symbol, column : String | Symbol? = nil, primary_key : String | Symbol? = nil, name : String? = nil, on_update : Symbol = TableBuilder::Base::DEFAULT_ON_EVENT_ACTION, on_delete : Symbol = TableBuilder::Base::DEFAULT_ON_EVENT_ACTION) #

Adds a new foreign key.

from_table is the table with the key column, to_table contains the referenced primary key.

The foreign key will be named after the following pattern: fk_cr_. identifier is a 10 character long string which is deterministically generated from the from_table and column. A custom name can be specified with the name argument.

add_foreign_key(:comments, :posts)

Creating a foreign key with specific primary and foreign keys

add_foreign_key(:comments, :posts, column: :article_id, primary: :uid)

Creating a foreign key with a specific name

add_foreign_key(:comments, :posts, name: "comments_posts_fk")

Specify ON DELETE or ON UPDATE action:

add_foreign_key(:comments, :posts, on_delete: :cascade)

Supported values: :no_action, :restrict (default), :cascade, :set_null.


[View source]
def add_index(table_name : String | Symbol, fields : Array(Symbol), type : Symbol? = nil, name : String? = nil, lengths : Hash(Symbol, Int32) = {} of Symbol => Int32, orders : Hash(Symbol, Symbol) = {} of Symbol => Symbol) #

Adds a new index to the table_name.

The index will be named after the table and the column name(s), unless you specify name.

Allowed type:

  • nil (default)
  • :unique
  • :fulltext (MySQL only)
  • :spatial (MySQL only)
add_index(:contacts, :email)
# => CREATE INDEX contacts_email_idx on contacts(email)

Creating a unique index:

add_index(:accounts, [:branch_id, :party_id], :unique)
# => CREATE UNIQUE INDEX accounts_branch_id_party_id_idx ON accounts(branch_id, party_id)

Creating a named index:

add_index(:accounts, [:branch_id, :party_id], :unique, name: "by_branch_party")
# => CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id)

Creates an index with specific key length

add_index(:accounts, :name, name: "by_name", length: 10)
# => CREATE INDEX by_name ON accounts(name(10))
# for multiple fields

add_index(:accounts, &i(name surname), name: 'by_name_surname', lengths: { :name => 10, :surname => 15 })
# => CREATE INDEX by_name_surname ON accounts(name(10), surname(15))

NOTE SQLite doesn't support index length.

Creating an index with a sort order:

add_index(:accounts, %i(branch_id party_id surname), orders: {:branch_id => :desc, :party_id => :asc})
# => CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)

NOTE MySQL only supports index order from 8.0.1 onwards (earlier versions will raise an exception).


[View source]
def add_index(table_name : String | Symbol, field : Symbol, type : Symbol? = nil, name : String? = nil, length : Int32? = nil, order : Symbol? = nil) #

[View source]
def after_down_failure #

#down failure handler if migration_failure_handler_method is set to :callback.

By default it is executed under a transaction.


[View source]
def after_up_failure #

#up failure handler if migration_failure_handler_method is set to :callback.

By default it is executed under a transaction.


[View source]
def change_enum(name : String | Symbol, options : Hash(Symbol, Array(String))) #

Changes database enum name by given options.

# To add new values
change_enum(:gender_enum, {:add_values => ["unknown"]})

# To rename value
change_enum(:gender_enum, {:rename_values => ["unknown", "other"]})

# To remove values
change_enum(:gender_enum, {:remove_values => ["other"]})

Also see TableBuilder::CreateTable#enum.

It is possible to rename only one enum value at a time.

NOTE not all adapters support this method.


[View source]
def change_table(table : String | Symbol, &) #

Changes existing table.

change_table(:users) do |t|
  t.change_column :age, :integer, {:default => 0}
  t.add_column :description, :text
  t.drop_column :details
end

For more details see TableBuilder::ChangeTable.


[View source]
def column_exists?(table, name) : Bool #

Returns whether column of table with name exists.

Delegate to Adapter::Base#column_exists?.


[View source]
def create_enum(name : String | Symbol, values : Array(String)) #

Creates database enum

create_enum(:gender, %w(unspecified female male))

NOTE not all adapters support this method.


[View source]
def create_join_table(table1 : String | Symbol, table2 : String | Symbol, table_name : String? = nil, &) #

Creates a new join table with the name created using the lexical order of the first 2 arguments.

Created join table has no primary key, generated fields has no indexes.

# Creates a table called 'addresses_contacts'
create_join_table(:contacts, :addresses)

[View source]
def create_join_table(table1, table2, table_name : String? = nil) #

[View source]
def create_materialized_view(name : String | Symbol, source) #

Creates a new materialized database view with the name name.

create_view(:youth_contacts, Jennifer::Query["contacts"].where { and(_age >= sql("14"), _age <= sql("24")) })

The source query can't have any arguments therefore all literals should be escaped manually and passed using

QueryBuilder::Expression#sql.

NOTE only Postgres supports this method.


[View source]
def create_table(name : String | Symbol, id : Bool = true, &) #

Creates a new table with the name name.

create_table(:contacts) do |t|
  t.field :name, :string, {:size => 30}
end
# or with shorthand
create_table(:contacts) do |t|
  t.string :name, {:size => 30}
end

By default, #create_table generates id : Int32 primary key (INTEGER database type). To prevent such behavior - set id argument to false.

create_table(:passport, false) do |t|
  t.string :puid, {:size => 10, :primary => true}
  t.string :name, {:size => 30}
end

For more details about new table definition see TableBuilder::CreateTable.


[View source]
def create_view(name : String | Symbol, source) #

Creates a new database view with the name name.

create_view(:youth_contacts, Jennifer::Query["contacts"].where { and(_age >= sql("14"), _age <= sql("24")) })

The source query can't have any arguments therefore all literals should be escaped manually and passed using QueryBuilder::Expression#sql.


[View source]
abstract def down #

Includes all transformations required to remove migration.

By default it is executed under a transaction.


[View source]
def drop_enum(name : String | Symbol) #

Drops a database enum by given name.

drop_enum(:gender)

NOTE not all adapters support this method.


[View source]
def drop_foreign_key(from_table : String | Symbol, to_table : String | Symbol, column : String | Symbol? = nil, name : String? = nil) #

Removes the given foreign key from from_table to to_table.

Arguments:

  • column - the foreign key column name on current_table; defaults to `Inflector.foreign_key(Inflector.singularize(to_table));
  • primary_key - the primary key column name on to_table. Defaults to "id";
  • name - the constraint name. Defaults to `"fc_cr_<identifier>".
drop_foreign_key(:accounts, :branches)

Removes foreign key with specific column:

drop_foreign_key(:accounts, :branches, :column_name)

Removes foreign key with specific name:

drop_foreign_key(:accounts, :branches, name: "special_fk_name")

[View source]
def drop_index(table : String | Symbol, fields : Array(Symbol) = [] of Symbol, name : String? = nil) #

Removes the given index from the table.

drop_index(:accounts, :branch_id)

Drop the index on multiple fields

drop_index(:accounts, %i(branch_id party_id))

Drop index with specific name:

drop_index(:accounts, name: "by_branch_name")

[View source]
def drop_index(table : String | Symbol, field : Symbol?, name : String? = nil) #

[View source]
def drop_join_table(table1, table2) #

Drops the join table specified by the given arguments.

See #create_join_table for details.

drop_join_table(:contacts, :addresses)

[View source]
def drop_materialized_view(name : String | Symbol) #

Drops a materialized view from the database.

drop_materialized_view(:youth_contacts)

[View source]
def drop_table(table : String | Symbol) #

Drops a table from the database.

drop_table(:users)

[View source]
def drop_view(name : String | Symbol) #

Drops a view from the database.

drop_view(:youth_contacts)

[View source]
def enum_exists?(name : String | Symbol) : Bool #

Checks whether enum with given name exists.

NOTE PostreSQL only.


[View source]
def exec(string : String) #

Executes given string SQL.

exec <<-SQL
  ALTER TABLE profiles
    ADD CONSTRAINT type
    CHECK (type IN('FacebookProfile', 'TwitterProfile'))
SQL

[View source]
def foreign_key_exists?(from_table, to_table = nil, column = nil, name : String? = nil) : Bool #

Checks to see if a foreign key exists on a table for a given foreign key definition.

Delegate to Adapter::Base#foreign_key_exists?.


[View source]
def index_exists?(*args, **opts) : Bool #

Returns whether index for the *table` with name or fields exists.

Delegate to Adapter::Base#index_exists?.


[View source]
def table_exists?(table : String | Symbol) : Bool #

Returns where table with given table name exists.

Delegate to Adapter::Base#table_exists?.


[View source]
abstract def up #

Includes all transformations required to implement migration.

By default it is executed under a transaction.


[View source]
def view_exists?(name : String | Symbol) : Bool #

Check whether view with given name exists.

Delegate to Adapter::Base#view_exists?.


[View source]