class Enginery::Migrator

Constants

NAME_REGEXP
TIME_FORMAT

Public Class Methods

new(dst_root, setups = {}) click to toggle source
# File lib/enginery/migrator.rb, line 8
def initialize dst_root, setups = {}
  @dst_root, @setups = dst_root, setups
  @migrations = Dir[dst_path(:migrations, '**/*%s' % MIGRATION_SUFFIX)].inject([]) do |map,f|
    step, time, name = File.basename(f).scan(NAME_REGEXP).flatten
    step && time && name && map << [step.to_i, time, name, f.sub(dst_path.migrations, '')]
    map
  end.sort {|a,b| a.first <=> b.first}.freeze
end

Public Instance Methods

guess_orm() click to toggle source
# File lib/enginery/migrator.rb, line 157
def guess_orm
  orm = (@setups[:orm] || Cfg[:orm] || fail('No project-wide ORM detected.
    Please update config/config.yml by adding
    orm: [DataMapper|ActiveRecord|Sequel]')).to_s.strip
  (ORM_MATCHERS.find {|o,m| orm =~ m} || fail('"%s" ORM not supported')).first
end
last_run(file) click to toggle source
# File lib/enginery/migrator.rb, line 128
def last_run file
  create_tracking_table_if_needed
  return unless track = track_exists?(file)
  [track.vector, track.performed_at]
end
list() click to toggle source

list available migrations with date of last run, if any

# File lib/enginery/migrator.rb, line 107
def list
  create_tracking_table_if_needed
  o indent('--'), '-=---'
  @migrations.each do |(step,time,name,file)|
    track = track_exists?(file)
    last_perform = track ? '%s on %s' % [track.vector, track.performed_at] : 'none'
    o indent(step), ' : ', name
    o indent('created at'), ' : ', DateTime.strptime(time, TIME_FORMAT).rfc2822
    o indent('last performed'), ' : ', last_perform
    o indent('--'), '-=---'
  end
end
new(name) click to toggle source

generate new migration. it will create a [n]..[name].rb migration file in base/migrations/ and column_transitions.yml file in base/migrations/track/ migration file will contain “up” and “down” sections. column_transitions file will keep track of column type changes.

# File lib/enginery/migrator.rb, line 23
def new name
  (name.nil? || name.empty?) && fail("Please provide migration name via second argument")
  (name =~ /[^\w|\d|\-|\.|\:]/) && fail("Migration name can contain only alphanumerics, dashes, semicolons and dots")
  @migrations.any? {|m| m[2] == name} && fail('"%s" migration already exists' % name)
  
  max = (@migrations.max {|m| m.first}||[0]).first
  model = @setups[:create_table] || @setups[:update_table]
  context = {model: model, name: name, step: max + 1}

  [:create_table, :update_table].each do |o|
    context[o] = (m = constant_defined?(@setups[o])) ? model_to_table(m) : nil
  end
  table = context[:create_table] || context[:update_table] ||
    fail('No model provided or provided one does not exists!')

  [:create_columns, :update_columns].each do |o|
    context[o] = transitions(table, (@setups[o]||[]).map {|(n,t)| [n, opted_column_type(t)]})
  end
  context[:rename_columns] = @setups[:rename_columns]||[]

  engine = Tenjin::Engine.new(path: [src_path.migrations], cache: false)
  source_code = engine.render('%s.erb' % guess_orm, context.merge(context: context))

  o
  o '--- %s model - generating "%s" migration ---' % [model, name]
  o
  o '  Serial Number: %s' % context[:step]
  o
  time = Time.now.strftime(TIME_FORMAT)
  path = dst_path(:migrations, class_to_route(model))
  FileUtils.mkdir_p(path)
  file = File.join(path, [context[:step], time, name, 'rb']*'.')
  write_file file, source_code
  output_source_code source_code.split("\n")
  name
end
outstanding_migrations(vector) click to toggle source
# File lib/enginery/migrator.rb, line 120
def outstanding_migrations vector
  create_tracking_table_if_needed
  serials = @migrations.inject([]) do |l,(step,time,name,file)|
    track_exists?(File.basename(file), vector) ? l : l.push(step)
  end
  serials_to_files(vector, *serials)
end
run(vector, file, force_run = nil) click to toggle source
  • validate migration file name

  • apply migration in given direction if migration was not previously performed in given direction or :force option given

  • create a track in TRACKING_TABLE so on consequent requests we may know whether migration was already performed

# File lib/enginery/migrator.rb, line 86
def run vector, file, force_run = nil
  vector = validate_vector(vector)
  
  (migration = @migrations.find {|m| m.last == file}) ||
    fail('"%s" is not a valid migration file' % file)
  
  create_tracking_table_if_needed

  track = track_exists?(file, vector)
  if track && !force_run
    o
    o '*** Skipping "%s: %s" migration ***' % [migration[0], migration[2]]
    o '  It was already performed %s on %s' % [track.vector.upcase, track.performed_at]
    o '  Use :force option to run it anyway - enginery m:%s:force ...' % vector
    o
    return
  end
  apply!(migration, vector) && persist_track(file, vector)
end
serials_to_files(vector, *serials) click to toggle source

convert given range or a single migration into files to be run ex: 1-5 will run migrations from one to 5 inclusive

1 2 4 will run 1st, 2nd, and 4th migrations
2 will run only 2nd migration
# File lib/enginery/migrator.rb, line 64
def serials_to_files vector, *serials
  vector = validate_vector(vector)
  serials.map do |serial|
    if serial =~ /\-/
      a, z = serial.split('-')
      (a..z).to_a
    else
      serial
    end
  end.flatten.map do |e|
    @migrations.find {|m| m.first == e.to_i} ||
      fail('Wrong range provided. "%s" is not a recognized migration step' % e)
  end.sort do |a,b|
    vector == :up ? a.first <=> b.first : b.first <=> a.first
  end.map(&:last)
end
update_model_file(context, vector) click to toggle source
# File lib/enginery/migrator.rb, line 134
def update_model_file context, vector
  model = context[:model]
  file = dst_path(:models, class_to_route(model) + MODEL_SUFFIX)
  return unless File.file?(file)

  lines, properties = File.readlines(file), []
  lines.each_with_index do |l,i|
    property = l.scan(/(\s+)?property\s+[\W]?(\w+)\W+(\w+)(.*)/).flatten
    properties << (property << i) if property[1] && property[2]
  end
  return if properties.empty?

  new_lines = case vector.to_s.downcase.to_sym
  when :up
     add_properties(lines, properties, context)
  when :down
    remove_properties(lines, properties, context)
  end

  return unless new_lines
  File.open(file, 'w') {|f| f << new_lines.join}
end

Private Instance Methods

add_properties(lines, properties, context) click to toggle source
# File lib/enginery/migrator.rb, line 210
def add_properties lines, properties, context
  property_setup, new_properties = nil, []

  context[:create_columns].each do |(n,t)|
    next if properties.find {|p| p[1].to_s == n.to_s}
    property_setup = [properties.last.first, n, t.to_s.split('::').last]
    new_properties << '%sproperty :%s, %s' % property_setup
  end
  if new_properties.any?
    lines[properties.last.last] += (new_properties.join("\n") + "\n")
  end
    
  context[:rename_columns].each do |(cn,nn)|
    next unless property = properties.find {|p| p[1].to_s == cn.to_s}
    property_setup = [property[0], nn, *property[2..3]]
    lines[property.last] = "%sproperty :%s, %s%s\n" % property_setup
  end

  context[:update_columns].each do |(n,t)|
    next unless property = properties.find {|p| p[1].to_s == n.to_s}
    property_setup = [*property[0..1], t.to_s.split('::').last, property[3]]
    lines[property.last] = "%sproperty :%s, %s%s\n" % property_setup
  end

  property_setup ? lines : nil
end
apply!(migration, vector, orm = guess_orm) click to toggle source

load migration file and call corresponding methods that will run migration up/down

# File lib/enginery/migrator.rb, line 167
def apply! migration, vector, orm = guess_orm
  o
  o '*** Performing %s step #%s ***' % [vector, migration.first]
  o '     Label: %s' % migration[2]
  o '       ORM: %s' % orm
  begin
    
    load dst_path(:migrations, migration.last)

    case orm
    when :DataMapper

      update_model_file(MigratorContext, vector)

      mj, mn, pt = DataMapper::VERSION.scan(/\d+/).map(&:to_i)
      if MigratorContext[:rename_columns].any? && [1,2,0] == [mj,mn,pt]
        o '    status: Skipped as renaming columns is broken on DataMapper 1.2.0'
        return false
      end

      MigratorInstance.instance_exec do
        # when using perform_up/down DataMapper will create a tracking table
        # and decide whether migration should be run, based on needs_up? and needs_down?
        # Enginery keeps own tracks and does not need DataMapper's tracking table
        # nor decisions on running migrations,
        # so using instance_exec to apply migrations directly.
        if action = instance_variable_get('@%s_action' % vector)
          action.call
        end
      end
    when :ActiveRecord
      MigratorInstance.new.send vector
    when :Sequel
      model = constant_defined?(MigratorContext[:model])
      MigratorInstance.apply model.db, vector
    end
    o '    status: OK'
    true
  rescue => e
    fail e.message, *e.backtrace
  end
end
capitalize(smth) click to toggle source

someString.capitalize will return Somestring. we need SomeString instead, which is returned by this method

# File lib/enginery/migrator.rb, line 335
def capitalize smth
  smth.to_s.match(/(\w)(.*)/) {|m| m[1].upcase << m[2]}
end
create_tracking_table_if_needed() click to toggle source
# File lib/enginery/migrator.rb, line 261
def create_tracking_table_if_needed
  require src_path(:migrations, 'tracking_table/%s.rb' % guess_orm)
  case guess_orm
  when :DataMapper
    TracksMigrator.instance_exec { @up_action.call }
  when :ActiveRecord
    TracksMigrator.new.up
  when :Sequel
    TracksMigrator.apply Sequel::Model.db, :up
  end
end
default_column_type(orm = guess_orm) click to toggle source
# File lib/enginery/migrator.rb, line 310
def default_column_type orm = guess_orm
  case orm
  when :ActiveRecord
    'string'
  when :DataMapper, :Sequel
    'String'
  end
end
indent(smth) click to toggle source
# File lib/enginery/migrator.rb, line 351
def indent smth
  string = smth.to_s
  ident_size = 20 - string.size
  ident_size =  0 if ident_size < 0
  INDENT + ' '*ident_size + string
end
invalid_vector!(vector) click to toggle source
# File lib/enginery/migrator.rb, line 347
def invalid_vector! vector
  fail('%s is a unrecognized vector. Use either "up" or "down"' % vector.inspect)
end
model_to_table(model) click to toggle source

get the actual db table of a given model

# File lib/enginery/migrator.rb, line 301
def model_to_table model
  case guess_orm
  when :DataMapper
    model.repository.adapter.resource_naming_convention.call(model)
  when :ActiveRecord, :Sequel
    model.table_name
  end
end
opted_column_type(type, orm = nil) click to toggle source

convert given string into column type suitable for migration file

# File lib/enginery/migrator.rb, line 320
def opted_column_type type, orm = nil
  orm  ||= guess_orm
  type ||= default_column_type(orm)
  case orm
  when :DataMapper
    'DataMapper::Property::%s' % capitalize(type)
  when :Sequel
    type.to_s =~ /text/i ? "String, text: true" : capitalize(type)
  else
    type
  end
end
persist_track(migration, vector) click to toggle source
# File lib/enginery/migrator.rb, line 284
def persist_track migration, vector
  key = {migration: migration}
  row = key.merge(performed_at: DateTime.now.rfc2822, vector: vector.to_s)
  case guess_orm
  when :DataMapper
    TracksModel.all(key).destroy!
    TracksModel.create(row)
  when :ActiveRecord
    TracksModel.delete_all(key)
    TracksModel.create(row)
  when :Sequel
    TracksModel.where(key).delete
    TracksModel.insert(row)
  end
end
remove_properties(lines, properties, context) click to toggle source
# File lib/enginery/migrator.rb, line 238
def remove_properties lines, properties, context
  property = nil

  context[:create_columns].each do |(n)|
    next unless property = properties.find {|p| p[1].to_s == n.to_s}
    lines[property.last] = nil
  end

  context[:rename_columns].each do |(cn,nn)|
    next unless property = properties.find {|p| p[1].to_s == nn.to_s}
    property[1] = cn
    lines[property.last] = "%sproperty :%s, %s%s\n" % property
  end

  context[:update_columns].each do |(n,nt,ot)|
    next unless property = properties.find {|p| p[1].to_s == n.to_s}
    property[2] = ot.to_s.split('::').last
    lines[property.last] = "%sproperty :%s, %s%s\n" % property
  end

  property ? lines : nil
end
track_exists?(migration, vector = nil) click to toggle source
# File lib/enginery/migrator.rb, line 273
def track_exists? migration, vector = nil
  conditions = {migration: migration}
  conditions[:vector] = vector.to_s if vector # #to_s required on Sequel
  case guess_orm
  when :ActiveRecord, :DataMapper
    TracksModel.first(conditions: conditions)
  when :Sequel
    TracksModel.first(conditions)
  end
end
transitions(table, columns) click to toggle source
# File lib/enginery/migrator.rb, line 358
def transitions table, columns
  transitions_file = dst_path(:migrations, 'transitions.yml')
  transitions = File.file?(transitions_file) ? (YAML.load(File.read(transitions_file)) rescue {}) : {}
  transitions[table] ||= {}
  columns.each do |column|
    column[2] = transitions[table][column.first]
    transitions[table][column.first] = column[1]
  end
  File.open(transitions_file, 'w') {|f| f << YAML.dump(transitions)}
  columns
end
validate_vector(vector) click to toggle source
# File lib/enginery/migrator.rb, line 339
def validate_vector vector
  invalid_vector!(vector) unless vector.is_a?(String)
  (vector =~ /\Au/i) && (vector = :up)
  (vector =~ /\Ad/i) && (vector = :down)
  invalid_vector!(vector) unless vector.is_a?(Symbol)
  vector
end