data-migration 1.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.
- checksums.yaml +7 -0
- data/.editorconfig +14 -0
- data/.github/dependabot.yml +27 -0
- data/.github/workflows/_trunk_check.yml +15 -0
- data/.github/workflows/test.yml +42 -0
- data/.gitignore +24 -0
- data/.ruby-version +1 -0
- data/.trunk/.gitignore +9 -0
- data/.trunk/configs/.markdownlint.yaml +2 -0
- data/.trunk/configs/.shellcheckrc +7 -0
- data/.trunk/configs/.yamllint.yaml +7 -0
- data/.trunk/trunk.yaml +39 -0
- data/.vscode/extensions.json +18 -0
- data/.vscode/settings.json +7 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +246 -0
- data/LICENSE.md +21 -0
- data/README.md +141 -0
- data/data-migration.gemspec +44 -0
- data/lib/data_migration/config.rb +84 -0
- data/lib/data_migration/job.rb +63 -0
- data/lib/data_migration/task.rb +127 -0
- data/lib/data_migration.rb +37 -0
- data/lib/generators/data_migration_generator.rb +13 -0
- data/lib/generators/install_generator.rb +17 -0
- data/lib/generators/templates/data_migration.rb.tt +26 -0
- data/lib/generators/templates/install_data_migration_tasks.rb.tt +27 -0
- data/spec/data_migration/config_spec.rb +116 -0
- data/spec/data_migration/job_spec.rb +96 -0
- data/spec/data_migration/task_spec.rb +152 -0
- data/spec/data_migration_spec.rb +65 -0
- data/spec/fixtures/data_migrations/20241206200111_create_users.rb +17 -0
- data/spec/fixtures/data_migrations/20241206200112_create_bad_users.rb +5 -0
- data/spec/fixtures/data_migrations/20241206200113_change_users.rb +5 -0
- data/spec/fixtures/data_migrations/20241206200114_create_batch_users.rb +9 -0
- data/spec/fixtures/schema.rb +26 -0
- data/spec/generators/install_generator_spec.rb +48 -0
- data/spec/generators/migration_generator_spec.rb +50 -0
- data/spec/rails_helper.rb +21 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/junit_formatter.rb +6 -0
- data/spec/support/rails_helpers.rb +53 -0
- data/usr/bin/release.sh +35 -0
- metadata +246 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
Gem::Specification.new do |gem|
|
2
|
+
gem.name = "data-migration"
|
3
|
+
gem.version = File.read(File.expand_path("../lib/data_migration.rb", __FILE__)).match(/VERSION\s*=\s*"(.*?)"/)[1]
|
4
|
+
|
5
|
+
repository_url = "https://github.com/amkisko/data-migration.rb"
|
6
|
+
root_files = %w[CHANGELOG.md LICENSE.md README.md]
|
7
|
+
root_files << "#{gem.name}.gemspec"
|
8
|
+
|
9
|
+
gem.license = "MIT"
|
10
|
+
|
11
|
+
gem.platform = Gem::Platform::RUBY
|
12
|
+
|
13
|
+
gem.authors = ["Andrei Makarov"]
|
14
|
+
gem.email = ["andrei@kiskolabs.com"]
|
15
|
+
gem.homepage = repository_url
|
16
|
+
gem.summary = "Data migrations kit for ActiveRecord and ActiveJob"
|
17
|
+
gem.description = gem.summary
|
18
|
+
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",
|
23
|
+
"rubygems_mfa_required" => "true"
|
24
|
+
}
|
25
|
+
|
26
|
+
gem.files = `git ls-files`.split("\n")
|
27
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
28
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
29
|
+
|
30
|
+
gem.required_ruby_version = ">= 3"
|
31
|
+
gem.require_paths = ["lib"]
|
32
|
+
|
33
|
+
gem.add_dependency "rails", "> 5"
|
34
|
+
gem.add_dependency "activejob", "> 5"
|
35
|
+
gem.add_dependency "activerecord", "> 5"
|
36
|
+
gem.add_dependency "activesupport", "> 5"
|
37
|
+
|
38
|
+
gem.add_development_dependency "bundler", "~> 2"
|
39
|
+
gem.add_development_dependency "rspec", "~> 3"
|
40
|
+
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
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module DataMigration
|
2
|
+
class Config
|
3
|
+
def schema_migrations_path
|
4
|
+
@schema_migrations_path ||= "db/migrate"
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_writer :data_migrations_path
|
8
|
+
def data_migrations_path
|
9
|
+
@data_migrations_path ||= "db/data_migrations"
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_writer :data_migrations_full_path
|
13
|
+
def data_migrations_full_path
|
14
|
+
@data_migrations_full_path ||= Rails.root.join(data_migrations_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def data_migrations_path_glob
|
18
|
+
"#{data_migrations_full_path}/*.rb"
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_writer :generate_spec
|
22
|
+
def generate_spec?
|
23
|
+
@generate_spec.nil? ? true : @generate_spec
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_writer :job_class
|
27
|
+
def job_class
|
28
|
+
@job_class ||= DataMigration::Job
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_writer :task_class
|
32
|
+
def task_class
|
33
|
+
@task_class ||= DataMigration::Task
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_writer :operator_resolver
|
37
|
+
def operator_resolver(&block)
|
38
|
+
if block_given?
|
39
|
+
@operator_resolver = block
|
40
|
+
else
|
41
|
+
@operator_resolver ||= -> do
|
42
|
+
if Object.const_defined?(:ActionReporter)
|
43
|
+
ActionReporter.current_user
|
44
|
+
elsif Object.const_defined?(:Audited)
|
45
|
+
Audited.store[:audited_user]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_writer :monitoring_context
|
52
|
+
def monitoring_context(&block)
|
53
|
+
if block_given?
|
54
|
+
@monitoring_context = block
|
55
|
+
else
|
56
|
+
@monitoring_context ||= ->(migration) do
|
57
|
+
context = {
|
58
|
+
data_migration_name: migration.name,
|
59
|
+
data_migration_id: migration.try(:to_global_id) || migration.id,
|
60
|
+
data_migration_operator_id: migration.operator&.try(:to_global_id) || migration.operator&.id
|
61
|
+
}
|
62
|
+
if Object.const_defined?(:ActionReporter)
|
63
|
+
ActionReporter.current_user ||= migration.operator
|
64
|
+
ActionReporter.context(**context)
|
65
|
+
elsif Object.const_defined?(:Audited)
|
66
|
+
Audited.store[:audited_user] ||= migration.operator
|
67
|
+
else
|
68
|
+
Rails.logger.info("Data migration context: #{context.inspect}")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
attr_writer :job_queue_name
|
75
|
+
def job_queue_name
|
76
|
+
@job_queue_name ||= :default
|
77
|
+
end
|
78
|
+
|
79
|
+
attr_writer :default_jobs_limit
|
80
|
+
def default_jobs_limit
|
81
|
+
@default_jobs_limit ||= 10
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "active_job"
|
2
|
+
|
3
|
+
module DataMigration
|
4
|
+
class Job < ActiveJob::Base
|
5
|
+
queue_as { DataMigration.config.job_queue_name }
|
6
|
+
|
7
|
+
discard_on StandardError
|
8
|
+
|
9
|
+
def perform(task_id, *job_args, **job_kwargs)
|
10
|
+
task = DataMigration::Task.find(task_id)
|
11
|
+
DataMigration.config.monitoring_context.call(task)
|
12
|
+
|
13
|
+
migration_name = task.name
|
14
|
+
migration_path = task.file_path
|
15
|
+
|
16
|
+
unless task.file_exists?
|
17
|
+
DataMigration.notify("#{migration_name} not found")
|
18
|
+
return
|
19
|
+
end
|
20
|
+
|
21
|
+
task.job_check_in!(job_id, job_args:, job_kwargs:)
|
22
|
+
|
23
|
+
require migration_path
|
24
|
+
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)
|
28
|
+
|
29
|
+
if task.started_at.blank?
|
30
|
+
task.update!(started_at: Time.current, status: :started)
|
31
|
+
end
|
32
|
+
|
33
|
+
if task.requires_pause?
|
34
|
+
DataMigration::Job.set(wait: task.pause_minutes.minutes).perform_later(task_id, *job_args, **job_kwargs)
|
35
|
+
task.update!(status: :paused)
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
Thread.current[:data_migration_enqueue_called] ||= {}
|
40
|
+
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
|
44
|
+
end
|
45
|
+
|
46
|
+
task.update!(status: :performing, pause_minutes: 0)
|
47
|
+
klass.new.perform(**job_kwargs)
|
48
|
+
task.job_check_out!(job_id)
|
49
|
+
|
50
|
+
enqueue_called = Thread.current[:data_migration_enqueue_called].delete(klass.name)
|
51
|
+
enqueue_kwargs = Thread.current[:data_migration_enqueue_kwargs].delete(klass.name)
|
52
|
+
if enqueue_called
|
53
|
+
if enqueue_kwargs[:background] == false
|
54
|
+
self.class.new.perform(task_id, *job_args, **enqueue_kwargs)
|
55
|
+
else
|
56
|
+
DataMigration::Job.perform_later(task_id, *job_args, **enqueue_kwargs)
|
57
|
+
end
|
58
|
+
else
|
59
|
+
task.update!(completed_at: Time.current, status: :completed)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require "active_record"
|
2
|
+
|
3
|
+
module DataMigration
|
4
|
+
class JobConcurrencyLimitError < StandardError; end
|
5
|
+
class JobConflictError < StandardError; end
|
6
|
+
|
7
|
+
def self.tasks_table_name
|
8
|
+
"data_migration_tasks"
|
9
|
+
end
|
10
|
+
|
11
|
+
class Task < ActiveRecord::Base
|
12
|
+
self.table_name = DataMigration.tasks_table_name
|
13
|
+
|
14
|
+
belongs_to :operator, polymorphic: true, optional: true
|
15
|
+
|
16
|
+
before_validation do
|
17
|
+
self.operator ||= DataMigration.config.operator_resolver.call
|
18
|
+
self.jobs_limit ||= DataMigration.config.default_jobs_limit
|
19
|
+
end
|
20
|
+
|
21
|
+
enum :status, {
|
22
|
+
started: "started",
|
23
|
+
performing: "performing",
|
24
|
+
paused: "paused",
|
25
|
+
completed: "completed"
|
26
|
+
}
|
27
|
+
|
28
|
+
validates :name, presence: true
|
29
|
+
validates :pause_minutes, numericality: { greater_than_or_equal_to: 0, only_integer: true }, if: -> { pause_minutes.present? }
|
30
|
+
validates :jobs_limit, numericality: { greater_than_or_equal_to: 0, only_integer: true }, if: -> { jobs_limit.present? }
|
31
|
+
validate :file_should_exist
|
32
|
+
|
33
|
+
after_save do
|
34
|
+
if saved_change_to_started_at? && started_at.present?
|
35
|
+
DataMigration.notify("#{user_title} started")
|
36
|
+
end
|
37
|
+
if saved_change_to_completed_at? && completed_at.present?
|
38
|
+
DataMigration.notify("#{user_title} finished")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
scope :not_started, -> { where(status: nil, started_at: nil) }
|
43
|
+
scope :started, -> { where(status: :started) }
|
44
|
+
scope :paused, -> { where(status: :paused) }
|
45
|
+
scope :performing, -> { where(status: :performing) }
|
46
|
+
scope :completed, -> { where(status: :completed) }
|
47
|
+
|
48
|
+
def self.job_class
|
49
|
+
DataMigration.config.job_class
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.perform_now(name, **kwargs)
|
53
|
+
create!(name:).perform_now(**kwargs)
|
54
|
+
end
|
55
|
+
|
56
|
+
def perform_now(**perform_args)
|
57
|
+
update!(kwargs: perform_args)
|
58
|
+
self.class.job_class.perform_now(id, **perform_args)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.perform_later(name, **kwargs)
|
62
|
+
create!(name:).perform_later(**kwargs)
|
63
|
+
end
|
64
|
+
|
65
|
+
def perform_later(**perform_args)
|
66
|
+
update!(kwargs: perform_args)
|
67
|
+
self.class.job_class.perform_later(id, **perform_args)
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.prepare(name, pause_minutes: nil, jobs_limit: nil)
|
71
|
+
create!(name:, pause_minutes:, jobs_limit:)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.root_path
|
75
|
+
DataMigration.config.data_migrations_full_path
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.list
|
79
|
+
Dir[DataMigration.config.data_migrations_path_glob].map { |f| File.basename(f, ".*") }
|
80
|
+
end
|
81
|
+
|
82
|
+
def file_path
|
83
|
+
"#{self.class.root_path}/#{name}.rb"
|
84
|
+
end
|
85
|
+
|
86
|
+
def file_exists?
|
87
|
+
self.class.list.include?(name)
|
88
|
+
end
|
89
|
+
|
90
|
+
def not_started?
|
91
|
+
status.nil? && started_at.nil?
|
92
|
+
end
|
93
|
+
|
94
|
+
def job_check_in!(job_id, job_args: [], job_kwargs: {})
|
95
|
+
self.current_jobs ||= {}
|
96
|
+
|
97
|
+
raise DataMigration::JobConflictError, "#{user_title} already has job ##{job_id}" if current_jobs.key?(job_id)
|
98
|
+
raise DataMigration::JobConcurrencyLimitError, "#{user_title} reached limit of #{jobs_limit} jobs" if jobs_limit.present? && current_jobs.size >= jobs_limit
|
99
|
+
|
100
|
+
self.current_jobs[job_id] = {
|
101
|
+
ts: Time.current,
|
102
|
+
args: job_args,
|
103
|
+
kwargs: job_kwargs
|
104
|
+
}
|
105
|
+
save!
|
106
|
+
end
|
107
|
+
|
108
|
+
def job_check_out!(job_id)
|
109
|
+
self.current_jobs.delete(job_id)
|
110
|
+
save!
|
111
|
+
end
|
112
|
+
|
113
|
+
def user_title
|
114
|
+
"Data migration ##{id} #{name}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def requires_pause?
|
118
|
+
pause_minutes.positive? && !paused?
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def file_should_exist
|
124
|
+
errors.add(:name, "is not found") unless file_exists?
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "data_migration/config"
|
2
|
+
require "data_migration/job"
|
3
|
+
require "data_migration/task"
|
4
|
+
|
5
|
+
module DataMigration
|
6
|
+
VERSION = "1.0.0".freeze
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def config
|
11
|
+
@@config ||= DataMigration::Config.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def configure
|
15
|
+
yield config
|
16
|
+
end
|
17
|
+
|
18
|
+
def notify(message, context: {})
|
19
|
+
if Object.const_defined?(:ActionReporter)
|
20
|
+
ActionReporter.notify(message, context:)
|
21
|
+
elsif Object.const_defined?(:Rails)
|
22
|
+
Rails.logger.info("#{message} #{context.inspect}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def perform_now(...)
|
27
|
+
DataMigration::Task.perform_now(...)
|
28
|
+
end
|
29
|
+
|
30
|
+
def perform_later(...)
|
31
|
+
DataMigration::Task.perform_later(...)
|
32
|
+
end
|
33
|
+
|
34
|
+
def prepare(...)
|
35
|
+
DataMigration::Task.prepare(...)
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "rails/generators/active_record"
|
3
|
+
require "rails/generators/active_record/migration/migration_generator"
|
4
|
+
|
5
|
+
class DataMigrationGenerator < ActiveRecord::Generators::MigrationGenerator
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
7
|
+
|
8
|
+
def create_migration_file
|
9
|
+
set_local_assigns!
|
10
|
+
validate_file_name!
|
11
|
+
migration_template "data_migration.rb", "#{DataMigration.config.data_migrations_path}/#{file_name}.rb"
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "rails/generators/active_record"
|
3
|
+
require "rails/generators/active_record/migration/migration_generator"
|
4
|
+
|
5
|
+
class InstallGenerator < ActiveRecord::Generators::MigrationGenerator
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
7
|
+
|
8
|
+
def create_migration_file
|
9
|
+
set_local_assigns!
|
10
|
+
validate_file_name!
|
11
|
+
migration_template "install_#{name}.rb", "#{DataMigration.config.schema_migrations_path}/#{file_name}.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
def migration_parent
|
15
|
+
"ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]"
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# USAGE: within remote console run one of the following commands
|
2
|
+
# - rails db:migrate:data <%= migration_number %>_<%= migration_file_name %>
|
3
|
+
# - DataMigration.perform_now("<%= migration_number %>_<%= migration_file_name %>")
|
4
|
+
# - DataMigration.perform_later("<%= migration_number %>_<%= migration_file_name %>")
|
5
|
+
# - DataMigration.prepare("<%= migration_number %>_<%= migration_file_name %>", pause: 1.minute, jobs_limit: 2).perform_later
|
6
|
+
|
7
|
+
class <%= migration_class_name %>
|
8
|
+
def perform(**kwargs)
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
12
|
+
<%- if DataMigration.config.generate_spec? %>
|
13
|
+
# TESTING:
|
14
|
+
# - bin/rspec <%= DataMigration.config.data_migrations_path %>/<%= migration_number %>_<%= migration_file_name %>.rb
|
15
|
+
if Rails.env.test? && Object.const_defined?(:RSpec)
|
16
|
+
require "rails_helper"
|
17
|
+
|
18
|
+
RSpec.describe <%= migration_class_name %>, type: :data_migration do
|
19
|
+
subject(:perform) { described_class.new.perform }
|
20
|
+
|
21
|
+
it do
|
22
|
+
expect { perform }.not_to raise_error
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
<%- end %>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class <%= migration_class_name %> < <%= migration_parent %>
|
2
|
+
def self.up
|
3
|
+
create_table :<%= DataMigration.tasks_table_name %>, force: true do |t|
|
4
|
+
t.string "name", null: false
|
5
|
+
|
6
|
+
t.jsonb "kwargs", default: {}, null: false
|
7
|
+
t.jsonb "current_jobs", default: {}, null: false
|
8
|
+
|
9
|
+
t.string "status"
|
10
|
+
t.datetime "started_at"
|
11
|
+
t.datetime "completed_at"
|
12
|
+
|
13
|
+
t.bigint "operator_id"
|
14
|
+
t.string "operator_type"
|
15
|
+
|
16
|
+
t.integer "pause_minutes", default: 0, null: false
|
17
|
+
t.integer "jobs_limit"
|
18
|
+
|
19
|
+
t.datetime "created_at", null: false
|
20
|
+
t.datetime "updated_at", null: false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.down
|
25
|
+
drop_table :<%= DataMigration.tasks_table_name %>
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe DataMigration::Config do
|
4
|
+
subject(:config) { described_class.new }
|
5
|
+
|
6
|
+
let(:current_user) do
|
7
|
+
User.new(id: 1)
|
8
|
+
end
|
9
|
+
let(:action_reporter) do
|
10
|
+
Class.new do
|
11
|
+
def current_user
|
12
|
+
User.new(id: 3)
|
13
|
+
end
|
14
|
+
|
15
|
+
def context(**kwargs)
|
16
|
+
kwargs
|
17
|
+
end
|
18
|
+
end.new
|
19
|
+
end
|
20
|
+
let(:audited) do
|
21
|
+
Class.new do
|
22
|
+
def store
|
23
|
+
{ audited_user: User.new(id: 4) }
|
24
|
+
end
|
25
|
+
end.new
|
26
|
+
end
|
27
|
+
let(:logger) do
|
28
|
+
Class.new do
|
29
|
+
def info(message)
|
30
|
+
puts message
|
31
|
+
end
|
32
|
+
end.new
|
33
|
+
end
|
34
|
+
let(:other_user) do
|
35
|
+
User.new(id: 2)
|
36
|
+
end
|
37
|
+
let(:migration) do
|
38
|
+
DataMigration::Task.new(id: 1, name: "test", operator: other_user)
|
39
|
+
end
|
40
|
+
|
41
|
+
before do
|
42
|
+
allow(Rails).to receive(:root).and_return(rails_root)
|
43
|
+
allow(Rails).to receive(:logger).and_return(logger)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "has default schema migrations path" do
|
47
|
+
expect(config.schema_migrations_path).to eq "db/migrate"
|
48
|
+
end
|
49
|
+
|
50
|
+
it "has default data migrations path" do
|
51
|
+
expect(config.data_migrations_path).to eq "db/data_migrations"
|
52
|
+
end
|
53
|
+
|
54
|
+
it "has default data migrations full path" do
|
55
|
+
expect(config.data_migrations_full_path).to eq Rails.root.join("db/data_migrations")
|
56
|
+
end
|
57
|
+
|
58
|
+
it "has default data migrations path glob" do
|
59
|
+
expect(config.data_migrations_path_glob).to eq Rails.root.join("db/data_migrations/*.rb").to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
it "has default generate spec" do
|
63
|
+
expect(config.generate_spec?).to be true
|
64
|
+
end
|
65
|
+
|
66
|
+
it "has default task class" do
|
67
|
+
expect(config.task_class).to eq DataMigration::Task
|
68
|
+
end
|
69
|
+
|
70
|
+
it "has default job class" do
|
71
|
+
expect(config.job_class).to eq DataMigration::Job
|
72
|
+
end
|
73
|
+
|
74
|
+
it "has default job queue name" do
|
75
|
+
expect(config.job_queue_name).to eq :default
|
76
|
+
end
|
77
|
+
|
78
|
+
it "has default default jobs limit" do
|
79
|
+
expect(config.default_jobs_limit).to eq 10
|
80
|
+
end
|
81
|
+
|
82
|
+
it "has default monitoring context" do
|
83
|
+
expect(config.monitoring_context).to be_a Proc
|
84
|
+
|
85
|
+
expect(Rails.logger).to receive(:info).with("Data migration context: {:data_migration_name=>\"test\", :data_migration_id=>1, :data_migration_operator_id=>2}")
|
86
|
+
expect { config.monitoring_context.call(migration) }.not_to raise_error
|
87
|
+
end
|
88
|
+
|
89
|
+
context "when ActionReporter is defined" do
|
90
|
+
before do
|
91
|
+
stub_const("ActionReporter", action_reporter)
|
92
|
+
end
|
93
|
+
|
94
|
+
it "sets monitoring context" do
|
95
|
+
expect(ActionReporter).to receive(:context).with(
|
96
|
+
data_migration_name: "test",
|
97
|
+
data_migration_id: 1,
|
98
|
+
data_migration_operator_id: 2
|
99
|
+
)
|
100
|
+
expect(ActionReporter).to receive(:current_user).and_return(nil)
|
101
|
+
expect(ActionReporter).to receive(:current_user=).with(other_user)
|
102
|
+
expect { config.monitoring_context.call(migration) }.not_to raise_error
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context "when Audited is defined" do
|
107
|
+
before do
|
108
|
+
stub_const("Audited", audited)
|
109
|
+
end
|
110
|
+
|
111
|
+
it "sets monitoring context" do
|
112
|
+
expect(Audited).to receive(:store).and_return(audited_user: User.new(id: 5))
|
113
|
+
expect { config.monitoring_context.call(migration) }.not_to raise_error
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe DataMigration::Job do
|
4
|
+
subject(:job) { DataMigration::Job.new }
|
5
|
+
|
6
|
+
let(:operator) { User.create!(email: "test@example.com") }
|
7
|
+
let(:task) { DataMigration::Task.create!(name: migration_name, operator: operator) }
|
8
|
+
|
9
|
+
let(:migration_name) { "20241206200111_create_users" }
|
10
|
+
let(:data_migrations_full_path) { File.expand_path("../fixtures/data_migrations", __dir__) }
|
11
|
+
|
12
|
+
before do
|
13
|
+
allow(Rails).to receive(:env).and_return(rails_env)
|
14
|
+
allow(Rails).to receive(:logger).and_return(rails_logger)
|
15
|
+
allow(DataMigration.config).to receive(:data_migrations_full_path).and_return(data_migrations_full_path)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "runs rspec spec/fixtures/data_migrations/20241206200111_create_users.rb" do
|
19
|
+
output = `RUN_MIGRATION_TESTS=1 rspec #{data_migrations_full_path}/#{migration_name}.rb`
|
20
|
+
expect(output).to include("1 example, 0 failures")
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#perform" do
|
24
|
+
subject(:perform) { job.perform(task.id, **job_kwargs) }
|
25
|
+
|
26
|
+
let(:job_kwargs) { {} }
|
27
|
+
|
28
|
+
it "updates the task status to completed" do
|
29
|
+
expect { perform }.to change { task.reload.status }.to("completed")
|
30
|
+
end
|
31
|
+
|
32
|
+
context "when migration file is not found" do
|
33
|
+
before do
|
34
|
+
task
|
35
|
+
allow_any_instance_of(DataMigration::Task).to receive(:file_exists?).and_return(false)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "updates the task status to failed" do
|
39
|
+
expect(DataMigration).to receive(:notify).with("#{migration_name} not found")
|
40
|
+
expect { perform }.not_to raise_error
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "when migration class is not found" do
|
45
|
+
let(:migration_name) { "20241206200112_create_bad_users" }
|
46
|
+
|
47
|
+
it "raises an error" do
|
48
|
+
expect { perform }.to raise_error("Data migration class #{migration_name.gsub(/^[0-9_]+/, "").camelize} not found")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "when migration class does not implement perform method" do
|
53
|
+
let(:migration_name) { "20241206200113_change_users" }
|
54
|
+
|
55
|
+
it "raises an error" do
|
56
|
+
expect { perform }.to raise_error("Data migration class #{migration_name.gsub(/^[0-9_]+/, "").camelize} must implement `perform` method")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "when there is an enqueue call" do
|
61
|
+
let(:migration_name) { "20241206200114_create_batch_users" }
|
62
|
+
|
63
|
+
it "runs the migration in foreground" do
|
64
|
+
expect { perform }.to change { User.count }.by(3)
|
65
|
+
expect(task.reload.current_jobs.count).to eq(0)
|
66
|
+
expect(task.status).to eq("completed")
|
67
|
+
expect(User.pluck(:email)).to match_array(["test@example.com", "test_1@example.com", "test_2@example.com"])
|
68
|
+
end
|
69
|
+
|
70
|
+
context "when background is true" do
|
71
|
+
let(:job_kwargs) { { background: true } }
|
72
|
+
|
73
|
+
before do
|
74
|
+
operator
|
75
|
+
end
|
76
|
+
|
77
|
+
it "runs the migration in background" do
|
78
|
+
expect { perform }.to change(User, :count).by(1)
|
79
|
+
expect(task.reload.status).to eq("performing")
|
80
|
+
expect(task.current_jobs.count).to eq(0)
|
81
|
+
expect(task.kwargs).to eq({})
|
82
|
+
|
83
|
+
expect { job.perform(task.id, index: 2, background: true) }.to change(User, :count).by(1)
|
84
|
+
expect(task.reload.status).to eq("performing")
|
85
|
+
expect(task.reload.current_jobs.count).to eq(0)
|
86
|
+
expect(task.kwargs).to eq({})
|
87
|
+
|
88
|
+
expect { job.perform(task.id, index: 3, background: true) }.to change(User, :count).by(0)
|
89
|
+
expect(task.reload.status).to eq("completed")
|
90
|
+
expect(task.reload.current_jobs.count).to eq(0)
|
91
|
+
expect(task.kwargs).to eq({})
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|