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:



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

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, &block) ⇒ Object



334
335
336
337
338
339
340
# File 'lib/nandi/migration.rb', line 334

def method_missing(name, *args, &block)
  if Nandi.config.custom_methods.key?(name)
    invoke_custom_method(name, *args, &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



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

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



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

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



228
229
230
231
232
233
234
# File 'lib/nandi/migration.rb', line 228

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.



182
183
184
185
186
187
188
189
# File 'lib/nandi/migration.rb', line 182

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



215
216
217
218
219
220
221
222
# File 'lib/nandi/migration.rb', line 215

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

  • use the ‘BTREE` index type which is the safest to create.

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.



126
127
128
129
130
131
132
# File 'lib/nandi/migration.rb', line 126

def add_index(table, fields, **kwargs)
  current_instructions << Instructions::AddIndex.new(
    **kwargs,
    table: table,
    fields: fields,
  )
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



274
275
276
277
278
279
280
# File 'lib/nandi/migration.rb', line 274

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.



289
290
291
292
293
294
295
# File 'lib/nandi/migration.rb', line 289

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:



161
162
163
164
165
166
167
# File 'lib/nandi/migration.rb', line 161

def create_table(table, **kwargs, &block)
  current_instructions << Instructions::CreateTable.new(
    **kwargs,
    table: table,
    columns_block: block,
  )
end

#disable_lock_timeout?Boolean

Returns:

  • (Boolean)


304
305
306
307
308
309
310
# File 'lib/nandi/migration.rb', line 304

def disable_lock_timeout?
  if self.class.lock_timeout.nil?
    strictest_lock == LockWeights::SHARE
  else
    false
  end
end

#disable_statement_timeout?Boolean

Returns:

  • (Boolean)


312
313
314
315
316
317
318
# File 'lib/nandi/migration.rb', line 312

def disable_statement_timeout?
  if self.class.statement_timeout.nil?
    strictest_lock == LockWeights::SHARE
  else
    false
  end
end

#downObject



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

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.



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

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



249
250
251
252
253
254
# File 'lib/nandi/migration.rb', line 249

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.



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

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

#irreversible_migrationObject

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



284
285
286
# File 'lib/nandi/migration.rb', line 284

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

#lock_timeoutObject

The current lock timeout.



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

def lock_timeout
  self.class.lock_timeout || default_lock_timeout
end

#mixinsObject



328
329
330
331
332
# File 'lib/nandi/migration.rb', line 328

def mixins
  (up_instructions + down_instructions).inject([]) do |mixins, i|
    i.respond_to?(:mixins) ? [*mixins, *i.mixins] : mixins
  end.uniq
end

#nameObject



320
321
322
# File 'lib/nandi/migration.rb', line 320

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.



196
197
198
199
200
201
202
# File 'lib/nandi/migration.rb', line 196

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.



149
150
151
# File 'lib/nandi/migration.rb', line 149

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



262
263
264
265
266
267
# File 'lib/nandi/migration.rb', line 262

def remove_not_null_constraint(table, column)
  current_instructions << Instructions::RemoveNotNullConstraint.new(
    table: table,
    column: column,
  )
end

#respond_to_missing?(name) ⇒ Boolean

Returns:

  • (Boolean)


324
325
326
# File 'lib/nandi/migration.rb', line 324

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

#statement_timeoutObject

The current statement timeout.



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

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.



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

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

#upObject

This method is abstract.

Raises:

  • (NotImplementedError)


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

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.



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

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.



298
299
300
301
302
# File 'lib/nandi/migration.rb', line 298

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



239
240
241
242
243
244
# File 'lib/nandi/migration.rb', line 239

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