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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 814685cc086ec84d8f1e877c80134db4c702b9d06602f4169f99dc82c1cd1ca8
4
- data.tar.gz: 726adc5a2087bec90a6fa1fc1ec965d633c4de35f69298da2b42e7e043026af8
3
+ metadata.gz: d70afb71a32546dd38ae87d4e39956c97bd095c56cc957266a159edb4cbfc3cb
4
+ data.tar.gz: 2b95f318d045045dba0cb712bc0aa647d95717688e2cb258702e9bea9c398d6f
5
5
  SHA512:
6
- metadata.gz: 665c315826067e28509312d5cf9ef8729803dfb105a123aad44f81df9c0c2f46cd185f9d5381939def677d68553a150c37d2e1d5f040b2b281dc6085b47e31d9
7
- data.tar.gz: ff6cb7e01f9071363eb561f392c80e8b8e97b371f853f79f35f1f4a594b8165f4e063d9225ec1ff3af992874a57ef081d16455a049fe547bfe8c6a354578a8d3
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.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.4.4)
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.10.0)
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.15.0)
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.6)
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.3.1)
128
- rack-test (1.1.0)
129
- rack (>= 1.0, < 3)
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.2)
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.4.0)
158
+ regexp_parser (2.5.0)
159
159
  rexml (3.2.5)
160
- rspec-core (3.10.1)
161
- rspec-support (~> 3.10.0)
162
- rspec-expectations (3.10.1)
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.10.0)
165
- rspec-mocks (3.10.2)
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.10.0)
168
- rspec-rails (5.0.1)
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.10.2)
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.18.0)
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.2)
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.1.0)
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.5.4)
205
+ zeitwerk (2.6.0)
206
206
 
207
207
  PLATFORMS
208
208
  ruby
data/README.md CHANGED
@@ -1,6 +1,31 @@
1
- # Sensible Data Migrations for Rails
1
+ # monarch_migrate [![Build Status][ci-image]][ci] [![Gem Version][version-image]][version]
2
2
 
3
- ## Why
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. Using it
13
- for data changes in the database comes second. Yet, adding or modifying data via
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
- The first issue is that application deployment now depends on the data migration
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 when there are [scripts][2] to
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 by separating data from schema migrations.
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 is assumed that:
56
+ It assumes that:
28
57
 
29
- - You run data migrations *only* on production and rely on seed [scripts][2] i.e. `dev:prime` for local development.
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 write tests your data migrations.
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 already ran.
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
- Let's start with an example.
92
+ ## Usage
93
+ <sup>[(Back to top)](#table-of-contents)</sup>
62
94
 
63
- Suppose we have designed a system where users have first and last names. Time passes and
64
- it becomes clear this is [wrong][3]. Now, we want to put things right and come
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
- To create the data migration to update existing user records, run:
99
+ ### Create a Data Migration
72
100
 
73
101
  ```shell
74
102
  rails generate data_migration backfill_users_name
75
103
  ```
76
104
 
77
- In contrast to regular migrations, there is no need to inherit any classes:
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
- As seen above, it is plain ruby code where you can refer to any object you
89
- need. Each data migration runs in a separate transaction.
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
- Or a specific version:
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 simply rerunning
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(subject).to change { user.reload.name }.to("Guybrush Threepwood")
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(subject).not_to change { user.reload.name }
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(subject).to change { user.reload.name }.to("Guybrush")
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
- Data migrations become obsolete, once the data manipulation successfully completes.
204
- So are the corresponding tests. These will fail after database columns are dropped
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
- 1. Implement the data migration using TDD.
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 also keep the test files in repo's history for posterity.
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 is a regular Rails migration for our example:
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][2], there is no need to run them anyway.
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 Postgres.
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
- To avoid this, run such tasks asynchronously.
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
- ## See Also
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
- [2]: https://thoughtbot.com/blog/priming-the-pump
328
- [3]: https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/
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 run(io = nil)
26
- io ||= File.open(File::NULL, "w")
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
- io.puts "Running data migration #{version}: #{name}"
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
- io.puts "Migration complete"
37
+ puts "Migration complete"
35
38
  rescue => e
36
- io.puts "Migration failed due to #{e}"
37
- raise ActiveRecord::Rollback
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
- io.puts
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(io = nil)
24
- io ||= File.open(File::NULL, "w")
25
-
23
+ def run
26
24
  if pending_migrations.any?
27
- io.puts "Running #{pending_migrations.size} data migrations"
28
- pending_migrations.sort_by(&:version).each { |m| m.run(io) }
25
+ puts "Running #{pending_migrations.size} data migrations"
26
+ pending_migrations.sort_by(&:version).each(&:run)
29
27
  else
30
- io.puts "No data migrations pending"
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($stdout)
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 "stringio"
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 = StringIO.new
21
- data_migration.run(output)
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
@@ -1,3 +1,3 @@
1
1
  module MonarchMigrate
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
3
3
  end
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.5.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-06-18 00:00:00.000000000 Z
11
+ date: 2022-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails