strong_migrations 1.6.0 → 2.8.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.
@@ -21,21 +21,17 @@ module StrongMigrations
21
21
 
22
22
  def target_version
23
23
  case adapter
24
- when /mysql/
24
+ when /mysql|trilogy/
25
25
  # could try to connect to database and check for MariaDB
26
26
  # but this should be fine
27
- '"8.0.12"'
27
+ "8.4"
28
28
  else
29
- "10"
29
+ "18"
30
30
  end
31
31
  end
32
32
 
33
33
  def adapter
34
- if ActiveRecord::VERSION::STRING.to_f >= 6.1
35
- ActiveRecord::Base.connection_db_config.adapter.to_s
36
- else
37
- ActiveRecord::Base.connection_config[:adapter].to_s
38
- end
34
+ ActiveRecord::Base.connection_db_config.adapter.to_s
39
35
  end
40
36
 
41
37
  def postgresql?
@@ -20,6 +20,9 @@ StrongMigrations.auto_analyze = true
20
20
  # end
21
21
  # end<% if postgresql? %>
22
22
 
23
+ # Remove invalid indexes when rerunning migrations
24
+ # StrongMigrations.remove_invalid_indexes = true
25
+
23
26
  # Make some operations safe by default
24
27
  # See https://github.com/ankane/strong_migrations#safe-by-default
25
28
  # StrongMigrations.safe_by_default = true<% end %>
@@ -13,11 +13,15 @@ module StrongMigrations
13
13
  end
14
14
 
15
15
  def set_statement_timeout(timeout)
16
- raise StrongMigrations::Error, "Statement timeout not supported for this database"
16
+ # do nothing
17
+ end
18
+
19
+ def set_transaction_timeout(timeout)
20
+ # do nothing
17
21
  end
18
22
 
19
23
  def set_lock_timeout(timeout)
20
- raise StrongMigrations::Error, "Lock timeout not supported for this database"
24
+ # do nothing
21
25
  end
22
26
 
23
27
  def check_lock_timeout(limit)
@@ -36,6 +40,13 @@ module StrongMigrations
36
40
  "reads and writes"
37
41
  end
38
42
 
43
+ def auto_incrementing_types
44
+ ["primary_key"]
45
+ end
46
+
47
+ def max_constraint_name_length
48
+ end
49
+
39
50
  private
40
51
 
41
52
  def connection
@@ -51,14 +62,6 @@ module StrongMigrations
51
62
  version =
52
63
  if target_version && StrongMigrations.developer_env?
53
64
  if target_version.is_a?(Hash)
54
- # Active Record 6.0 supports multiple databases
55
- # but connection.pool.spec.name always returns "primary"
56
- # in migrations with rails db:migrate
57
- if ActiveRecord::VERSION::STRING.to_f < 6.1
58
- # error class is not shown in db:migrate output so ensure message is descriptive
59
- raise StrongMigrations::Error, "StrongMigrations.target_version does not support multiple databases for Active Record < 6.1"
60
- end
61
-
62
65
  db_config_name = connection.pool.db_config.name
63
66
  target_version.stringify_keys.fetch(db_config_name) do
64
67
  # error class is not shown in db:migrate output so ensure message is descriptive
@@ -6,7 +6,7 @@ module StrongMigrations
6
6
  end
7
7
 
8
8
  def min_version
9
- "10.2"
9
+ "10.5"
10
10
  end
11
11
 
12
12
  def server_version
@@ -25,7 +25,7 @@ module StrongMigrations
25
25
  end
26
26
 
27
27
  def add_column_default_safe?
28
- server_version >= Gem::Version.new("10.3.2")
28
+ true
29
29
  end
30
30
  end
31
31
  end
@@ -8,7 +8,7 @@ module StrongMigrations
8
8
  end
9
9
 
10
10
  def min_version
11
- "5.7"
11
+ "8.0"
12
12
  end
13
13
 
14
14
  def server_version
@@ -44,7 +44,7 @@ module StrongMigrations
44
44
  end
45
45
 
46
46
  def add_column_default_safe?
47
- server_version >= Gem::Version.new("8.0.12")
47
+ true
48
48
  end
49
49
 
50
50
  def change_type_safe?(table, column, type, options, existing_column, existing_type)
@@ -65,9 +65,10 @@ module StrongMigrations
65
65
  sql = <<~SQL
66
66
  SELECT cs.MAXLEN
67
67
  FROM INFORMATION_SCHEMA.CHARACTER_SETS cs
68
- INNER JOIN INFORMATION_SCHEMA.COLLATIONS c ON c.CHARACTER_SET_NAME = cs.CHARACTER_SET_NAME
69
- INNER JOIN INFORMATION_SCHEMA.TABLES t ON t.TABLE_COLLATION = c.COLLATION_NAME
70
- WHERE t.TABLE_SCHEMA = database() AND t.TABLE_NAME = #{connection.quote(table)}
68
+ INNER JOIN INFORMATION_SCHEMA.COLUMNS c ON c.CHARACTER_SET_NAME = cs.CHARACTER_SET_NAME
69
+ WHERE c.TABLE_SCHEMA = database() AND
70
+ c.TABLE_NAME = #{connection.quote(table)} AND
71
+ c.COLUMN_NAME = #{connection.quote(column)}
71
72
  SQL
72
73
  row = connection.select_all(sql).first
73
74
  if row
@@ -91,6 +92,10 @@ module StrongMigrations
91
92
  "writes"
92
93
  end
93
94
 
95
+ def max_constraint_name_length
96
+ 64
97
+ end
98
+
94
99
  private
95
100
 
96
101
  # do not memoize
@@ -6,7 +6,7 @@ module StrongMigrations
6
6
  end
7
7
 
8
8
  def min_version
9
- "10"
9
+ "12"
10
10
  end
11
11
 
12
12
  def server_version
@@ -23,6 +23,11 @@ module StrongMigrations
23
23
  set_timeout("statement_timeout", timeout)
24
24
  end
25
25
 
26
+ def set_transaction_timeout(timeout)
27
+ # TODO make sure true version supports it as well?
28
+ set_timeout("transaction_timeout", timeout) if server_version >= Gem::Version.new("17")
29
+ end
30
+
26
31
  def set_lock_timeout(timeout)
27
32
  set_timeout("lock_timeout", timeout)
28
33
  end
@@ -42,7 +47,7 @@ module StrongMigrations
42
47
  end
43
48
 
44
49
  def add_column_default_safe?
45
- server_version >= Gem::Version.new("11")
50
+ true
46
51
  end
47
52
 
48
53
  def change_type_safe?(table, column, type, options, existing_column, existing_type)
@@ -95,7 +100,7 @@ module StrongMigrations
95
100
  "timestamp" => "timestamp without time zone",
96
101
  "timestamptz" => "timestamp with time zone"
97
102
  }
98
- maybe_safe = type_map.values.include?(existing_type) && precision >= existing_precision
103
+ maybe_safe = type_map.value?(existing_type) && precision >= existing_precision
99
104
 
100
105
  if maybe_safe
101
106
  new_type = type.to_s == "datetime" ? datetime_type : type.to_s
@@ -103,7 +108,7 @@ module StrongMigrations
103
108
  # resolve with fallback
104
109
  new_type = type_map[new_type] || new_type
105
110
 
106
- safe = new_type == existing_type || (server_version >= Gem::Version.new("12") && time_zone == "UTC")
111
+ safe = new_type == existing_type || time_zone == "UTC"
107
112
  end
108
113
  when "time"
109
114
  precision = options[:precision] || options[:limit] || 6
@@ -127,21 +132,6 @@ module StrongMigrations
127
132
  safe
128
133
  end
129
134
 
130
- def constraints(table_name)
131
- query = <<~SQL
132
- SELECT
133
- conname AS name,
134
- pg_get_constraintdef(oid) AS def
135
- FROM
136
- pg_constraint
137
- WHERE
138
- contype = 'c' AND
139
- convalidated AND
140
- conrelid = #{connection.quote(connection.quote_table_name(table_name))}::regclass
141
- SQL
142
- select_all(query.squish).to_a
143
- end
144
-
145
135
  def writes_blocked?
146
136
  query = <<~SQL
147
137
  SELECT
@@ -169,6 +159,19 @@ module StrongMigrations
169
159
  rows.empty? || rows.any? { |r| r["provolatile"] == "v" }
170
160
  end
171
161
 
162
+ def auto_incrementing_types
163
+ ["primary_key", "serial", "bigserial"]
164
+ end
165
+
166
+ def max_constraint_name_length
167
+ 63
168
+ end
169
+
170
+ def constraints(table, column)
171
+ # TODO improve column check
172
+ connection.check_constraints(table).select { |c| /\b#{Regexp.escape(column.to_s)}\b/.match?(c.expression) }
173
+ end
174
+
172
175
  private
173
176
 
174
177
  def set_timeout(setting, timeout)
@@ -206,15 +209,9 @@ module StrongMigrations
206
209
  end
207
210
 
208
211
  def datetime_type
209
- key =
210
- if ActiveRecord::VERSION::MAJOR >= 7
211
- # https://github.com/rails/rails/pull/41084
212
- # no need to support custom datetime_types
213
- connection.class.datetime_type
214
- else
215
- # https://github.com/rails/rails/issues/21126#issuecomment-327895275
216
- :datetime
217
- end
212
+ # https://github.com/rails/rails/pull/41084
213
+ # no need to support custom datetime_types
214
+ key = connection.class.datetime_type
218
215
 
219
216
  # could be timestamp, timestamp without time zone, timestamp with time zone, etc
220
217
  connection.class.const_get(:NATIVE_DATABASE_TYPES).fetch(key).fetch(:name)
@@ -5,26 +5,38 @@ module StrongMigrations
5
5
 
6
6
  attr_accessor :direction, :transaction_disabled, :timeouts_set
7
7
 
8
+ class << self
9
+ attr_accessor :safe
10
+ end
11
+
8
12
  def initialize(migration)
9
13
  @migration = migration
14
+ reset
15
+ end
16
+
17
+ def reset
10
18
  @new_tables = []
11
19
  @new_columns = []
12
- @safe = false
13
20
  @timeouts_set = false
14
21
  @committed = false
22
+ @transaction_disabled = false
23
+ @skip_retries = false
15
24
  end
16
25
 
17
- def safety_assured
18
- previous_value = @safe
26
+ def self.safety_assured
27
+ previous_value = safe
19
28
  begin
20
- @safe = true
29
+ self.safe = true
21
30
  yield
22
31
  ensure
23
- @safe = previous_value
32
+ self.safe = previous_value
24
33
  end
25
34
  end
26
35
 
27
- def perform(method, *args)
36
+ def perform(method, *args, &block)
37
+ return yield if skip?
38
+
39
+ check_adapter
28
40
  check_version_supported
29
41
  set_timeouts
30
42
  check_lock_timeout
@@ -45,6 +57,8 @@ module StrongMigrations
45
57
  check_add_index(*args)
46
58
  when :add_reference, :add_belongs_to
47
59
  check_add_reference(method, *args)
60
+ when :add_unique_constraint
61
+ check_add_unique_constraint(*args)
48
62
  when :change_column
49
63
  check_change_column(*args)
50
64
  when :change_column_default
@@ -65,6 +79,10 @@ module StrongMigrations
65
79
  check_remove_index(*args)
66
80
  when :rename_column
67
81
  check_rename_column
82
+ when :rename_enum_value
83
+ check_rename_enum_value(*args)
84
+ when :rename_schema
85
+ check_rename_schema
68
86
  when :rename_table
69
87
  check_rename_table
70
88
  when :validate_check_constraint
@@ -78,9 +96,11 @@ module StrongMigrations
78
96
  @committed = true
79
97
  end
80
98
 
81
- # custom checks
82
- StrongMigrations.checks.each do |check|
83
- @migration.instance_exec(method, args, &check)
99
+ if !safe?
100
+ # custom checks
101
+ StrongMigrations.checks.each do |check|
102
+ @migration.instance_exec(method, args, &check)
103
+ end
84
104
  end
85
105
  end
86
106
 
@@ -89,19 +109,26 @@ module StrongMigrations
89
109
  # TODO figure out how to handle methods that generate multiple statements
90
110
  # like add_reference(table, ref, index: {algorithm: :concurrently})
91
111
  # lock timeout after first statement will cause retry to fail
92
- retry_lock_timeouts { yield }
112
+ retry_lock_timeouts { perform_method(method, *args, &block) }
93
113
  else
94
- yield
114
+ perform_method(method, *args, &block)
95
115
  end
96
116
 
97
117
  # outdated statistics + a new index can hurt performance of existing queries
98
- if StrongMigrations.auto_analyze && direction == :up && method == :add_index
118
+ if StrongMigrations.auto_analyze && direction == :up && adds_index?(method, *args)
99
119
  adapter.analyze_table(args[0])
100
120
  end
101
121
 
102
122
  result
103
123
  end
104
124
 
125
+ def perform_method(method, *args)
126
+ if StrongMigrations.remove_invalid_indexes && direction == :up && method == :add_index && postgresql?
127
+ remove_invalid_index_if_needed(*args)
128
+ end
129
+ yield
130
+ end
131
+
105
132
  def retry_lock_timeouts(check_committed: false)
106
133
  retries = 0
107
134
  begin
@@ -118,8 +145,36 @@ module StrongMigrations
118
145
  end
119
146
  end
120
147
 
148
+ def version_safe?
149
+ version && version <= StrongMigrations.start_after
150
+ end
151
+
152
+ def skip?
153
+ StrongMigrations.skipped_databases.map(&:to_s).include?(db_config_name)
154
+ end
155
+
156
+ def set_transaction_timeout
157
+ return if defined?(@transaction_timeout_set)
158
+
159
+ if StrongMigrations.transaction_timeout
160
+ adapter.set_transaction_timeout(StrongMigrations.transaction_timeout)
161
+ end
162
+
163
+ @transaction_timeout_set = true
164
+ end
165
+
121
166
  private
122
167
 
168
+ def check_adapter
169
+ return if defined?(@adapter_checked)
170
+
171
+ if adapter.instance_of?(Adapters::AbstractAdapter)
172
+ warn "[strong_migrations] Unsupported adapter: #{connection.adapter_name}. Use StrongMigrations.skip_database(#{db_config_name.to_sym.inspect}) to silence this warning."
173
+ end
174
+
175
+ @adapter_checked = true
176
+ end
177
+
123
178
  def check_version_supported
124
179
  return if defined?(@version_checked)
125
180
 
@@ -158,11 +213,7 @@ module StrongMigrations
158
213
  end
159
214
 
160
215
  def safe?
161
- @safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe?
162
- end
163
-
164
- def version_safe?
165
- version && version <= StrongMigrations.start_after
216
+ self.class.safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe? || @migration.reverting?
166
217
  end
167
218
 
168
219
  def version
@@ -175,7 +226,7 @@ module StrongMigrations
175
226
  case connection.adapter_name
176
227
  when /postg/i # PostgreSQL, PostGIS
177
228
  Adapters::PostgreSQLAdapter
178
- when /mysql/i
229
+ when /mysql|trilogy/i
179
230
  if connection.try(:mariadb?)
180
231
  Adapters::MariaDBAdapter
181
232
  else
@@ -193,12 +244,57 @@ module StrongMigrations
193
244
  @migration.connection
194
245
  end
195
246
 
247
+ def db_config_name
248
+ connection.pool.db_config.name
249
+ end
250
+
196
251
  def retry_lock_timeouts?(method)
197
252
  (
198
253
  StrongMigrations.lock_timeout_retries > 0 &&
199
254
  !in_transaction? &&
200
- method != :transaction
255
+ method != :transaction &&
256
+ !@skip_retries
201
257
  )
202
258
  end
259
+
260
+ def without_retries
261
+ previous_value = @skip_retries
262
+ begin
263
+ @skip_retries = true
264
+ yield
265
+ ensure
266
+ @skip_retries = previous_value
267
+ end
268
+ end
269
+
270
+ def adds_index?(method, *args)
271
+ case method
272
+ when :add_index
273
+ true
274
+ when :add_reference, :add_belongs_to
275
+ options = args.extract_options!
276
+ !!options.fetch(:index, true)
277
+ else
278
+ false
279
+ end
280
+ end
281
+
282
+ # REINDEX INDEX CONCURRENTLY leaves a new invalid index if it fails, so use remove_index instead
283
+ def remove_invalid_index_if_needed(*args)
284
+ options = args.extract_options!
285
+
286
+ # ensures has same options as existing index
287
+ # check args to avoid errors with index_exists?
288
+ return unless args.size == 2 && connection.index_exists?(*args, **options.merge(valid: false))
289
+
290
+ table, columns = args
291
+ index_name = options.fetch(:name, connection.index_name(table, columns))
292
+
293
+ @migration.say("Attempting to remove invalid index")
294
+ without_retries do
295
+ # TODO pass index schema for extra safety?
296
+ @migration.remove_index(table, **{name: index_name}.merge(options.slice(:algorithm)))
297
+ end
298
+ end
203
299
  end
204
300
  end