Class: Nandi::Migration Abstract

Inherits:
Object
  • Object
show all
Includes:
Validation::FailureHelpers
Defined in:
lib/nandi/migration.rb

Overview

This class is 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.)

Examples:

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

Defined Under Namespace

Modules: LockWeights Classes: InstructionSet

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Validation::FailureHelpers

#assert, #collect_errors, #failure, #success

Constructor Details

#initialize(validator) ⇒ Migration

Returns a new instance of Migration.

Parameters:



72
73
74
75
76
# File 'lib/nandi/migration.rb', line 72

def initialize(validator)
  @validator = validator
  @instructions = Hash.new { |h, k| h[k] = InstructionSet.new([]) }
  validate
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args, **kwargs, &block) ⇒ Object



360
361
362
363
364
365
366
# File 'lib/nandi/migration.rb', line 360

def method_missing(name, *args, **kwargs, &block)
  if Nandi.config.custom_methods.key?(name)
    invoke_custom_method(name, *args, **kwargs, &block)
  else
    super
  end
end

Class Attribute Details

.lock_timeoutObject (readonly)

Returns the value of attribute lock_timeout.



45
46
47
# File 'lib/nandi/migration.rb', line 45

def lock_timeout
  @lock_timeout
end

.statement_timeoutObject (readonly)

Returns the value of attribute statement_timeout.



45
46
47
# File 'lib/nandi/migration.rb', line 45

def statement_timeout
  @statement_timeout
end

Class Method Details

.set_lock_timeout(timeout) ⇒ Object

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.

Parameters:

  • timeout (Integer)

    New lock timeout in ms



57
58
59
# File 'lib/nandi/migration.rb', line 57

def set_lock_timeout(timeout)
  @lock_timeout = timeout
end

.set_statement_timeout(timeout) ⇒ Object

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.

Parameters:

  • timeout (Integer)

    New lock timeout in ms



65
66
67
# File 'lib/nandi/migration.rb', line 65

def set_statement_timeout(timeout)
  @statement_timeout = timeout
end

Instance Method Details

#add_check_constraint(table, name, check) ⇒ Object

Add a check constraint, in the NOT VALID state.

Parameters:

  • table (Symbol, String)

    The name of the table with the column

  • name (Symbol, String)

    The name of the constraint to create

  • check (Symbol, String)

    The predicate to check



254
255
256
257
258
259
260
# 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) ⇒ Object

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.

Parameters:

  • table (Symbol, String)

    The name of the table to add the column to

  • name (Symbol, String)

    The name of the column

  • type (Symbol, String)

    The type of the column

  • kwargs (Hash)

    Arbitrary options to be passed to the backend.



183
184
185
186
187
188
189
190
# 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) ⇒ Object

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.

Parameters:

  • table (Symbol, String)

    The name of the table with the reference column

  • target (Symbol, String)

    The name of the referenced table

  • column (Symbol, String) (defaults to: nil)

    The name of the reference column. If omitted, will default to the singular of target + “_id”

  • name (Symbol, String) (defaults to: nil)

    The name of the constraint to create. Defaults to table_target_fk



241
242
243
244
245
246
247
248
# 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) ⇒ Object

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.

Parameters:

  • table (Symbol, String)

    The name of the table to add the index to

  • fields (Symbol, String, Array)

    The field or fields to use in the index

  • kwargs (Hash)

    Arbitrary options to pass to the backend adapter. Attempts to remove ‘CONCURRENTLY` or change the index type will be ignored.



127
128
129
130
131
132
133
# 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) ⇒ Object

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!

Parameters:

  • table (Symbol, String)

    The name of the table to add the column to

  • ref_name (Symbol, String)

    The referenced column name

  • kwargs (Hash)

    Arbitrary options to be passed to the backend.



197
198
199
200
201
202
203
# 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) ⇒ Object

Changes the default value for this column when new rows are inserted into the table.

Parameters:

  • table (Symbol, String)

    The name of the table with the column

  • column (Symbol, String)

    The name of the column to change

  • value (Object)

    The new default value



300
301
302
303
304
305
306
# 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) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



315
316
317
318
319
320
321
# 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) {|columns_reader| ... } ⇒ Object

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

Examples:

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

Parameters:

  • table (String, Symbol)

    The name of the new table

Yield Parameters:



162
163
164
165
166
167
168
# 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?Boolean

Returns:

  • (Boolean)


330
331
332
333
334
335
336
# 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?Boolean

Returns:

  • (Boolean)


338
339
340
341
342
343
344
# 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

#downObject



108
# File 'lib/nandi/migration.rb', line 108

def down; end

#down_instructionsObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



84
85
86
# File 'lib/nandi/migration.rb', line 84

def down_instructions
  compile_instructions(:down)
end

#drop_constraint(table, name) ⇒ Object

Drops an existing constraint.

Parameters:

  • table (Symbol, String)

    The name of the table with the constraint

  • name (Symbol, String)

    The name of the constraint



275
276
277
278
279
280
# 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) ⇒ Object

Drops an existing table

Parameters:

  • table (String, Symbol)

    The name of the table to drop.



172
173
174
# File 'lib/nandi/migration.rb', line 172

def drop_table(table)
  current_instructions << Instructions::DropTable.new(table: table)
end

#irreversible_migrationObject

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



310
311
312
# File 'lib/nandi/migration.rb', line 310

def irreversible_migration
  current_instructions << Instructions::IrreversibleMigration.new
end

#lock_timeoutObject

The current lock timeout.



89
90
91
# File 'lib/nandi/migration.rb', line 89

def lock_timeout
  self.class.lock_timeout || default_lock_timeout
end

#mixinsObject



354
355
356
357
358
# 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

#nameObject



346
347
348
# File 'lib/nandi/migration.rb', line 346

def name
  self.class.name
end

#remove_column(table, name, **extra_args) ⇒ Object

Remove an existing column.

Parameters:

  • table (Symbol, String)

    The name of the table to remove the column from.

  • name (Symbol, String)

    The name of the column

  • extra_args (Hash)

    Arbitrary options to be passed to the backend.



222
223
224
225
226
227
228
# 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) ⇒ Object

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.

Parameters:

  • table (Symbol, String)

    The name of the table to add the index to

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



150
151
152
# 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) ⇒ Object

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.

Parameters:

  • table (Symbol, String)

    The name of the table with the constraint

  • column (Symbol, String)

    The name of the column to remove NOT NULL constraint from



288
289
290
291
292
293
# 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) ⇒ Object

Removes a reference column.

Parameters:

  • table (Symbol, String)

    The name of the table to remove the reference from

  • ref_name (Symbol, String)

    The referenced column name

  • kwargs (Hash)

    Arbitrary options to be passed to the backend.



209
210
211
212
213
214
215
# 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) ⇒ Boolean

Returns:

  • (Boolean)


350
351
352
# File 'lib/nandi/migration.rb', line 350

def respond_to_missing?(name)
  Nandi.config.custom_methods.key?(name) || super
end

#statement_timeoutObject

The current statement timeout.



94
95
96
# File 'lib/nandi/migration.rb', line 94

def statement_timeout
  self.class.statement_timeout || default_statement_timeout
end

#strictest_lockObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



99
100
101
# File 'lib/nandi/migration.rb', line 99

def strictest_lock
  @instructions.values.map(&:strictest_lock).max
end

#upObject

This method is abstract.

Raises:

  • (NotImplementedError)


104
105
106
# File 'lib/nandi/migration.rb', line 104

def up
  raise NotImplementedError
end

#up_instructionsObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



79
80
81
# File 'lib/nandi/migration.rb', line 79

def up_instructions
  compile_instructions(:up)
end

#validateObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



324
325
326
327
328
# 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) ⇒ Object

Validates an existing foreign key constraint.

Parameters:

  • table (Symbol, String)

    The name of the table with the constraint

  • name (Symbol, String)

    The name of the constraint



265
266
267
268
269
270
# File 'lib/nandi/migration.rb', line 265

def validate_constraint(table, name)
  current_instructions << Instructions::ValidateConstraint.new(
    table: table,
    name: name,
  )
end