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,590 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erb"
|
4
|
+
require "openssl"
|
5
|
+
require "set"
|
6
|
+
|
7
|
+
module OnlineMigrations
|
8
|
+
# @private
|
9
|
+
class CommandChecker
|
10
|
+
attr_accessor :direction
|
11
|
+
|
12
|
+
def initialize(migration)
|
13
|
+
@migration = migration
|
14
|
+
@safe = false
|
15
|
+
@new_tables = []
|
16
|
+
@lock_timeout_checked = false
|
17
|
+
@foreign_key_tables = Set.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def safety_assured
|
21
|
+
@prev_value = @safe
|
22
|
+
@safe = true
|
23
|
+
yield
|
24
|
+
ensure
|
25
|
+
@safe = @prev_value
|
26
|
+
end
|
27
|
+
|
28
|
+
def check(command, *args, &block)
|
29
|
+
check_lock_timeout
|
30
|
+
|
31
|
+
unless safe?
|
32
|
+
do_check(command, *args, &block)
|
33
|
+
|
34
|
+
run_custom_checks(command, args)
|
35
|
+
|
36
|
+
if @foreign_key_tables.count { |t| !new_table?(t) } > 1
|
37
|
+
raise_error :multiple_foreign_keys
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def check_lock_timeout
|
46
|
+
limit = OnlineMigrations.config.lock_timeout_limit
|
47
|
+
|
48
|
+
if limit && !@lock_timeout_checked
|
49
|
+
lock_timeout = connection.select_value("SHOW lock_timeout")
|
50
|
+
lock_timeout_sec = timeout_to_sec(lock_timeout)
|
51
|
+
|
52
|
+
if lock_timeout_sec == 0
|
53
|
+
Utils.warn("DANGER: No lock timeout set")
|
54
|
+
elsif lock_timeout_sec > limit
|
55
|
+
Utils.warn("DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}")
|
56
|
+
end
|
57
|
+
|
58
|
+
@lock_timeout_checked = true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def timeout_to_sec(timeout)
|
63
|
+
units = {
|
64
|
+
"us" => 10**-6,
|
65
|
+
"ms" => 10**-3,
|
66
|
+
"s" => 1,
|
67
|
+
"min" => 60,
|
68
|
+
"h" => 60 * 60,
|
69
|
+
"d" => 60 * 60 * 24,
|
70
|
+
}
|
71
|
+
|
72
|
+
timeout_sec = timeout.to_i
|
73
|
+
|
74
|
+
units.each do |k, v|
|
75
|
+
if timeout.end_with?(k)
|
76
|
+
timeout_sec *= v
|
77
|
+
break
|
78
|
+
end
|
79
|
+
end
|
80
|
+
timeout_sec
|
81
|
+
end
|
82
|
+
|
83
|
+
def safe?
|
84
|
+
@safe ||
|
85
|
+
ENV["SAFETY_ASSURED"] ||
|
86
|
+
(direction == :down && !OnlineMigrations.config.check_down) ||
|
87
|
+
version <= OnlineMigrations.config.start_after
|
88
|
+
end
|
89
|
+
|
90
|
+
def version
|
91
|
+
@migration.version || @migration.class.version
|
92
|
+
end
|
93
|
+
|
94
|
+
def do_check(command, *args, **options, &block)
|
95
|
+
case command
|
96
|
+
when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
|
97
|
+
check_columns_removal(command, *args, **options)
|
98
|
+
else
|
99
|
+
if respond_to?(command, true)
|
100
|
+
send(command, *args, **options, &block)
|
101
|
+
else
|
102
|
+
# assume it is safe
|
103
|
+
true
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def create_table(table_name, **options, &block)
|
109
|
+
raise_error :create_table if options[:force]
|
110
|
+
|
111
|
+
# Probably, it would be good idea to also check for foreign keys
|
112
|
+
# with short integer types, and for mismatched primary key vs foreign key types.
|
113
|
+
# But I think this check is enough for now.
|
114
|
+
raise_error :short_primary_key_type if short_primary_key_type?(options)
|
115
|
+
|
116
|
+
if block
|
117
|
+
collect_foreign_keys(&block)
|
118
|
+
check_for_hash_indexes(&block) if postgresql_version < Gem::Version.new("10")
|
119
|
+
end
|
120
|
+
|
121
|
+
@new_tables << table_name.to_s
|
122
|
+
end
|
123
|
+
|
124
|
+
def create_join_table(table1, table2, **options, &block)
|
125
|
+
raise_error :create_table if options[:force]
|
126
|
+
raise_error :short_primary_key_type if short_primary_key_type?(options)
|
127
|
+
|
128
|
+
if block
|
129
|
+
collect_foreign_keys(&block)
|
130
|
+
check_for_hash_indexes(&block) if postgresql_version < Gem::Version.new("10")
|
131
|
+
end
|
132
|
+
|
133
|
+
table_name = options[:table_name] || derive_join_table_name(table1, table2)
|
134
|
+
@new_tables << table_name.to_s
|
135
|
+
end
|
136
|
+
|
137
|
+
def change_table(*)
|
138
|
+
raise_error :change_table, header: "Possibly dangerous operation"
|
139
|
+
end
|
140
|
+
|
141
|
+
def rename_table(table_name, new_name, **)
|
142
|
+
if !new_table?(table_name)
|
143
|
+
raise_error :rename_table,
|
144
|
+
table_name: table_name,
|
145
|
+
new_name: new_name
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def add_column(table_name, column_name, type, **options)
|
150
|
+
volatile_default = false
|
151
|
+
if !new_or_small_table?(table_name) && !options[:default].nil? &&
|
152
|
+
(postgresql_version < Gem::Version.new("11") || (volatile_default = Utils.volatile_default?(connection, type, options[:default])))
|
153
|
+
|
154
|
+
raise_error :add_column_with_default,
|
155
|
+
code: command_str(:add_column_with_default, table_name, column_name, type, options),
|
156
|
+
not_null: options[:null] == false,
|
157
|
+
volatile_default: volatile_default
|
158
|
+
end
|
159
|
+
|
160
|
+
if type.to_s == "json"
|
161
|
+
raise_error :add_column_json,
|
162
|
+
code: command_str(:add_column, table_name, column_name, :jsonb, options)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def rename_column(table_name, column_name, new_column, **)
|
167
|
+
if !new_table?(table_name)
|
168
|
+
raise_error :rename_column,
|
169
|
+
table_name: table_name,
|
170
|
+
column_name: column_name,
|
171
|
+
new_column: new_column,
|
172
|
+
model: table_name.to_s.classify,
|
173
|
+
partial_writes: Utils.ar_partial_writes?,
|
174
|
+
partial_writes_setting: Utils.ar_partial_writes_setting
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def change_column(table_name, column_name, type, **options)
|
179
|
+
return if new_table?(table_name)
|
180
|
+
|
181
|
+
type = type.to_sym
|
182
|
+
|
183
|
+
existing_column = connection.columns(table_name).find { |c| c.name == column_name.to_s }
|
184
|
+
if existing_column
|
185
|
+
existing_type = existing_column.type.to_sym
|
186
|
+
|
187
|
+
safe =
|
188
|
+
case type
|
189
|
+
when :string
|
190
|
+
# safe to increase limit or remove it
|
191
|
+
# not safe to decrease limit or add a limit
|
192
|
+
case existing_type
|
193
|
+
when :string
|
194
|
+
!options[:limit] || (existing_column.limit && options[:limit] >= existing_column.limit)
|
195
|
+
when :text
|
196
|
+
!options[:limit]
|
197
|
+
end
|
198
|
+
when :text
|
199
|
+
# safe to change varchar to text (and text to text)
|
200
|
+
[:string, :text].include?(existing_type)
|
201
|
+
when :numeric, :decimal
|
202
|
+
# numeric and decimal are equivalent and can be used interchangably
|
203
|
+
[:numeric, :decimal].include?(existing_type) &&
|
204
|
+
(
|
205
|
+
(
|
206
|
+
# unconstrained
|
207
|
+
!options[:precision] && !options[:scale]
|
208
|
+
) || (
|
209
|
+
# increased precision, same scale
|
210
|
+
options[:precision] && existing_column.precision &&
|
211
|
+
options[:precision] >= existing_column.precision &&
|
212
|
+
options[:scale] == existing_column.scale
|
213
|
+
)
|
214
|
+
)
|
215
|
+
when :datetime, :timestamp, :timestamptz
|
216
|
+
[:timestamp, :timestamptz].include?(existing_type) &&
|
217
|
+
postgresql_version >= Gem::Version.new("12") &&
|
218
|
+
connection.select_value("SHOW timezone") == "UTC"
|
219
|
+
else
|
220
|
+
type == existing_type &&
|
221
|
+
options[:limit] == existing_column.limit &&
|
222
|
+
options[:precision] == existing_column.precision &&
|
223
|
+
options[:scale] == existing_column.scale
|
224
|
+
end
|
225
|
+
|
226
|
+
# unsafe to set NOT NULL for safe types
|
227
|
+
if safe && existing_column.null && options[:null] == false
|
228
|
+
raise_error :change_column_with_not_null
|
229
|
+
end
|
230
|
+
|
231
|
+
if !safe
|
232
|
+
raise_error :change_column,
|
233
|
+
initialize_change_code: command_str(:initialize_column_type_change, table_name, column_name, type, **options),
|
234
|
+
backfill_code: command_str(:backfill_column_for_type_change, table_name, column_name, **options),
|
235
|
+
finalize_code: command_str(:finalize_column_type_change, table_name, column_name),
|
236
|
+
cleanup_code: command_str(:cleanup_change_column_type_concurrently, table_name, column_name),
|
237
|
+
cleanup_down_code: command_str(:initialize_column_type_change, table_name, column_name, existing_type)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def change_column_null(table_name, column_name, allow_null, default = nil, **)
|
243
|
+
if !allow_null && !new_or_small_table?(table_name)
|
244
|
+
safe = false
|
245
|
+
# In PostgreSQL 12+ you can add a check constraint to the table
|
246
|
+
# and then "promote" it to NOT NULL for the column.
|
247
|
+
if postgresql_version >= Gem::Version.new("12")
|
248
|
+
safe = check_constraints(table_name).any? do |c|
|
249
|
+
c["def"] == "CHECK ((#{column_name} IS NOT NULL))" ||
|
250
|
+
c["def"] == "CHECK ((#{connection.quote_column_name(column_name)} IS NOT NULL))"
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
if !safe
|
255
|
+
constraint_name = "#{table_name}_#{column_name}_null"
|
256
|
+
vars = {
|
257
|
+
add_constraint_code: command_str(:add_not_null_constraint, table_name, column_name, name: constraint_name, validate: false),
|
258
|
+
backfill_code: nil,
|
259
|
+
validate_constraint_code: command_str(:validate_not_null_constraint, table_name, column_name, name: constraint_name),
|
260
|
+
remove_constraint_code: nil,
|
261
|
+
}
|
262
|
+
|
263
|
+
if !default.nil?
|
264
|
+
vars[:backfill_code] = command_str(:update_column_in_batches, table_name, column_name, default)
|
265
|
+
end
|
266
|
+
|
267
|
+
if postgresql_version >= Gem::Version.new("12")
|
268
|
+
vars[:remove_constraint_code] = command_str(:remove_check_constraint, table_name, name: constraint_name)
|
269
|
+
vars[:change_column_null_code] = command_str(:change_column_null, table_name, column_name, true)
|
270
|
+
end
|
271
|
+
|
272
|
+
raise_error :change_column_null, **vars
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def check_columns_removal(command, *args, **options)
|
278
|
+
case command
|
279
|
+
when :remove_column
|
280
|
+
table_name, column_name = args
|
281
|
+
columns = [column_name]
|
282
|
+
when :remove_columns
|
283
|
+
table_name, *columns = args
|
284
|
+
when :remove_timestamps
|
285
|
+
table_name = args[0]
|
286
|
+
columns = [:created_at, :updated_at]
|
287
|
+
else
|
288
|
+
table_name, reference = args
|
289
|
+
columns = [:"#{reference}_id"]
|
290
|
+
columns << :"#{reference}_type" if options[:polymorphic]
|
291
|
+
end
|
292
|
+
|
293
|
+
if !new_table?(table_name)
|
294
|
+
indexes = connection.indexes(table_name).select do |index|
|
295
|
+
(index.columns & columns.map(&:to_s)).any?
|
296
|
+
end
|
297
|
+
|
298
|
+
raise_error :remove_column,
|
299
|
+
model: table_name.to_s.classify,
|
300
|
+
columns: columns.inspect,
|
301
|
+
command: command_str(command, *args),
|
302
|
+
table_name: table_name.inspect,
|
303
|
+
indexes: indexes.map { |i| i.name.to_sym.inspect }
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def add_timestamps(table_name, **options)
|
308
|
+
volatile_default = false
|
309
|
+
if !new_or_small_table?(table_name) && !options[:default].nil? &&
|
310
|
+
(postgresql_version < Gem::Version.new("11") || (volatile_default = Utils.volatile_default?(connection, :datetime, options[:default])))
|
311
|
+
|
312
|
+
raise_error :add_timestamps_with_default,
|
313
|
+
code: [command_str(:add_column_with_default, table_name, :created_at, :datetime, options),
|
314
|
+
command_str(:add_column_with_default, table_name, :updated_at, :datetime, options)].join("\n "),
|
315
|
+
not_null: options[:null] == false,
|
316
|
+
volatile_default: volatile_default
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def add_reference(table_name, ref_name, **options)
|
321
|
+
# Always added by default in 5.0+
|
322
|
+
index = options.fetch(:index) { Utils.ar_version >= 5.0 }
|
323
|
+
|
324
|
+
if index.is_a?(Hash) && index[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
|
325
|
+
raise_error :add_hash_index
|
326
|
+
end
|
327
|
+
|
328
|
+
concurrently_set = index.is_a?(Hash) && index[:algorithm] == :concurrently
|
329
|
+
bad_index = index && !concurrently_set
|
330
|
+
|
331
|
+
foreign_key = options.fetch(:foreign_key, false)
|
332
|
+
|
333
|
+
if foreign_key
|
334
|
+
foreign_table_name = Utils.foreign_table_name(ref_name, options)
|
335
|
+
@foreign_key_tables << foreign_table_name.to_s
|
336
|
+
end
|
337
|
+
|
338
|
+
validate_foreign_key = !foreign_key.is_a?(Hash) ||
|
339
|
+
(!foreign_key.key?(:validate) || foreign_key[:validate] == true)
|
340
|
+
bad_foreign_key = foreign_key && validate_foreign_key
|
341
|
+
|
342
|
+
if !new_or_small_table?(table_name) && (bad_index || bad_foreign_key)
|
343
|
+
raise_error :add_reference,
|
344
|
+
code: command_str(:add_reference_concurrently, table_name, ref_name, **options),
|
345
|
+
bad_index: bad_index,
|
346
|
+
bad_foreign_key: bad_foreign_key
|
347
|
+
end
|
348
|
+
end
|
349
|
+
alias add_belongs_to add_reference
|
350
|
+
|
351
|
+
def add_index(table_name, column_name, **options)
|
352
|
+
if options[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
|
353
|
+
raise_error :add_hash_index
|
354
|
+
elsif options[:algorithm] != :concurrently && !new_or_small_table?(table_name)
|
355
|
+
raise_error :add_index,
|
356
|
+
command: command_str(:add_index, table_name, column_name, **options.merge(algorithm: :concurrently))
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def remove_index(table_name, column_name = nil, **options)
|
361
|
+
options[:column] ||= column_name
|
362
|
+
|
363
|
+
if options[:algorithm] != :concurrently && !new_or_small_table?(table_name)
|
364
|
+
raise_error :remove_index,
|
365
|
+
command: command_str(:remove_index, table_name, **options.merge(algorithm: :concurrently))
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def add_foreign_key(from_table, to_table, **options)
|
370
|
+
if !new_or_small_table?(from_table)
|
371
|
+
validate = options.fetch(:validate, true)
|
372
|
+
|
373
|
+
if validate
|
374
|
+
raise_error :add_foreign_key,
|
375
|
+
add_code: command_str(:add_foreign_key, from_table, to_table, **options.merge(validate: false)),
|
376
|
+
validate_code: command_str(:validate_foreign_key, from_table, to_table)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
@foreign_key_tables << to_table.to_s
|
381
|
+
end
|
382
|
+
|
383
|
+
def validate_foreign_key(*)
|
384
|
+
if crud_blocked?
|
385
|
+
raise_error :validate_foreign_key
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
def add_check_constraint(table_name, expression, **options)
|
390
|
+
if !new_or_small_table?(table_name) && options[:validate] != false
|
391
|
+
name = options[:name] || check_constraint_name(table_name, expression)
|
392
|
+
|
393
|
+
raise_error :add_check_constraint,
|
394
|
+
add_code: command_str(:add_check_constraint, table_name, expression, **options.merge(validate: false)),
|
395
|
+
validate_code: command_str(:validate_check_constraint, table_name, name: name)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
def validate_constraint(*)
|
400
|
+
if crud_blocked?
|
401
|
+
raise_error :validate_constraint
|
402
|
+
end
|
403
|
+
end
|
404
|
+
alias validate_check_constraint validate_constraint
|
405
|
+
alias validate_not_null_constraint validate_constraint
|
406
|
+
alias validate_text_limit_constraint validate_constraint
|
407
|
+
|
408
|
+
def add_not_null_constraint(table_name, column_name, **options)
|
409
|
+
if !new_or_small_table?(table_name) && options[:validate] != false
|
410
|
+
raise_error :add_not_null_constraint,
|
411
|
+
add_code: command_str(:add_not_null_constraint, table_name, column_name, **options.merge(validate: false)),
|
412
|
+
validate_code: command_str(:validate_not_null_constraint, table_name, column_name, **options.except(:validate))
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
def add_text_limit_constraint(table_name, column_name, limit, **options)
|
417
|
+
if !new_or_small_table?(table_name) && options[:validate] != false
|
418
|
+
raise_error :add_text_limit_constraint,
|
419
|
+
add_code: command_str(:add_text_limit_constraint, table_name, column_name, limit, **options.merge(validate: false)),
|
420
|
+
validate_code: command_str(:validate_text_limit_constraint, table_name, column_name, **options.except(:validate))
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
def execute(*)
|
425
|
+
raise_error :execute, header: "Possibly dangerous operation"
|
426
|
+
end
|
427
|
+
alias exec_query execute
|
428
|
+
|
429
|
+
def short_primary_key_type?(options)
|
430
|
+
pk_type =
|
431
|
+
case options[:id]
|
432
|
+
when false
|
433
|
+
nil
|
434
|
+
when Hash
|
435
|
+
options[:id][:type]
|
436
|
+
when nil
|
437
|
+
# default type is used
|
438
|
+
connection.native_database_types[:primary_key].split.first
|
439
|
+
else
|
440
|
+
options[:id]
|
441
|
+
end
|
442
|
+
|
443
|
+
pk_type && !["bigserial", "bigint", "uuid"].include?(pk_type.to_s)
|
444
|
+
end
|
445
|
+
|
446
|
+
def collect_foreign_keys(&block)
|
447
|
+
collector = ForeignKeysCollector.new
|
448
|
+
collector.collect(&block)
|
449
|
+
@foreign_key_tables |= collector.referenced_tables
|
450
|
+
end
|
451
|
+
|
452
|
+
def check_for_hash_indexes(&block)
|
453
|
+
indexes = collect_indexes(&block)
|
454
|
+
if indexes.any? { |index| index.using == "hash" }
|
455
|
+
raise_error :add_hash_index
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def collect_indexes(&block)
|
460
|
+
collector = IndexesCollector.new
|
461
|
+
collector.collect(&block)
|
462
|
+
collector.indexes
|
463
|
+
end
|
464
|
+
|
465
|
+
def new_or_small_table?(table_name)
|
466
|
+
small_tables = OnlineMigrations.config.small_tables
|
467
|
+
|
468
|
+
new_table?(table_name) ||
|
469
|
+
small_tables.include?(table_name.to_s)
|
470
|
+
end
|
471
|
+
|
472
|
+
def new_table?(table_name)
|
473
|
+
@new_tables.include?(table_name.to_s)
|
474
|
+
end
|
475
|
+
|
476
|
+
def postgresql_version
|
477
|
+
version =
|
478
|
+
if Utils.developer_env? && (target_version = OnlineMigrations.config.target_version)
|
479
|
+
target_version.to_s
|
480
|
+
else
|
481
|
+
# For rails 6.0+ we can use connection.database_version
|
482
|
+
pg_connection = connection.instance_variable_get(:@connection)
|
483
|
+
database_version = pg_connection.server_version
|
484
|
+
patch = database_version % 100
|
485
|
+
database_version /= 100
|
486
|
+
minor = database_version % 100
|
487
|
+
database_version /= 100
|
488
|
+
major = database_version
|
489
|
+
"#{major}.#{minor}.#{patch}"
|
490
|
+
end
|
491
|
+
|
492
|
+
Gem::Version.new(version)
|
493
|
+
end
|
494
|
+
|
495
|
+
def connection
|
496
|
+
@migration.connection
|
497
|
+
end
|
498
|
+
|
499
|
+
def raise_error(message_key, header: nil, **vars)
|
500
|
+
return if !OnlineMigrations.config.check_enabled?(message_key, version: version)
|
501
|
+
|
502
|
+
template = OnlineMigrations.config.error_messages.fetch(message_key)
|
503
|
+
|
504
|
+
vars[:migration_name] = @migration.name
|
505
|
+
vars[:migration_parent] = Utils.migration_parent_string
|
506
|
+
vars[:model_parent] = Utils.model_parent_string
|
507
|
+
|
508
|
+
if RUBY_VERSION >= "2.6"
|
509
|
+
message = ERB.new(template, trim_mode: "<>").result_with_hash(vars)
|
510
|
+
else
|
511
|
+
# `result_with_hash` was added in ruby 2.5
|
512
|
+
b = TOPLEVEL_BINDING.dup
|
513
|
+
vars.each_pair do |key, value|
|
514
|
+
b.local_variable_set(key, value)
|
515
|
+
end
|
516
|
+
message = ERB.new(template, nil, "<>").result(b)
|
517
|
+
end
|
518
|
+
|
519
|
+
@migration.stop!(message, header: header || "Dangerous operation detected")
|
520
|
+
end
|
521
|
+
|
522
|
+
def command_str(command, *args)
|
523
|
+
arg_list = args[0..-2].map(&:inspect)
|
524
|
+
|
525
|
+
last_arg = args.last
|
526
|
+
if last_arg.is_a?(Hash)
|
527
|
+
if last_arg.any?
|
528
|
+
arg_list << last_arg.map do |k, v|
|
529
|
+
case v
|
530
|
+
when Hash
|
531
|
+
# pretty index: { algorithm: :concurrently }
|
532
|
+
"#{k}: { #{v.map { |k2, v2| "#{k2}: #{v2.inspect}" }.join(', ')} }"
|
533
|
+
when Array, Numeric, String, Symbol, TrueClass, FalseClass
|
534
|
+
"#{k}: #{v.inspect}"
|
535
|
+
else
|
536
|
+
"<paste value here>"
|
537
|
+
end
|
538
|
+
end.join(", ")
|
539
|
+
end
|
540
|
+
else
|
541
|
+
arg_list << last_arg.inspect
|
542
|
+
end
|
543
|
+
|
544
|
+
"#{command} #{arg_list.join(', ')}"
|
545
|
+
end
|
546
|
+
|
547
|
+
def crud_blocked?
|
548
|
+
locks_query = <<~SQL
|
549
|
+
SELECT relation::regclass::text
|
550
|
+
FROM pg_locks
|
551
|
+
WHERE mode IN ('ShareLock', 'ShareRowExclusiveLock', 'ExclusiveLock', 'AccessExclusiveLock')
|
552
|
+
AND pid = pg_backend_pid()
|
553
|
+
SQL
|
554
|
+
|
555
|
+
connection.select_values(locks_query).any?
|
556
|
+
end
|
557
|
+
|
558
|
+
def check_constraint_name(table_name, expression)
|
559
|
+
identifier = "#{table_name}_#{expression}_chk"
|
560
|
+
hashed_identifier = OpenSSL::Digest::SHA256.hexdigest(identifier).first(10)
|
561
|
+
|
562
|
+
"chk_rails_#{hashed_identifier}"
|
563
|
+
end
|
564
|
+
|
565
|
+
def check_constraints(table_name)
|
566
|
+
constraints_query = <<~SQL
|
567
|
+
SELECT pg_get_constraintdef(oid) AS def
|
568
|
+
FROM pg_constraint
|
569
|
+
WHERE contype = 'c'
|
570
|
+
AND convalidated
|
571
|
+
AND conrelid = #{connection.quote(table_name)}::regclass
|
572
|
+
SQL
|
573
|
+
|
574
|
+
connection.select_all(constraints_query).to_a
|
575
|
+
end
|
576
|
+
|
577
|
+
# From ActiveRecord
|
578
|
+
def derive_join_table_name(table1, table2)
|
579
|
+
[table1.to_s, table2.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
|
580
|
+
end
|
581
|
+
|
582
|
+
def run_custom_checks(method, args)
|
583
|
+
OnlineMigrations.config.checks.each do |options, check|
|
584
|
+
if !options[:start_after] || version > options[:start_after]
|
585
|
+
@migration.instance_exec(method, args, &check)
|
586
|
+
end
|
587
|
+
end
|
588
|
+
end
|
589
|
+
end
|
590
|
+
end
|