online_migrations 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/.github/workflows/test.yml +112 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +113 -0
- data/.yardopts +1 -0
- data/BACKGROUND_MIGRATIONS.md +288 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +27 -0
- data/Gemfile.lock +108 -0
- data/LICENSE.txt +21 -0
- data/README.md +1067 -0
- data/Rakefile +23 -0
- data/gemfiles/activerecord_42.gemfile +6 -0
- data/gemfiles/activerecord_50.gemfile +5 -0
- data/gemfiles/activerecord_51.gemfile +5 -0
- data/gemfiles/activerecord_52.gemfile +5 -0
- data/gemfiles/activerecord_60.gemfile +5 -0
- data/gemfiles/activerecord_61.gemfile +5 -0
- data/gemfiles/activerecord_70.gemfile +5 -0
- data/gemfiles/activerecord_head.gemfile +5 -0
- data/lib/generators/online_migrations/background_migration_generator.rb +29 -0
- data/lib/generators/online_migrations/install_generator.rb +34 -0
- data/lib/generators/online_migrations/templates/background_migration.rb.tt +22 -0
- data/lib/generators/online_migrations/templates/initializer.rb.tt +94 -0
- data/lib/generators/online_migrations/templates/migration.rb.tt +46 -0
- data/lib/online_migrations/background_migration.rb +64 -0
- data/lib/online_migrations/background_migrations/advisory_lock.rb +62 -0
- data/lib/online_migrations/background_migrations/backfill_column.rb +52 -0
- data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +36 -0
- data/lib/online_migrations/background_migrations/config.rb +98 -0
- data/lib/online_migrations/background_migrations/copy_column.rb +90 -0
- data/lib/online_migrations/background_migrations/migration.rb +210 -0
- data/lib/online_migrations/background_migrations/migration_helpers.rb +238 -0
- data/lib/online_migrations/background_migrations/migration_job.rb +92 -0
- data/lib/online_migrations/background_migrations/migration_job_runner.rb +63 -0
- data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +27 -0
- data/lib/online_migrations/background_migrations/migration_runner.rb +97 -0
- data/lib/online_migrations/background_migrations/migration_status_validator.rb +45 -0
- data/lib/online_migrations/background_migrations/scheduler.rb +49 -0
- data/lib/online_migrations/batch_iterator.rb +87 -0
- data/lib/online_migrations/change_column_type_helpers.rb +587 -0
- data/lib/online_migrations/command_checker.rb +590 -0
- data/lib/online_migrations/command_recorder.rb +137 -0
- data/lib/online_migrations/config.rb +198 -0
- data/lib/online_migrations/copy_trigger.rb +91 -0
- data/lib/online_migrations/database_tasks.rb +19 -0
- data/lib/online_migrations/error_messages.rb +388 -0
- data/lib/online_migrations/foreign_key_definition.rb +17 -0
- data/lib/online_migrations/foreign_keys_collector.rb +33 -0
- data/lib/online_migrations/indexes_collector.rb +48 -0
- data/lib/online_migrations/lock_retrier.rb +250 -0
- data/lib/online_migrations/migration.rb +63 -0
- data/lib/online_migrations/migrator.rb +23 -0
- data/lib/online_migrations/schema_cache.rb +96 -0
- data/lib/online_migrations/schema_statements.rb +1042 -0
- data/lib/online_migrations/utils.rb +140 -0
- data/lib/online_migrations/version.rb +5 -0
- data/lib/online_migrations.rb +74 -0
- data/online_migrations.gemspec +28 -0
- metadata +119 -0
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
# @private
|
5
|
+
module CommandRecorder
|
6
|
+
REVERSIBLE_AND_IRREVERSIBLE_METHODS = [
|
7
|
+
:update_column_in_batches,
|
8
|
+
:initialize_column_rename,
|
9
|
+
:revert_initialize_column_rename,
|
10
|
+
:finalize_column_rename,
|
11
|
+
:revert_finalize_column_rename,
|
12
|
+
:initialize_table_rename,
|
13
|
+
:revert_initialize_table_rename,
|
14
|
+
:finalize_table_rename,
|
15
|
+
:revert_finalize_table_rename,
|
16
|
+
:swap_column_names,
|
17
|
+
:add_column_with_default,
|
18
|
+
:add_not_null_constraint,
|
19
|
+
:add_text_limit_constraint,
|
20
|
+
:add_reference_concurrently,
|
21
|
+
:change_column_type_in_background,
|
22
|
+
:enqueue_background_migration,
|
23
|
+
|
24
|
+
# column type change helpers
|
25
|
+
:initialize_column_type_change,
|
26
|
+
:initialize_columns_type_change,
|
27
|
+
:revert_initialize_column_type_change,
|
28
|
+
:revert_initialize_columns_type_change,
|
29
|
+
:backfill_column_for_type_change,
|
30
|
+
:backfill_columns_for_type_change,
|
31
|
+
:finalize_column_type_change,
|
32
|
+
:finalize_columns_type_change,
|
33
|
+
:revert_finalize_column_type_change,
|
34
|
+
:cleanup_column_type_change,
|
35
|
+
:cleanup_columns_type_change,
|
36
|
+
]
|
37
|
+
|
38
|
+
REVERSIBLE_AND_IRREVERSIBLE_METHODS.each do |method|
|
39
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
40
|
+
def #{method}(*args, &block) # def create_table(*args, &block)
|
41
|
+
record(:"#{method}", args, &block) # record(:create_table, args, &block)
|
42
|
+
end # end
|
43
|
+
RUBY
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
module StraightReversions
|
48
|
+
{
|
49
|
+
initialize_column_rename: :revert_initialize_column_rename,
|
50
|
+
finalize_column_rename: :revert_finalize_column_rename,
|
51
|
+
initialize_table_rename: :revert_initialize_table_rename,
|
52
|
+
finalize_table_rename: :revert_finalize_table_rename,
|
53
|
+
add_not_null_constraint: :remove_not_null_constraint,
|
54
|
+
initialize_column_type_change: :revert_initialize_column_type_change,
|
55
|
+
initialize_columns_type_change: :revert_initialize_columns_type_change,
|
56
|
+
finalize_column_type_change: :revert_finalize_column_type_change,
|
57
|
+
finalize_columns_type_change: :revert_finalize_columns_type_change,
|
58
|
+
}.each do |cmd, inv|
|
59
|
+
[[inv, cmd], [cmd, inv]].each do |method, inverse|
|
60
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
61
|
+
def invert_#{method}(args, &block) # def invert_create_table(args, &block)
|
62
|
+
[:#{inverse}, args, block] # [:drop_table, args, block]
|
63
|
+
end # end
|
64
|
+
RUBY
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
include StraightReversions
|
70
|
+
|
71
|
+
def invert_swap_column_names(args)
|
72
|
+
table_name, column1, column2 = args
|
73
|
+
[:swap_column_names, [table_name, column2, column1]]
|
74
|
+
end
|
75
|
+
|
76
|
+
def invert_add_column_with_default(args)
|
77
|
+
table_name, column_name, = args
|
78
|
+
[:remove_column, [table_name, column_name]]
|
79
|
+
end
|
80
|
+
|
81
|
+
def invert_revert_initialize_column_rename(args)
|
82
|
+
_table, column, new_column = args
|
83
|
+
if !column || !new_column
|
84
|
+
raise ActiveRecord::IrreversibleMigration,
|
85
|
+
"invert_revert_initialize_column_rename is only reversible if given a column and new_column."
|
86
|
+
end
|
87
|
+
[:initialize_column_rename, args]
|
88
|
+
end
|
89
|
+
|
90
|
+
def invert_finalize_table_rename(args)
|
91
|
+
_table_name, new_name = args
|
92
|
+
unless new_name
|
93
|
+
raise ActiveRecord::IrreversibleMigration,
|
94
|
+
"finalize_table_rename is only reversible if given a new_name."
|
95
|
+
end
|
96
|
+
[:revert_finalize_table_rename, args]
|
97
|
+
end
|
98
|
+
|
99
|
+
def invert_revert_initialize_column_type_change(args)
|
100
|
+
unless args[2]
|
101
|
+
raise ActiveRecord::IrreversibleMigration,
|
102
|
+
"revert_initialize_column_type_change is only reversible if given a new_type."
|
103
|
+
end
|
104
|
+
super
|
105
|
+
end
|
106
|
+
|
107
|
+
def invert_revert_initialize_columns_type_change(args)
|
108
|
+
if args[1].empty?
|
109
|
+
raise ActiveRecord::IrreversibleMigration,
|
110
|
+
"revert_initialize_columns_type_change is only reversible if given a columns_and_types."
|
111
|
+
end
|
112
|
+
super
|
113
|
+
end
|
114
|
+
|
115
|
+
def invert_add_not_null_constraint(args)
|
116
|
+
options = args.extract_options!
|
117
|
+
table_name, column = args
|
118
|
+
options.delete(:validate)
|
119
|
+
[:remove_not_null_constraint, [table_name, column, **options]]
|
120
|
+
end
|
121
|
+
|
122
|
+
def invert_add_text_limit_constraint(args)
|
123
|
+
options = args.extract_options!
|
124
|
+
table_name, column, _limit = args
|
125
|
+
options.delete(:validate)
|
126
|
+
[:remove_text_limit_constraint, [table_name, column, **options]]
|
127
|
+
end
|
128
|
+
|
129
|
+
def invert_remove_text_limit_constraint(args)
|
130
|
+
unless args[2]
|
131
|
+
raise ActiveRecord::IrreversibleMigration, "remove_text_limit_constraint is only reversible if given a limit."
|
132
|
+
end
|
133
|
+
|
134
|
+
super
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
# Class representing configuration options for the gem.
|
5
|
+
class Config
|
6
|
+
include ErrorMessages
|
7
|
+
|
8
|
+
# The migration version starting from which checks are performed
|
9
|
+
# @return [Integer]
|
10
|
+
#
|
11
|
+
attr_accessor :start_after
|
12
|
+
|
13
|
+
# The database version against which the checks will be performed
|
14
|
+
#
|
15
|
+
# If your development database version is different from production, you can specify
|
16
|
+
# the production version so the right checks run in development.
|
17
|
+
#
|
18
|
+
# @example Set specific target version
|
19
|
+
# OnlineMigrations.config.target_version = 10
|
20
|
+
#
|
21
|
+
attr_accessor :target_version
|
22
|
+
|
23
|
+
# Whether to perform checks when migrating down
|
24
|
+
#
|
25
|
+
# Disabled by default
|
26
|
+
# @return [Boolean]
|
27
|
+
#
|
28
|
+
attr_accessor :check_down
|
29
|
+
|
30
|
+
# Error messages
|
31
|
+
#
|
32
|
+
# @return [Hash] Keys are error names, values are error messages
|
33
|
+
# @example To change a message
|
34
|
+
# OnlineMigrations.config.error_messages[:remove_column] = "Your custom instructions"
|
35
|
+
#
|
36
|
+
attr_accessor :error_messages
|
37
|
+
|
38
|
+
# Maximum allowed lock timeout value (in seconds)
|
39
|
+
#
|
40
|
+
# If set lock timeout is greater than this value, the migration will fail.
|
41
|
+
# The default value is 10 seconds.
|
42
|
+
#
|
43
|
+
# @return [Numeric]
|
44
|
+
#
|
45
|
+
attr_accessor :lock_timeout_limit
|
46
|
+
|
47
|
+
# List of tables with permanently small number of records
|
48
|
+
#
|
49
|
+
# These are usually tables like "settings", "prices", "plans" etc.
|
50
|
+
# It is considered safe to perform most of the dangerous operations on them,
|
51
|
+
# like adding indexes, columns etc.
|
52
|
+
#
|
53
|
+
# @return [Array<String, Symbol>]
|
54
|
+
#
|
55
|
+
attr_reader :small_tables
|
56
|
+
|
57
|
+
# Tables that are in the process of being renamed
|
58
|
+
#
|
59
|
+
# @return [Hash] Keys are old table names, values - new table names
|
60
|
+
# @example To add a table
|
61
|
+
# OnlineMigrations.config.table_renames["users"] = "clients"
|
62
|
+
#
|
63
|
+
attr_accessor :table_renames
|
64
|
+
|
65
|
+
# Columns that are in the process of being renamed
|
66
|
+
#
|
67
|
+
# @return [Hash] Keys are table names, values - hashes with old column names as keys
|
68
|
+
# and new column names as values
|
69
|
+
# @example To add a column
|
70
|
+
# OnlineMigrations.config.column_renames["users] = { "name" => "first_name" }
|
71
|
+
#
|
72
|
+
attr_accessor :column_renames
|
73
|
+
|
74
|
+
# Lock retrier in use (see LockRetrier)
|
75
|
+
#
|
76
|
+
# No retries are performed by default.
|
77
|
+
# @return [OnlineMigrations::LockRetrier]
|
78
|
+
#
|
79
|
+
attr_reader :lock_retrier
|
80
|
+
|
81
|
+
# Returns a list of custom checks
|
82
|
+
#
|
83
|
+
# Use `add_check` to add custom checks
|
84
|
+
#
|
85
|
+
# @return [Array<Array<Hash>, Proc>]
|
86
|
+
#
|
87
|
+
attr_reader :checks
|
88
|
+
|
89
|
+
# Returns a list of enabled checks
|
90
|
+
#
|
91
|
+
# All checks are enabled by default. To disable/enable a check use `disable_check`/`enable_check`.
|
92
|
+
# For the list of available checks look at `lib/error_messages` folder.
|
93
|
+
#
|
94
|
+
# @return [Array]
|
95
|
+
#
|
96
|
+
attr_reader :enabled_checks
|
97
|
+
|
98
|
+
# Configuration object to configure background migrations
|
99
|
+
#
|
100
|
+
# @return [BackgroundMigrationsConfig]
|
101
|
+
# @see BackgroundMigrationsConfig
|
102
|
+
#
|
103
|
+
attr_reader :background_migrations
|
104
|
+
|
105
|
+
def initialize
|
106
|
+
@table_renames = {}
|
107
|
+
@column_renames = {}
|
108
|
+
@error_messages = ERROR_MESSAGES
|
109
|
+
@lock_timeout_limit = 10.seconds
|
110
|
+
|
111
|
+
@lock_retrier = ExponentialLockRetrier.new(
|
112
|
+
attempts: 30,
|
113
|
+
base_delay: 0.01.seconds,
|
114
|
+
max_delay: 1.minute,
|
115
|
+
lock_timeout: 0.05.seconds
|
116
|
+
)
|
117
|
+
|
118
|
+
@background_migrations = BackgroundMigrations::Config.new
|
119
|
+
|
120
|
+
@checks = []
|
121
|
+
@start_after = 0
|
122
|
+
@small_tables = []
|
123
|
+
@check_down = false
|
124
|
+
@enabled_checks = @error_messages.keys.map { |k| [k, {}] }.to_h
|
125
|
+
end
|
126
|
+
|
127
|
+
def lock_retrier=(value)
|
128
|
+
@lock_retrier = value || NullLockRetrier.new
|
129
|
+
end
|
130
|
+
|
131
|
+
def small_tables=(table_names)
|
132
|
+
@small_tables = table_names.map(&:to_s)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Enables specific check
|
136
|
+
#
|
137
|
+
# For the list of available checks look at `lib/error_messages` module.
|
138
|
+
#
|
139
|
+
# @param name [Symbol] check name
|
140
|
+
# @param start_after [Integer] migration version from which this check will be performed
|
141
|
+
# @return [void]
|
142
|
+
#
|
143
|
+
def enable_check(name, start_after: nil)
|
144
|
+
enabled_checks[name] = { start_after: start_after }
|
145
|
+
end
|
146
|
+
|
147
|
+
# Disables specific check
|
148
|
+
#
|
149
|
+
# For the list of available checks look at `lib/error_messages` module.
|
150
|
+
#
|
151
|
+
# @param name [Symbol] check name
|
152
|
+
# @return [void]
|
153
|
+
#
|
154
|
+
def disable_check(name)
|
155
|
+
enabled_checks.delete(name)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Test whether specific check is enabled
|
159
|
+
#
|
160
|
+
# For the list of available checks look at `lib/error_messages` module.
|
161
|
+
#
|
162
|
+
# @param name [Symbol] check name
|
163
|
+
# @param version [Integer] migration version
|
164
|
+
# @return [void]
|
165
|
+
#
|
166
|
+
def check_enabled?(name, version: nil)
|
167
|
+
if enabled_checks[name]
|
168
|
+
start_after = enabled_checks[name][:start_after] || OnlineMigrations.config.start_after
|
169
|
+
!version || version > start_after
|
170
|
+
else
|
171
|
+
false
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Adds custom check
|
176
|
+
#
|
177
|
+
# @param start_after [Integer] migration version from which this check will be performed
|
178
|
+
#
|
179
|
+
# @yield [method, args] a block to be called with custom check
|
180
|
+
# @yieldparam method [Symbol] method name
|
181
|
+
# @yieldparam args [Array] method arguments
|
182
|
+
#
|
183
|
+
# @return [void]
|
184
|
+
#
|
185
|
+
# Use `stop!` method to stop the migration
|
186
|
+
#
|
187
|
+
# @example
|
188
|
+
# OnlineMigrations.config.add_check do |method, args|
|
189
|
+
# if method == :add_column && args[0].to_s == "users"
|
190
|
+
# stop!("No more columns on the users table")
|
191
|
+
# end
|
192
|
+
# end
|
193
|
+
#
|
194
|
+
def add_check(start_after: nil, &block)
|
195
|
+
@checks << [{ start_after: start_after }, block]
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
|
5
|
+
module OnlineMigrations
|
6
|
+
# @private
|
7
|
+
class CopyTrigger
|
8
|
+
def self.on_table(table_name, connection:)
|
9
|
+
new(table_name, connection)
|
10
|
+
end
|
11
|
+
|
12
|
+
def name(from_columns, to_columns)
|
13
|
+
from_columns, to_columns = normalize_column_names(from_columns, to_columns)
|
14
|
+
|
15
|
+
joined_column_names = from_columns.zip(to_columns).flatten.join("_")
|
16
|
+
identifier = "#{table_name}_#{joined_column_names}"
|
17
|
+
hashed_identifier = OpenSSL::Digest::SHA256.hexdigest(identifier).first(10)
|
18
|
+
"trigger_#{hashed_identifier}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def create(from_columns, to_columns)
|
22
|
+
from_columns, to_columns = normalize_column_names(from_columns, to_columns)
|
23
|
+
trigger_name = name(from_columns, to_columns)
|
24
|
+
assignment_clauses = assignment_clauses_for_columns(from_columns, to_columns)
|
25
|
+
|
26
|
+
connection.execute(<<~SQL)
|
27
|
+
CREATE OR REPLACE FUNCTION #{trigger_name}() RETURNS TRIGGER AS $$
|
28
|
+
BEGIN
|
29
|
+
#{assignment_clauses};
|
30
|
+
RETURN NEW;
|
31
|
+
END;
|
32
|
+
$$ LANGUAGE plpgsql;
|
33
|
+
SQL
|
34
|
+
|
35
|
+
connection.execute(<<~SQL)
|
36
|
+
DROP TRIGGER IF EXISTS #{trigger_name} ON #{quoted_table_name}
|
37
|
+
SQL
|
38
|
+
|
39
|
+
connection.execute(<<~SQL)
|
40
|
+
CREATE TRIGGER #{trigger_name}
|
41
|
+
BEFORE INSERT OR UPDATE
|
42
|
+
ON #{quoted_table_name}
|
43
|
+
FOR EACH ROW
|
44
|
+
EXECUTE PROCEDURE #{trigger_name}();
|
45
|
+
SQL
|
46
|
+
end
|
47
|
+
|
48
|
+
def remove(from_columns, to_columns)
|
49
|
+
trigger_name = name(from_columns, to_columns)
|
50
|
+
|
51
|
+
connection.execute("DROP TRIGGER IF EXISTS #{trigger_name} ON #{quoted_table_name}")
|
52
|
+
connection.execute("DROP FUNCTION IF EXISTS #{trigger_name}()")
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
attr_reader :table_name, :connection
|
57
|
+
|
58
|
+
def initialize(table_name, connection)
|
59
|
+
@table_name = table_name
|
60
|
+
@connection = connection
|
61
|
+
end
|
62
|
+
|
63
|
+
def quoted_table_name
|
64
|
+
@quoted_table_name ||= connection.quote_table_name(table_name)
|
65
|
+
end
|
66
|
+
|
67
|
+
def normalize_column_names(from_columns, to_columns)
|
68
|
+
from_columns = Array.wrap(from_columns)
|
69
|
+
to_columns = Array.wrap(to_columns)
|
70
|
+
|
71
|
+
if from_columns.size != to_columns.size
|
72
|
+
raise ArgumentError, "Number of source and destination columns must match"
|
73
|
+
end
|
74
|
+
|
75
|
+
[from_columns, to_columns]
|
76
|
+
end
|
77
|
+
|
78
|
+
def assignment_clauses_for_columns(from_columns, to_columns)
|
79
|
+
combined_column_names = to_columns.zip(from_columns)
|
80
|
+
|
81
|
+
assignment_clauses = combined_column_names.map do |(new_name, old_name)|
|
82
|
+
new_name = connection.quote_column_name(new_name)
|
83
|
+
old_name = connection.quote_column_name(old_name)
|
84
|
+
|
85
|
+
"NEW.#{new_name} := NEW.#{old_name}"
|
86
|
+
end
|
87
|
+
|
88
|
+
assignment_clauses.join(";\n ")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
# @private
|
5
|
+
module DatabaseTasks
|
6
|
+
def migrate(*)
|
7
|
+
super
|
8
|
+
rescue => e # rubocop:disable Style/RescueStandardError
|
9
|
+
if e.cause.is_a?(OnlineMigrations::Error)
|
10
|
+
# strip cause
|
11
|
+
def e.cause
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
raise e
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|