strong_migrations 0.7.7 → 2.2.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.
@@ -1,602 +1,277 @@
1
1
  module StrongMigrations
2
2
  class Checker
3
+ include Checks
3
4
  include SafeMethods
4
5
 
5
- attr_accessor :direction, :transaction_disabled
6
+ attr_accessor :direction, :transaction_disabled, :timeouts_set
7
+
8
+ class << self
9
+ attr_accessor :safe
10
+ end
6
11
 
7
12
  def initialize(migration)
8
13
  @migration = migration
14
+ reset
15
+ end
16
+
17
+ def reset
9
18
  @new_tables = []
10
- @safe = false
19
+ @new_columns = []
11
20
  @timeouts_set = false
12
- @lock_timeout_checked = false
21
+ @committed = false
22
+ @transaction_disabled = false
23
+ @skip_retries = false
13
24
  end
14
25
 
15
- def safety_assured
16
- previous_value = @safe
26
+ def self.safety_assured
27
+ previous_value = safe
17
28
  begin
18
- @safe = true
29
+ self.safe = true
19
30
  yield
20
31
  ensure
21
- @safe = previous_value
32
+ self.safe = previous_value
22
33
  end
23
34
  end
24
35
 
25
- def perform(method, *args)
36
+ def perform(method, *args, &block)
37
+ return yield if skip?
38
+
39
+ check_adapter
40
+ check_version_supported
26
41
  set_timeouts
27
42
  check_lock_timeout
28
43
 
29
44
  if !safe? || safe_by_default_method?(method)
45
+ # TODO better pattern
46
+ # see checks.rb for methods
30
47
  case method
31
- when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
32
- columns =
33
- case method
34
- when :remove_timestamps
35
- ["created_at", "updated_at"]
36
- when :remove_column
37
- [args[1].to_s]
38
- when :remove_columns
39
- args[1..-1].map(&:to_s)
40
- else
41
- options = args[2] || {}
42
- reference = args[1]
43
- cols = []
44
- cols << "#{reference}_type" if options[:polymorphic]
45
- cols << "#{reference}_id"
46
- cols
47
- end
48
-
49
- code = "self.ignored_columns = #{columns.inspect}"
50
-
51
- raise_error :remove_column,
52
- model: args[0].to_s.classify,
53
- code: code,
54
- command: command_str(method, args),
55
- column_suffix: columns.size > 1 ? "s" : ""
56
- when :change_table
57
- raise_error :change_table, header: "Possibly dangerous operation"
58
- when :rename_table
59
- raise_error :rename_table
60
- when :rename_column
61
- raise_error :rename_column
62
- when :add_index
63
- table, columns, options = args
64
- options ||= {}
65
-
66
- if columns.is_a?(Array) && columns.size > 3 && !options[:unique]
67
- raise_error :add_index_columns, header: "Best practice"
68
- end
69
- if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
70
- return safe_add_index(table, columns, options) if StrongMigrations.safe_by_default
71
- raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)])
72
- end
73
- when :remove_index
74
- table, options = args
75
- unless options.is_a?(Hash)
76
- options = {column: options}
77
- end
78
- options ||= {}
79
-
80
- if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
81
- return safe_remove_index(table, options) if StrongMigrations.safe_by_default
82
- raise_error :remove_index, command: command_str("remove_index", [table, options.merge(algorithm: :concurrently)])
83
- end
48
+ when :add_check_constraint
49
+ check_add_check_constraint(*args)
84
50
  when :add_column
85
- table, column, type, options = args
86
- options ||= {}
87
- default = options[:default]
88
-
89
- if !default.nil? && !((postgresql? && postgresql_version >= Gem::Version.new("11")) || (mysql? && mysql_version >= Gem::Version.new("8.0.12")) || (mariadb? && mariadb_version >= Gem::Version.new("10.3.2")))
90
-
91
- if options[:null] == false
92
- options = options.except(:null)
93
- append = "
94
-
95
- Then add the NOT NULL constraint in separate migrations."
96
- end
97
-
98
- raise_error :add_column_default,
99
- add_command: command_str("add_column", [table, column, type, options.except(:default)]),
100
- change_command: command_str("change_column_default", [table, column, default]),
101
- remove_command: command_str("remove_column", [table, column]),
102
- code: backfill_code(table, column, default),
103
- append: append,
104
- rewrite_blocks: rewrite_blocks
105
- end
106
-
107
- if type.to_s == "json" && postgresql?
108
- raise_error :add_column_json,
109
- command: command_str("add_column", [table, column, :jsonb, options])
110
- end
51
+ check_add_column(*args)
52
+ when :add_exclusion_constraint
53
+ check_add_exclusion_constraint(*args)
54
+ when :add_foreign_key
55
+ check_add_foreign_key(*args)
56
+ when :add_index
57
+ check_add_index(*args)
58
+ when :add_reference, :add_belongs_to
59
+ check_add_reference(method, *args)
60
+ when :add_unique_constraint
61
+ check_add_unique_constraint(*args)
111
62
  when :change_column
112
- table, column, type, options = args
113
- options ||= {}
114
-
115
- safe = false
116
- existing_column = connection.columns(table).find { |c| c.name.to_s == column.to_s }
117
- if existing_column
118
- existing_type = existing_column.sql_type.split("(").first
119
- if postgresql?
120
- case type.to_s
121
- when "string"
122
- # safe to increase limit or remove it
123
- # not safe to decrease limit or add a limit
124
- case existing_type
125
- when "character varying"
126
- safe = !options[:limit] || (existing_column.limit && options[:limit] >= existing_column.limit)
127
- when "text"
128
- safe = !options[:limit]
129
- end
130
- when "text"
131
- # safe to change varchar to text (and text to text)
132
- safe = ["character varying", "text"].include?(existing_type)
133
- when "numeric", "decimal"
134
- # numeric and decimal are equivalent and can be used interchangably
135
- safe = ["numeric", "decimal"].include?(existing_type) &&
136
- (
137
- (
138
- # unconstrained
139
- !options[:precision] && !options[:scale]
140
- ) || (
141
- # increased precision, same scale
142
- options[:precision] && existing_column.precision &&
143
- options[:precision] >= existing_column.precision &&
144
- options[:scale] == existing_column.scale
145
- )
146
- )
147
- when "datetime", "timestamp", "timestamptz"
148
- safe = ["timestamp without time zone", "timestamp with time zone"].include?(existing_type) &&
149
- postgresql_version >= Gem::Version.new("12") &&
150
- connection.select_all("SHOW timezone").first["TimeZone"] == "UTC"
151
- end
152
- elsif mysql? || mariadb?
153
- case type.to_s
154
- when "string"
155
- # https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-operations.html
156
- # https://mariadb.com/kb/en/innodb-online-ddl-operations-with-the-instant-alter-algorithm/#changing-the-data-type-of-a-column
157
- # increased limit, but doesn't change number of length bytes
158
- # 1-255 = 1 byte, 256-65532 = 2 bytes, 65533+ = too big for varchar
159
- limit = options[:limit] || 255
160
- safe = ["varchar"].include?(existing_type) &&
161
- limit >= existing_column.limit &&
162
- (limit <= 255 || existing_column.limit > 255)
163
- end
164
- end
165
- end
166
-
167
- # unsafe to set NOT NULL for safe types
168
- if safe && existing_column.null && options[:null] == false
169
- raise_error :change_column_with_not_null
170
- end
171
-
172
- raise_error :change_column, rewrite_blocks: rewrite_blocks unless safe
63
+ check_change_column(*args)
64
+ when :change_column_default
65
+ check_change_column_default(*args)
66
+ when :change_column_null
67
+ check_change_column_null(*args)
68
+ when :change_table
69
+ check_change_table
70
+ when :create_join_table
71
+ check_create_join_table(*args)
173
72
  when :create_table
174
- table, options = args
175
- options ||= {}
176
-
177
- raise_error :create_table if options[:force]
178
-
179
- # keep track of new tables of add_index check
180
- @new_tables << table.to_s
181
- when :add_reference, :add_belongs_to
182
- table, reference, options = args
183
- options ||= {}
184
-
185
- if postgresql?
186
- index_value = options.fetch(:index, true)
187
- concurrently_set = index_value.is_a?(Hash) && index_value[:algorithm] == :concurrently
188
- bad_index = index_value && !concurrently_set
189
-
190
- if bad_index || options[:foreign_key]
191
- if index_value.is_a?(Hash)
192
- options[:index] = options[:index].merge(algorithm: :concurrently)
193
- else
194
- options = options.merge(index: {algorithm: :concurrently})
195
- end
196
-
197
- return safe_add_reference(table, reference, options) if StrongMigrations.safe_by_default
198
-
199
- if options.delete(:foreign_key)
200
- headline = "Adding a foreign key blocks writes on both tables."
201
- append = "
202
-
203
- Then add the foreign key in separate migrations."
204
- else
205
- headline = "Adding an index non-concurrently locks the table."
206
- end
207
-
208
- raise_error :add_reference,
209
- headline: headline,
210
- command: command_str(method, [table, reference, options]),
211
- append: append
212
- end
213
- end
73
+ check_create_table(*args)
214
74
  when :execute
215
- raise_error :execute, header: "Possibly dangerous operation"
216
- when :change_column_null
217
- table, column, null, default = args
218
- if !null
219
- if postgresql?
220
- safe = false
221
- if postgresql_version >= Gem::Version.new("12")
222
- safe = constraints(table).any? { |c| c["def"] == "CHECK ((#{column} IS NOT NULL))" || c["def"] == "CHECK ((#{connection.quote_column_name(column)} IS NOT NULL))" }
223
- end
224
-
225
- unless safe
226
- # match https://github.com/nullobject/rein
227
- constraint_name = "#{table}_#{column}_null"
228
-
229
- add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column])
230
- validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
231
- remove_code = constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])
232
-
233
- validate_constraint_code =
234
- if ar_version >= 6.1
235
- String.new(command_str(:validate_check_constraint, [table, {name: constraint_name}]))
236
- else
237
- String.new(safety_assured_str(validate_code))
238
- end
239
-
240
- if postgresql_version >= Gem::Version.new("12")
241
- change_args = [table, column, null]
242
-
243
- validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}"
244
-
245
- if ar_version >= 6.1
246
- validate_constraint_code << "\n #{command_str(:remove_check_constraint, [table, {name: constraint_name}])}"
247
- else
248
- validate_constraint_code << "\n #{safety_assured_str(remove_code)}"
249
- end
250
- end
251
-
252
- return safe_change_column_null(add_code, validate_code, change_args, remove_code) if StrongMigrations.safe_by_default
253
-
254
- add_constraint_code =
255
- if ar_version >= 6.1
256
- # only quote when needed
257
- expr_column = column.to_s =~ /\A[a-z0-9_]+\z/ ? column : connection.quote_column_name(column)
258
- command_str(:add_check_constraint, [table, "#{expr_column} IS NOT NULL", {name: constraint_name, validate: false}])
259
- else
260
- safety_assured_str(add_code)
261
- end
262
-
263
- raise_error :change_column_null_postgresql,
264
- add_constraint_code: add_constraint_code,
265
- validate_constraint_code: validate_constraint_code
266
- end
267
- elsif mysql? || mariadb?
268
- raise_error :change_column_null_mysql
269
- elsif !default.nil?
270
- raise_error :change_column_null,
271
- code: backfill_code(table, column, default)
272
- end
273
- end
274
- when :add_foreign_key
275
- from_table, to_table, options = args
276
- options ||= {}
277
-
278
- # always validated before 5.2
279
- validate = options.fetch(:validate, true) || ar_version < 5.2
280
-
281
- if postgresql? && validate
282
- if ar_version < 5.2
283
- # fk name logic from rails
284
- primary_key = options[:primary_key] || "id"
285
- column = options[:column] || "#{to_table.to_s.singularize}_id"
286
- hashed_identifier = Digest::SHA256.hexdigest("#{from_table}_#{column}_fk").first(10)
287
- fk_name = options[:name] || "fk_rails_#{hashed_identifier}"
288
-
289
- add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) NOT VALID", [from_table, fk_name, column, to_table, primary_key])
290
- validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name])
291
-
292
- return safe_add_foreign_key_code(from_table, to_table, add_code, validate_code) if StrongMigrations.safe_by_default
293
-
294
- raise_error :add_foreign_key,
295
- add_foreign_key_code: safety_assured_str(add_code),
296
- validate_foreign_key_code: safety_assured_str(validate_code)
297
- else
298
- return safe_add_foreign_key(from_table, to_table, options) if StrongMigrations.safe_by_default
299
-
300
- raise_error :add_foreign_key,
301
- add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
302
- validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
303
- end
304
- end
305
- when :validate_foreign_key
306
- if postgresql? && writes_blocked?
307
- raise_error :validate_foreign_key
308
- end
309
- when :add_check_constraint
310
- table, expression, options = args
311
- options ||= {}
312
-
313
- if !new_table?(table)
314
- if postgresql? && options[:validate] != false
315
- add_options = options.merge(validate: false)
316
- name = options[:name] || @migration.check_constraint_options(table, expression, options)[:name]
317
- validate_options = {name: name}
318
-
319
- return safe_add_check_constraint(table, expression, add_options, validate_options) if StrongMigrations.safe_by_default
320
-
321
- raise_error :add_check_constraint,
322
- add_check_constraint_code: command_str("add_check_constraint", [table, expression, add_options]),
323
- validate_check_constraint_code: command_str("validate_check_constraint", [table, validate_options])
324
- elsif mysql? || mariadb?
325
- raise_error :add_check_constraint_mysql
326
- end
327
- end
75
+ check_execute
76
+ when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
77
+ check_remove_column(method, *args)
78
+ when :remove_index
79
+ check_remove_index(*args)
80
+ when :rename_column
81
+ check_rename_column
82
+ when :rename_table
83
+ check_rename_table
328
84
  when :validate_check_constraint
329
- if postgresql? && writes_blocked?
330
- raise_error :validate_check_constraint
331
- end
85
+ check_validate_check_constraint
86
+ when :validate_foreign_key
87
+ check_validate_foreign_key
88
+ when :commit_db_transaction
89
+ # if committed, likely no longer in DDL transaction
90
+ # and no longer eligible to be retried at migration level
91
+ # okay to have false positives
92
+ @committed = true
332
93
  end
333
94
 
334
- StrongMigrations.checks.each do |check|
335
- @migration.instance_exec(method, args, &check)
95
+ if !safe?
96
+ # custom checks
97
+ StrongMigrations.checks.each do |check|
98
+ @migration.instance_exec(method, args, &check)
99
+ end
336
100
  end
337
101
  end
338
102
 
339
- result = yield
103
+ result =
104
+ if retry_lock_timeouts?(method)
105
+ # TODO figure out how to handle methods that generate multiple statements
106
+ # like add_reference(table, ref, index: {algorithm: :concurrently})
107
+ # lock timeout after first statement will cause retry to fail
108
+ retry_lock_timeouts { perform_method(method, *args, &block) }
109
+ else
110
+ perform_method(method, *args, &block)
111
+ end
340
112
 
341
113
  # outdated statistics + a new index can hurt performance of existing queries
342
114
  if StrongMigrations.auto_analyze && direction == :up && method == :add_index
343
- analyze_table(args[0])
115
+ adapter.analyze_table(args[0])
344
116
  end
345
117
 
346
118
  result
347
119
  end
348
120
 
349
- private
350
-
351
- def set_timeouts
352
- if !@timeouts_set
353
- if StrongMigrations.statement_timeout
354
- statement =
355
- if postgresql?
356
- "SET statement_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.statement_timeout))}"
357
- elsif mysql?
358
- # use ceil to prevent no timeout for values under 1 ms
359
- "SET max_execution_time = #{connection.quote((StrongMigrations.statement_timeout.to_f * 1000).ceil)}"
360
- elsif mariadb?
361
- "SET max_statement_time = #{connection.quote(StrongMigrations.statement_timeout)}"
362
- else
363
- raise StrongMigrations::Error, "Statement timeout not supported for this database"
364
- end
365
-
366
- connection.select_all(statement)
367
- end
368
-
369
- if StrongMigrations.lock_timeout
370
- statement =
371
- if postgresql?
372
- "SET lock_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.lock_timeout))}"
373
- elsif mysql? || mariadb?
374
- "SET lock_wait_timeout = #{connection.quote(StrongMigrations.lock_timeout)}"
375
- else
376
- raise StrongMigrations::Error, "Lock timeout not supported for this database"
377
- end
378
-
379
- connection.select_all(statement)
380
- end
381
-
382
- @timeouts_set = true
121
+ def perform_method(method, *args)
122
+ if StrongMigrations.remove_invalid_indexes && direction == :up && method == :add_index && postgresql?
123
+ remove_invalid_index_if_needed(*args)
383
124
  end
125
+ yield
384
126
  end
385
127
 
386
- def connection
387
- @migration.connection
388
- end
389
-
390
- def version
391
- @migration.version
392
- end
393
-
394
- def safe?
395
- @safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe?
128
+ def retry_lock_timeouts(check_committed: false)
129
+ retries = 0
130
+ begin
131
+ yield
132
+ rescue ActiveRecord::LockWaitTimeout => e
133
+ if retries < StrongMigrations.lock_timeout_retries && !(check_committed && @committed)
134
+ retries += 1
135
+ delay = StrongMigrations.lock_timeout_retry_delay
136
+ @migration.say("Lock timeout. Retrying in #{delay} seconds...")
137
+ sleep(delay)
138
+ retry
139
+ end
140
+ raise e
141
+ end
396
142
  end
397
143
 
398
144
  def version_safe?
399
145
  version && version <= StrongMigrations.start_after
400
146
  end
401
147
 
402
- def postgresql?
403
- connection.adapter_name =~ /postg/i # PostgreSQL, PostGIS
148
+ def skip?
149
+ StrongMigrations.skipped_databases.map(&:to_s).include?(db_config_name)
404
150
  end
405
151
 
406
- def postgresql_version
407
- @postgresql_version ||= begin
408
- target_version(StrongMigrations.target_postgresql_version) do
409
- # only works with major versions
410
- connection.select_all("SHOW server_version_num").first["server_version_num"].to_i / 10000
411
- end
412
- end
413
- end
152
+ private
414
153
 
415
- def mysql?
416
- connection.adapter_name =~ /mysql/i && !connection.try(:mariadb?)
417
- end
154
+ def check_adapter
155
+ return if defined?(@adapter_checked)
418
156
 
419
- def mysql_version
420
- @mysql_version ||= begin
421
- target_version(StrongMigrations.target_mysql_version) do
422
- connection.select_all("SELECT VERSION()").first["VERSION()"].split("-").first
423
- end
157
+ if adapter.instance_of?(Adapters::AbstractAdapter)
158
+ warn "[strong_migrations] Unsupported adapter: #{connection.adapter_name}. Use StrongMigrations.skip_database(#{db_config_name.to_sym.inspect}) to silence this warning."
424
159
  end
425
- end
426
160
 
427
- def mariadb?
428
- connection.adapter_name =~ /mysql/i && connection.try(:mariadb?)
161
+ @adapter_checked = true
429
162
  end
430
163
 
431
- def mariadb_version
432
- @mariadb_version ||= begin
433
- target_version(StrongMigrations.target_mariadb_version) do
434
- connection.select_all("SELECT VERSION()").first["VERSION()"].split("-").first
435
- end
436
- end
437
- end
164
+ def check_version_supported
165
+ return if defined?(@version_checked)
438
166
 
439
- def target_version(target_version)
440
- target_version ||= StrongMigrations.target_version
441
- version =
442
- if target_version && StrongMigrations.developer_env?
443
- target_version.to_s
444
- else
445
- yield
167
+ min_version = adapter.min_version
168
+ if min_version
169
+ version = adapter.server_version
170
+ if version < Gem::Version.new(min_version)
171
+ raise UnsupportedVersion, "#{adapter.name} version (#{version}) not supported in this version of Strong Migrations (#{StrongMigrations::VERSION})"
446
172
  end
447
- Gem::Version.new(version)
448
- end
449
-
450
- def ar_version
451
- ActiveRecord::VERSION::STRING.to_f
452
- end
453
-
454
- def check_lock_timeout
455
- limit = StrongMigrations.lock_timeout_limit
456
-
457
- if limit && !@lock_timeout_checked
458
- if postgresql?
459
- lock_timeout = connection.select_all("SHOW lock_timeout").first["lock_timeout"]
460
- lock_timeout_sec = timeout_to_sec(lock_timeout)
461
- if lock_timeout_sec == 0
462
- warn "[strong_migrations] DANGER: No lock timeout set"
463
- elsif lock_timeout_sec > limit
464
- warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
465
- end
466
- elsif mysql? || mariadb?
467
- lock_timeout = connection.select_all("SHOW VARIABLES LIKE 'lock_wait_timeout'").first["Value"]
468
- # lock timeout is an integer
469
- if lock_timeout.to_i > limit
470
- warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
471
- end
472
- end
473
- @lock_timeout_checked = true
474
173
  end
475
- end
476
174
 
477
- # units: https://www.postgresql.org/docs/current/config-setting.html
478
- def timeout_to_sec(timeout)
479
- units = {
480
- "us" => 0.001,
481
- "ms" => 1,
482
- "s" => 1000,
483
- "min" => 1000 * 60,
484
- "h" => 1000 * 60 * 60,
485
- "d" => 1000 * 60 * 60 * 24
486
- }
487
- timeout_ms = timeout.to_i
488
- units.each do |k, v|
489
- if timeout.end_with?(k)
490
- timeout_ms *= v
491
- break
492
- end
493
- end
494
- timeout_ms / 1000.0
175
+ @version_checked = true
495
176
  end
496
177
 
497
- def postgresql_timeout(timeout)
498
- if timeout.is_a?(String)
499
- timeout
500
- else
501
- # use ceil to prevent no timeout for values under 1 ms
502
- (timeout.to_f * 1000).ceil
503
- end
504
- end
178
+ def set_timeouts
179
+ return if @timeouts_set
505
180
 
506
- def analyze_table(table)
507
- if postgresql?
508
- connection.execute "ANALYZE #{connection.quote_table_name(table.to_s)}"
509
- elsif mariadb? || mysql?
510
- connection.execute "ANALYZE TABLE #{connection.quote_table_name(table.to_s)}"
181
+ if StrongMigrations.statement_timeout
182
+ adapter.set_statement_timeout(StrongMigrations.statement_timeout)
183
+ end
184
+ if StrongMigrations.lock_timeout
185
+ adapter.set_lock_timeout(StrongMigrations.lock_timeout)
511
186
  end
512
- end
513
187
 
514
- def constraints(table_name)
515
- query = <<~SQL
516
- SELECT
517
- conname AS name,
518
- pg_get_constraintdef(oid) AS def
519
- FROM
520
- pg_constraint
521
- WHERE
522
- contype = 'c' AND
523
- convalidated AND
524
- conrelid = #{connection.quote(connection.quote_table_name(table_name))}::regclass
525
- SQL
526
- connection.select_all(query.squish).to_a
188
+ @timeouts_set = true
527
189
  end
528
190
 
529
- def raise_error(message_key, header: nil, append: nil, **vars)
530
- return unless StrongMigrations.check_enabled?(message_key, version: version)
531
-
532
- message = StrongMigrations.error_messages[message_key] || "Missing message"
533
- message = message + append if append
191
+ def check_lock_timeout
192
+ return if defined?(@lock_timeout_checked)
534
193
 
535
- vars[:migration_name] = @migration.class.name
536
- vars[:migration_suffix] = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
537
- vars[:base_model] = "ApplicationRecord"
194
+ if StrongMigrations.lock_timeout_limit
195
+ adapter.check_lock_timeout(StrongMigrations.lock_timeout_limit)
196
+ end
538
197
 
539
- # escape % not followed by {
540
- message = message.gsub(/%(?!{)/, "%%") % vars if message.include?("%")
541
- @migration.stop!(message, header: header || "Dangerous operation detected")
198
+ @lock_timeout_checked = true
542
199
  end
543
200
 
544
- def constraint_str(statement, identifiers)
545
- # not all identifiers are tables, but this method of quoting should be fine
546
- statement % identifiers.map { |v| connection.quote_table_name(v) }
201
+ def safe?
202
+ self.class.safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe? || @migration.reverting?
547
203
  end
548
204
 
549
- def safety_assured_str(code)
550
- "safety_assured do\n execute '#{code}' \n end"
205
+ def version
206
+ @migration.version
551
207
  end
552
208
 
553
- def command_str(command, args)
554
- str_args = args[0..-2].map { |a| a.inspect }
555
-
556
- # prettier last arg
557
- last_arg = args[-1]
558
- if last_arg.is_a?(Hash)
559
- if last_arg.any?
560
- str_args << last_arg.map do |k, v|
561
- if v.is_a?(Hash)
562
- # pretty index: {algorithm: :concurrently}
563
- "#{k}: {#{v.map { |k2, v2| "#{k2}: #{v2.inspect}" }.join(", ")}}"
209
+ def adapter
210
+ @adapter ||= begin
211
+ cls =
212
+ case connection.adapter_name
213
+ when /postg/i # PostgreSQL, PostGIS
214
+ Adapters::PostgreSQLAdapter
215
+ when /mysql|trilogy/i
216
+ if connection.try(:mariadb?)
217
+ Adapters::MariaDBAdapter
564
218
  else
565
- "#{k}: #{v.inspect}"
219
+ Adapters::MySQLAdapter
566
220
  end
567
- end.join(", ")
568
- end
569
- else
570
- str_args << last_arg.inspect
221
+ else
222
+ Adapters::AbstractAdapter
223
+ end
224
+
225
+ cls.new(self)
571
226
  end
227
+ end
572
228
 
573
- "#{command} #{str_args.join(", ")}"
229
+ def connection
230
+ @migration.connection
574
231
  end
575
232
 
576
- def writes_blocked?
577
- query = <<~SQL
578
- SELECT
579
- relation::regclass::text
580
- FROM
581
- pg_locks
582
- WHERE
583
- mode IN ('ShareRowExclusiveLock', 'AccessExclusiveLock') AND
584
- pid = pg_backend_pid()
585
- SQL
586
- connection.select_all(query.squish).any?
233
+ def db_config_name
234
+ connection.pool.db_config.name
587
235
  end
588
236
 
589
- def rewrite_blocks
590
- mysql? || mariadb? ? "writes" : "reads and writes"
237
+ def retry_lock_timeouts?(method)
238
+ (
239
+ StrongMigrations.lock_timeout_retries > 0 &&
240
+ !in_transaction? &&
241
+ method != :transaction &&
242
+ !@skip_retries
243
+ )
591
244
  end
592
245
 
593
- def backfill_code(table, column, default)
594
- model = table.to_s.classify
595
- "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
246
+ def without_retries
247
+ previous_value = @skip_retries
248
+ begin
249
+ @skip_retries = true
250
+ yield
251
+ ensure
252
+ @skip_retries = previous_value
253
+ end
596
254
  end
597
255
 
598
- def new_table?(table)
599
- @new_tables.include?(table.to_s)
256
+ # REINDEX INDEX CONCURRENTLY leaves a new invalid index if it fails, so use remove_index instead
257
+ def remove_invalid_index_if_needed(*args)
258
+ options = args.extract_options!
259
+
260
+ # ensures has same options as existing index
261
+ # check args to avoid errors with index_exists?
262
+ return unless args.size == 2 && connection.index_exists?(*args, **options.merge(valid: false))
263
+
264
+ table, columns = args
265
+ index_name = options.fetch(:name, connection.index_name(table, columns))
266
+
267
+ # valid option is ignored for Active Record < 7.1, so check name as well
268
+ return if ar_version < 7.1 && !adapter.index_invalid?(table, index_name)
269
+
270
+ @migration.say("Attempting to remove invalid index")
271
+ without_retries do
272
+ # TODO pass index schema for extra safety?
273
+ @migration.remove_index(table, **{name: index_name}.merge(options.slice(:algorithm)))
274
+ end
600
275
  end
601
276
  end
602
277
  end