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 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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-09-04
4
+
5
+ - Initial release
@@ -0,0 +1,6 @@
1
+ # Code of conduct
2
+
3
+ By participating in this project, you agree to abide by the
4
+ [thoughtbot code of conduct][1].
5
+
6
+ [1]: https://thoughtbot.com/open-source-code-of-conduct
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
+ ![thoughtbot](https://thoughtbot.com/thoughtbot-logo-for-readmes.svg)
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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
9
+ task build: :spec
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DataCustoms
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "data_customs/migration"
4
+ require_relative "data_customs/railtie"
5
+ require_relative "data_customs/version"
6
+
7
+ module DataCustoms
8
+ end
@@ -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
@@ -0,0 +1,10 @@
1
+ class <%= class_name %> < DataCustoms::Migration
2
+ def up
3
+ # Your migration code goes here.
4
+ end
5
+
6
+ def verify!
7
+ # Your verification code goes here.
8
+ # Raise an exception to cause the migration to fail.
9
+ end
10
+ 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: []