dekiru-data_migration 1.1.1 → 1.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: df1675b37a0388bddd1b67330d5a1b17c4557fe799de88589ab9f0eea8384a4e
4
- data.tar.gz: ec25183013962b62b2bd7c15ae3eae1961dd8c4866c48081f8715b9733f8c6ce
3
+ metadata.gz: b46a5373c84760557f19d957cdc79d285e1c11812090007b4ccc2dfd7d81cb82
4
+ data.tar.gz: a3ba1a45dc98df3ba559adefcfbda99ccab9cc76e64ea83cd94f3f815d5541df
5
5
  SHA512:
6
- metadata.gz: ce709a7039fd0fb49fb03dc2a325a9e947849bfa5ca5406c9ced48c15d1a586d7d81a2452389cbbd7d3b98754bcb6d45a762054489112496b28d69f1163e1f94
7
- data.tar.gz: df6853cd129775bdc22d9f045f2b38b3bc7fd9da7135aaac9f8a9747d8dd0c782fd137d346310d613ee338e42eef5a1e1b4772c70ca6e210fc340d87f2474bb3
6
+ metadata.gz: 3febccaa7208f1375ef412fcb77c48450d613733900130b6f388e4ae5e099aa0dc4787ab6877cc13769d442cf21ceb21ed058e364423e6a862d49ec3ecc151c9
7
+ data.tar.gz: 6e39a7f6148affaa5e50461e32cf546cde57dcda1e6409a705c6ff617904116c70689d5e71df6233f8fd73e4a430074efbcdfe2c4875b3b8d8adfbaf681633ca
@@ -0,0 +1,5 @@
1
+ {
2
+ "cSpell.words": [
3
+ "demodulize"
4
+ ]
5
+ }
data/README.md CHANGED
@@ -74,6 +74,49 @@ end
74
74
  DemoMigration.run
75
75
  ```
76
76
 
77
+ ### Batch Processing with `migrate_batch`
78
+
79
+ When you need to update or delete a large number of records efficiently, you can define `migrate_batch` instead of `migrate_record`. It processes records in batches using ActiveRecord's `in_batches`, yielding each batch as an `ActiveRecord::Relation` so you can run a single `update_all` / `delete_all` query per batch. The progress bar advances per batch rather than per record.
80
+
81
+ `migrate_batch` and `migrate_record` are mutually exclusive — if you implement `migrate_batch`, the batch path is used automatically.
82
+
83
+ ```ruby
84
+ # scripts/20230118_deactivate_stale_users.rb
85
+ class DeactivateStaleUsersMigration < Dekiru::DataMigration::Migration
86
+ def migration_targets
87
+ User.where(active: true).where("last_login_at < ?", 1.year.ago)
88
+ end
89
+
90
+ def migrate_batch(relation)
91
+ relation.update_all(active: false)
92
+ end
93
+ end
94
+
95
+ DeactivateStaleUsersMigration.run
96
+ ```
97
+
98
+ If you use the block-based `Operator` directly, pass `in_batches` to `each_with_progress`:
99
+
100
+ ```ruby
101
+ # scripts/deactivate_stale_users.rb
102
+ Dekiru::DataMigration::Operator.execute('Deactivate stale users') do
103
+ targets = User.where(active: true).where("last_login_at < ?", 1.year.ago)
104
+
105
+ log "Target user count: #{targets.count}"
106
+ each_with_progress(targets.in_batches) do |batch|
107
+ batch.update_all(active: false)
108
+ end
109
+ end
110
+ ```
111
+
112
+ #### Specifying the batch size
113
+
114
+ Pass `batch_size` to `run` to control how many records are fetched per batch. It applies to both paths — `in_batches(of:)` for `migrate_batch` and `find_each(batch_size:)` for `migrate_record`. When omitted, ActiveRecord's default of 1000 is used.
115
+
116
+ ```ruby
117
+ DeactivateStaleUsersMigration.run(batch_size: 500)
118
+ ```
119
+
77
120
  ### Testing Migration Classes
78
121
 
79
122
  The class-based approach makes it easy to write unit tests:
@@ -286,7 +329,7 @@ Available options:
286
329
  Outputs log messages. Properly handled even during progress bar display.
287
330
 
288
331
  ### `find_each_with_progress(scope, options = {}, &block)`
289
- Executes `find_each` with a progress bar for ActiveRecord scopes.
332
+ Executes `find_each` with a progress bar for ActiveRecord scopes. Pass `batch_size:` in `options` to control how many records are fetched per batch (forwarded to `find_each(batch_size:)`).
290
333
 
291
334
  ### `each_with_progress(enum, options = {}, &block)`
292
335
  Executes processing with a progress bar for any Enumerable objects.
@@ -4,10 +4,15 @@ module Dekiru
4
4
  module DataMigration
5
5
  # Base class for data migration with testable method separation
6
6
  class Migration
7
+ attr_accessor :batch_size
8
+
7
9
  def self.run(options = {})
8
10
  migration = new
9
11
  title = migration.title
10
12
 
13
+ options = options.dup
14
+ migration.batch_size = options.delete(:batch_size)
15
+
11
16
  Operator.execute(title, options) do
12
17
  migration.instance_variable_set(:@operator_context, self)
13
18
  migration.migrate
@@ -24,8 +29,10 @@ module Dekiru
24
29
  log "Target count: #{targets.count}"
25
30
  confirm?
26
31
 
27
- find_each_with_progress(targets) do |record|
28
- migrate_record(record)
32
+ if migrate_batch_overridden?
33
+ migrate_in_batches(targets)
34
+ else
35
+ migrate_each_record(targets)
29
36
  end
30
37
 
31
38
  log "Migration completed"
@@ -39,8 +46,30 @@ module Dekiru
39
46
  raise NotImplementedError, "#{self.class}#migrate_record must be implemented"
40
47
  end
41
48
 
49
+ def migrate_batch(relation)
50
+ raise NotImplementedError, "#{self.class}#migrate_batch must be implemented"
51
+ end
52
+
42
53
  private
43
54
 
55
+ def migrate_in_batches(targets)
56
+ batches = batch_size ? targets.in_batches(of: batch_size) : targets.in_batches
57
+ each_with_progress(batches) do |batch|
58
+ migrate_batch(batch)
59
+ end
60
+ end
61
+
62
+ def migrate_each_record(targets)
63
+ options = batch_size ? { batch_size: batch_size } : {}
64
+ find_each_with_progress(targets, options) do |record|
65
+ migrate_record(record)
66
+ end
67
+ end
68
+
69
+ def migrate_batch_overridden?
70
+ self.class.instance_method(:migrate_batch).owner != Dekiru::DataMigration::Migration
71
+ end
72
+
44
73
  def confirm?
45
74
  if @operator_context
46
75
  @operator_context.send(:confirm?)
@@ -82,7 +82,10 @@ module Dekiru
82
82
  def find_each_with_progress(target_scope, options = {}, &block)
83
83
  # `LocalJumpError: no block given (yield)` が出る場合、 find_each メソッドが enumerator を返していない可能性があります
84
84
  # 直接 each_with_progress を使うか、 find_each が enumerator を返すように修正してください
85
- each_with_progress(target_scope.find_each, options, &block)
85
+ options = options.dup
86
+ batch_size = options.delete(:batch_size)
87
+ scope = batch_size ? target_scope.find_each(batch_size: batch_size) : target_scope.find_each
88
+ each_with_progress(scope, options, &block)
86
89
  end
87
90
 
88
91
  private
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dekiru
4
4
  module DataMigration
5
- VERSION = "1.1.1"
5
+ VERSION = "1.3.0"
6
6
  end
7
7
  end
@@ -29,7 +29,7 @@ bin/rails generate maintenance_script <PascalCaseName>
29
29
 
30
30
  ### 2. Edit the generated file
31
31
 
32
- Implement `migration_targets` and `migrate_record` in the generated file.
32
+ Implement `migration_targets` and `migrate_record` (or `migrate_batch` instead, for bulk-processing a large number of records) in the generated file.
33
33
 
34
34
  ### Common Operation Patterns
35
35
 
@@ -66,6 +66,21 @@ def migrate_record(record)
66
66
  end
67
67
  ```
68
68
 
69
+ **Bulk update/delete a large number of records (batch processing)**:
70
+
71
+ Implement `migrate_batch` instead of `migrate_record` to receive each batch as an `ActiveRecord::Relation` via `in_batches`, allowing you to run `update_all` / `delete_all` in a single query. Use this to process a large number of records efficiently.
72
+ ```ruby
73
+ def migration_targets
74
+ User.where(active: true).where("last_login_at < ?", 1.year.ago)
75
+ end
76
+
77
+ def migrate_batch(relation)
78
+ relation.update_all(active: false) # one query per batch
79
+ end
80
+ ```
81
+
82
+ Pass `batch_size` to `run` to change the batch size (default 1000). It applies to both `migrate_batch` (`in_batches`) and `migrate_record` (`find_each`): `MyMigration.run(batch_size: 500)`.
83
+
69
84
  ## Execution and Verification Commands
70
85
 
71
86
  ```bash
@@ -83,4 +98,5 @@ bin/rails runner "p TargetModel.where(...).count"
83
98
 
84
99
  - `purge` deletes synchronously (immediately removes from storage). Use `purge_later` for async deletion
85
100
  - `migration_targets` must return an ActiveRecord relation (processed in batches via `find_each`)
101
+ - `migrate_batch`'s `update_all` / `delete_all` issue SQL directly without running callbacks or validations. `updated_at` is not auto-updated either, so include it explicitly if needed
86
102
  - Always verify the target record count before running in production
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dekiru-data_migration
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SonicGarden
@@ -48,6 +48,7 @@ extra_rdoc_files: []
48
48
  files:
49
49
  - ".rspec"
50
50
  - ".rubocop.yml"
51
+ - ".vscode/settings.json"
51
52
  - CODE_OF_CONDUCT.md
52
53
  - LICENSE.txt
53
54
  - README.md