Module: YiffSpace::MigrationSync

Defined in:
lib/yiffspace/migration_sync.rb

Overview

Computes and applies pending db/migrate/.rb migrations and db/fixes/.rb fixes together, in whichever order their cross requirements demand - a fix can require a migration (YiffSpace::FixTracker.requires_migration!) and a migration can require a fix (YiffSpace::Fixers::RequiresFix#requires_fix). Migrations stay ordered among themselves by version, fixes among themselves by YiffSpace::FixTracker.sort_key, and those cross requirements are woven in via a topological sort. #plan/#apply are used by rake fixes:migrate_all (see lib/tasks/fixes.rake).

Defined Under Namespace

Classes: Step, UnresolvableOrder

Class Method Summary collapse

Class Method Details

.apply(step) ⇒ Object

Applies a single Step from #plan - a migration is brought up to (and including) its own version, a fix is run and recorded, same as FixTracker.run!.



105
106
107
108
109
110
111
112
# File 'lib/yiffspace/migration_sync.rb', line 105

def apply(step)
  case step.kind
  when :migration
    migration_context.up(step.ref.version) { |candidate| candidate.version == step.ref.version }
  when :fix
    FixTracker.run!(FixTracker.fix_path(step.ref))
  end
end

.describe(step) ⇒ Object



96
97
98
99
100
101
# File 'lib/yiffspace/migration_sync.rb', line 96

def describe(step)
  case step.kind
  when :migration then "Migrating #{step.ref.name} (#{step.ref.version})..."
  when :fix then "Running #{step.ref}..."
  end
end


52
53
54
55
56
57
58
59
60
# File 'lib/yiffspace/migration_sync.rb', line 52

def link_fix_requirements(fixes, link)
  fixes.each do |name|
    version = FixTracker.required_migration_for(name)
    next unless version
    raise(UnresolvableOrder, "#{name} requires migration #{version}, which does not exist") unless migration_exists?(version)

    link.call([:migration, version.to_i], [:fix, name])
  end
end


62
63
64
65
66
67
68
69
70
71
# File 'lib/yiffspace/migration_sync.rb', line 62

def link_migration_requirements(migrations, link)
  migrations.each do |m|
    migration_class_for(m).required_fixes.each do |id_or_name|
      names = FixTracker.resolve(id_or_name)
      raise(UnresolvableOrder, "#{m.name} requires fix #{id_or_name.inspect}, which does not exist") if names.empty?

      names.each { |name| link.call([:fix, name], [:migration, m.version]) }
    end
  end
end

.migration_class_for(proxy) ⇒ Object

MigrationProxy defers loading the migration class until needed, via a private #migration accessor - reading required_fixes off it needs that class loaded early too, so reach past that privacy rather than reimplementing Rails' own file-loading/constantizing.



126
127
128
# File 'lib/yiffspace/migration_sync.rb', line 126

def migration_class_for(proxy)
  proxy.send(:migration).class
end

.migration_context ⇒ Object



130
131
132
# File 'lib/yiffspace/migration_sync.rb', line 130

def migration_context
  FixTracker.migration_context
end

.migration_exists?(version) ⇒ Boolean

Returns:

  • (Boolean)


119
120
121
# File 'lib/yiffspace/migration_sync.rb', line 119

def migration_exists?(version)
  migration_context.migrations.any? { |m| m.version == version.to_i }
end

.pending_migrations ⇒ Object



114
115
116
117
# File 'lib/yiffspace/migration_sync.rb', line 114

def pending_migrations
  applied = migration_context.get_all_versions
  migration_context.migrations.reject { |m| applied.include?(m.version) }
end

.plan ⇒ Object

The ordered list of pending Steps - migrations and fixes interleaved wherever a requires_fix/requires_migration! forces it, otherwise each kept in its own natural order.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/yiffspace/migration_sync.rb', line 24

def plan
  migrations = pending_migrations
  fixes = FixTracker.pending

  steps = {}
  migrations.each { |m| steps[[:migration, m.version]] = Step.new(:migration, m, [:migration, m.version]) }
  fixes.each { |f| steps[[:fix, f]] = Step.new(:fix, f, [:fix, f]) }

  edges = Hash.new { |h, k| h[k] = [] }
  indegree = steps.each_key.index_with(0)

  link = lambda do |before, after|
    next unless steps.key?(before) && steps.key?(after)

    edges[before] << after
    indegree[after] += 1
  end

  # Preserve each kind's own natural order as implicit dependencies.
  migrations.each_cons(2) { |a, b| link.call([:migration, a.version], [:migration, b.version]) }
  fixes.each_cons(2) { |a, b| link.call([:fix, a], [:fix, b]) }

  link_fix_requirements(fixes, link)
  link_migration_requirements(migrations, link)

  topological_sort(steps, edges, indegree)
end

.topological_sort(steps, edges, indegree) ⇒ Object

Kahn's algorithm - ready nodes are processed FIFO in steps' insertion order (migrations before fixes when both are ready at once), so requirements only ever reorder things when they actually have to.

Raises:



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/yiffspace/migration_sync.rb', line 76

def topological_sort(steps, edges, indegree)
  ready = indegree.select { |_, count| count.zero? }.keys
  ordered = []

  until ready.empty?
    key = ready.shift
    ordered << steps.fetch(key)

    edges[key].each do |dependent|
      indegree[dependent] -= 1
      ready << dependent if indegree[dependent].zero?
    end
  end

  return ordered if ordered.size == steps.size

  stuck = (steps.keys - ordered.map(&:key)).map { |kind, ref| "#{kind}:#{ref}" }
  raise(UnresolvableOrder, "circular requirement between #{stuck.join(', ')}")
end