class Nandi::Migration

@abstract A migration must implement up (the forward migration), and may

also implement #down (the rollback sequence).

The base class for migrations; Nandi's equivalent of ActiveRecord::Migration. All the statements in the migration are statically analysed together to rule out migrations with a high risk of causing availability issues. Additionally, our implementations of some statements will rule out certain common footguns (for example, creating an index without using the `CONCURRENTLY` parameter.) @example

class CreateWidgetsTable < Nandi::Migration
  def up
    create_table :widgets do |t|
      t.column :weight, :number
      t.column :name, :text, default: "Unknown widget"
    end
  end

  def down
    drop_table :widgets
  end
end

Attributes

lock_timeout[R]
statement_timeout[R]
validator[R]

Public Class Methods

new(validator) click to toggle source

@param validator [Nandi::Validator]

# File lib/nandi/migration.rb, line 72
def initialize(validator)
  @validator = validator
  @instructions = Hash.new { |h, k| h[k] = InstructionSet.new([]) }
  validate
end
set_lock_timeout(timeout) click to toggle source

Override the default lock timeout for the duration of the migration. This may be helpful when making changes to very busy tables, when a lock is less likely to be immediately available. @param timeout [Integer] New lock timeout in ms

# File lib/nandi/migration.rb, line 57
def set_lock_timeout(timeout)
  @lock_timeout = timeout
end
set_statement_timeout(timeout) click to toggle source

Override the default statement timeout for the duration of the migration. This may be helpful when making changes that are likely to take a lot of time, like adding a new index on a large table. @param timeout [Integer] New lock timeout in ms

# File lib/nandi/migration.rb, line 65
def set_statement_timeout(timeout)
  @statement_timeout = timeout
end

Public Instance Methods

add_check_constraint(table, name, check) click to toggle source

Add a check constraint, in the NOT VALID state. @param table [Symbol, String] The name of the table with the column @param name [Symbol, String] The name of the constraint to create @param check [Symbol, String] The predicate to check

# File lib/nandi/migration.rb, line 254
def add_check_constraint(table, name, check)
  current_instructions << Instructions::AddCheckConstraint.new(
    table: table,
    name: name,
    check: check,
  )
end
add_column(table, name, type, **kwargs) click to toggle source

Adds a new column. Nandi will explicitly set the column to be NULL, as validating a new NOT NULL constraint can be very expensive on large tables and cause availability issues. @param table [Symbol, String] The name of the table to add the column to @param name [Symbol, String] The name of the column @param type [Symbol, String] The type of the column @param kwargs [Hash] Arbitrary options to be passed to the backend.

# File lib/nandi/migration.rb, line 183
def add_column(table, name, type, **kwargs)
  current_instructions << Instructions::AddColumn.new(
    table: table,
    name: name,
    type: type,
    **kwargs,
  )
end
add_foreign_key(table, target, column: nil, name: nil) click to toggle source

Add a foreign key constraint. The generated SQL will include the NOT VALID parameter, which will prevent immediate validation of the constraint, which locks the target table for writes potentially for a long time. Use the separate validate_constraint method, in a separate migration; this only takes a row-level lock as it scans through. @param table [Symbol, String] The name of the table with the reference column @param target [Symbol, String] The name of the referenced table @param column [Symbol, String] The name of the reference column. If omitted, will

default to the singular of target + "_id"

@param name [Symbol, String] The name of the constraint to create. Defaults to

table_target_fk
# File lib/nandi/migration.rb, line 241
def add_foreign_key(table, target, column: nil, name: nil)
  current_instructions << Instructions::AddForeignKey.new(
    table: table,
    target: target,
    column: column,
    name: name,
  )
end
add_index(table, fields, **kwargs) click to toggle source

Adds a new index to the database.

Nandi will:

  • add the `CONCURRENTLY` option, which means the change takes a less restrictive lock at the cost of not running in a DDL transaction

  • default to the `BTREE` index type, as it is commonly a good fit.

Because index creation is particularly failure-prone, and because we cannot run in a transaction and therefore risk partially applied migrations that (in a Rails environment) require manual intervention, Nandi Validates that, if there is a add_index statement in the migration, it must be the only statement. @param table [Symbol, String] The name of the table to add the index to @param fields [Symbol, String, Array] The field or fields to use in the

index

@param kwargs [Hash] Arbitrary options to pass to the backend adapter.

Attempts to remove `CONCURRENTLY` or change the index type will be ignored.
# File lib/nandi/migration.rb, line 127
def add_index(table, fields, **kwargs)
  current_instructions << Instructions::AddIndex.new(
    **kwargs,
    table: table,
    fields: fields,
  )
end
add_reference(table, ref_name, **kwargs) click to toggle source

Adds a new reference column. Nandi will validate that the foreign key flag is not set to true; use `add_foreign_key` and `validate_foreign_key` instead! @param table [Symbol, String] The name of the table to add the column to @param ref_name [Symbol, String] The referenced column name @param kwargs [Hash] Arbitrary options to be passed to the backend.

# File lib/nandi/migration.rb, line 197
def add_reference(table, ref_name, **kwargs)
  current_instructions << Instructions::AddReference.new(
    table: table,
    ref_name: ref_name,
    **kwargs,
  )
end
change_column_default(table, column, value) click to toggle source

Changes the default value for this column when new rows are inserted into the table. @param table [Symbol, String] The name of the table with the column @param column [Symbol, String] The name of the column to change @param value [Object] The new default value

# File lib/nandi/migration.rb, line 300
def change_column_default(table, column, value)
  current_instructions << Instructions::ChangeColumnDefault.new(
    table: table,
    column: column,
    value: value,
  )
end
compile_instructions(direction) click to toggle source

@api private

# File lib/nandi/migration.rb, line 315
def compile_instructions(direction)
  @direction = direction

  public_send(direction) unless current_instructions.any?

  current_instructions
end
create_table(table, **kwargs, &block) click to toggle source

Creates a new table. Yields a ColumnsReader object as a block, to allow adding columns. @example

create_table :widgets do |t|
  t.text :foo, default: true
end

@param table [String, Symbol] The name of the new table @yieldparam columns_reader [Nandi::Instructions::CreateTable::ColumnsReader]

# File lib/nandi/migration.rb, line 162
def create_table(table, **kwargs, &block)
  current_instructions << Instructions::CreateTable.new(
    **kwargs,
    table: table,
    columns_block: block,
  )
end
disable_lock_timeout?() click to toggle source
# File lib/nandi/migration.rb, line 330
def disable_lock_timeout?
  if self.class.lock_timeout.nil?
    strictest_lock == LockWeights::SHARE
  else
    false
  end
end
disable_statement_timeout?() click to toggle source
# File lib/nandi/migration.rb, line 338
def disable_statement_timeout?
  if self.class.statement_timeout.nil?
    strictest_lock == LockWeights::SHARE
  else
    false
  end
end
down() click to toggle source
# File lib/nandi/migration.rb, line 108
def down; end
down_instructions() click to toggle source

@api private

# File lib/nandi/migration.rb, line 84
def down_instructions
  compile_instructions(:down)
end
drop_constraint(table, name) click to toggle source

Drops an existing constraint. @param table [Symbol, String] The name of the table with the constraint @param name [Symbol, String] The name of the constraint

# File lib/nandi/migration.rb, line 275
def drop_constraint(table, name)
  current_instructions << Instructions::DropConstraint.new(
    table: table,
    name: name,
  )
end
drop_table(table) click to toggle source

Drops an existing table @param table [String, Symbol] The name of the table to drop.

# File lib/nandi/migration.rb, line 172
def drop_table(table)
  current_instructions << Instructions::DropTable.new(table: table)
end
irreversible_migration() click to toggle source

Raises an `ActiveRecord::IrreversibleMigration` error for use in irreversible migrations

# File lib/nandi/migration.rb, line 310
def irreversible_migration
  current_instructions << Instructions::IrreversibleMigration.new
end
lock_timeout() click to toggle source

The current lock timeout.

# File lib/nandi/migration.rb, line 89
def lock_timeout
  self.class.lock_timeout || default_lock_timeout
end
method_missing(name, *args, &block) click to toggle source
Calls superclass method
# File lib/nandi/migration.rb, line 360
def method_missing(name, *args, &block)
  if Nandi.config.custom_methods.key?(name)
    invoke_custom_method(name, *args, &block)
  else
    super
  end
end
mixins() click to toggle source
# File lib/nandi/migration.rb, line 354
def mixins
  (up_instructions + down_instructions).inject([]) do |mixins, i|
    i.respond_to?(:mixins) ? [*mixins, *i.mixins] : mixins
  end.uniq
end
name() click to toggle source
# File lib/nandi/migration.rb, line 346
def name
  self.class.name
end
remove_column(table, name, **extra_args) click to toggle source

Remove an existing column. @param table [Symbol, String] The name of the table to remove the column

from.

@param name [Symbol, String] The name of the column @param extra_args [Hash] Arbitrary options to be passed to the backend.

# File lib/nandi/migration.rb, line 222
def remove_column(table, name, **extra_args)
  current_instructions << Instructions::RemoveColumn.new(
    **extra_args,
    table: table,
    name: name,
  )
end
remove_index(table, target) click to toggle source

Drop an index from the database.

Nandi will add the `CONCURRENTLY` option, which means the change takes a less restrictive lock at the cost of not running in a DDL transaction.

Because we cannot run in a transaction and therefore risk partially applied migrations that (in a Rails environment) require manual intervention, Nandi Validates that, if there is a remove_index statement in the migration, it must be the only statement. @param table [Symbol, String] The name of the table to add the index to @param target [Symbol, String, Array, Hash] This can be either the field (or

array of fields) in the index to be dropped, or a hash of options, which
must include either a `column` key (which is the same: a field or list
of fields) or a `name` key, which is the name of the index to be dropped.
# File lib/nandi/migration.rb, line 150
def remove_index(table, target)
  current_instructions << Instructions::RemoveIndex.new(table: table, field: target)
end
remove_not_null_constraint(table, column) click to toggle source

Drops an existing NOT NULL constraint. Please note that this migration is not safely reversible; to enforce NOT NULL like behaviour, use a CHECK constraint and validate it in a separate migration. @param table [Symbol, String] The name of the table with the constraint @param column [Symbol, String] The name of the column to remove NOT NULL

constraint from
# File lib/nandi/migration.rb, line 288
def remove_not_null_constraint(table, column)
  current_instructions << Instructions::RemoveNotNullConstraint.new(
    table: table,
    column: column,
  )
end
remove_reference(table, ref_name, **kwargs) click to toggle source

Removes a reference column. @param table [Symbol, String] The name of the table to remove the reference from @param ref_name [Symbol, String] The referenced column name @param kwargs [Hash] Arbitrary options to be passed to the backend.

# File lib/nandi/migration.rb, line 209
def remove_reference(table, ref_name, **kwargs)
  current_instructions << Instructions::RemoveReference.new(
    table: table,
    ref_name: ref_name,
    **kwargs,
  )
end
respond_to_missing?(name) click to toggle source
Calls superclass method
# File lib/nandi/migration.rb, line 350
def respond_to_missing?(name)
  Nandi.config.custom_methods.key?(name) || super
end
statement_timeout() click to toggle source

The current statement timeout.

# File lib/nandi/migration.rb, line 94
def statement_timeout
  self.class.statement_timeout || default_statement_timeout
end
strictest_lock() click to toggle source

@api private

# File lib/nandi/migration.rb, line 99
def strictest_lock
  @instructions.values.map(&:strictest_lock).max
end
up() click to toggle source

@abstract

# File lib/nandi/migration.rb, line 104
def up
  raise NotImplementedError
end
up_instructions() click to toggle source

@api private

# File lib/nandi/migration.rb, line 79
def up_instructions
  compile_instructions(:up)
end
validate() click to toggle source

@api private

# File lib/nandi/migration.rb, line 324
def validate
  validator.call(self)
rescue NotImplementedError => e
  Validation::Result.new << failure(e.message)
end
validate_constraint(table, name) click to toggle source

Validates an existing foreign key constraint. @param table [Symbol, String] The name of the table with the constraint @param name [Symbol, String] The name of the constraint

# File lib/nandi/migration.rb, line 265
def validate_constraint(table, name)
  current_instructions << Instructions::ValidateConstraint.new(
    table: table,
    name: name,
  )
end

Private Instance Methods

current_instructions() click to toggle source
# File lib/nandi/migration.rb, line 372
def current_instructions
  @instructions[@direction]
end
default_lock_timeout() click to toggle source
# File lib/nandi/migration.rb, line 380
def default_lock_timeout
  Nandi.config.access_exclusive_lock_timeout
end
default_statement_timeout() click to toggle source
# File lib/nandi/migration.rb, line 376
def default_statement_timeout
  Nandi.config.access_exclusive_statement_timeout
end
invoke_custom_method(name, *args, &block) click to toggle source
# File lib/nandi/migration.rb, line 384
def invoke_custom_method(name, *args, &block)
  klass = Nandi.config.custom_methods[name]
  current_instructions << klass.new(*args, &block)
end