Module: YiffSpace::FixTracker
- Defined in:
- lib/yiffspace/fix_tracker.rb
Overview
Tracks which db/fixes/.rb scripts a host app has applied, in a fixes table (see
db/migrate/_create_fixes.rb) the same way ActiveRecord's own schema_migrations table
tracks migrations. Used by the fixes:* rake tasks (lib/tasks/fixes.rake).
Defined Under Namespace
Classes: MigrationRequired
Constant Summary collapse
- REQUIRES_MIGRATION_PATTERN =
A fix script calls this as its first statement to declare that a migration must already be applied before it can run, e.g.:
YiffSpace::FixTracker.requires_migration!("20260822004454")Written as a literal string call like that (not built dynamically) so YiffSpace::MigrationSync can also read it back out of the file with #required_migration_for, without loading/running the fix, to order
fixes:migrate_allcorrectly. /^\s*YiffSpace::FixTracker\.requires_migration!\(\s*["']([^"']+)["']\s*\)/
Class Method Summary collapse
-
.all_fixes ⇒ Object
Only numbered one-time fixes are tracked/auto-applied - a host app's db/fixes may also hold reusable on-demand maintenance scripts that aren't meant to run automatically as part of
fixes:migrate. - .applied ⇒ Object
- .applied?(name) ⇒ Boolean
-
.dump_structure_sql! ⇒ Object
Mirrors how ActiveRecord appends
INSERT INTO "schema_migrations"to structure.sql after a schema dump (see Connection#dump_schema_versions) - appends the currently-applied fixes the same way, so loading structure.sql into a fresh database marks them applied too instead of leavingfixes:migrateto re-run every historical fix. - .fix_path(name) ⇒ Object
-
.key_for(name) ⇒ Object
db/fixes filenames are "
_description.rb", occasionally " _ _description.rb" (e.g. 1_1_..., 1_2_...) for a handful of fixes split into ordered steps - the fixestable mirrors this as an (id, index) pair, with index null for fixes with no subtype. - .migration_applied?(version) ⇒ Boolean
- .migration_context ⇒ Object
- .pending ⇒ Object
- .record!(name) ⇒ Object
-
.required_migration_for(name) ⇒ Object
Statically reads a fix script's declared requires_migration! version, if any, without loading/running it - nil if the fix has no such call.
- .requires_migration!(version) ⇒ Object
-
.resolve(id_or_name) ⇒ Object
Resolves a YiffSpace::Fixers::RequiresFix id_or_name to the fix name(s) it refers to - an Integer id covers every step sharing that id (e.g. 2 -> %w[2_1_first_step 2_2_second_step]), a name matches that one fix file exactly.
- .run!(path) ⇒ Object
-
.sort_key(name) ⇒ Object
Sort on the (id, index) parts, not the filename string, so 12 doesn't sort before 2_2.
Class Method Details
.all_fixes ⇒ Object
Only numbered one-time fixes are tracked/auto-applied - a host app's db/fixes may also hold
reusable on-demand maintenance scripts that aren't meant to run automatically as part of
fixes:migrate. Those stay reachable via fixes:run[name].
80 81 82 |
# File 'lib/yiffspace/fix_tracker.rb', line 80 def all_fixes Rails.root.glob("db/fixes/*.rb").map { |path| File.basename(path, ".rb") }.grep(/\A\d/).sort_by { |name| sort_key(name) } end |
.applied ⇒ Object
84 85 86 |
# File 'lib/yiffspace/fix_tracker.rb', line 84 def applied ActiveRecord::Base.connection.select_rows('SELECT id, "index" FROM fixes').to_set end |
.applied?(name) ⇒ Boolean
88 89 90 91 92 93 |
# File 'lib/yiffspace/fix_tracker.rb', line 88 def applied?(name) id, index = key_for(name) conn = ActiveRecord::Base.connection index_clause = index.nil? ? '"index" IS NULL' : %("index" = #{index}) conn.select_value("SELECT 1 FROM fixes WHERE id = #{id} AND #{index_clause}").present? end |
.dump_structure_sql! ⇒ Object
Mirrors how ActiveRecord appends INSERT INTO "schema_migrations" to structure.sql after
a schema dump (see Connection#dump_schema_versions) - appends the currently-applied fixes
the same way, so loading structure.sql into a fresh database marks them applied too instead
of leaving fixes:migrate to re-run every historical fix. Hooked into db:schema:dump in
lib/tasks/fixes.rake, so it runs on every schema dump, not just fixes:migrate.
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/yiffspace/fix_tracker.rb', line 121 def dump_structure_sql! db_config = ActiveRecord::Base.connection_db_config return unless db_config.schema_format == :sql filename = ActiveRecord::Tasks::DatabaseTasks.schema_dump_path(db_config) return unless filename && File.exist?(filename) conn = ActiveRecord::Base.connection return unless conn.table_exists?(:fixes) rows = conn.select_rows('SELECT id, "index" FROM fixes ORDER BY id DESC, "index" DESC NULLS LAST') return if rows.empty? values = rows.map { |id, index| "(#{id}, #{index || 'NULL'})" }.join(",\n") File.open(filename, "a") do |f| f.puts(" INSERT INTO \"fixes\" (id, \"index\") VALUES\n \#{values};\n TEXT\n end\nend\n") |
.fix_path(name) ⇒ Object
23 24 25 |
# File 'lib/yiffspace/fix_tracker.rb', line 23 def fix_path(name) Rails.root.glob("db/fixes/#{name}.rb").first end |
.key_for(name) ⇒ Object
db/fixes filenames are "fixes
table mirrors this as an (id, index) pair, with index null for fixes with no subtype.
65 66 67 68 69 |
# File 'lib/yiffspace/fix_tracker.rb', line 65 def key_for(name) id, maybe_index = name.split("_", 3) index = maybe_index =~ /\A\d+\z/ ? maybe_index.to_i : nil [id.to_i, index] end |
.migration_applied?(version) ⇒ Boolean
33 34 35 |
# File 'lib/yiffspace/fix_tracker.rb', line 33 def migration_applied?(version) migration_context.get_all_versions.include?(version.to_i) end |
.migration_context ⇒ Object
58 59 60 |
# File 'lib/yiffspace/fix_tracker.rb', line 58 def migration_context ActiveRecord::Tasks::DatabaseTasks.migration_connection_pool.migration_context end |
.pending ⇒ Object
95 96 97 98 |
# File 'lib/yiffspace/fix_tracker.rb', line 95 def pending applied_keys = applied all_fixes.reject { |name| applied_keys.include?(key_for(name)) } end |
.record!(name) ⇒ Object
100 101 102 103 104 105 106 107 |
# File 'lib/yiffspace/fix_tracker.rb', line 100 def record!(name) id, index = key_for(name) conn = ActiveRecord::Base.connection conn.execute(" INSERT INTO fixes (id, \"index\") VALUES (\#{id}, \#{index || 'NULL'})\n ON CONFLICT (id, \"index\") DO NOTHING\n SQL\nend\n".squish) |
.required_migration_for(name) ⇒ Object
Statically reads a fix script's declared requires_migration! version, if any, without loading/running it - nil if the fix has no such call.
39 40 41 42 43 44 |
# File 'lib/yiffspace/fix_tracker.rb', line 39 def required_migration_for(name) path = fix_path(name) return nil unless path path.read.match(REQUIRES_MIGRATION_PATTERN)&.captures&.first end |
.requires_migration!(version) ⇒ Object
27 28 29 30 31 |
# File 'lib/yiffspace/fix_tracker.rb', line 27 def requires_migration!(version) return if migration_applied?(version) raise(MigrationRequired, "migration #{version} must be applied before this fix can run") end |
.resolve(id_or_name) ⇒ Object
Resolves a YiffSpace::Fixers::RequiresFix id_or_name to the fix name(s) it refers to - an Integer id covers every step sharing that id (e.g. 2 -> %w[2_1_first_step 2_2_second_step]), a name matches that one fix file exactly.
49 50 51 52 53 54 55 56 |
# File 'lib/yiffspace/fix_tracker.rb', line 49 def resolve(id_or_name) if id_or_name.is_a?(Integer) all_fixes.select { |name| key_for(name).first == id_or_name } else name = id_or_name.to_s.delete_suffix(".rb") all_fixes.select { |candidate| candidate == name } end end |
.run!(path) ⇒ Object
109 110 111 112 113 114 |
# File 'lib/yiffspace/fix_tracker.rb', line 109 def run!(path) name = File.basename(path, ".rb") load(File.(path)) record!(name) name end |
.sort_key(name) ⇒ Object
Sort on the (id, index) parts, not the filename string, so 12 doesn't sort before 2_2.
72 73 74 75 |
# File 'lib/yiffspace/fix_tracker.rb', line 72 def sort_key(name) id, index = key_for(name) [id, index || 0] end |