data-migration 1.2.0 → 2.0.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +38 -8
  4. data/data-migration.gemspec +27 -15
  5. data/lib/data-migration.rb +1 -1
  6. data/lib/data_migration/config.rb +1 -1
  7. data/lib/data_migration/job.rb +27 -9
  8. data/lib/data_migration/task.rb +22 -19
  9. data/lib/generators/data_migration/install_generator.rb +28 -0
  10. data/lib/generators/{templates → data_migration/templates}/install_data_migration_tasks.rb.tt +4 -0
  11. metadata +187 -67
  12. data/.editorconfig +0 -14
  13. data/.github/dependabot.yml +0 -27
  14. data/.github/workflows/_trunk_check.yml +0 -15
  15. data/.github/workflows/test.yml +0 -42
  16. data/.gitignore +0 -24
  17. data/.ruby-version +0 -1
  18. data/.trunk/.gitignore +0 -9
  19. data/.trunk/configs/.markdownlint.yaml +0 -2
  20. data/.trunk/configs/.shellcheckrc +0 -7
  21. data/.trunk/configs/.yamllint.yaml +0 -7
  22. data/.trunk/trunk.yaml +0 -39
  23. data/.vscode/extensions.json +0 -18
  24. data/.vscode/settings.json +0 -7
  25. data/Gemfile +0 -3
  26. data/Gemfile.lock +0 -246
  27. data/lib/generators/install_generator.rb +0 -17
  28. data/spec/data_migration/config_spec.rb +0 -116
  29. data/spec/data_migration/job_spec.rb +0 -96
  30. data/spec/data_migration/task_spec.rb +0 -152
  31. data/spec/data_migration_spec.rb +0 -65
  32. data/spec/fixtures/data_migrations/20241206200111_create_users.rb +0 -17
  33. data/spec/fixtures/data_migrations/20241206200112_create_bad_users.rb +0 -5
  34. data/spec/fixtures/data_migrations/20241206200113_change_users.rb +0 -5
  35. data/spec/fixtures/data_migrations/20241206200114_create_batch_users.rb +0 -9
  36. data/spec/fixtures/schema.rb +0 -26
  37. data/spec/generators/install_generator_spec.rb +0 -48
  38. data/spec/generators/migration_generator_spec.rb +0 -50
  39. data/spec/rails_helper.rb +0 -21
  40. data/spec/spec_helper.rb +0 -30
  41. data/spec/support/junit_formatter.rb +0 -6
  42. data/spec/support/rails_helpers.rb +0 -53
  43. data/usr/bin/release.sh +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a8f110139404aa9a93001f7d7d62df44dfa39ed17c6467c3042b71071f0f5a9
4
- data.tar.gz: 17ea84c1d597d6ba5907f0841bee35cf231225f9ac6febfa0dfa4b6c697105d3
3
+ metadata.gz: f317e1c1d38d56179e7236be0d37be8105859b653792c8eb4bdae60c0010affa
4
+ data.tar.gz: df14fb4e41b46c7fffebf89d9367b810128216f285b15da18d8c5b3297f653d4
5
5
  SHA512:
6
- metadata.gz: 2cb6a04608ff5fa007f3bc3e5e6fd819413d73b9041ee8ec542de029719d6c089706528e3ec72e9d0442d75596709cb2646f004c00bce6a9100a063be2e91eab
7
- data.tar.gz: 53b024625cc45959ac7cc94c5ccb5bf22f1ff463a04cfea4f847a43292b688092aaf9f3e757193fd15c17c47f2a9539b208be3153e043cca313f3b78682ff36c
6
+ metadata.gz: 11da0da9e0b6593df4bd94a728d66f2ecbd5672e3a9819ec90bf58d32f811053b7e8f7890631faa7ea99253f9652e14f5383ece6af4f367ba16971a8cd7b1051
7
+ data.tar.gz: e93699e6817f780e7d12457742bd4d053ad029b6478f1a86cce911a75bfb49b6567bfb8822030dfdd8a1474fc4ffc47d6ec92519d041449250643b6b7c2c784f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 2.0.0 (2026-09-04)
4
+
5
+ - BREAKING: Require Ruby 3.4 or newer.
6
+ - Add `rfcs/` starter pack: process (RFC 0001), positioning (RFC 0002), and Standards Track design RFCs 0003–0005 for task status, operator run path, and jobs.
7
+ - Report stopped data migrations as failed and release their job slots (RFC 0006).
8
+ - Serialize task job-slot updates across worker instances.
9
+ - Limit the gem package to runtime code and release documentation.
10
+
11
+ ## 1.3.0
12
+
13
+ - Fix data migration tasks table generator
14
+ - Update README with better examples
15
+
3
16
  ## 1.2.0
4
17
 
5
18
  - Wrap Task.status enum with ActiveRecord version check
data/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # data-migration.rb
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/data-migration.svg)](https://badge.fury.io/rb/data-migration) [![Test Status](https://github.com/amkisko/data-migration.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/data-migration.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/data-migration.rb/graph/badge.svg?token=881AFPL643)](https://codecov.io/gh/amkisko/data-migration.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/data-migration.svg)](https://badge.fury.io/rb/data-migration) [![Test Status](https://github.com/amkisko/data-migration.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/data-migration.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/data-migration.rb/graph/badge.svg?token=881AFPL643)](https://app.codecov.io/github/amkisko/data-migration.rb)
4
4
 
5
5
  Data migrations kit for ActiveRecord and ActiveJob.
6
6
 
7
- Sponsored by [Kisko Labs](https://www.kiskolabs.com).
7
+ Requires Ruby 3.4 or newer.
8
+
9
+ Design changes go through `rfcs/` (RFC 0001).
8
10
 
9
11
  ## Data migrations concept
10
12
 
@@ -39,26 +41,32 @@ Using RubyGems:
39
41
  gem install data-migration
40
42
  ```
41
43
 
42
- ## Gemfile
44
+ ### Gemfile
43
45
 
44
46
  ```ruby
45
47
  gem "data-migration"
46
48
  ```
47
49
 
48
- ## Usage
49
-
50
- ### Run data migrations
50
+ ### Data migration tasks table
51
51
 
52
52
  ```sh
53
- bin/rails db:migrate:data 20241207120000_create_users
53
+ bin/rails g data_migration:install data_migration_tasks
54
54
  ```
55
55
 
56
+ ## Usage
57
+
56
58
  ### Generate data migration job
57
59
 
58
60
  ```sh
59
61
  bin/rails g data_migration create_users
60
62
  ```
61
63
 
64
+ ### Run data migrations
65
+
66
+ ```sh
67
+ bin/rails db:migrate:data 20241207120000_create_users
68
+ ```
69
+
62
70
  ## Configuration
63
71
 
64
72
  ### Set data migrations directory
@@ -110,6 +118,7 @@ end
110
118
  - ActiveRecord migrations generator is used to generate data migration files
111
119
  - Data migrations are not reversible, it is operator's responsibility to ensure that data migration has correct effect
112
120
  - Keep migrations logic stable and predictable, e.g. by checking uniqueness of created/updated records
121
+ - Task rows persist migration keyword arguments and active job metadata; pass record references instead of secrets or unnecessary personal data
113
122
 
114
123
  ## Contributing
115
124
 
@@ -126,7 +135,7 @@ Contribution policy:
126
135
 
127
136
  ## Publishing
128
137
 
129
- Prefer using script `usr/bin/release.sh`, it will ensure that repository is synced and after publishing gem will create a tag.
138
+ Prefer using script `usr/bin/release.rb`, it will ensure that repository is synced and after publishing gem will create a tag.
130
139
 
131
140
  ```sh
132
141
  GEM_VERSION=$(grep -Eo "VERSION\s*=\s*\".+\"" lib/data-migration.rb | grep -Eo "[0-9.]{5,}")
@@ -136,6 +145,27 @@ gem push data-migration-$GEM_VERSION.gem
136
145
  git tag $GEM_VERSION && git push --tags && gh release create $GEM_VERSION --generate-notes
137
146
  ```
138
147
 
148
+ ## Links
149
+
150
+ - [GitHub](https://github.com/amkisko/data-migration.rb)
151
+ - [GitLab](https://gitlab.com/amkisko/data-migration.rb)
152
+ - [RubyGems](https://rubygems.org/gems/data-migration)
153
+ - [Versions Atom](https://rubygems.org/gems/data-migration/versions.atom) (feed id `8154436273761`)
154
+ - [libraries.io](https://libraries.io/rubygems/data-migration)
155
+ - [Deps.dev](https://deps.dev/rubygems/data-migration)
156
+ - [SonarCloud](https://sonarcloud.io/project/overview?id=amkisko_data-migration.rb)
157
+ - [Snyk](https://snyk.io/test/github/amkisko/data-migration.rb)
158
+ - [Codecov](https://app.codecov.io/github/amkisko/data-migration.rb)
159
+ - [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/amkisko/data-migration.rb)
160
+
139
161
  ## License
140
162
 
141
163
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
164
+
165
+ ## Sponsors
166
+
167
+ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
168
+
169
+ <a href="https://www.kiskolabs.com">
170
+ <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
171
+ </a>
@@ -2,7 +2,6 @@ Gem::Specification.new do |gem|
2
2
  gem.name = "data-migration"
3
3
  gem.version = File.read(File.expand_path("../lib/data-migration.rb", __FILE__)).match(/VERSION\s*=\s*"(.*?)"/)[1]
4
4
 
5
- repository_url = "https://github.com/amkisko/data-migration.rb"
6
5
  root_files = %w[CHANGELOG.md LICENSE.md README.md]
7
6
  root_files << "#{gem.name}.gemspec"
8
7
 
@@ -11,23 +10,24 @@ Gem::Specification.new do |gem|
11
10
  gem.platform = Gem::Platform::RUBY
12
11
 
13
12
  gem.authors = ["Andrei Makarov"]
14
- gem.email = ["andrei@kiskolabs.com"]
15
- gem.homepage = repository_url
13
+ gem.email = ["contact@kiskolabs.com"]
14
+ gem.homepage = "https://github.com/amkisko/data-migration.rb"
16
15
  gem.summary = "Data migrations kit for ActiveRecord and ActiveJob"
17
- gem.description = gem.summary
16
+ gem.description = "Run and track data migrations through ActiveRecord tasks and ActiveJob workers."
18
17
  gem.metadata = {
19
- "homepage" => repository_url,
20
- "source_code_uri" => repository_url,
21
- "bug_tracker_uri" => "#{repository_url}/issues",
22
- "changelog_uri" => "#{repository_url}/blob/main/CHANGELOG.md",
18
+ "homepage" => "https://github.com/amkisko/data-migration.rb",
19
+ "source_code_uri" => "https://github.com/amkisko/data-migration.rb",
20
+ "bug_tracker_uri" => "https://github.com/amkisko/data-migration.rb/issues",
21
+ "changelog_uri" => "https://github.com/amkisko/data-migration.rb/blob/main/CHANGELOG.md",
23
22
  "rubygems_mfa_required" => "true"
24
23
  }
25
24
 
26
- gem.files = `git ls-files`.split("\n")
27
- gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
25
+ gem.files = `git ls-files`.split("\n").select do |file|
26
+ root_files.include?(file) || file.start_with?("bin/", "lib/")
27
+ end
28
28
  gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
29
29
 
30
- gem.required_ruby_version = ">= 3"
30
+ gem.required_ruby_version = ">= 3.4"
31
31
  gem.require_paths = ["lib"]
32
32
 
33
33
  gem.add_dependency "rails", "> 5"
@@ -35,10 +35,22 @@ Gem::Specification.new do |gem|
35
35
  gem.add_dependency "activerecord", "> 5"
36
36
  gem.add_dependency "activesupport", "> 5"
37
37
 
38
- gem.add_development_dependency "bundler", "~> 2"
38
+ gem.add_development_dependency "bundler", ">= 2"
39
39
  gem.add_development_dependency "rspec", "~> 3"
40
+ gem.add_development_dependency "polyrun", ">= 2.2.0"
41
+ gem.add_development_dependency "prosopite", "~> 2.0"
42
+ gem.add_development_dependency "webmock", "~> 3"
40
43
  gem.add_development_dependency "rspec_junit_formatter", "~> 0.6"
41
- gem.add_development_dependency "simplecov", "~> 0.21"
42
- gem.add_development_dependency "simplecov-cobertura", "~> 2"
43
- gem.add_development_dependency "sqlite3", "~> 2.4"
44
+ gem.add_development_dependency "simplecov", "~> 0.22"
45
+ gem.add_development_dependency "simplecov-cobertura", "~> 3"
46
+ gem.add_development_dependency "rbs", "~> 3"
47
+ gem.add_development_dependency "standard", "~> 1.52"
48
+ gem.add_development_dependency "standard-custom", "~> 1.0"
49
+ gem.add_development_dependency "standard-performance", "~> 1.8"
50
+ gem.add_development_dependency "standard-rails", "~> 1.5"
51
+ gem.add_development_dependency "standard-rspec", "~> 0.3"
52
+ gem.add_development_dependency "rubocop-rails", "~> 2.33"
53
+ gem.add_development_dependency "rubocop-rspec", "~> 3.8"
54
+ gem.add_development_dependency "rubocop-thread_safety", "~> 0.7"
55
+ gem.add_development_dependency "appraisal", "~> 2"
44
56
  end
@@ -3,7 +3,7 @@ require "data_migration/job"
3
3
  require "data_migration/task"
4
4
 
5
5
  module DataMigration
6
- VERSION = "1.2.0".freeze
6
+ VERSION = "2.0.0".freeze
7
7
 
8
8
  module_function
9
9
 
@@ -20,7 +20,7 @@ module DataMigration
20
20
 
21
21
  attr_writer :generate_spec
22
22
  def generate_spec?
23
- @generate_spec.nil? ? true : @generate_spec
23
+ @generate_spec.nil? || @generate_spec
24
24
  end
25
25
 
26
26
  attr_writer :job_class
@@ -7,6 +7,10 @@ module DataMigration
7
7
  discard_on StandardError
8
8
 
9
9
  def perform(task_id, *job_args, **job_kwargs)
10
+ checked_in = false
11
+ failed = false
12
+ migration_started = false
13
+ migration_class = nil
10
14
  task = DataMigration::Task.find(task_id)
11
15
  DataMigration.config.monitoring_context.call(task)
12
16
 
@@ -15,16 +19,19 @@ module DataMigration
15
19
 
16
20
  unless task.file_exists?
17
21
  DataMigration.notify("#{migration_name} not found")
22
+ task.update_columns(status: task.class.statuses.fetch("failed"), updated_at: Time.current)
18
23
  return
19
24
  end
20
25
 
21
26
  task.job_check_in!(job_id, job_args: job_args, job_kwargs: job_kwargs)
27
+ checked_in = true
28
+ migration_started = true
22
29
 
23
30
  require migration_path
24
31
  klass_name = migration_name.gsub(/^[0-9_]+/, "").camelize
25
- klass = klass_name.safe_constantize
26
- raise "Data migration class #{klass_name} not found" unless klass.is_a?(Class)
27
- raise "Data migration class #{klass_name} must implement `perform` method" unless klass.method_defined?(:perform)
32
+ migration_class = klass_name.safe_constantize
33
+ raise "Data migration class #{klass_name} not found" unless migration_class.is_a?(Class)
34
+ raise "Data migration class #{klass_name} must implement `perform` method" unless migration_class.method_defined?(:perform)
28
35
 
29
36
  if task.started_at.blank?
30
37
  task.update!(started_at: Time.current, status: :started)
@@ -38,17 +45,18 @@ module DataMigration
38
45
 
39
46
  Thread.current[:data_migration_enqueue_called] ||= {}
40
47
  Thread.current[:data_migration_enqueue_kwargs] ||= {}
41
- klass.define_method(:enqueue) do |**enqueue_kwargs|
42
- Thread.current[:data_migration_enqueue_called][klass.name] = true
43
- Thread.current[:data_migration_enqueue_kwargs][klass.name] = enqueue_kwargs
48
+ migration_class.define_method(:enqueue) do |**enqueue_kwargs|
49
+ Thread.current[:data_migration_enqueue_called][migration_class.name] = true
50
+ Thread.current[:data_migration_enqueue_kwargs][migration_class.name] = enqueue_kwargs
44
51
  end
45
52
 
46
53
  task.update!(status: :performing, pause_minutes: 0)
47
- klass.new.perform(**job_kwargs)
54
+ migration_class.new.perform(**job_kwargs)
48
55
  task.job_check_out!(job_id)
56
+ checked_in = false
49
57
 
50
- enqueue_called = Thread.current[:data_migration_enqueue_called].delete(klass.name)
51
- enqueue_kwargs = Thread.current[:data_migration_enqueue_kwargs].delete(klass.name)
58
+ enqueue_called = Thread.current[:data_migration_enqueue_called].delete(migration_class.name)
59
+ enqueue_kwargs = Thread.current[:data_migration_enqueue_kwargs].delete(migration_class.name)
52
60
  if enqueue_called
53
61
  if enqueue_kwargs[:background] == false
54
62
  self.class.new.perform(task_id, *job_args, **enqueue_kwargs)
@@ -58,6 +66,16 @@ module DataMigration
58
66
  else
59
67
  task.update!(completed_at: Time.current, status: :completed)
60
68
  end
69
+ rescue
70
+ failed = migration_started
71
+ raise
72
+ ensure
73
+ task.job_check_out!(job_id, status: (failed ? :failed : nil)) if checked_in
74
+ task.update_columns(status: task.class.statuses.fetch("failed"), updated_at: Time.current) if failed && !checked_in
75
+ if migration_class
76
+ Thread.current[:data_migration_enqueue_called]&.delete(migration_class.name)
77
+ Thread.current[:data_migration_enqueue_kwargs]&.delete(migration_class.name)
78
+ end
61
79
  end
62
80
  end
63
81
  end
@@ -23,13 +23,10 @@ module DataMigration
23
23
  started: "started",
24
24
  performing: "performing",
25
25
  paused: "paused",
26
+ failed: "failed",
26
27
  completed: "completed"
27
28
  }
28
- if ActiveRecord::VERSION::MAJOR >= 7
29
- enum :status, STATUS_OPTIONS
30
- else
31
- enum status: STATUS_OPTIONS
32
- end
29
+ enum :status, STATUS_OPTIONS
33
30
 
34
31
  validates :name, presence: true
35
32
  validates :pause_minutes, numericality: {greater_than_or_equal_to: 0, only_integer: true}, if: -> { pause_minutes.present? }
@@ -49,6 +46,7 @@ module DataMigration
49
46
  scope :started, -> { where(status: :started) }
50
47
  scope :paused, -> { where(status: :paused) }
51
48
  scope :performing, -> { where(status: :performing) }
49
+ scope :failed, -> { where(status: :failed) }
52
50
  scope :completed, -> { where(status: :completed) }
53
51
 
54
52
  def self.job_class
@@ -98,22 +96,27 @@ module DataMigration
98
96
  end
99
97
 
100
98
  def job_check_in!(job_id, job_args: [], job_kwargs: {})
101
- self.current_jobs ||= {}
102
-
103
- raise DataMigration::JobConflictError, "#{user_title} already has job ##{job_id}" if current_jobs.key?(job_id)
104
- raise DataMigration::JobConcurrencyLimitError, "#{user_title} reached limit of #{jobs_limit} jobs" if jobs_limit.present? && current_jobs.size >= jobs_limit
105
-
106
- self.current_jobs[job_id] = {
107
- ts: Time.current,
108
- args: job_args,
109
- kwargs: job_kwargs
110
- }
111
- save!
99
+ with_lock do
100
+ self.current_jobs ||= {}
101
+
102
+ raise DataMigration::JobConflictError, "#{user_title} already has job ##{job_id}" if current_jobs.key?(job_id)
103
+ raise DataMigration::JobConcurrencyLimitError, "#{user_title} reached limit of #{jobs_limit} jobs" if jobs_limit.present? && current_jobs.size >= jobs_limit
104
+
105
+ current_jobs[job_id] = {
106
+ ts: Time.current,
107
+ args: job_args,
108
+ kwargs: job_kwargs
109
+ }
110
+ save!
111
+ end
112
112
  end
113
113
 
114
- def job_check_out!(job_id)
115
- self.current_jobs.delete(job_id)
116
- save!
114
+ def job_check_out!(job_id, status: nil)
115
+ with_lock do
116
+ current_jobs.delete(job_id)
117
+ self.status = status if status
118
+ save!(validate: false)
119
+ end
117
120
  end
118
121
 
119
122
  def user_title
@@ -0,0 +1,28 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record"
3
+ require "rails/generators/active_record/migration/migration_generator"
4
+
5
+ module DataMigration
6
+ module Generators
7
+ class InstallGenerator < ActiveRecord::Generators::MigrationGenerator
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ attr_reader :table_exists, :table_columns
11
+ def create_migration_file
12
+ if ActiveRecord::Base.connection.table_exists?(name)
13
+ puts "\e[31mWARNING: Table `#{name}` already exists\e[0m"
14
+ @table_exists = true
15
+ @table_columns = ActiveRecord::Base.connection.columns(name)
16
+ end
17
+
18
+ set_local_assigns!
19
+ validate_file_name!
20
+ migration_template "install_#{name}.rb", "#{DataMigration.config.schema_migrations_path}/install_#{file_name}.rb"
21
+ end
22
+
23
+ def migration_parent
24
+ "ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,4 +1,8 @@
1
1
  class <%= migration_class_name %> < <%= migration_parent %>
2
+ <% if table_exists %>
3
+ # Current columns: "<%= table_columns.map(&:name).join("\", \"") %>"
4
+ <% end %>
5
+
2
6
  def self.up
3
7
  create_table :<%= DataMigration.tasks_table_name %>, force: true do |t|
4
8
  t.string "name", null: false