dekiru-data_migration 0.2.0 → 1.1.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 +4 -4
- data/README.md +96 -1
- data/lib/dekiru/data_migration/migration.rb +81 -0
- data/lib/dekiru/data_migration/version.rb +1 -1
- data/lib/dekiru/data_migration.rb +1 -0
- data/lib/generators/maintenance_script/maintenance_script_generator.rb +4 -1
- data/lib/generators/maintenance_script/templates/maintenance_script.rb.erb +16 -2
- data/sig/dekiru/data_migration/migration.rbs +23 -0
- data/skills/data-migration-script/SKILL.md +86 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 342d506a7d85aa5b54c2023c5bdd2f13d7ccc968e562bde836571461e67541c8
|
|
4
|
+
data.tar.gz: a30d3c9877ac51fac63dd7a0d69333750e453cd5020ae398b1361428d1185a68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19478e294f25f04c5757bd1118f9786bd285b1fbb802a9f91aed81db88fb37503c03ef954c30c6e3d1a7c4476c32a6378c499092a68f786ff5dfb17a4903f2d8
|
|
7
|
+
data.tar.gz: bc354a7e66c1960dde53c49ded487dd4ad4a8b9d6cd0e1852f82c8b94f20bdb08f6565329ee751de9622cefd984fa9f3a7771b9feeb71d945e9aa144874d0049
|
data/README.md
CHANGED
|
@@ -50,6 +50,60 @@ Dekiru::DataMigration::Operator.execute('Grant admin privileges to users') do
|
|
|
50
50
|
end
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
## Data Migration Class (Recommended)
|
|
54
|
+
|
|
55
|
+
You can also define migration logic as a class, which makes testing easier:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# scripts/20230118_demo_migration.rb
|
|
59
|
+
class DemoMigration < Dekiru::DataMigration::Migration
|
|
60
|
+
def migration_targets
|
|
61
|
+
User.where("email LIKE '%sonicgarden%'").where(admin: false)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def migrate_record(user)
|
|
65
|
+
user.update!(admin: true)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def migrate
|
|
69
|
+
super
|
|
70
|
+
log "Updated user count: #{User.where(admin: true).count}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
DemoMigration.run
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Testing Migration Classes
|
|
78
|
+
|
|
79
|
+
The class-based approach makes it easy to write unit tests:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
# spec/migrations/demo_migration_spec.rb
|
|
83
|
+
RSpec.describe DemoMigration do
|
|
84
|
+
let(:migration) { described_class.new }
|
|
85
|
+
|
|
86
|
+
describe '#migration_targets' do
|
|
87
|
+
it 'returns correct migration targets' do
|
|
88
|
+
create_list(:user, 3, email: 'test@sonicgarden.jp', admin: false)
|
|
89
|
+
create_list(:user, 2, email: 'other@example.com', admin: false)
|
|
90
|
+
|
|
91
|
+
targets = migration.migration_targets
|
|
92
|
+
expect(targets.count).to eq(3)
|
|
93
|
+
expect(targets.all? { |u| u.email.include?('sonicgarden') }).to be true
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe '#migrate_record' do
|
|
98
|
+
it 'updates user to admin' do
|
|
99
|
+
user = create(:user, admin: false)
|
|
100
|
+
expect { migration.migrate_record(user) }
|
|
101
|
+
.to change { user.reload.admin }.from(false).to(true)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
53
107
|
Execution result:
|
|
54
108
|
```
|
|
55
109
|
$ bin/rails r scripts/demo.rb
|
|
@@ -98,7 +152,7 @@ Are you sure to commit? (yes/no) > yes
|
|
|
98
152
|
|
|
99
153
|
## Generating Maintenance Scripts
|
|
100
154
|
|
|
101
|
-
You can generate maintenance scripts that use `Dekiru::DataMigration::
|
|
155
|
+
You can generate maintenance scripts that use `Dekiru::DataMigration::Migration` with the generator. The filename will be prefixed with the execution date.
|
|
102
156
|
|
|
103
157
|
```bash
|
|
104
158
|
$ bin/rails g maintenance_script demo_migration
|
|
@@ -109,6 +163,29 @@ Generated file example:
|
|
|
109
163
|
# scripts/20230118_demo_migration.rb
|
|
110
164
|
# frozen_string_literal: true
|
|
111
165
|
|
|
166
|
+
class DemoMigration < Dekiru::DataMigration::Migration
|
|
167
|
+
def migration_targets
|
|
168
|
+
# 移行対象を返すActiveRecord::Relationを定義
|
|
169
|
+
# 例: User.where(some_condition: true)
|
|
170
|
+
raise NotImplementedError, 'migration_targets method must be implemented'
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def migrate_record(record)
|
|
174
|
+
# 個別レコードの更新処理を定義
|
|
175
|
+
# 例: record.update!(some_attribute: new_value)
|
|
176
|
+
raise NotImplementedError, 'migrate_record method must be implemented'
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
DemoMigration.run
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Legacy Block-based Approach
|
|
184
|
+
|
|
185
|
+
For backward compatibility, you can still use the block-based approach:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# scripts/legacy_demo.rb
|
|
112
189
|
Dekiru::DataMigration::Operator.execute('demo_migration') do
|
|
113
190
|
# write here
|
|
114
191
|
end
|
|
@@ -213,3 +290,21 @@ Executes `find_each` with a progress bar for ActiveRecord scopes.
|
|
|
213
290
|
|
|
214
291
|
### `each_with_progress(enum, options = {}, &block)`
|
|
215
292
|
Executes processing with a progress bar for any Enumerable objects.
|
|
293
|
+
|
|
294
|
+
## Agent Skills
|
|
295
|
+
|
|
296
|
+
This repository provides an agent skill for creating data migration scripts.
|
|
297
|
+
|
|
298
|
+
### Available Skills
|
|
299
|
+
|
|
300
|
+
#### `data-migration-script`
|
|
301
|
+
|
|
302
|
+
Automatically creates data migration and deletion scripts using the `dekiru-data_migration` gem.
|
|
303
|
+
|
|
304
|
+
**Triggers**: The agent will use this skill when you ask to "create a data migration script", "create a script to delete unnecessary records", or similar requests involving DB operations via scripts (bulk updates, deleting orphaned records, deleting ActiveStorage files, etc.).
|
|
305
|
+
|
|
306
|
+
**Install**:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
gh skill install SonicGarden/dekiru-data_migration
|
|
310
|
+
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dekiru
|
|
4
|
+
module DataMigration
|
|
5
|
+
# Base class for data migration with testable method separation
|
|
6
|
+
class Migration
|
|
7
|
+
def self.run(options = {})
|
|
8
|
+
migration = new
|
|
9
|
+
title = migration.title
|
|
10
|
+
|
|
11
|
+
Operator.execute(title, options) do
|
|
12
|
+
migration.instance_variable_set(:@operator_context, self)
|
|
13
|
+
migration.migrate
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def title
|
|
18
|
+
self.class.name.demodulize.underscore.humanize
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def migrate
|
|
22
|
+
targets = migration_targets
|
|
23
|
+
|
|
24
|
+
log "Target count: #{targets.count}"
|
|
25
|
+
confirm?
|
|
26
|
+
|
|
27
|
+
find_each_with_progress(targets) do |record|
|
|
28
|
+
migrate_record(record)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
log "Migration completed"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def migration_targets
|
|
35
|
+
raise NotImplementedError, "#{self.class}#migration_targets must be implemented"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def migrate_record(record)
|
|
39
|
+
raise NotImplementedError, "#{self.class}#migrate_record must be implemented"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def confirm?
|
|
45
|
+
if @operator_context
|
|
46
|
+
@operator_context.send(:confirm?)
|
|
47
|
+
else
|
|
48
|
+
# Default behavior during test (no confirmation)
|
|
49
|
+
puts "Confirmation skipped in test mode"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def log(message)
|
|
54
|
+
if @operator_context
|
|
55
|
+
@operator_context.send(:log, message)
|
|
56
|
+
else
|
|
57
|
+
# Default behavior during test
|
|
58
|
+
puts message
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def find_each_with_progress(scope, options = {}, &block)
|
|
63
|
+
if @operator_context
|
|
64
|
+
@operator_context.send(:find_each_with_progress, scope, options, &block)
|
|
65
|
+
else
|
|
66
|
+
# Default behavior during test (no progress bar)
|
|
67
|
+
scope.find_each(&block)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def each_with_progress(enum, options = {}, &block)
|
|
72
|
+
if @operator_context
|
|
73
|
+
@operator_context.send(:each_with_progress, enum, options, &block)
|
|
74
|
+
else
|
|
75
|
+
# Default behavior during test (no progress bar)
|
|
76
|
+
enum.each(&block)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -14,8 +14,11 @@ class MaintenanceScriptGenerator < Rails::Generators::NamedBase
|
|
|
14
14
|
source_root File.expand_path("templates", __dir__)
|
|
15
15
|
|
|
16
16
|
def copy_maintenance_script_file
|
|
17
|
+
@filename_date = filename_date
|
|
18
|
+
@class_name = "#{name.classify}#{@filename_date}"
|
|
19
|
+
|
|
17
20
|
template "maintenance_script.rb.erb",
|
|
18
|
-
"#{Dekiru::DataMigration.configuration.maintenance_script_directory}/#{filename_date}_#{file_name}.rb"
|
|
21
|
+
"#{Dekiru::DataMigration.configuration.maintenance_script_directory}/#{@filename_date}_#{file_name}.rb"
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
private
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
using Dekiru::DataMigration::DangerousMethodGuard
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
class <%= @class_name %> < Dekiru::DataMigration::Migration
|
|
6
|
+
def title
|
|
7
|
+
'<%= @name %>'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def migration_targets
|
|
11
|
+
# Define ActiveRecord::Relation that returns the target migration
|
|
12
|
+
# User.where(some_condition: true)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def migrate_record(record)
|
|
16
|
+
# Define the update process for individual records
|
|
17
|
+
# user.update!(some_attribute: new_value)
|
|
18
|
+
end
|
|
7
19
|
end
|
|
20
|
+
|
|
21
|
+
<%= @class_name %>.run if __FILE__ == $PROGRAM_NAME
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Dekiru
|
|
2
|
+
module DataMigration
|
|
3
|
+
class Migration
|
|
4
|
+
def self.run: (?Hash[Symbol, untyped] options) -> bool
|
|
5
|
+
|
|
6
|
+
def title: () -> String
|
|
7
|
+
|
|
8
|
+
def migrate: () -> void
|
|
9
|
+
|
|
10
|
+
def migration_targets: () -> untyped
|
|
11
|
+
|
|
12
|
+
def migrate_record: (untyped record) -> void
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def log: (String message) -> void
|
|
17
|
+
|
|
18
|
+
def find_each_with_progress: (untyped scope, ?Hash[Symbol, untyped] options) { (untyped) -> void } -> void
|
|
19
|
+
|
|
20
|
+
def each_with_progress: [T] (Enumerable[T] enum, ?Hash[Symbol, untyped] options) { (T) -> void } -> void
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: data-migration-script
|
|
3
|
+
description: Skill for creating data migration and deletion scripts using the dekiru-data_migration gem in the social-apartment project. Always use this skill when asked to "create a script using dekiru-data_migration", "create a data migration script", "create a script to delete unnecessary records", or "turn this into a script". Use proactively for DB operations via scripts such as deleting orphaned records after feature removal or code changes, bulk data updates, and deleting ActiveStorage files.
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Data Migration Script Creation
|
|
8
|
+
|
|
9
|
+
Create data migration and deletion scripts using the `dekiru-data_migration` gem.
|
|
10
|
+
|
|
11
|
+
## Preliminary Research
|
|
12
|
+
|
|
13
|
+
Before writing a script, confirm the following:
|
|
14
|
+
|
|
15
|
+
1. **Understand the changes**: Review related Issues and PRs to understand what was deleted or changed
|
|
16
|
+
2. **Identify target records**: Check the schema (`db/schema.rb`) and current model code to identify the conditions for records that need to be deleted or updated
|
|
17
|
+
|
|
18
|
+
## Script Creation Steps
|
|
19
|
+
|
|
20
|
+
### 1. Generate a file with the generator
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bin/rails generate maintenance_script <PascalCaseName>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- `<PascalCaseName>` is the name describing the operation in PascalCase (e.g., `DeleteDeliveryNotificationImages`)
|
|
27
|
+
- Generated file: `scripts/YYYYMMDD_delete_delivery_notification_images.rb`
|
|
28
|
+
- Today's date (8 digits) is automatically appended to the class name
|
|
29
|
+
|
|
30
|
+
### 2. Edit the generated file
|
|
31
|
+
|
|
32
|
+
Implement `migration_targets` and `migrate_record` in the generated file.
|
|
33
|
+
|
|
34
|
+
### Common Operation Patterns
|
|
35
|
+
|
|
36
|
+
**Deleting ActiveStorage attachments**:
|
|
37
|
+
```ruby
|
|
38
|
+
def migration_targets
|
|
39
|
+
ActiveStorage::Attachment.where(record_type: 'ModelName', name: 'attachment_name')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def migrate_record(record)
|
|
43
|
+
record.purge # synchronously delete attachment and blob
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Updating record attributes**:
|
|
48
|
+
```ruby
|
|
49
|
+
def migrate_record(record)
|
|
50
|
+
record.update!(attribute: new_value)
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Deleting records**:
|
|
55
|
+
```ruby
|
|
56
|
+
def migrate_record(record)
|
|
57
|
+
record.destroy!
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Conditional skip**:
|
|
62
|
+
```ruby
|
|
63
|
+
def migrate_record(record)
|
|
64
|
+
return if record.some_condition?
|
|
65
|
+
record.update!(...)
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Execution and Verification Commands
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Check target record count (before execution)
|
|
73
|
+
bin/rails runner "p TargetModel.where(...).count"
|
|
74
|
+
|
|
75
|
+
# Run the script
|
|
76
|
+
bin/rails runner scripts/YYYYMMDD_description.rb
|
|
77
|
+
|
|
78
|
+
# Verify after execution
|
|
79
|
+
bin/rails runner "p TargetModel.where(...).count"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Notes
|
|
83
|
+
|
|
84
|
+
- `purge` deletes synchronously (immediately removes from storage). Use `purge_later` for async deletion
|
|
85
|
+
- `migration_targets` must return an ActiveRecord relation (processed in batches via `find_each`)
|
|
86
|
+
- Always verify the target record count before running in production
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dekiru-data_migration
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SonicGarden
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-05-04 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -54,6 +54,7 @@ files:
|
|
|
54
54
|
- Rakefile
|
|
55
55
|
- lib/dekiru/data_migration.rb
|
|
56
56
|
- lib/dekiru/data_migration/dangerous_method_guard.rb
|
|
57
|
+
- lib/dekiru/data_migration/migration.rb
|
|
57
58
|
- lib/dekiru/data_migration/operator.rb
|
|
58
59
|
- lib/dekiru/data_migration/transaction_provider.rb
|
|
59
60
|
- lib/dekiru/data_migration/version.rb
|
|
@@ -62,7 +63,9 @@ files:
|
|
|
62
63
|
- lib/generators/maintenance_script/maintenance_script_generator.rb
|
|
63
64
|
- lib/generators/maintenance_script/templates/maintenance_script.rb.erb
|
|
64
65
|
- sig/dekiru/data_migration.rbs
|
|
66
|
+
- sig/dekiru/data_migration/migration.rbs
|
|
65
67
|
- sig/dekiru/data_migration/operator.rbs
|
|
68
|
+
- skills/data-migration-script/SKILL.md
|
|
66
69
|
homepage: https://github.com/SonicGarden/dekiru-data_migration
|
|
67
70
|
licenses:
|
|
68
71
|
- MIT
|