migsupo 0.2.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: f6bd6bc4627dd5769bb11ae7f872a7234a09eccf52b4ed9661e162d7049dc87a
4
- data.tar.gz: 796a9c73842e770248796ce1f35a5def6e006fe9c44b31485fd9ca21f98ac31f
3
+ metadata.gz: 46769b47cf0ccd1fddb7f3391c43cdec1cbe20df56cb3d18efdafc22c9427c09
4
+ data.tar.gz: a8edc433d3472182ddcfd5be264704c6d21ad58d70e491934c3f92cbf53cd3ed
5
5
  SHA512:
6
- metadata.gz: 88c10374ed224574113524e17ea4856f8671f04022a669899136691c87387580f57f49d91458887261a962d143d033875a633b8e829ccff4fdaa9e8e338ed9a5
7
- data.tar.gz: 14719373ed30592817a1b0e9107dd613fb3936c1c47a977fd0e38d8067bcf0ef982b2bae5df1cac74308a7bcecfa480559fa069b988ea3d32f378f7d641d3250
6
+ metadata.gz: 03a340761dfb39ff21b8d80d98f1901b2e553c6b57b8178e0927fdb78db6f0247034c98d2e159379b754700d8ecd7e46ac45d24966794344a920dd93d1a49aae
7
+ data.tar.gz: cbeba7b4ec1da9528edec1ef4efd7c52d25101d3e6f8f071f4bddde68d23a30a059740b902a99475b620cea0f39aa12aa17a67bdc752fd6813045c6262b6b9cf
data/README.md CHANGED
@@ -126,6 +126,10 @@ rails db:generate_migration:check
126
126
 
127
127
  Adopt a hand-made database change as the truth. See [coherent](#coherent--when-the-db-was-changed-by-hand).
128
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
+
129
133
  ## `coherent` — when the DB was changed by hand
130
134
 
131
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.
@@ -180,6 +184,51 @@ Nothing special. `db/migrate/*.rb` is a normal migration, so staging and product
180
184
  - Only what migsupo models is tracked: tables, columns, indexes. Foreign keys, extensions and check constraints are neither diffed nor written to the Schemafile.
181
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.
182
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
+
183
232
  ## Environment Variables
184
233
 
185
234
  | Variable | Default | Description |
@@ -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
@@ -133,3 +133,81 @@ namespace :db do
133
133
  end
134
134
  end
135
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.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/migsupo.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "migsupo/loader/schema_rb_loader"
10
10
  require_relative "migsupo/differ/diff_calculator"
11
11
  require_relative "migsupo/generator/migration_generator"
12
12
  require_relative "migsupo/coherent"
13
+ require_relative "migsupo/squash"
13
14
 
14
15
  module Migsupo
15
16
  class << self
@@ -39,6 +40,14 @@ module Migsupo
39
40
  .generate(diff, output_dir: output_dir, dry_run: dry_run)
40
41
  end
41
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
+
42
51
  private
43
52
 
44
53
  def build_loader(loader)
@@ -62,14 +71,6 @@ module Migsupo
62
71
  "db/schema.rb"
63
72
  end
64
73
  end
65
-
66
- def detect_rails_version
67
- return configuration.migration_version if configuration.migration_version
68
- return nil unless defined?(Rails)
69
-
70
- major_minor = Rails::VERSION::STRING.split(".").first(2).join(".")
71
- major_minor
72
- end
73
74
  end
74
75
  end
75
76
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: migsupo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - masak1yu
@@ -129,6 +129,7 @@ files:
129
129
  - lib/migsupo/schema/index_definition.rb
130
130
  - lib/migsupo/schema/schema_definition.rb
131
131
  - lib/migsupo/schema/table_definition.rb
132
+ - lib/migsupo/squash.rb
132
133
  - lib/migsupo/tasks/migsupo.rake
133
134
  - lib/migsupo/version.rb
134
135
  - migsupo.gemspec