strong_migrations 1.4.4 → 2.5.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.0"
28
28
  else
29
29
  "10"
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,25 +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
- @safe = false
19
+ @new_columns = []
12
20
  @timeouts_set = false
13
21
  @committed = false
22
+ @transaction_disabled = false
23
+ @skip_retries = false
14
24
  end
15
25
 
16
- def safety_assured
17
- previous_value = @safe
26
+ def self.safety_assured
27
+ previous_value = safe
18
28
  begin
19
- @safe = true
29
+ self.safe = true
20
30
  yield
21
31
  ensure
22
- @safe = previous_value
32
+ self.safe = previous_value
23
33
  end
24
34
  end
25
35
 
26
- def perform(method, *args)
36
+ def perform(method, *args, &block)
37
+ return yield if skip?
38
+
39
+ check_adapter
27
40
  check_version_supported
28
41
  set_timeouts
29
42
  check_lock_timeout
@@ -44,8 +57,12 @@ module StrongMigrations
44
57
  check_add_index(*args)
45
58
  when :add_reference, :add_belongs_to
46
59
  check_add_reference(method, *args)
60
+ when :add_unique_constraint
61
+ check_add_unique_constraint(*args)
47
62
  when :change_column
48
63
  check_change_column(*args)
64
+ when :change_column_default
65
+ check_change_column_default(*args)
49
66
  when :change_column_null
50
67
  check_change_column_null(*args)
51
68
  when :change_table
@@ -62,6 +79,8 @@ module StrongMigrations
62
79
  check_remove_index(*args)
63
80
  when :rename_column
64
81
  check_rename_column
82
+ when :rename_schema
83
+ check_rename_schema
65
84
  when :rename_table
66
85
  check_rename_table
67
86
  when :validate_check_constraint
@@ -75,9 +94,11 @@ module StrongMigrations
75
94
  @committed = true
76
95
  end
77
96
 
78
- # custom checks
79
- StrongMigrations.checks.each do |check|
80
- @migration.instance_exec(method, args, &check)
97
+ if !safe?
98
+ # custom checks
99
+ StrongMigrations.checks.each do |check|
100
+ @migration.instance_exec(method, args, &check)
101
+ end
81
102
  end
82
103
  end
83
104
 
@@ -86,19 +107,26 @@ module StrongMigrations
86
107
  # TODO figure out how to handle methods that generate multiple statements
87
108
  # like add_reference(table, ref, index: {algorithm: :concurrently})
88
109
  # lock timeout after first statement will cause retry to fail
89
- retry_lock_timeouts { yield }
110
+ retry_lock_timeouts { perform_method(method, *args, &block) }
90
111
  else
91
- yield
112
+ perform_method(method, *args, &block)
92
113
  end
93
114
 
94
115
  # outdated statistics + a new index can hurt performance of existing queries
95
- if StrongMigrations.auto_analyze && direction == :up && method == :add_index
116
+ if StrongMigrations.auto_analyze && direction == :up && adds_index?(method, *args)
96
117
  adapter.analyze_table(args[0])
97
118
  end
98
119
 
99
120
  result
100
121
  end
101
122
 
123
+ def perform_method(method, *args)
124
+ if StrongMigrations.remove_invalid_indexes && direction == :up && method == :add_index && postgresql?
125
+ remove_invalid_index_if_needed(*args)
126
+ end
127
+ yield
128
+ end
129
+
102
130
  def retry_lock_timeouts(check_committed: false)
103
131
  retries = 0
104
132
  begin
@@ -115,8 +143,26 @@ module StrongMigrations
115
143
  end
116
144
  end
117
145
 
146
+ def version_safe?
147
+ version && version <= StrongMigrations.start_after
148
+ end
149
+
150
+ def skip?
151
+ StrongMigrations.skipped_databases.map(&:to_s).include?(db_config_name)
152
+ end
153
+
118
154
  private
119
155
 
156
+ def check_adapter
157
+ return if defined?(@adapter_checked)
158
+
159
+ if adapter.instance_of?(Adapters::AbstractAdapter)
160
+ warn "[strong_migrations] Unsupported adapter: #{connection.adapter_name}. Use StrongMigrations.skip_database(#{db_config_name.to_sym.inspect}) to silence this warning."
161
+ end
162
+
163
+ @adapter_checked = true
164
+ end
165
+
120
166
  def check_version_supported
121
167
  return if defined?(@version_checked)
122
168
 
@@ -137,6 +183,9 @@ module StrongMigrations
137
183
  if StrongMigrations.statement_timeout
138
184
  adapter.set_statement_timeout(StrongMigrations.statement_timeout)
139
185
  end
186
+ if StrongMigrations.transaction_timeout
187
+ adapter.set_transaction_timeout(StrongMigrations.transaction_timeout)
188
+ end
140
189
  if StrongMigrations.lock_timeout
141
190
  adapter.set_lock_timeout(StrongMigrations.lock_timeout)
142
191
  end
@@ -155,11 +204,7 @@ module StrongMigrations
155
204
  end
156
205
 
157
206
  def safe?
158
- @safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe?
159
- end
160
-
161
- def version_safe?
162
- version && version <= StrongMigrations.start_after
207
+ self.class.safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe? || @migration.reverting?
163
208
  end
164
209
 
165
210
  def version
@@ -172,7 +217,7 @@ module StrongMigrations
172
217
  case connection.adapter_name
173
218
  when /postg/i # PostgreSQL, PostGIS
174
219
  Adapters::PostgreSQLAdapter
175
- when /mysql/i
220
+ when /mysql|trilogy/i
176
221
  if connection.try(:mariadb?)
177
222
  Adapters::MariaDBAdapter
178
223
  else
@@ -190,12 +235,57 @@ module StrongMigrations
190
235
  @migration.connection
191
236
  end
192
237
 
238
+ def db_config_name
239
+ connection.pool.db_config.name
240
+ end
241
+
193
242
  def retry_lock_timeouts?(method)
194
243
  (
195
244
  StrongMigrations.lock_timeout_retries > 0 &&
196
245
  !in_transaction? &&
197
- method != :transaction
246
+ method != :transaction &&
247
+ !@skip_retries
198
248
  )
199
249
  end
250
+
251
+ def without_retries
252
+ previous_value = @skip_retries
253
+ begin
254
+ @skip_retries = true
255
+ yield
256
+ ensure
257
+ @skip_retries = previous_value
258
+ end
259
+ end
260
+
261
+ def adds_index?(method, *args)
262
+ case method
263
+ when :add_index
264
+ true
265
+ when :add_reference, :add_belongs_to
266
+ options = args.extract_options!
267
+ !!options.fetch(:index, true)
268
+ else
269
+ false
270
+ end
271
+ end
272
+
273
+ # REINDEX INDEX CONCURRENTLY leaves a new invalid index if it fails, so use remove_index instead
274
+ def remove_invalid_index_if_needed(*args)
275
+ options = args.extract_options!
276
+
277
+ # ensures has same options as existing index
278
+ # check args to avoid errors with index_exists?
279
+ return unless args.size == 2 && connection.index_exists?(*args, **options.merge(valid: false))
280
+
281
+ table, columns = args
282
+ index_name = options.fetch(:name, connection.index_name(table, columns))
283
+
284
+ @migration.say("Attempting to remove invalid index")
285
+ without_retries do
286
+ # TODO pass index schema for extra safety?
287
+ @migration.remove_index(table, **{name: index_name}.merge(options.slice(:algorithm)))
288
+ end
289
+ end
200
290
  end
201
291
  end