data_customs 0.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +216 -0
- data/Rakefile +9 -0
- data/lib/data_customs/migration.rb +40 -0
- data/lib/data_customs/railtie.rb +13 -0
- data/lib/data_customs/tasks/data_customs.rake +18 -0
- data/lib/data_customs/version.rb +5 -0
- data/lib/data_customs.rb +8 -0
- data/lib/generators/data_migration/data_migration_generator.rb +14 -0
- data/lib/generators/data_migration/templates/data_migration.rb.tt +10 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7d7c7b4003b90a683a9ea760719dc9f3fee13c2e41d8e913904d0c25d05cfb3b
|
4
|
+
data.tar.gz: 23abed296ee83d222794a3467f9fba98430cbb15463505f21ce7be11ff1b4c74
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f2f015a1096f50f09f5f535dcdd7cfe5d5d2c2093763af254cc69743d542cfe21fe82f90f8d42c40d11a38951ce80046ec413c9944ec02ee5107df4207f6e4c3
|
7
|
+
data.tar.gz: a50e40759553bebca3718c77d3095417798ae8c946524a45216d0bf331fc3a9cf08bcaf3e74a53b015b0d7f4523dcadcbfa15c73ec3918cbce4d97b7f46a9783
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Matheus Richard
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
# 🛃 Data Customs
|
2
|
+
|
3
|
+
> [!WARNING]
|
4
|
+
> This project is in early development. The API may change before reaching
|
5
|
+
> version 1.0.0.
|
6
|
+
|
7
|
+
A simple gem to help you perform data migrations in your Rails app. The premise
|
8
|
+
is simple: bundle the migration code along with a verification test to assert
|
9
|
+
that the migration achieves the desired effect. If either the migration or the
|
10
|
+
verification step fails, the entire migration is rolled back.
|
11
|
+
|
12
|
+
> Can't I just test my rake task or migration script?
|
13
|
+
|
14
|
+
Great question. Yes, you should test your migration code (including the Data
|
15
|
+
Customs migrations). The problem is that **production data is always different
|
16
|
+
from your test data**, and **in unexpected ways**. This means that even if your
|
17
|
+
tests pass, the migration might still fail when run in production.
|
18
|
+
|
19
|
+
Data Customs provides a safety net by ensuring that if the migration doesn't do
|
20
|
+
what you expect it to do, the changes are rolled back. This way, you can
|
21
|
+
investigate the issue and fix it without leaving your data in a half-migrated
|
22
|
+
(or bad!) state.
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Install the gem and add to the application's Gemfile by executing:
|
27
|
+
|
28
|
+
```bash
|
29
|
+
bundle add data_customs
|
30
|
+
```
|
31
|
+
|
32
|
+
If bundler is not being used to manage dependencies, install the gem by
|
33
|
+
executing:
|
34
|
+
|
35
|
+
```bash
|
36
|
+
gem install data_customs
|
37
|
+
```
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
### Generating a data migration
|
42
|
+
|
43
|
+
You can create a new data migration by running:
|
44
|
+
|
45
|
+
```bash
|
46
|
+
rails generate data_migration MigrationName
|
47
|
+
```
|
48
|
+
|
49
|
+
That will create a file at `db/data_migrations/migration_name.rb`.
|
50
|
+
|
51
|
+
### Writing a data migration
|
52
|
+
|
53
|
+
After generating a migration, implement the `up` method with the code that
|
54
|
+
performs the data migration, and the `verify!` method with the code that
|
55
|
+
asserts that the migration was successful.
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class AddDefaultUsername < DataCustoms::Migration
|
59
|
+
def up
|
60
|
+
User.where.missing(:username).update_all(username: "guest")
|
61
|
+
end
|
62
|
+
|
63
|
+
def verify!
|
64
|
+
if User.exists?(username: nil)
|
65
|
+
raise "Some users still have no usernames!"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
AddDefaultUsername.run # runs the migration and rolls back if necessary
|
71
|
+
```
|
72
|
+
|
73
|
+
Use any exception to indicate failure in the `verify!` method.
|
74
|
+
|
75
|
+
> [!TIP]
|
76
|
+
> Include modules like `RSpec::Matchers` or `Minitest::Assertions` in your
|
77
|
+
> migration class to use your favorite assertions and matchers inside `verify!`.
|
78
|
+
|
79
|
+
If you need to pass arguments to the data migration, you can use an initializer
|
80
|
+
and `run` will forward any arguments to it.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
class AddDefaultUsername < DataCustoms::Migration
|
84
|
+
def initialize(default_username "guest")
|
85
|
+
@default_username = default_username
|
86
|
+
end
|
87
|
+
|
88
|
+
def up
|
89
|
+
User.where.missing(:username).update_all(username: @default_username)
|
90
|
+
end
|
91
|
+
|
92
|
+
def verify!
|
93
|
+
if User.exists?(username: nil)
|
94
|
+
raise "Some users still have no usernames!"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
AddDefaultUsername.run("anonymous")
|
100
|
+
```
|
101
|
+
|
102
|
+
#### Dealing with large datasets
|
103
|
+
|
104
|
+
The migration code runs inside a transaction, so be careful when dealing with
|
105
|
+
large datasets, as [it will block the database][blocking] for the duration of
|
106
|
+
the transaction.
|
107
|
+
|
108
|
+
Data Customs provides a few helpers to make working in batches easier and
|
109
|
+
automatically throttles the migration to avoid overwhelming the database.
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
class AddDefaultUsername < DataCustoms::Migration
|
113
|
+
def up
|
114
|
+
batch(User.where.missing(:username)) do |relation|
|
115
|
+
relation.update_all(username: "guest")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
Or, if you need access to each individual record:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class AddDefaultUsername < DataCustoms::Migration
|
125
|
+
def up
|
126
|
+
find_each(User.where.missing(:username)) do |record|
|
127
|
+
# do something with record
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
133
|
+
For both methods, you can configure the batch size and the pause between batches:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
class LongMigration < DataCustoms::Migration
|
137
|
+
def up
|
138
|
+
batch(records, batch_size: 500, throttle_seconds: 0.1) do |relation|
|
139
|
+
# ...
|
140
|
+
end
|
141
|
+
|
142
|
+
find_each(records, batch_size: 500, throttle_seconds: 0.1) do |record|
|
143
|
+
# ...
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
### Running a data migration in the command line
|
150
|
+
|
151
|
+
These migrations don't run automatically. You need to invoke them manually.
|
152
|
+
Since they might take some time, you can choose the best time to run them.
|
153
|
+
|
154
|
+
If you want to run a data migration from the command line, you can use the
|
155
|
+
`data_customs:run` task. It accepts the migration class name (either in
|
156
|
+
PascalCase or snake_case) as an argument:
|
157
|
+
|
158
|
+
```bash
|
159
|
+
rails data_customs:run NAME=AddDefaultUsername
|
160
|
+
# or
|
161
|
+
rake data_customs:run NAME=add_default_username
|
162
|
+
```
|
163
|
+
|
164
|
+
If you need to pass arguments to the migration, you can do so by passing them as
|
165
|
+
additional arguments to the task:
|
166
|
+
|
167
|
+
```bash
|
168
|
+
rails data_customs:run NAME=AddDefaultUsername ARGS="anonymous"
|
169
|
+
```
|
170
|
+
|
171
|
+
## Development
|
172
|
+
|
173
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
174
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
175
|
+
prompt that will allow you to experiment.
|
176
|
+
|
177
|
+
## Contributing
|
178
|
+
|
179
|
+
Bug reports and pull requests are welcome on GitHub at
|
180
|
+
<https://github.com/thoughtbot/data_customs>.
|
181
|
+
|
182
|
+
This project is intended to be a safe, welcoming space for collaboration, and
|
183
|
+
contributors are expected to adhere to the [code of
|
184
|
+
conduct](https://github.com/thoughtbot/data_customs/blob/main/CODE_OF_CONDUCT.md).
|
185
|
+
|
186
|
+
## License
|
187
|
+
|
188
|
+
Open source templates are Copyright (c) thoughtbot, inc. It contains free
|
189
|
+
software that may be redistributed under the terms specified in the
|
190
|
+
[LICENSE](https://github.com/thoughtbot/data_customs/blob/main/LICENSE.txt)
|
191
|
+
file.
|
192
|
+
|
193
|
+
## Code of Conduct
|
194
|
+
|
195
|
+
Everyone interacting in the DataCustoms project's codebases, issue trackers,
|
196
|
+
chat rooms and mailing lists is expected to follow the [code of
|
197
|
+
conduct](https://github.com/thoughtbot/data_customs/blob/main/CODE_OF_CONDUCT.md).
|
198
|
+
|
199
|
+
<!-- START /templates/footer.md -->
|
200
|
+
|
201
|
+
## About thoughtbot
|
202
|
+
|
203
|
+

|
204
|
+
|
205
|
+
This repo is maintained and funded by thoughtbot, inc. The names and logos for
|
206
|
+
thoughtbot are trademarks of thoughtbot, inc.
|
207
|
+
|
208
|
+
We love open source software! See [our other projects][community]. We are
|
209
|
+
[available for hire][hire].
|
210
|
+
|
211
|
+
[community]: https://thoughtbot.com/community?utm_source=github
|
212
|
+
[hire]: https://thoughtbot.com/hire-us?utm_source=github
|
213
|
+
|
214
|
+
<!-- END /templates/footer.md -->
|
215
|
+
|
216
|
+
[blocking]: https://github.com/ankane/strong_migrations?tab=readme-ov-file#backfilling-data
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DataCustoms
|
4
|
+
class Migration
|
5
|
+
DEFAULT_BATCH_SIZE = 1000
|
6
|
+
DEFAULT_THROTTLE = 0.01
|
7
|
+
|
8
|
+
def self.run(...) = new(...).run
|
9
|
+
|
10
|
+
def up = raise NotImplementedError
|
11
|
+
|
12
|
+
def verify! = raise NotImplementedError
|
13
|
+
|
14
|
+
def run
|
15
|
+
ActiveRecord::Base.transaction do
|
16
|
+
up
|
17
|
+
verify!
|
18
|
+
puts "🛃 Data migration ran successfully!"
|
19
|
+
rescue => e
|
20
|
+
warn "🛃 Data migration failed"
|
21
|
+
raise e
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def batch(scope, batch_size: DEFAULT_BATCH_SIZE, throttle_seconds: DEFAULT_THROTTLE)
|
28
|
+
scope.in_batches(of: batch_size) do |relation|
|
29
|
+
yield relation
|
30
|
+
sleep(throttle_seconds) if throttle_seconds.positive?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_each(scope, **, &)
|
35
|
+
batch(scope, **) do |relation|
|
36
|
+
relation.each(&)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "rails/railtie"
|
2
|
+
|
3
|
+
module DataCustoms
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
rake_tasks do
|
6
|
+
load File.expand_path("tasks/data_customs.rake", __dir__)
|
7
|
+
end
|
8
|
+
|
9
|
+
generators do
|
10
|
+
require "generators/data_migration/data_migration_generator"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
namespace :data_customs do
|
2
|
+
desc 'Run a single data migration from db/data_migrations'
|
3
|
+
task run: :environment do
|
4
|
+
name = ENV['NAME']
|
5
|
+
abort '❌ Missing migration name (e.g. `rake data_customs:run NAME=fix_users`)' unless name
|
6
|
+
|
7
|
+
path = Rails.root.join('db', 'data_migrations', "#{name.underscore}.rb")
|
8
|
+
abort "❌ Migration not found: #{path}" unless File.exist?(path)
|
9
|
+
|
10
|
+
require path
|
11
|
+
migration_class = name.camelize.constantize
|
12
|
+
if args = ENV['ARGS']
|
13
|
+
migration_class.run(args.split(','))
|
14
|
+
else
|
15
|
+
migration_class.run
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/data_customs.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
require "rails/generators/named_base"
|
5
|
+
|
6
|
+
module DataMigration
|
7
|
+
class DataMigrationGenerator < Rails::Generators::NamedBase
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def create_data_migration_file
|
11
|
+
template "data_migration.rb.tt", File.join("db/data_migrations", "#{file_name.underscore}.rb")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: data_customs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matheus Richard
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rails
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '7.1'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '7.1'
|
26
|
+
description: A simple gem to help you perform data migrations in your Rails app. Define
|
27
|
+
the work and a verification step steps, if any of them fail, your migration will
|
28
|
+
be rolled back.
|
29
|
+
email:
|
30
|
+
- matheusrichardt@gmail.com
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- CHANGELOG.md
|
36
|
+
- CODE_OF_CONDUCT.md
|
37
|
+
- LICENSE.txt
|
38
|
+
- README.md
|
39
|
+
- Rakefile
|
40
|
+
- lib/data_customs.rb
|
41
|
+
- lib/data_customs/migration.rb
|
42
|
+
- lib/data_customs/railtie.rb
|
43
|
+
- lib/data_customs/tasks/data_customs.rake
|
44
|
+
- lib/data_customs/version.rb
|
45
|
+
- lib/generators/data_migration/data_migration_generator.rb
|
46
|
+
- lib/generators/data_migration/templates/data_migration.rb.tt
|
47
|
+
homepage: https://github.com/thoughtbot/data_customs
|
48
|
+
licenses:
|
49
|
+
- MIT
|
50
|
+
metadata:
|
51
|
+
homepage_uri: https://github.com/thoughtbot/data_customs
|
52
|
+
source_code_uri: https://github.com/thoughtbot/data_customs
|
53
|
+
changelog_uri: https://github.com/thoughtbot/data_customs/blob/main/CHANGELOG.md
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.2.0
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubygems_version: 3.6.9
|
69
|
+
specification_version: 4
|
70
|
+
summary: A simple gem to help you perform data migrations in your Rails app.
|
71
|
+
test_files: []
|