strong_migrations 0.7.6 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,599 +1,211 @@
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
9
14
  @new_tables = []
10
- @safe = false
15
+ @new_columns = []
11
16
  @timeouts_set = false
12
- @lock_timeout_checked = false
17
+ @committed = false
13
18
  end
14
19
 
15
- def safety_assured
16
- previous_value = @safe
20
+ def self.safety_assured
21
+ previous_value = safe
17
22
  begin
18
- @safe = true
23
+ self.safe = true
19
24
  yield
20
25
  ensure
21
- @safe = previous_value
26
+ self.safe = previous_value
22
27
  end
23
28
  end
24
29
 
25
30
  def perform(method, *args)
31
+ check_version_supported
26
32
  set_timeouts
27
33
  check_lock_timeout
28
34
 
29
35
  if !safe? || safe_by_default_method?(method)
36
+ # TODO better pattern
37
+ # see checks.rb for methods
30
38
  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
39
+ when :add_check_constraint
40
+ check_add_check_constraint(*args)
84
41
  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
42
+ check_add_column(*args)
43
+ when :add_exclusion_constraint
44
+ check_add_exclusion_constraint(*args)
45
+ when :add_foreign_key
46
+ check_add_foreign_key(*args)
47
+ when :add_index
48
+ check_add_index(*args)
49
+ when :add_reference, :add_belongs_to
50
+ check_add_reference(method, *args)
51
+ when :add_unique_constraint
52
+ check_add_unique_constraint(*args)
111
53
  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
54
+ check_change_column(*args)
55
+ when :change_column_default
56
+ check_change_column_default(*args)
57
+ when :change_column_null
58
+ check_change_column_null(*args)
59
+ when :change_table
60
+ check_change_table
61
+ when :create_join_table
62
+ check_create_join_table(*args)
173
63
  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
64
+ check_create_table(*args)
214
65
  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
66
+ check_execute
67
+ when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
68
+ check_remove_column(method, *args)
69
+ when :remove_index
70
+ check_remove_index(*args)
71
+ when :rename_column
72
+ check_rename_column
73
+ when :rename_table
74
+ check_rename_table
328
75
  when :validate_check_constraint
329
- if postgresql? && writes_blocked?
330
- raise_error :validate_check_constraint
331
- end
76
+ check_validate_check_constraint
77
+ when :validate_foreign_key
78
+ check_validate_foreign_key
79
+ when :commit_db_transaction
80
+ # if committed, likely no longer in DDL transaction
81
+ # and no longer eligible to be retried at migration level
82
+ # okay to have false positives
83
+ @committed = true
332
84
  end
333
85
 
334
- StrongMigrations.checks.each do |check|
335
- @migration.instance_exec(method, args, &check)
86
+ if !safe?
87
+ # custom checks
88
+ StrongMigrations.checks.each do |check|
89
+ @migration.instance_exec(method, args, &check)
90
+ end
336
91
  end
337
92
  end
338
93
 
339
- result = yield
94
+ result =
95
+ if retry_lock_timeouts?(method)
96
+ # TODO figure out how to handle methods that generate multiple statements
97
+ # like add_reference(table, ref, index: {algorithm: :concurrently})
98
+ # lock timeout after first statement will cause retry to fail
99
+ retry_lock_timeouts { yield }
100
+ else
101
+ yield
102
+ end
340
103
 
341
104
  # outdated statistics + a new index can hurt performance of existing queries
342
105
  if StrongMigrations.auto_analyze && direction == :up && method == :add_index
343
- if postgresql?
344
- connection.execute "ANALYZE #{connection.quote_table_name(args[0].to_s)}"
345
- elsif mariadb? || mysql?
346
- connection.execute "ANALYZE TABLE #{connection.quote_table_name(args[0].to_s)}"
347
- end
106
+ adapter.analyze_table(args[0])
348
107
  end
349
108
 
350
109
  result
351
110
  end
352
111
 
353
- private
354
-
355
- def set_timeouts
356
- if !@timeouts_set
357
- if StrongMigrations.statement_timeout
358
- statement =
359
- if postgresql?
360
- "SET statement_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.statement_timeout))}"
361
- elsif mysql?
362
- # use ceil to prevent no timeout for values under 1 ms
363
- "SET max_execution_time = #{connection.quote((StrongMigrations.statement_timeout.to_f * 1000).ceil)}"
364
- elsif mariadb?
365
- "SET max_statement_time = #{connection.quote(StrongMigrations.statement_timeout)}"
366
- else
367
- raise StrongMigrations::Error, "Statement timeout not supported for this database"
368
- end
369
-
370
- connection.select_all(statement)
371
- end
372
-
373
- if StrongMigrations.lock_timeout
374
- statement =
375
- if postgresql?
376
- "SET lock_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.lock_timeout))}"
377
- elsif mysql? || mariadb?
378
- "SET lock_wait_timeout = #{connection.quote(StrongMigrations.lock_timeout)}"
379
- else
380
- raise StrongMigrations::Error, "Lock timeout not supported for this database"
381
- end
382
-
383
- connection.select_all(statement)
112
+ def retry_lock_timeouts(check_committed: false)
113
+ retries = 0
114
+ begin
115
+ yield
116
+ rescue ActiveRecord::LockWaitTimeout => e
117
+ if retries < StrongMigrations.lock_timeout_retries && !(check_committed && @committed)
118
+ retries += 1
119
+ delay = StrongMigrations.lock_timeout_retry_delay
120
+ @migration.say("Lock timeout. Retrying in #{delay} seconds...")
121
+ sleep(delay)
122
+ retry
384
123
  end
385
-
386
- @timeouts_set = true
124
+ raise e
387
125
  end
388
126
  end
389
127
 
390
- def connection
391
- @migration.connection
392
- end
393
-
394
- def version
395
- @migration.version
396
- end
397
-
398
- def safe?
399
- @safe || ENV["SAFETY_ASSURED"] || @migration.is_a?(ActiveRecord::Schema) ||
400
- (direction == :down && !StrongMigrations.check_down) || version_safe?
401
- end
402
-
403
128
  def version_safe?
404
129
  version && version <= StrongMigrations.start_after
405
130
  end
406
131
 
407
- def postgresql?
408
- connection.adapter_name =~ /postg/i # PostgreSQL, PostGIS
409
- end
410
-
411
- def postgresql_version
412
- @postgresql_version ||= begin
413
- target_version(StrongMigrations.target_postgresql_version) do
414
- # only works with major versions
415
- connection.select_all("SHOW server_version_num").first["server_version_num"].to_i / 10000
416
- end
417
- end
418
- end
419
-
420
- def mysql?
421
- connection.adapter_name =~ /mysql/i && !connection.try(:mariadb?)
422
- end
423
-
424
- def mysql_version
425
- @mysql_version ||= begin
426
- target_version(StrongMigrations.target_mysql_version) do
427
- connection.select_all("SELECT VERSION()").first["VERSION()"].split("-").first
428
- end
429
- end
430
- end
132
+ private
431
133
 
432
- def mariadb?
433
- connection.adapter_name =~ /mysql/i && connection.try(:mariadb?)
434
- end
134
+ def check_version_supported
135
+ return if defined?(@version_checked)
435
136
 
436
- def mariadb_version
437
- @mariadb_version ||= begin
438
- target_version(StrongMigrations.target_mariadb_version) do
439
- connection.select_all("SELECT VERSION()").first["VERSION()"].split("-").first
137
+ min_version = adapter.min_version
138
+ if min_version
139
+ version = adapter.server_version
140
+ if version < Gem::Version.new(min_version)
141
+ raise UnsupportedVersion, "#{adapter.name} version (#{version}) not supported in this version of Strong Migrations (#{StrongMigrations::VERSION})"
440
142
  end
441
143
  end
442
- end
443
-
444
- def target_version(target_version)
445
- target_version ||= StrongMigrations.target_version
446
- version =
447
- if target_version && StrongMigrations.developer_env?
448
- target_version.to_s
449
- else
450
- yield
451
- end
452
- Gem::Version.new(version)
453
- end
454
144
 
455
- def ar_version
456
- ActiveRecord::VERSION::STRING.to_f
145
+ @version_checked = true
457
146
  end
458
147
 
459
- def check_lock_timeout
460
- limit = StrongMigrations.lock_timeout_limit
461
-
462
- if limit && !@lock_timeout_checked
463
- if postgresql?
464
- lock_timeout = connection.select_all("SHOW lock_timeout").first["lock_timeout"]
465
- lock_timeout_sec = timeout_to_sec(lock_timeout)
466
- if lock_timeout_sec == 0
467
- warn "[strong_migrations] DANGER: No lock timeout set"
468
- elsif lock_timeout_sec > limit
469
- warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
470
- end
471
- elsif mysql? || mariadb?
472
- lock_timeout = connection.select_all("SHOW VARIABLES LIKE 'lock_wait_timeout'").first["Value"]
473
- # lock timeout is an integer
474
- if lock_timeout.to_i > limit
475
- warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
476
- end
477
- end
478
- @lock_timeout_checked = true
479
- end
480
- end
148
+ def set_timeouts
149
+ return if @timeouts_set
481
150
 
482
- # units: https://www.postgresql.org/docs/current/config-setting.html
483
- def timeout_to_sec(timeout)
484
- units = {
485
- "us" => 0.001,
486
- "ms" => 1,
487
- "s" => 1000,
488
- "min" => 1000 * 60,
489
- "h" => 1000 * 60 * 60,
490
- "d" => 1000 * 60 * 60 * 24
491
- }
492
- timeout_ms = timeout.to_i
493
- units.each do |k, v|
494
- if timeout.end_with?(k)
495
- timeout_ms *= v
496
- break
497
- end
151
+ if StrongMigrations.statement_timeout
152
+ adapter.set_statement_timeout(StrongMigrations.statement_timeout)
498
153
  end
499
- timeout_ms / 1000.0
500
- end
501
-
502
- def postgresql_timeout(timeout)
503
- if timeout.is_a?(String)
504
- timeout
505
- else
506
- # use ceil to prevent no timeout for values under 1 ms
507
- (timeout.to_f * 1000).ceil
154
+ if StrongMigrations.lock_timeout
155
+ adapter.set_lock_timeout(StrongMigrations.lock_timeout)
508
156
  end
509
- end
510
157
 
511
- def constraints(table_name)
512
- query = <<~SQL
513
- SELECT
514
- conname AS name,
515
- pg_get_constraintdef(oid) AS def
516
- FROM
517
- pg_constraint
518
- WHERE
519
- contype = 'c' AND
520
- convalidated AND
521
- conrelid = #{connection.quote(connection.quote_table_name(table_name))}::regclass
522
- SQL
523
- connection.select_all(query.squish).to_a
158
+ @timeouts_set = true
524
159
  end
525
160
 
526
- def raise_error(message_key, header: nil, append: nil, **vars)
527
- return unless StrongMigrations.check_enabled?(message_key, version: version)
528
-
529
- message = StrongMigrations.error_messages[message_key] || "Missing message"
530
- message = message + append if append
161
+ def check_lock_timeout
162
+ return if defined?(@lock_timeout_checked)
531
163
 
532
- vars[:migration_name] = @migration.class.name
533
- vars[:migration_suffix] = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
534
- vars[:base_model] = "ApplicationRecord"
164
+ if StrongMigrations.lock_timeout_limit
165
+ adapter.check_lock_timeout(StrongMigrations.lock_timeout_limit)
166
+ end
535
167
 
536
- # escape % not followed by {
537
- message = message.gsub(/%(?!{)/, "%%") % vars if message.include?("%")
538
- @migration.stop!(message, header: header || "Dangerous operation detected")
168
+ @lock_timeout_checked = true
539
169
  end
540
170
 
541
- def constraint_str(statement, identifiers)
542
- # not all identifiers are tables, but this method of quoting should be fine
543
- statement % identifiers.map { |v| connection.quote_table_name(v) }
171
+ def safe?
172
+ self.class.safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe? || @migration.reverting?
544
173
  end
545
174
 
546
- def safety_assured_str(code)
547
- "safety_assured do\n execute '#{code}' \n end"
175
+ def version
176
+ @migration.version
548
177
  end
549
178
 
550
- def command_str(command, args)
551
- str_args = args[0..-2].map { |a| a.inspect }
552
-
553
- # prettier last arg
554
- last_arg = args[-1]
555
- if last_arg.is_a?(Hash)
556
- if last_arg.any?
557
- str_args << last_arg.map do |k, v|
558
- if v.is_a?(Hash)
559
- # pretty index: {algorithm: :concurrently}
560
- "#{k}: {#{v.map { |k2, v2| "#{k2}: #{v2.inspect}" }.join(", ")}}"
179
+ def adapter
180
+ @adapter ||= begin
181
+ cls =
182
+ case connection.adapter_name
183
+ when /postg/i # PostgreSQL, PostGIS
184
+ Adapters::PostgreSQLAdapter
185
+ when /mysql|trilogy/i
186
+ if connection.try(:mariadb?)
187
+ Adapters::MariaDBAdapter
561
188
  else
562
- "#{k}: #{v.inspect}"
189
+ Adapters::MySQLAdapter
563
190
  end
564
- end.join(", ")
565
- end
566
- else
567
- str_args << last_arg.inspect
568
- end
569
-
570
- "#{command} #{str_args.join(", ")}"
571
- end
572
-
573
- def writes_blocked?
574
- query = <<~SQL
575
- SELECT
576
- relation::regclass::text
577
- FROM
578
- pg_locks
579
- WHERE
580
- mode IN ('ShareRowExclusiveLock', 'AccessExclusiveLock') AND
581
- pid = pg_backend_pid()
582
- SQL
583
- connection.select_all(query.squish).any?
584
- end
191
+ else
192
+ Adapters::AbstractAdapter
193
+ end
585
194
 
586
- def rewrite_blocks
587
- mysql? || mariadb? ? "writes" : "reads and writes"
195
+ cls.new(self)
196
+ end
588
197
  end
589
198
 
590
- def backfill_code(table, column, default)
591
- model = table.to_s.classify
592
- "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
199
+ def connection
200
+ @migration.connection
593
201
  end
594
202
 
595
- def new_table?(table)
596
- @new_tables.include?(table.to_s)
203
+ def retry_lock_timeouts?(method)
204
+ (
205
+ StrongMigrations.lock_timeout_retries > 0 &&
206
+ !in_transaction? &&
207
+ method != :transaction
208
+ )
597
209
  end
598
210
  end
599
211
  end