data-migration 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|