yiffspace-fixers 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +17 -2
- data/lib/yiffspace/fix_tracker.rb +24 -4
- data/lib/yiffspace/fixers/requires_fix.rb +40 -0
- data/lib/yiffspace/fixers/script.rb +12 -0
- data/lib/yiffspace/fixers/version.rb +1 -1
- data/lib/yiffspace/migration_sync.rb +24 -3
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 410c5b95f3bf8123d1cb102448230885bfd3f9017fc7b58e9b98a57c744c3e6b
|
|
4
|
+
data.tar.gz: e1676aa2ef8a186c1886ae40b02354acc6dc1ffe0cb77c272f623ff218c0f5cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 40c9c084ed52eadd521474d6a08f0a96d1dc521ce8746af8d2180ff186a99df2fce72e424672b47b00e42f64bda790f83d5a1cc8202ef0b06646031bd1ce189f
|
|
7
|
+
data.tar.gz: 876bade619a962e9c7035bb8263f51c4adc977fe0cb2afb6db4625eed7528115d4f24ac785b0fe2eae736594575b808025a46e6baad5bc41daad85e2e41b97ca
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
## Unreleased
|
|
2
2
|
|
|
3
|
+
- Migrations can run fixes themselves with `run_fix(id_or_name)`, which runs and records any of
|
|
4
|
+
them not yet applied, and does nothing on rollback. `fixes:migrate_all` reads these calls out
|
|
5
|
+
of pending migrations and folds those fixes into the migration's step instead of scheduling
|
|
6
|
+
them separately. A fix run this way can `requires_migration!` the migration running it.
|
|
7
|
+
- Fixes run through `YiffSpace::FixTracker.run!` (so every `fixes:*` task) can call
|
|
8
|
+
`requires_migration!` unqualified, via `YiffSpace::Fixers::Script` - each fix is now loaded
|
|
9
|
+
wrapped in its own module including it, rather than straight into the top level, so it isn't
|
|
10
|
+
defined anywhere else in the app. Constants a fix defines now land in that wrapper too, instead
|
|
11
|
+
of on `Object`. `required_migration_for` reads the unqualified form as well.
|
|
3
12
|
- Fixed `lib/yiffspace/fixers.rb` never actually defining `YiffSpace::Fixers` -
|
|
4
13
|
it set up the `for_gem_extension` Zeitwerk loader but skipped the namespace
|
|
5
14
|
module the convention requires the root file to define, so referencing
|
data/README.md
CHANGED
|
@@ -32,11 +32,12 @@ $ bin/rails fixes:migrate
|
|
|
32
32
|
A fix can require that a migration has already been applied, and a migration can require that a
|
|
33
33
|
fix has already been applied - useful when one depends on schema or data the other provides.
|
|
34
34
|
`bin/rails fixes:migrate_all` runs both `db/migrate` and `db/fixes` together, in whichever order
|
|
35
|
-
those requirements demand
|
|
35
|
+
those requirements demand. `requires_migration!` is available unqualified inside fixes run by the
|
|
36
|
+
`fixes:*` tasks (`YiffSpace::FixTracker.requires_migration!` works too, and anywhere else):
|
|
36
37
|
|
|
37
38
|
```ruby
|
|
38
39
|
# db/fixes/12_backfill_widget_type.rb
|
|
39
|
-
|
|
40
|
+
requires_migration!("20260822004454")
|
|
40
41
|
|
|
41
42
|
# db/migrate/20260901000000_remove_legacy_widget_column.rb
|
|
42
43
|
class RemoveLegacyWidgetColumn < ActiveRecord::Migration[8.1]
|
|
@@ -48,6 +49,20 @@ class RemoveLegacyWidgetColumn < ActiveRecord::Migration[8.1]
|
|
|
48
49
|
end
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
A migration can also run a fix itself with `run_fix`, inside its own transaction - skipped if the
|
|
53
|
+
fix is already applied, and when the migration is rolled back. `fixes:migrate_all` leaves those
|
|
54
|
+
fixes to the migration rather than running them on their own, and a fix run this way can
|
|
55
|
+
`requires_migration!` the migration running it:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
class AddWidgetType < ActiveRecord::Migration[8.1]
|
|
59
|
+
def change
|
|
60
|
+
add_column(:widgets, :type, :string)
|
|
61
|
+
run_fix(12)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
51
66
|
See `YiffSpace::FixTracker`, `YiffSpace::FixerTemplate`, `YiffSpace::Fixers::RequiresFix`, and
|
|
52
67
|
`YiffSpace::MigrationSync` for the full API.
|
|
53
68
|
|
|
@@ -11,12 +11,14 @@ module YiffSpace
|
|
|
11
11
|
# A fix script calls this as its first statement to declare that a migration must already be
|
|
12
12
|
# applied before it can run, e.g.:
|
|
13
13
|
#
|
|
14
|
-
#
|
|
14
|
+
# requires_migration!("20260822004454")
|
|
15
|
+
#
|
|
16
|
+
# (unqualified via YiffSpace::Fixers::Script, or as YiffSpace::FixTracker.requires_migration!)
|
|
15
17
|
#
|
|
16
18
|
# Written as a literal string call like that (not built dynamically) so YiffSpace::MigrationSync
|
|
17
19
|
# can also read it back out of the file with #required_migration_for, without loading/running
|
|
18
20
|
# the fix, to order `fixes:migrate_all` correctly.
|
|
19
|
-
REQUIRES_MIGRATION_PATTERN = /^\s*YiffSpace::FixTracker\.requires_migration!\(\s*["']([^"']+)["']\s*\)/
|
|
21
|
+
REQUIRES_MIGRATION_PATTERN = /^\s*(?:YiffSpace::FixTracker\.)?requires_migration!\(\s*["']([^"']+)["']\s*\)/
|
|
20
22
|
|
|
21
23
|
module_function
|
|
22
24
|
|
|
@@ -25,7 +27,7 @@ module YiffSpace
|
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def requires_migration!(version)
|
|
28
|
-
return if migration_applied?(version)
|
|
30
|
+
return if migration_applied?(version) || migration_running?(version)
|
|
29
31
|
|
|
30
32
|
raise(MigrationRequired, "migration #{version} must be applied before this fix can run")
|
|
31
33
|
end
|
|
@@ -34,6 +36,23 @@ module YiffSpace
|
|
|
34
36
|
migration_context.get_all_versions.include?(version.to_i)
|
|
35
37
|
end
|
|
36
38
|
|
|
39
|
+
# A migration isn't recorded as applied until it finishes, so a fix it runs with
|
|
40
|
+
# YiffSpace::Fixers::RequiresFix#run_fix is let through requires_migration! for it via this.
|
|
41
|
+
def running_migration(version)
|
|
42
|
+
running_migrations << version.to_i
|
|
43
|
+
yield
|
|
44
|
+
ensure
|
|
45
|
+
running_migrations.pop
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def migration_running?(version)
|
|
49
|
+
running_migrations.include?(version.to_i)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def running_migrations
|
|
53
|
+
@running_migrations ||= []
|
|
54
|
+
end
|
|
55
|
+
|
|
37
56
|
# Statically reads a fix script's declared requires_migration! version, if any, without
|
|
38
57
|
# loading/running it - nil if the fix has no such call.
|
|
39
58
|
def required_migration_for(name)
|
|
@@ -108,7 +127,8 @@ module YiffSpace
|
|
|
108
127
|
|
|
109
128
|
def run!(path)
|
|
110
129
|
name = File.basename(path, ".rb")
|
|
111
|
-
|
|
130
|
+
# A fresh wrapper per fix, so any constants a script defines don't carry over to the next one.
|
|
131
|
+
load(File.expand_path(path), Module.new.include(Fixers::Script))
|
|
112
132
|
record!(name)
|
|
113
133
|
name
|
|
114
134
|
end
|
|
@@ -18,10 +18,32 @@ module YiffSpace
|
|
|
18
18
|
# out of order), and read back ahead of time by YiffSpace::MigrationSync to order
|
|
19
19
|
# `fixes:migrate_all` correctly. Included into ActiveRecord::Migration by
|
|
20
20
|
# YiffSpace::Fixers::Engine.
|
|
21
|
+
#
|
|
22
|
+
# A migration can also run a fix itself, as part of migrating, with `run_fix`:
|
|
23
|
+
#
|
|
24
|
+
# def change
|
|
25
|
+
# add_column(:widgets, :type, :string)
|
|
26
|
+
# run_fix(3)
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# Each fix it covers is run and recorded the same as YiffSpace::FixTracker.run!, unless it's
|
|
30
|
+
# already applied, and inside the migration's transaction. Fixes can't be undone, so it does
|
|
31
|
+
# nothing when the migration is rolled back. A fix run this way can requires_migration! the
|
|
32
|
+
# migration running it.
|
|
21
33
|
module RequiresFix
|
|
22
34
|
# Raised when a migration runs before one of its required fixes has been applied.
|
|
23
35
|
class FixRequired < StandardError; end
|
|
24
36
|
|
|
37
|
+
# Same idea as YiffSpace::FixTracker::REQUIRES_MIGRATION_PATTERN - lets
|
|
38
|
+
# YiffSpace::MigrationSync read the fixes a migration runs out of its file, so
|
|
39
|
+
# `fixes:migrate_all` leaves them to the migration instead of running them on their own.
|
|
40
|
+
RUN_FIX_PATTERN = /^\s*run_fix\(\s*(?:(\d+)|["']([^"']+)["'])\s*\)/
|
|
41
|
+
|
|
42
|
+
# The id_or_name of every run_fix call in a migration file.
|
|
43
|
+
def self.fixes_run_by(path)
|
|
44
|
+
File.read(path).scan(RUN_FIX_PATTERN).map { |id, name| id ? id.to_i : name }
|
|
45
|
+
end
|
|
46
|
+
|
|
25
47
|
def self.included(base)
|
|
26
48
|
base.extend(ClassMethods)
|
|
27
49
|
base.prepend(InstanceMethods)
|
|
@@ -46,6 +68,24 @@ module YiffSpace
|
|
|
46
68
|
super
|
|
47
69
|
end
|
|
48
70
|
|
|
71
|
+
# id_or_name: same as requires_fix.
|
|
72
|
+
def run_fix(id_or_name)
|
|
73
|
+
return if reverting?
|
|
74
|
+
|
|
75
|
+
names = YiffSpace::FixTracker.resolve(id_or_name)
|
|
76
|
+
raise(ArgumentError, "#{self.class.name} runs fix #{id_or_name.inspect}, which does not exist") if names.empty?
|
|
77
|
+
|
|
78
|
+
names.each do |name|
|
|
79
|
+
if YiffSpace::FixTracker.applied?(name)
|
|
80
|
+
say("#{name} already applied, skipping")
|
|
81
|
+
else
|
|
82
|
+
say_with_time("run_fix(#{name.inspect})") do
|
|
83
|
+
YiffSpace::FixTracker.running_migration(version) { YiffSpace::FixTracker.run!(YiffSpace::FixTracker.fix_path(name)) }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
49
89
|
private
|
|
50
90
|
|
|
51
91
|
def check_required_fixes!
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module YiffSpace
|
|
4
|
+
module Fixers
|
|
5
|
+
# Helpers available unqualified at the top level of a db/fixes/*.rb script when it's run
|
|
6
|
+
# through YiffSpace::FixTracker.run! - the script is loaded wrapped in a module including
|
|
7
|
+
# this, so they never leak onto Object for the rest of the app.
|
|
8
|
+
module Script
|
|
9
|
+
delegate(:requires_migration!, to: "YiffSpace::FixTracker")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -4,8 +4,9 @@ module YiffSpace
|
|
|
4
4
|
# Computes and applies pending db/migrate/*.rb migrations and db/fixes/*.rb fixes together, in
|
|
5
5
|
# whichever order their cross requirements demand - a fix can require a migration
|
|
6
6
|
# (YiffSpace::FixTracker.requires_migration!) and a migration can require a fix
|
|
7
|
-
# (YiffSpace::Fixers::RequiresFix#requires_fix)
|
|
8
|
-
#
|
|
7
|
+
# (YiffSpace::Fixers::RequiresFix#requires_fix), or run it itself (RequiresFix#run_fix).
|
|
8
|
+
# Migrations stay ordered among themselves by version, fixes among themselves by
|
|
9
|
+
# YiffSpace::FixTracker.sort_key, and those cross
|
|
9
10
|
# requirements are woven in via a topological sort. #plan/#apply are used by `rake
|
|
10
11
|
# fixes:migrate_all` (see lib/tasks/fixes.rake).
|
|
11
12
|
module MigrationSync
|
|
@@ -24,15 +25,22 @@ module YiffSpace
|
|
|
24
25
|
def plan
|
|
25
26
|
migrations = pending_migrations
|
|
26
27
|
fixes = FixTracker.pending
|
|
28
|
+
run_by = fixes_run_by(migrations)
|
|
27
29
|
|
|
28
30
|
steps = {}
|
|
29
31
|
migrations.each { |m| steps[[:migration, m.version]] = Step.new(:migration, m, [:migration, m.version]) }
|
|
30
|
-
fixes.each { |f| steps[[:fix, f]] = Step.new(:fix, f, [:fix, f]) }
|
|
32
|
+
fixes.each { |f| steps[[:fix, f]] = Step.new(:fix, f, [:fix, f]) unless run_by.key?([:fix, f]) }
|
|
31
33
|
|
|
32
34
|
edges = Hash.new { |h, k| h[k] = [] }
|
|
33
35
|
indegree = steps.each_key.index_with(0)
|
|
34
36
|
|
|
35
37
|
link = lambda do |before, after|
|
|
38
|
+
# A fix a migration runs (see #fixes_run_by) has no step of its own, its requirements and
|
|
39
|
+
# ordering carry over to that migration instead. One requiring that same migration links
|
|
40
|
+
# it to itself, which needs nothing - FixTracker.running_migration covers it at runtime.
|
|
41
|
+
before = run_by.fetch(before, before)
|
|
42
|
+
after = run_by.fetch(after, after)
|
|
43
|
+
next if before == after
|
|
36
44
|
next unless steps.key?(before) && steps.key?(after)
|
|
37
45
|
|
|
38
46
|
edges[before] << after
|
|
@@ -70,6 +78,19 @@ module YiffSpace
|
|
|
70
78
|
end
|
|
71
79
|
end
|
|
72
80
|
|
|
81
|
+
# Maps [:fix, name] => [:migration, version] for every pending fix a pending migration runs
|
|
82
|
+
# itself with YiffSpace::Fixers::RequiresFix#run_fix.
|
|
83
|
+
def fixes_run_by(migrations)
|
|
84
|
+
migrations.each_with_object({}) do |m, run_by|
|
|
85
|
+
Fixers::RequiresFix.fixes_run_by(m.filename).each do |id_or_name|
|
|
86
|
+
names = FixTracker.resolve(id_or_name)
|
|
87
|
+
raise(UnresolvableOrder, "#{m.name} runs fix #{id_or_name.inspect}, which does not exist") if names.empty?
|
|
88
|
+
|
|
89
|
+
names.each { |name| run_by[[:fix, name]] ||= [:migration, m.version] }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
73
94
|
# Kahn's algorithm - ready nodes are processed FIFO in `steps`' insertion order (migrations
|
|
74
95
|
# before fixes when both are ready at once), so requirements only ever reorder things when
|
|
75
96
|
# they actually have to.
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yiffspace-fixers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Donovan_DMC
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -75,6 +75,7 @@ files:
|
|
|
75
75
|
- lib/yiffspace/fixers.rb
|
|
76
76
|
- lib/yiffspace/fixers/engine.rb
|
|
77
77
|
- lib/yiffspace/fixers/requires_fix.rb
|
|
78
|
+
- lib/yiffspace/fixers/script.rb
|
|
78
79
|
- lib/yiffspace/fixers/version.rb
|
|
79
80
|
- lib/yiffspace/migration_sync.rb
|
|
80
81
|
homepage: https://yiff.space
|