migsupo 0.1.0 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f22d99bd89a407d32621aadf871aa3a6059291b338aa2df6b9f12cd29fa6abf
4
- data.tar.gz: 555ccc440d31b410b015f4975b3835f590fa15e900c759d166b3ad1cbb842327
3
+ metadata.gz: 46769b47cf0ccd1fddb7f3391c43cdec1cbe20df56cb3d18efdafc22c9427c09
4
+ data.tar.gz: a8edc433d3472182ddcfd5be264704c6d21ad58d70e491934c3f92cbf53cd3ed
5
5
  SHA512:
6
- metadata.gz: c9d49e0b2092d947a31783bad5c5052b658da6be8b383f4473e91d0afa82e59a0f198288fcffd9eeb03b3a787b85771cefa2bac502fd806616d8460bd5419362
7
- data.tar.gz: cb2ff4341a47c1c646b32cce37dc771b1e85acb1d42d97b42394f8d15f174716707495d118731d8043a8d468eb8fd17414f041ceff7001b23fc4b0539c5c89cb
6
+ metadata.gz: 03a340761dfb39ff21b8d80d98f1901b2e553c6b57b8178e0927fdb78db6f0247034c98d2e159379b754700d8ecd7e46ac45d24966794344a920dd93d1a49aae
7
+ data.tar.gz: cbeba7b4ec1da9528edec1ef4efd7c52d25101d3e6f8f071f4bddde68d23a30a059740b902a99475b620cea0f39aa12aa17a67bdc752fd6813045c6262b6b9cf
data/README.md CHANGED
@@ -122,6 +122,113 @@ Exit with code 1 if the Schemafile and the current database are not in sync. Use
122
122
  rails db:generate_migration:check
123
123
  ```
124
124
 
125
+ ### `rails db:coherent` / `rails db:coherent:apply`
126
+
127
+ Adopt a hand-made database change as the truth. See [coherent](#coherent--when-the-db-was-changed-by-hand).
128
+
129
+ ### `rails db:squash` / `rails db:squash:apply`
130
+
131
+ Collapse the whole migration history into a single `Init` migration. See [squash](#squash--collapse-the-history-into-one-init-migration).
132
+
133
+ ## `coherent` — when the DB was changed by hand
134
+
135
+ Sometimes someone edits the database directly — an emergency `ALTER TABLE` in production, say — and that state is the one you want to keep. The normal flow cannot express this: the Schemafile is behind, and running a migration to catch up would touch columns that are already correct.
136
+
137
+ `coherent` takes the **DB as the source of truth** and moves everything else up to it: it writes the migration file that *would* have produced the change (so every other environment gets it through the normal `rails db:migrate`), updates the Schemafile, and then records the migration as applied on this database **without executing it**.
138
+
139
+ ```
140
+ DB (hand-edited, = truth)
141
+ ├─ db:coherent → db/migrate/*.rb (generated) + Schemafile (updated)
142
+ └─ db:coherent:apply → INSERT INTO schema_migrations ← columns untouched
143
+ db/schema.rb re-dumped
144
+ ```
145
+
146
+ ### 1. Generate
147
+
148
+ ```bash
149
+ rails db:coherent
150
+ ```
151
+
152
+ ```
153
+ Adopting the following from the database:
154
+ add_column users.nickname (string)
155
+
156
+ Generated migration(s):
157
+ db/migrate/20260901120000_add_columns_to_users.rb
158
+ Updated Schemafile: Schemafile
159
+
160
+ Review them, then record the history without touching the database:
161
+ rails db:coherent:apply VERSION=20260901120000
162
+ ```
163
+
164
+ The diff runs in the opposite direction from `db:generate_migration`: the DB is the desired state and the Schemafile is the current one, so a column that exists only in the DB comes out as `add_column`, not `remove_column`.
165
+
166
+ ### 2. Review, then rewrite history only
167
+
168
+ ```bash
169
+ rails db:coherent:apply VERSION=20260901120000
170
+ ```
171
+
172
+ This inserts the version into `schema_migrations` and re-dumps `db/schema.rb`. No DDL is executed — the columns are already where they should be.
173
+
174
+ `VERSION` is required (comma-separate several). Run without it to list the pending migrations. Before writing anything, `apply` re-checks that the DB and the Schemafile match: if they do not, the migration genuinely needs to run and the task aborts.
175
+
176
+ ### On other environments
177
+
178
+ Nothing special. `db/migrate/*.rb` is a normal migration, so staging and production pick it up with `rails db:migrate`.
179
+
180
+ ### Caveats
181
+
182
+ - `db:coherent` regenerates the whole Schemafile from the DB, so hand-written comments and ordering in it are lost. Review the diff (`git diff Schemafile`) before committing.
183
+ - `rename_hints` are not applied — a column renamed by hand comes out as `remove_column` + `add_column`.
184
+ - Only what migsupo models is tracked: tables, columns, indexes. Foreign keys, extensions and check constraints are neither diffed nor written to the Schemafile.
185
+ - Primary key options (`id: :uuid`, `primary_key: "uid"`) are not read back from the database, so the regenerated Schemafile drops them. Diffs are unaffected — table options are not compared — but re-add them by hand if you ever recreate the table from the Schemafile.
186
+
187
+ ## `squash` — collapse the history into one Init migration
188
+
189
+ When `db/migrate` has grown long, `squash` replaces every migration file with a single `<timestamp>_init.rb` that creates the whole schema as declared in the Schemafile, and then rewrites `schema_migrations` to hold only that version. No DDL is executed.
190
+
191
+ ```
192
+ db/migrate/*.rb (N files)
193
+ ├─ db:squash → db/migrate/<ts>_init.rb (all other files deleted)
194
+ └─ db:squash:apply → schema_migrations = [<ts>] ← tables untouched
195
+ db/schema.rb re-dumped
196
+ ```
197
+
198
+ ### 1. Generate
199
+
200
+ ```bash
201
+ rails db:squash
202
+ rails db:squash DRY_RUN=true # print the Init migration, change nothing
203
+ ```
204
+
205
+ It aborts unless both hold:
206
+
207
+ - every migration file has been applied (pending ones may carry data migrations the Schemafile cannot express)
208
+ - the DB matches the Schemafile (the Init migration must reproduce this exact DB)
209
+
210
+ ### 2. Review, then rewrite history
211
+
212
+ ```bash
213
+ rails db:squash:apply VERSION=20261103120000
214
+ ```
215
+
216
+ `VERSION` can be omitted when `db/migrate` holds exactly one file (right after `db:squash`); that one is used.
217
+
218
+ ### On other environments
219
+
220
+ Unlike `coherent`, this is **not** picked up by plain `rails db:migrate` — the Init migration would try to create tables that already exist. On every existing environment:
221
+
222
+ 1. Deploy the pre-squash code and run `rails db:migrate` so the DB matches the Schemafile.
223
+ 2. Deploy the squashed code and run `rails db:squash:apply VERSION=<ts>` **before** `rails db:migrate`.
224
+
225
+ New environments just run `rails db:migrate` (or `db:schema:load`).
226
+
227
+ ### Caveats
228
+
229
+ - Data migrations and raw SQL in the old files are gone. Keep them elsewhere if you still need them.
230
+ - Only what migsupo models is emitted: tables, columns, indexes. Foreign keys, extensions and check constraints are not in the Init migration.
231
+
125
232
  ## Environment Variables
126
233
 
127
234
  | Variable | Default | Description |
@@ -0,0 +1,71 @@
1
+ require_relative "parser/schemafile_parser"
2
+ require_relative "loader/active_record_loader"
3
+ require_relative "differ/diff_calculator"
4
+ require_relative "generator/schemafile_dumper"
5
+
6
+ module Migsupo
7
+ # For the case where the DB was changed by hand and that state is now the
8
+ # truth. Coherent brings the Schemafile and the migration history up to the
9
+ # DB without touching a single column.
10
+ module Coherent
11
+ module_function
12
+
13
+ # desired/current are swapped on purpose: the migration has to move the
14
+ # declared state up to what the DB already looks like.
15
+ # ponytail: rename_hints point the other way here, so they are dropped -
16
+ # a hand-renamed column comes out as remove + add.
17
+ def diff(schemafile_path: nil)
18
+ schemafile_path ||= Migsupo.configuration.schemafile_path
19
+ declared = Parser::SchemafileParser.parse(schemafile_path)
20
+
21
+ Differ::DiffCalculator.new.calculate(desired: actual_schema, current: declared)
22
+ end
23
+
24
+ def dump_schemafile(path: nil)
25
+ path ||= Migsupo.configuration.schemafile_path
26
+ File.write(path, Generator::SchemafileDumper.new.dump(actual_schema))
27
+ path
28
+ end
29
+
30
+ # Writes history only - no DDL is executed. Returns the versions inserted.
31
+ def mark_applied(versions)
32
+ applied = applied_versions
33
+ inserted = versions.map(&:to_s) - applied
34
+
35
+ table = connection.quote_table_name("schema_migrations")
36
+ inserted.each do |version|
37
+ connection.execute("INSERT INTO #{table} (version) VALUES (#{connection.quote(version)})")
38
+ end
39
+ inserted
40
+ end
41
+
42
+ def applied_versions
43
+ connection.select_values(
44
+ "SELECT version FROM #{connection.quote_table_name('schema_migrations')}"
45
+ ).map(&:to_s)
46
+ end
47
+
48
+ # [version, filename] for every migration file not yet in schema_migrations.
49
+ def pending(migrations_dir = nil)
50
+ migrations_dir ||= Migsupo.configuration.migrations_dir
51
+ applied = applied_versions
52
+
53
+ migration_files(migrations_dir).reject { |version, _| applied.include?(version) }
54
+ end
55
+
56
+ def migration_files(migrations_dir)
57
+ Dir.glob(File.join(migrations_dir, "*.rb")).sort.filter_map do |path|
58
+ version = File.basename(path)[/\A\d+/]
59
+ [version, File.basename(path)] if version
60
+ end
61
+ end
62
+
63
+ def actual_schema
64
+ Loader::ActiveRecordLoader.new(ignored_tables: Migsupo.configuration.ignored_tables).load_schema
65
+ end
66
+
67
+ def connection
68
+ ActiveRecord::Base.connection
69
+ end
70
+ end
71
+ end
@@ -32,6 +32,12 @@ module Migsupo
32
32
  RUBY
33
33
  end
34
34
 
35
+ # Renders one create_table block (plus its add_index lines) - the same shape
36
+ # a Schemafile uses.
37
+ def build_table(table)
38
+ render_create_table(Differ::Operations::CreateTable.new(table))
39
+ end
40
+
35
41
  private
36
42
 
37
43
  def version_suffix
@@ -102,7 +108,7 @@ module Migsupo
102
108
 
103
109
  def render_drop_table(op, direction:)
104
110
  if direction == :down
105
- render_create_table(Operations::CreateTable.new(op.table))
111
+ render_create_table(Differ::Operations::CreateTable.new(op.table))
106
112
  else
107
113
  "drop_table #{op.table_name.inspect}"
108
114
  end
@@ -0,0 +1,18 @@
1
+ require_relative "migration_builder"
2
+
3
+ module Migsupo
4
+ module Generator
5
+ # Renders a SchemaDefinition back out as Schemafile DSL.
6
+ # Only what migsupo models (tables / columns / indexes) is emitted, so the
7
+ # output always round-trips through SchemafileParser.
8
+ class SchemafileDumper
9
+ def initialize
10
+ @builder = MigrationBuilder.new
11
+ end
12
+
13
+ def dump(schema)
14
+ "#{schema.tables.values.map { |table| @builder.build_table(table) }.join("\n\n")}\n"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ require_relative "parser/schemafile_parser"
2
+ require_relative "generator/migration_builder"
3
+ require_relative "differ/operations/create_table"
4
+ require_relative "coherent"
5
+
6
+ module Migsupo
7
+ # Collapses the whole migration history into a single Init migration built
8
+ # from the Schemafile, then rewrites schema_migrations to match - no DDL.
9
+ module Squash
10
+ module_function
11
+
12
+ # Built from the Schemafile, not the DB: the DB loader drops primary key
13
+ # options (id: :uuid etc.), the Schemafile keeps them.
14
+ def build(schemafile_path: nil, rails_version: nil)
15
+ schemafile_path ||= Migsupo.configuration.schemafile_path
16
+ schema = Parser::SchemafileParser.parse(schemafile_path)
17
+ ops = schema.tables.values.map { |table| Differ::Operations::CreateTable.new(table) }
18
+
19
+ Generator::MigrationBuilder.new(rails_version: rails_version).build(ops, class_name: "Init")
20
+ end
21
+
22
+ # Deletes every migration file and writes <timestamp>_init.rb. Returns its path.
23
+ def write(content, migrations_dir: nil)
24
+ migrations_dir ||= Migsupo.configuration.migrations_dir
25
+ Dir.glob(File.join(migrations_dir, "*.rb")).each { |path| File.delete(path) }
26
+
27
+ path = File.join(migrations_dir, "#{Time.now.strftime('%Y%m%d%H%M%S')}_init.rb")
28
+ File.write(path, content)
29
+ path
30
+ end
31
+
32
+ # Replaces all history with the single version. Atomic so a failure
33
+ # cannot leave schema_migrations empty.
34
+ def rewrite_history(version)
35
+ connection = Coherent.connection
36
+ table = connection.quote_table_name("schema_migrations")
37
+
38
+ connection.transaction do
39
+ connection.execute("DELETE FROM #{table}")
40
+ connection.execute("INSERT INTO #{table} (version) VALUES (#{connection.quote(version.to_s)})")
41
+ end
42
+ end
43
+ end
44
+ end
@@ -54,3 +54,160 @@ namespace :db do
54
54
  end
55
55
  end
56
56
  end
57
+
58
+ namespace :db do
59
+ desc "Take the current DB as truth: generate migrations for hand-made changes and update the Schemafile"
60
+ task coherent: :environment do
61
+ schemafile_path = ENV.fetch("SCHEMAFILE", Migsupo.configuration.schemafile_path)
62
+ output_dir = ENV.fetch("OUTPUT_DIR", Migsupo.configuration.migrations_dir)
63
+ dry_run = ENV["DRY_RUN"] == "true"
64
+
65
+ diff = Migsupo::Coherent.diff(schemafile_path: schemafile_path)
66
+
67
+ if diff.empty?
68
+ puts "No differences between the database and the Schemafile. Nothing for coherent to do."
69
+ next
70
+ end
71
+
72
+ puts "Adopting the following from the database:"
73
+ puts diff.to_s
74
+ puts
75
+
76
+ files = Migsupo.generate_migrations(diff, output_dir: output_dir, dry_run: dry_run)
77
+ next if dry_run
78
+
79
+ Migsupo::Coherent.dump_schemafile(path: schemafile_path)
80
+ versions = files.map { |f| File.basename(f)[/\A\d+/] }
81
+
82
+ puts "Generated migration(s):"
83
+ files.each { |f| puts " #{f}" }
84
+ puts "Updated Schemafile: #{schemafile_path}"
85
+ puts
86
+ puts "Review them, then record the history without touching the database:"
87
+ puts " rails db:coherent:apply VERSION=#{versions.join(',')}"
88
+ end
89
+
90
+ namespace :coherent do
91
+ desc "Mark the given migrations as applied without running them (VERSION=ts[,ts...])"
92
+ task apply: :environment do
93
+ schemafile_path = ENV.fetch("SCHEMAFILE", Migsupo.configuration.schemafile_path)
94
+ output_dir = ENV.fetch("OUTPUT_DIR", Migsupo.configuration.migrations_dir)
95
+ versions = ENV["VERSION"].to_s.split(",").map(&:strip).reject(&:empty?)
96
+
97
+ if versions.empty?
98
+ puts "VERSION is required (e.g. VERSION=20260901120000)."
99
+ pending = Migsupo::Coherent.pending(output_dir)
100
+ if pending.empty?
101
+ puts "No pending migrations."
102
+ else
103
+ puts "Pending migrations:"
104
+ pending.each { |version, name| puts " #{version} #{name}" }
105
+ end
106
+ exit 1
107
+ end
108
+
109
+ known = Migsupo::Coherent.migration_files(output_dir).to_h
110
+ unknown = versions.reject { |v| known.key?(v) }
111
+ unless unknown.empty?
112
+ puts "No migration found in #{output_dir} for: #{unknown.join(', ')}"
113
+ exit 1
114
+ end
115
+
116
+ # Recording history only is sound when the DB already matches the
117
+ # Schemafile. A remaining diff means the migration genuinely needs to run.
118
+ diff = Migsupo::Coherent.diff(schemafile_path: schemafile_path)
119
+ unless diff.empty?
120
+ puts "The database and the Schemafile do not match. Run rails db:coherent first:"
121
+ puts diff.to_s
122
+ exit 1
123
+ end
124
+
125
+ inserted = Migsupo::Coherent.mark_applied(versions)
126
+ skipped = versions - inserted
127
+
128
+ inserted.each { |v| puts "Recorded as applied: #{v} #{known[v]}" }
129
+ skipped.each { |v| puts "Already recorded, skipped: #{v} #{known[v]}" }
130
+
131
+ Rake::Task["db:schema:dump"].invoke
132
+ puts "Updated db/schema.rb."
133
+ end
134
+ end
135
+ end
136
+
137
+ namespace :db do
138
+ desc "Collapse every migration into a single Init migration built from the Schemafile"
139
+ task squash: :environment do
140
+ schemafile_path = ENV.fetch("SCHEMAFILE", Migsupo.configuration.schemafile_path)
141
+ output_dir = ENV.fetch("OUTPUT_DIR", Migsupo.configuration.migrations_dir)
142
+ dry_run = ENV["DRY_RUN"] == "true"
143
+
144
+ # Pending files may hold data migrations that the Schemafile cannot express.
145
+ pending = Migsupo::Coherent.pending(output_dir)
146
+ unless pending.empty?
147
+ puts "Run the pending migrations before squashing:"
148
+ pending.each { |version, name| puts " #{version} #{name}" }
149
+ exit 1
150
+ end
151
+
152
+ # Init replaces the history only if it reproduces this exact DB.
153
+ diff = Migsupo.calculate_diff(schemafile_path: schemafile_path, loader: :active_record)
154
+ unless diff.empty?
155
+ puts "The database and the Schemafile do not match. Bring them in sync before squashing:"
156
+ puts diff.to_s
157
+ exit 1
158
+ end
159
+
160
+ content = Migsupo::Squash.build(schemafile_path: schemafile_path, rails_version: Migsupo.detect_rails_version)
161
+ if dry_run
162
+ puts content
163
+ next
164
+ end
165
+
166
+ removed = Migsupo::Coherent.migration_files(output_dir).size
167
+ path = Migsupo::Squash.write(content, migrations_dir: output_dir)
168
+ version = File.basename(path)[/\A\d+/]
169
+
170
+ puts "Removed #{removed} migration file(s) and generated:"
171
+ puts " #{path}"
172
+ puts
173
+ puts "Review it, then replace the history on every environment (no DDL is executed):"
174
+ puts " rails db:squash:apply VERSION=#{version}"
175
+ end
176
+
177
+ namespace :squash do
178
+ desc "Replace schema_migrations with the single Init version (VERSION=ts)"
179
+ task apply: :environment do
180
+ schemafile_path = ENV.fetch("SCHEMAFILE", Migsupo.configuration.schemafile_path)
181
+ output_dir = ENV.fetch("OUTPUT_DIR", Migsupo.configuration.migrations_dir)
182
+ version = ENV["VERSION"].to_s.strip
183
+
184
+ if version.empty?
185
+ files = Migsupo::Coherent.migration_files(output_dir)
186
+ unless files.size == 1
187
+ puts "VERSION is required when #{output_dir} does not hold exactly one migration (found #{files.size})."
188
+ exit 1
189
+ end
190
+ version = files.first.first
191
+ puts "Using #{files.first.last}"
192
+ end
193
+
194
+ unless Migsupo::Coherent.migration_files(output_dir).to_h[version]
195
+ puts "No migration found in #{output_dir} for: #{version}"
196
+ exit 1
197
+ end
198
+
199
+ diff = Migsupo.calculate_diff(schemafile_path: schemafile_path, loader: :active_record)
200
+ unless diff.empty?
201
+ puts "The database and the Schemafile do not match. Run rails db:migrate on the pre-squash code first:"
202
+ puts diff.to_s
203
+ exit 1
204
+ end
205
+
206
+ Migsupo::Squash.rewrite_history(version)
207
+ puts "schema_migrations now holds only: #{version}"
208
+
209
+ Rake::Task["db:schema:dump"].invoke
210
+ puts "Updated db/schema.rb."
211
+ end
212
+ end
213
+ end
@@ -1,3 +1,3 @@
1
1
  module Migsupo
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/migsupo.rb CHANGED
@@ -9,6 +9,8 @@ require_relative "migsupo/loader/active_record_loader"
9
9
  require_relative "migsupo/loader/schema_rb_loader"
10
10
  require_relative "migsupo/differ/diff_calculator"
11
11
  require_relative "migsupo/generator/migration_generator"
12
+ require_relative "migsupo/coherent"
13
+ require_relative "migsupo/squash"
12
14
 
13
15
  module Migsupo
14
16
  class << self
@@ -38,6 +40,14 @@ module Migsupo
38
40
  .generate(diff, output_dir: output_dir, dry_run: dry_run)
39
41
  end
40
42
 
43
+ def detect_rails_version
44
+ return configuration.migration_version if configuration.migration_version
45
+ return nil unless defined?(Rails)
46
+
47
+ major_minor = Rails::VERSION::STRING.split(".").first(2).join(".")
48
+ major_minor
49
+ end
50
+
41
51
  private
42
52
 
43
53
  def build_loader(loader)
@@ -61,14 +71,6 @@ module Migsupo
61
71
  "db/schema.rb"
62
72
  end
63
73
  end
64
-
65
- def detect_rails_version
66
- return configuration.migration_version if configuration.migration_version
67
- return nil unless defined?(Rails)
68
-
69
- major_minor = Rails::VERSION::STRING.split(".").first(2).join(".")
70
- major_minor
71
- end
72
74
  end
73
75
  end
74
76
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: migsupo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - masak1yu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-24 00:00:00.000000000 Z
11
+ date: 1980-01-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -103,6 +103,7 @@ extra_rdoc_files: []
103
103
  files:
104
104
  - README.md
105
105
  - lib/migsupo.rb
106
+ - lib/migsupo/coherent.rb
106
107
  - lib/migsupo/configuration.rb
107
108
  - lib/migsupo/differ/diff.rb
108
109
  - lib/migsupo/differ/diff_calculator.rb
@@ -117,6 +118,7 @@ files:
117
118
  - lib/migsupo/generator/migration_builder.rb
118
119
  - lib/migsupo/generator/migration_generator.rb
119
120
  - lib/migsupo/generator/naming.rb
121
+ - lib/migsupo/generator/schemafile_dumper.rb
120
122
  - lib/migsupo/loader/active_record_loader.rb
121
123
  - lib/migsupo/loader/schema_rb_loader.rb
122
124
  - lib/migsupo/parser/dsl_context.rb
@@ -127,6 +129,7 @@ files:
127
129
  - lib/migsupo/schema/index_definition.rb
128
130
  - lib/migsupo/schema/schema_definition.rb
129
131
  - lib/migsupo/schema/table_definition.rb
132
+ - lib/migsupo/squash.rb
130
133
  - lib/migsupo/tasks/migsupo.rake
131
134
  - lib/migsupo/version.rb
132
135
  - migsupo.gemspec
@@ -149,7 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
152
  - !ruby/object:Gem::Version
150
153
  version: '0'
151
154
  requirements: []
152
- rubygems_version: 3.2.32
155
+ rubygems_version: 3.4.19
153
156
  signing_key:
154
157
  specification_version: 4
155
158
  summary: Generate Rails migrations from a Schemafile diff