monarch_migrate 0.5.0 → 0.6.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/Gemfile.lock +75 -75
- data/README.md +138 -55
- data/lib/monarch_migrate/migration.rb +15 -7
- data/lib/monarch_migrate/migrator.rb +4 -6
- data/lib/monarch_migrate/tasks.rake +1 -1
- data/lib/monarch_migrate/testing.rb +5 -4
- data/lib/monarch_migrate/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d70afb71a32546dd38ae87d4e39956c97bd095c56cc957266a159edb4cbfc3cb
|
4
|
+
data.tar.gz: 2b95f318d045045dba0cb712bc0aa647d95717688e2cb258702e9bea9c398d6f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f97b549b57586af15371dfadba856b9d506989d210a76ae21cc85e20d0eff097ca58fd26a1901760c8755d0b45be6583bfabc2f9f71e288cd0949d1ab98df150
|
7
|
+
data.tar.gz: 6f773f40eb0028229a75aa58055b3765580261f4c50d21bd01ab64ab5b5f8f001129626f442d022825d9038729a6bdf2519dcff425b31febeed681230257347c
|
data/Gemfile.lock
CHANGED
@@ -1,73 +1,73 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
monarch_migrate (0.
|
4
|
+
monarch_migrate (0.6.0)
|
5
5
|
rails (>= 5.2.0)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
actioncable (7.0.3)
|
11
|
-
actionpack (= 7.0.3)
|
12
|
-
activesupport (= 7.0.3)
|
10
|
+
actioncable (7.0.3.1)
|
11
|
+
actionpack (= 7.0.3.1)
|
12
|
+
activesupport (= 7.0.3.1)
|
13
13
|
nio4r (~> 2.0)
|
14
14
|
websocket-driver (>= 0.6.1)
|
15
|
-
actionmailbox (7.0.3)
|
16
|
-
actionpack (= 7.0.3)
|
17
|
-
activejob (= 7.0.3)
|
18
|
-
activerecord (= 7.0.3)
|
19
|
-
activestorage (= 7.0.3)
|
20
|
-
activesupport (= 7.0.3)
|
15
|
+
actionmailbox (7.0.3.1)
|
16
|
+
actionpack (= 7.0.3.1)
|
17
|
+
activejob (= 7.0.3.1)
|
18
|
+
activerecord (= 7.0.3.1)
|
19
|
+
activestorage (= 7.0.3.1)
|
20
|
+
activesupport (= 7.0.3.1)
|
21
21
|
mail (>= 2.7.1)
|
22
22
|
net-imap
|
23
23
|
net-pop
|
24
24
|
net-smtp
|
25
|
-
actionmailer (7.0.3)
|
26
|
-
actionpack (= 7.0.3)
|
27
|
-
actionview (= 7.0.3)
|
28
|
-
activejob (= 7.0.3)
|
29
|
-
activesupport (= 7.0.3)
|
25
|
+
actionmailer (7.0.3.1)
|
26
|
+
actionpack (= 7.0.3.1)
|
27
|
+
actionview (= 7.0.3.1)
|
28
|
+
activejob (= 7.0.3.1)
|
29
|
+
activesupport (= 7.0.3.1)
|
30
30
|
mail (~> 2.5, >= 2.5.4)
|
31
31
|
net-imap
|
32
32
|
net-pop
|
33
33
|
net-smtp
|
34
34
|
rails-dom-testing (~> 2.0)
|
35
|
-
actionpack (7.0.3)
|
36
|
-
actionview (= 7.0.3)
|
37
|
-
activesupport (= 7.0.3)
|
35
|
+
actionpack (7.0.3.1)
|
36
|
+
actionview (= 7.0.3.1)
|
37
|
+
activesupport (= 7.0.3.1)
|
38
38
|
rack (~> 2.0, >= 2.2.0)
|
39
39
|
rack-test (>= 0.6.3)
|
40
40
|
rails-dom-testing (~> 2.0)
|
41
41
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
42
|
-
actiontext (7.0.3)
|
43
|
-
actionpack (= 7.0.3)
|
44
|
-
activerecord (= 7.0.3)
|
45
|
-
activestorage (= 7.0.3)
|
46
|
-
activesupport (= 7.0.3)
|
42
|
+
actiontext (7.0.3.1)
|
43
|
+
actionpack (= 7.0.3.1)
|
44
|
+
activerecord (= 7.0.3.1)
|
45
|
+
activestorage (= 7.0.3.1)
|
46
|
+
activesupport (= 7.0.3.1)
|
47
47
|
globalid (>= 0.6.0)
|
48
48
|
nokogiri (>= 1.8.5)
|
49
|
-
actionview (7.0.3)
|
50
|
-
activesupport (= 7.0.3)
|
49
|
+
actionview (7.0.3.1)
|
50
|
+
activesupport (= 7.0.3.1)
|
51
51
|
builder (~> 3.1)
|
52
52
|
erubi (~> 1.4)
|
53
53
|
rails-dom-testing (~> 2.0)
|
54
54
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
55
|
-
activejob (7.0.3)
|
56
|
-
activesupport (= 7.0.3)
|
55
|
+
activejob (7.0.3.1)
|
56
|
+
activesupport (= 7.0.3.1)
|
57
57
|
globalid (>= 0.3.6)
|
58
|
-
activemodel (7.0.3)
|
59
|
-
activesupport (= 7.0.3)
|
60
|
-
activerecord (7.0.3)
|
61
|
-
activemodel (= 7.0.3)
|
62
|
-
activesupport (= 7.0.3)
|
63
|
-
activestorage (7.0.3)
|
64
|
-
actionpack (= 7.0.3)
|
65
|
-
activejob (= 7.0.3)
|
66
|
-
activerecord (= 7.0.3)
|
67
|
-
activesupport (= 7.0.3)
|
58
|
+
activemodel (7.0.3.1)
|
59
|
+
activesupport (= 7.0.3.1)
|
60
|
+
activerecord (7.0.3.1)
|
61
|
+
activemodel (= 7.0.3.1)
|
62
|
+
activesupport (= 7.0.3.1)
|
63
|
+
activestorage (7.0.3.1)
|
64
|
+
actionpack (= 7.0.3.1)
|
65
|
+
activejob (= 7.0.3.1)
|
66
|
+
activerecord (= 7.0.3.1)
|
67
|
+
activesupport (= 7.0.3.1)
|
68
68
|
marcel (~> 1.0)
|
69
69
|
mini_mime (>= 1.1.0)
|
70
|
-
activesupport (7.0.3)
|
70
|
+
activesupport (7.0.3.1)
|
71
71
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
72
72
|
i18n (>= 1.6, < 2)
|
73
73
|
minitest (>= 5.1)
|
@@ -82,12 +82,12 @@ GEM
|
|
82
82
|
coderay (1.1.3)
|
83
83
|
concurrent-ruby (1.1.10)
|
84
84
|
crass (1.0.6)
|
85
|
-
diff-lcs (1.
|
85
|
+
diff-lcs (1.5.0)
|
86
86
|
digest (3.1.0)
|
87
87
|
erubi (1.10.0)
|
88
88
|
globalid (1.0.0)
|
89
89
|
activesupport (>= 5.0)
|
90
|
-
i18n (1.
|
90
|
+
i18n (1.12.0)
|
91
91
|
concurrent-ruby (~> 1.0)
|
92
92
|
loofah (2.18.0)
|
93
93
|
crass (~> 1.0.2)
|
@@ -98,7 +98,7 @@ GEM
|
|
98
98
|
method_source (1.0.0)
|
99
99
|
mini_mime (1.1.2)
|
100
100
|
mini_portile2 (2.8.0)
|
101
|
-
minitest (5.
|
101
|
+
minitest (5.16.2)
|
102
102
|
net-imap (0.2.3)
|
103
103
|
digest
|
104
104
|
net-protocol
|
@@ -114,7 +114,7 @@ GEM
|
|
114
114
|
net-protocol
|
115
115
|
timeout
|
116
116
|
nio4r (2.5.8)
|
117
|
-
nokogiri (1.13.
|
117
|
+
nokogiri (1.13.7)
|
118
118
|
mini_portile2 (~> 2.8.0)
|
119
119
|
racc (~> 1.4)
|
120
120
|
parallel (1.22.1)
|
@@ -124,48 +124,48 @@ GEM
|
|
124
124
|
coderay (~> 1.1)
|
125
125
|
method_source (~> 1.0)
|
126
126
|
racc (1.6.0)
|
127
|
-
rack (2.2.
|
128
|
-
rack-test (
|
129
|
-
rack (>= 1.
|
130
|
-
rails (7.0.3)
|
131
|
-
actioncable (= 7.0.3)
|
132
|
-
actionmailbox (= 7.0.3)
|
133
|
-
actionmailer (= 7.0.3)
|
134
|
-
actionpack (= 7.0.3)
|
135
|
-
actiontext (= 7.0.3)
|
136
|
-
actionview (= 7.0.3)
|
137
|
-
activejob (= 7.0.3)
|
138
|
-
activemodel (= 7.0.3)
|
139
|
-
activerecord (= 7.0.3)
|
140
|
-
activestorage (= 7.0.3)
|
141
|
-
activesupport (= 7.0.3)
|
127
|
+
rack (2.2.4)
|
128
|
+
rack-test (2.0.2)
|
129
|
+
rack (>= 1.3)
|
130
|
+
rails (7.0.3.1)
|
131
|
+
actioncable (= 7.0.3.1)
|
132
|
+
actionmailbox (= 7.0.3.1)
|
133
|
+
actionmailer (= 7.0.3.1)
|
134
|
+
actionpack (= 7.0.3.1)
|
135
|
+
actiontext (= 7.0.3.1)
|
136
|
+
actionview (= 7.0.3.1)
|
137
|
+
activejob (= 7.0.3.1)
|
138
|
+
activemodel (= 7.0.3.1)
|
139
|
+
activerecord (= 7.0.3.1)
|
140
|
+
activestorage (= 7.0.3.1)
|
141
|
+
activesupport (= 7.0.3.1)
|
142
142
|
bundler (>= 1.15.0)
|
143
|
-
railties (= 7.0.3)
|
143
|
+
railties (= 7.0.3.1)
|
144
144
|
rails-dom-testing (2.0.3)
|
145
145
|
activesupport (>= 4.2.0)
|
146
146
|
nokogiri (>= 1.6)
|
147
|
-
rails-html-sanitizer (1.4.
|
147
|
+
rails-html-sanitizer (1.4.3)
|
148
148
|
loofah (~> 2.3)
|
149
|
-
railties (7.0.3)
|
150
|
-
actionpack (= 7.0.3)
|
151
|
-
activesupport (= 7.0.3)
|
149
|
+
railties (7.0.3.1)
|
150
|
+
actionpack (= 7.0.3.1)
|
151
|
+
activesupport (= 7.0.3.1)
|
152
152
|
method_source
|
153
153
|
rake (>= 12.2)
|
154
154
|
thor (~> 1.0)
|
155
155
|
zeitwerk (~> 2.5)
|
156
156
|
rainbow (3.1.1)
|
157
157
|
rake (13.0.6)
|
158
|
-
regexp_parser (2.
|
158
|
+
regexp_parser (2.5.0)
|
159
159
|
rexml (3.2.5)
|
160
|
-
rspec-core (3.
|
161
|
-
rspec-support (~> 3.
|
162
|
-
rspec-expectations (3.
|
160
|
+
rspec-core (3.11.0)
|
161
|
+
rspec-support (~> 3.11.0)
|
162
|
+
rspec-expectations (3.11.0)
|
163
163
|
diff-lcs (>= 1.2.0, < 2.0)
|
164
|
-
rspec-support (~> 3.
|
165
|
-
rspec-mocks (3.
|
164
|
+
rspec-support (~> 3.11.0)
|
165
|
+
rspec-mocks (3.11.1)
|
166
166
|
diff-lcs (>= 1.2.0, < 2.0)
|
167
|
-
rspec-support (~> 3.
|
168
|
-
rspec-rails (5.
|
167
|
+
rspec-support (~> 3.11.0)
|
168
|
+
rspec-rails (5.1.2)
|
169
169
|
actionpack (>= 5.2)
|
170
170
|
activesupport (>= 5.2)
|
171
171
|
railties (>= 5.2)
|
@@ -173,7 +173,7 @@ GEM
|
|
173
173
|
rspec-expectations (~> 3.10)
|
174
174
|
rspec-mocks (~> 3.10)
|
175
175
|
rspec-support (~> 3.10)
|
176
|
-
rspec-support (3.
|
176
|
+
rspec-support (3.11.0)
|
177
177
|
rubocop (1.29.1)
|
178
178
|
parallel (~> 1.10)
|
179
179
|
parser (>= 3.1.0.0)
|
@@ -183,13 +183,13 @@ GEM
|
|
183
183
|
rubocop-ast (>= 1.17.0, < 2.0)
|
184
184
|
ruby-progressbar (~> 1.7)
|
185
185
|
unicode-display_width (>= 1.4.0, < 3.0)
|
186
|
-
rubocop-ast (1.
|
186
|
+
rubocop-ast (1.19.1)
|
187
187
|
parser (>= 3.1.1.0)
|
188
188
|
rubocop-performance (1.13.3)
|
189
189
|
rubocop (>= 1.7.0, < 2.0)
|
190
190
|
rubocop-ast (>= 0.4.0)
|
191
191
|
ruby-progressbar (1.11.0)
|
192
|
-
sqlite3 (1.4.
|
192
|
+
sqlite3 (1.4.4)
|
193
193
|
standard (1.12.1)
|
194
194
|
rubocop (= 1.29.1)
|
195
195
|
rubocop-performance (= 1.13.3)
|
@@ -198,11 +198,11 @@ GEM
|
|
198
198
|
timeout (0.3.0)
|
199
199
|
tzinfo (2.0.4)
|
200
200
|
concurrent-ruby (~> 1.0)
|
201
|
-
unicode-display_width (2.
|
201
|
+
unicode-display_width (2.2.0)
|
202
202
|
websocket-driver (0.7.5)
|
203
203
|
websocket-extensions (>= 0.1.0)
|
204
204
|
websocket-extensions (0.1.5)
|
205
|
-
zeitwerk (2.
|
205
|
+
zeitwerk (2.6.0)
|
206
206
|
|
207
207
|
PLATFORMS
|
208
208
|
ruby
|
data/README.md
CHANGED
@@ -1,6 +1,31 @@
|
|
1
|
-
#
|
1
|
+
# monarch_migrate [![Build Status][ci-image]][ci] [![Gem Version][version-image]][version]
|
2
2
|
|
3
|
-
|
3
|
+
A library for Rails developers who are not willing to leave data migrations to chance.
|
4
|
+
|
5
|
+
<br />
|
6
|
+
|
7
|
+
## Table of Contents
|
8
|
+
|
9
|
+
- [Rationale](#rationale)
|
10
|
+
- [Install](#install)
|
11
|
+
- [Usage](#usage)
|
12
|
+
- [Create a Data Migration](#create-a-data-migration)
|
13
|
+
- [Running Data Migrations](#running-data-migrations)
|
14
|
+
- [Display Status of Data Migrations](#display-status-of-data-migrations)
|
15
|
+
- [Reverting Data Migrations](#reverting-data-migrations)
|
16
|
+
- [Background Tasks in Data Migrations](#background-tasks-in-data-migrations)
|
17
|
+
- [Testing](#testing)
|
18
|
+
- [RSpec](#rspec)
|
19
|
+
- [TestUnit](#testunit)
|
20
|
+
- [Suggested Workflow](#suggested-workflow)
|
21
|
+
- [Known Issues and Limitations](#known-issues-and-limitations)
|
22
|
+
- [Trivia](#trivia)
|
23
|
+
- [Acknowledgments](#acknowledgments)
|
24
|
+
- [License](#license)
|
25
|
+
|
26
|
+
|
27
|
+
## Rationale
|
28
|
+
<sup>[(Back to top)](#table-of-contents)</sup>
|
4
29
|
|
5
30
|
<blockquote>
|
6
31
|
<p>The main purpose of Rails' migration feature is to issue commands that modify the schema using a consistent process. Migrations can also be used to add or modify data. This is useful in an existing database that can't be destroyed and recreated, such as a production database.</p>
|
@@ -9,30 +34,35 @@
|
|
9
34
|
</a>
|
10
35
|
</blockquote>
|
11
36
|
|
12
|
-
The motivation behind Rails' migration mechanism is schema modification.
|
13
|
-
for data changes in the database comes second. Yet, adding or
|
14
|
-
regular migrations can be problematic.
|
37
|
+
The motivation behind Rails' migration mechanism is schema modification.
|
38
|
+
Using it for data changes in the database comes second. Yet, adding or
|
39
|
+
modifying data via regular migrations can be problematic.
|
15
40
|
|
16
|
-
|
41
|
+
One issue is that application deployment now depends on the data migration
|
17
42
|
to be completed. This may not be a problem with small databases but large
|
18
43
|
databases with millions of records will respond with hanging or failed migrations.
|
19
44
|
|
20
45
|
Another issue is that data migration files tend to stay in `db/migrate` for posterity.
|
21
46
|
As a result, they will run whenever a developer sets up their local development environment.
|
22
|
-
This is unnecessary for a pristine database. Especially
|
47
|
+
This is unnecessary for a pristine database. Especially with [scripts][seed-scripts] to
|
23
48
|
seed the correct data.
|
24
49
|
|
25
|
-
The purpose of `monarch_migrate` is to solve the above issues
|
50
|
+
The purpose of `monarch_migrate` is to solve the above issues and to:
|
51
|
+
|
52
|
+
- Provide a [uniform process](#suggested-workflow) for modifying data in the database.
|
53
|
+
- Separate data migrations from schema migrations.
|
54
|
+
- Allow automated testing of data migrations.
|
26
55
|
|
27
|
-
It
|
56
|
+
It assumes that:
|
28
57
|
|
29
|
-
- You run data migrations
|
58
|
+
- You run data migrations only on production and rely on seed [scripts][seed-scripts] for local development.
|
30
59
|
- You run data migrations manually.
|
31
|
-
- You want to
|
60
|
+
- You want to test data migrations in a thorough and automated manner.
|
32
61
|
|
33
62
|
|
34
63
|
|
35
64
|
## Install
|
65
|
+
<sup>[(Back to top)](#table-of-contents)</sup>
|
36
66
|
|
37
67
|
Add the gem to your Gemfile:
|
38
68
|
|
@@ -48,45 +78,45 @@ After you install the gem, you need to run the generator:
|
|
48
78
|
rails generate monarch_migrate:install
|
49
79
|
```
|
50
80
|
|
51
|
-
The install generator creates a migration file that adds a `data_migration_records`
|
52
|
-
table. It is where the gem keeps track of data migrations we have
|
81
|
+
The install generator creates a schema migration file that adds a `data_migration_records`
|
82
|
+
table. It is where the gem keeps track of data migrations we have ran.
|
53
83
|
|
84
|
+
Run the schema migration to create the table:
|
54
85
|
|
86
|
+
```shell
|
87
|
+
rails db:migrate
|
88
|
+
```
|
55
89
|
|
56
|
-
## Usage
|
57
90
|
|
58
|
-
Data migrations have a similar structure to regular migrations in Rails. Files are
|
59
|
-
put into `db/data_migrate` and follow the same naming pattern.
|
60
91
|
|
61
|
-
|
92
|
+
## Usage
|
93
|
+
<sup>[(Back to top)](#table-of-contents)</sup>
|
62
94
|
|
63
|
-
|
64
|
-
|
65
|
-
up with the following plan:
|
95
|
+
Data migrations have a similar structure to regular migrations in Rails.
|
96
|
+
Files follow the same naming pattern but reside in `db/data_migrate`.
|
66
97
|
|
67
|
-
1. Add a `name` column to `users` table to hold person's full name.
|
68
|
-
2. Adapt the `User` model and use a data migration to update existing records.
|
69
|
-
3. Drop `first_name` and `last_name` columns.
|
70
98
|
|
71
|
-
|
99
|
+
### Create a Data Migration
|
72
100
|
|
73
101
|
```shell
|
74
102
|
rails generate data_migration backfill_users_name
|
75
103
|
```
|
76
104
|
|
77
|
-
|
105
|
+
Edit the newly created file to define the data migration:
|
78
106
|
|
79
107
|
```ruby
|
80
108
|
# db/data_migrate/20220605083010_backfill_users_name.rb
|
81
109
|
ActiveRecord::Base.connection.execute(<<-SQL)
|
82
110
|
UPDATE users SET name = concat(first_name, ' ', last_name) WHERE name IS NULL;
|
83
111
|
SQL
|
84
|
-
|
85
|
-
SearchIndex::RebuildJob.perform_later
|
86
112
|
```
|
87
113
|
|
88
|
-
|
89
|
-
|
114
|
+
In contrast to regular migrations, there is no need to inherit any classes. It is plain
|
115
|
+
ruby code where you can refer to any object you need. If the database adapter supports
|
116
|
+
transactions, each data migration will automatically be wrapped in a separate transaction.
|
117
|
+
|
118
|
+
|
119
|
+
### Running Data Migrations
|
90
120
|
|
91
121
|
To run pending data migrations:
|
92
122
|
|
@@ -94,19 +124,52 @@ To run pending data migrations:
|
|
94
124
|
rails data:migrate
|
95
125
|
```
|
96
126
|
|
97
|
-
|
127
|
+
To run a specific version:
|
98
128
|
|
99
129
|
```shell
|
100
130
|
rails data:migrate VERSION=20220605083010
|
101
131
|
```
|
102
132
|
|
103
133
|
|
134
|
+
### Display Status of Data Migrations
|
135
|
+
|
136
|
+
You can see the status of data migrations with:
|
137
|
+
|
138
|
+
```shell
|
139
|
+
rails data:migrate:status
|
140
|
+
```
|
141
|
+
|
142
|
+
### Reverting Data Migrations
|
143
|
+
|
144
|
+
Rollback functionality is not provided by design. Create another data migration instead.
|
145
|
+
|
146
|
+
|
147
|
+
### Background Tasks in Data Migrations
|
148
|
+
|
149
|
+
After the data manipulation is complete, you may want to trigger a long running task.
|
150
|
+
Yet, there are [some pitfalls](#long-running-tasks-in-migrations) to be aware of.
|
151
|
+
|
152
|
+
To avoid them, use an after-commit callback:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# db/data_migrate/20220605083010_backfill_users_name.rb
|
156
|
+
ActiveRecord::Base.connection.execute(<<-SQL)
|
157
|
+
UPDATE users SET name = concat(first_name, ' ', last_name) WHERE name IS NULL;
|
158
|
+
SQL
|
159
|
+
|
160
|
+
after_commit do
|
161
|
+
SearchIndex::RebuildJob.perform_later
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
|
104
166
|
## Testing
|
167
|
+
<sup>[(Back to top)](#table-of-contents)</sup>
|
105
168
|
|
106
|
-
Testing data migrations can be the difference between
|
107
|
-
the migration and having to recover from a data loss.
|
169
|
+
Testing data migrations can be the difference between rerunning
|
170
|
+
the migration and having to recover from a data loss. This is
|
171
|
+
why `monarch_migrate` includes test helpers for both RSpec and TestUnit.
|
108
172
|
|
109
|
-
This is why `monarch_migrate` includes test helpers for both RSpec and TestUnit.
|
110
173
|
|
111
174
|
### RSpec
|
112
175
|
|
@@ -128,7 +191,7 @@ describe "20220605083010_backfill_users_name", type: :data_migration do
|
|
128
191
|
# or if you're using FactoryBot:
|
129
192
|
# user = create(:user, first_name: "Guybrush", last_name: "Threepwood", name: nil)
|
130
193
|
|
131
|
-
expect
|
194
|
+
expect { subject }.to change { user.reload.name }.to("Guybrush Threepwood")
|
132
195
|
end
|
133
196
|
|
134
197
|
it "does not assign name to already migrated users" do
|
@@ -136,7 +199,7 @@ describe "20220605083010_backfill_users_name", type: :data_migration do
|
|
136
199
|
# or if you're using FactoryBot:
|
137
200
|
# user = create(:user, first_name: "", last_name: "", name: "Guybrush Threepwood")
|
138
201
|
|
139
|
-
expect
|
202
|
+
expect { subject }.not_to change { user.reload.name }
|
140
203
|
end
|
141
204
|
|
142
205
|
context "when the user has no last name" do
|
@@ -145,7 +208,7 @@ describe "20220605083010_backfill_users_name", type: :data_migration do
|
|
145
208
|
# or if you're using FactoryBot:
|
146
209
|
# user = create(:user, first_name: "Guybrush", last_name: nil, name: nil)
|
147
210
|
|
148
|
-
expect
|
211
|
+
expect { subject }.to change { user.reload.name }.to("Guybrush")
|
149
212
|
end
|
150
213
|
end
|
151
214
|
|
@@ -200,21 +263,30 @@ class BackfillUsersNameTest < MonarchMigrate::TestCase
|
|
200
263
|
end
|
201
264
|
```
|
202
265
|
|
203
|
-
|
204
|
-
|
205
|
-
e.g. `first_name` and `last_name`.
|
266
|
+
In certain cases, it makes sense to also test with manually running the data migration
|
267
|
+
against a production clone of the database.
|
206
268
|
|
207
|
-
One solution is to use the following development workflow:
|
208
269
|
|
209
|
-
|
270
|
+
|
271
|
+
## Suggested Workflow
|
272
|
+
<sup>[(Back to top)](#table-of-contents)</sup>
|
273
|
+
|
274
|
+
Data migrations become obsolete, once the data manipulation successfully completes. The same applies for the corresponding tests.
|
275
|
+
|
276
|
+
The suggested development workflow is:
|
277
|
+
|
278
|
+
1. Implement the data migration. Use TDD, if appropriate.
|
210
279
|
1. Commit, push and wait for CI to pass.
|
211
280
|
1. Request review from peers.
|
212
281
|
1. Once approved, remove the test files in a consecutive commit and push again.
|
213
282
|
1. Merge into trunk.
|
214
283
|
|
215
|
-
This will
|
284
|
+
This will keep the test files in repo's history for posterity.
|
285
|
+
|
286
|
+
|
216
287
|
|
217
288
|
## Known Issues and Limitations
|
289
|
+
<sup>[(Back to top)](#table-of-contents)</sup>
|
218
290
|
|
219
291
|
The following issues and limitations are not necessary inherent to `monarch_migrate`.
|
220
292
|
Some are innate to migrations in general.
|
@@ -233,7 +305,7 @@ Typically, data migrations relate closely to business models. In an ideal Rails
|
|
233
305
|
data manipulations would depend on model logic to enforce validation, conform to
|
234
306
|
business rules, etc. Hence, it is very tempting to use ActiveRecord models in migrations.
|
235
307
|
|
236
|
-
Here
|
308
|
+
Here a regular Rails migration:
|
237
309
|
|
238
310
|
```ruby
|
239
311
|
# db/migrate/20220605083010_backfill_users_name.rb
|
@@ -266,10 +338,10 @@ end
|
|
266
338
|
Unfortunately, with regular Rails migrations we will still face issue 4.
|
267
339
|
|
268
340
|
To avoid it, we need to separate data from schema migrations and not run data
|
269
|
-
migrations locally. With seed [scripts][
|
341
|
+
migrations locally. With seed [scripts][seed-scripts], there is no need to run them anyway.
|
270
342
|
|
271
343
|
Keep the above in mind when referencing ActiveRecord models in data migrations. Ideally,
|
272
|
-
limit their use and do as much processing as possible in
|
344
|
+
limit their use and do as much processing as possible in the database.
|
273
345
|
|
274
346
|
|
275
347
|
### Long-running Tasks in Migrations
|
@@ -278,11 +350,16 @@ As mentioned, each data migration runs in a separate transaction.
|
|
278
350
|
A long-running task within a migration keeps the transaction open for
|
279
351
|
the duration of the task. As a result, the migration may hang or fail.
|
280
352
|
|
281
|
-
|
353
|
+
You could run such tasks asynchronously (i.e. in a background job) but the task may start
|
354
|
+
before the transaction commits. This is a [known issue][sync-issue]. In addition,
|
355
|
+
a database under load can suffer from longer commit times.
|
356
|
+
|
357
|
+
See [Background Tasks in Data Migrations](#background-tasks-in-data-migrations).
|
282
358
|
|
283
359
|
|
284
360
|
|
285
361
|
## Trivia
|
362
|
+
<sup>[(Back to top)](#table-of-contents)</sup>
|
286
363
|
|
287
364
|
One of the most impressive migrations on Earth is the multi-generational
|
288
365
|
round trip of the monarch butterfly.
|
@@ -302,13 +379,8 @@ Genetically speaking, this is an incredible data migration!
|
|
302
379
|
|
303
380
|
|
304
381
|
|
305
|
-
##
|
306
|
-
|
307
|
-
Alternative gems
|
308
|
-
|
309
|
-
- https://github.com/OffgridElectric/rails-data-migrations
|
310
|
-
- https://github.com/ilyakatz/data-migrate
|
311
|
-
- https://github.com/jasonfb/nonschema_migrations
|
382
|
+
## Acknowledgments
|
383
|
+
<sup>[(Back to top)](#table-of-contents)</sup>
|
312
384
|
|
313
385
|
Articles
|
314
386
|
|
@@ -318,11 +390,22 @@ Articles
|
|
318
390
|
- [Ruby on Rails Model Patterns and Anti-patterns](https://blog.appsignal.com/2020/11/18/rails-model-patterns-and-anti-patterns.html)
|
319
391
|
- [Rails Migrations with Zero Downtime](https://www.cloudbees.com/blog/rails-migrations-zero-downtime)
|
320
392
|
|
393
|
+
Alternative gems
|
394
|
+
|
395
|
+
- https://github.com/OffgridElectric/rails-data-migrations
|
396
|
+
- https://github.com/ilyakatz/data-migrate
|
397
|
+
- https://github.com/jasonfb/nonschema_migrations
|
398
|
+
|
321
399
|
|
322
400
|
|
323
401
|
## License
|
402
|
+
<sup>[(Back to top)](#table-of-contents)</sup>
|
324
403
|
|
325
404
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
326
405
|
|
327
|
-
[
|
328
|
-
[
|
406
|
+
[ci-image]: https://github.com/lunohodov/monarch/actions/workflows/ci.yml/badge.svg
|
407
|
+
[ci]: https://github.com/lunohodov/monarch/actions/workflows/ci.yml
|
408
|
+
[seed-scripts]: https://thoughtbot.com/blog/priming-the-pump
|
409
|
+
[sync-issue]: https://github.com/mperham/sidekiq/wiki/FAQ#why-am-i-seeing-a-lot-of-cant-find-modelname-with-id12345-errors-with-sidekiq
|
410
|
+
[version-image]: https://badge.fury.io/rb/monarch_migrate.svg
|
411
|
+
[version]: https://badge.fury.io/rb/monarch_migrate
|
@@ -4,6 +4,7 @@ module MonarchMigrate
|
|
4
4
|
class Migration
|
5
5
|
def initialize(path)
|
6
6
|
@path = path.to_s
|
7
|
+
@after_commit_callback = nil
|
7
8
|
end
|
8
9
|
|
9
10
|
def filename
|
@@ -22,27 +23,34 @@ module MonarchMigrate
|
|
22
23
|
!MigrationRecord.exists?(version: version)
|
23
24
|
end
|
24
25
|
|
25
|
-
def
|
26
|
-
|
26
|
+
def after_commit(&block)
|
27
|
+
@after_commit_callback = block
|
28
|
+
end
|
27
29
|
|
30
|
+
def run
|
28
31
|
ActiveRecord::Base.connection.transaction do
|
29
|
-
|
32
|
+
puts "Running data migration #{version}: #{name}"
|
30
33
|
|
31
34
|
begin
|
32
35
|
instance_eval File.read(path), path
|
33
36
|
MigrationRecord.create!(version: version)
|
34
|
-
|
37
|
+
puts "Migration complete"
|
35
38
|
rescue => e
|
36
|
-
|
37
|
-
|
39
|
+
puts "Migration failed due to #{e}"
|
40
|
+
# Deliberately raising ActiveRecord::Rollback does not
|
41
|
+
# pass on the exception and the callback will be triggered
|
42
|
+
raise
|
38
43
|
end
|
39
44
|
|
40
|
-
|
45
|
+
puts
|
41
46
|
end
|
47
|
+
|
48
|
+
after_commit_callback&.call
|
42
49
|
end
|
43
50
|
|
44
51
|
private
|
45
52
|
|
46
53
|
attr_reader :path
|
54
|
+
attr_reader :after_commit_callback
|
47
55
|
end
|
48
56
|
end
|
@@ -20,14 +20,12 @@ module MonarchMigrate
|
|
20
20
|
migrations.select(&:pending?)
|
21
21
|
end
|
22
22
|
|
23
|
-
def run
|
24
|
-
io ||= File.open(File::NULL, "w")
|
25
|
-
|
23
|
+
def run
|
26
24
|
if pending_migrations.any?
|
27
|
-
|
28
|
-
pending_migrations.sort_by(&:version).each
|
25
|
+
puts "Running #{pending_migrations.size} data migrations"
|
26
|
+
pending_migrations.sort_by(&:version).each(&:run)
|
29
27
|
else
|
30
|
-
|
28
|
+
puts "No data migrations pending"
|
31
29
|
[]
|
32
30
|
end
|
33
31
|
end
|
@@ -3,7 +3,7 @@ require "monarch_migrate"
|
|
3
3
|
namespace :data do
|
4
4
|
desc "Run pending data migrations, or a single version specified by environment variable VERSION"
|
5
5
|
task migrate: :environment do
|
6
|
-
MonarchMigrate.migrator.run
|
6
|
+
MonarchMigrate.migrator.run
|
7
7
|
end
|
8
8
|
|
9
9
|
namespace :migrate do
|
@@ -1,11 +1,13 @@
|
|
1
1
|
require "active_support/core_ext/string"
|
2
|
-
require "
|
2
|
+
require "active_support/testing/stream"
|
3
3
|
|
4
4
|
module MonarchMigrate
|
5
5
|
module Testing
|
6
6
|
MigrationRunError = Class.new(RuntimeError)
|
7
7
|
|
8
8
|
module Helpers
|
9
|
+
include ::ActiveSupport::Testing::Stream
|
10
|
+
|
9
11
|
def data_migration
|
10
12
|
@data_migration ||=
|
11
13
|
begin
|
@@ -17,9 +19,8 @@ module MonarchMigrate
|
|
17
19
|
end
|
18
20
|
|
19
21
|
def run_data_migration
|
20
|
-
output =
|
21
|
-
|
22
|
-
output.string.tap { ensure_no_error(_1) }
|
22
|
+
output = capture(:stdout) { data_migration.run }
|
23
|
+
ensure_no_error(output)
|
23
24
|
end
|
24
25
|
|
25
26
|
protected
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: monarch_migrate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yanko Ivanov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|