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
-
.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!.
- .describe(step) ⇒ Object
- .link_fix_requirements(fixes, link) ⇒ Object
- .link_migration_requirements(migrations, link) ⇒ Object
-
.migration_class_for(proxy) ⇒ Object
MigrationProxy defers loading the migration class until needed, via a private #migration accessor - reading
required_fixesoff it needs that class loaded early too, so reach past that privacy rather than reimplementing Rails' own file-loading/constantizing. - .migration_context ⇒ Object
- .migration_exists?(version) ⇒ Boolean
- .pending_migrations ⇒ Object
-
.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.
-
.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.
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 |
.link_fix_requirements(fixes, link) ⇒ Object
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 |
.link_migration_requirements(migrations, link) ⇒ Object
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
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.
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 |