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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -1
- data/LICENSE.txt +1 -1
- data/README.md +285 -155
- data/lib/generators/strong_migrations/install_generator.rb +3 -7
- data/lib/generators/strong_migrations/templates/initializer.rb.tt +3 -0
- data/lib/strong_migrations/adapters/abstract_adapter.rb +13 -10
- data/lib/strong_migrations/adapters/mariadb_adapter.rb +2 -2
- data/lib/strong_migrations/adapters/mysql_adapter.rb +10 -5
- data/lib/strong_migrations/adapters/postgresql_adapter.rb +25 -28
- data/lib/strong_migrations/checker.rb +109 -19
- data/lib/strong_migrations/checks.rb +130 -85
- data/lib/strong_migrations/error_messages.rb +61 -14
- data/lib/strong_migrations/migration.rb +12 -6
- data/lib/strong_migrations/{database_tasks.rb → migration_context.rb} +20 -3
- data/lib/strong_migrations/migrator.rb +7 -5
- data/lib/strong_migrations/safe_methods.rb +60 -40
- data/lib/strong_migrations/schema_dumper.rb +15 -4
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/strong_migrations.rb +9 -6
- metadata +7 -11
@@ -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
|
-
|
27
|
+
"8.0"
|
28
28
|
else
|
29
29
|
"10"
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
33
|
def adapter
|
34
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
"
|
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
|
-
|
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.
|
69
|
-
|
70
|
-
|
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
|
-
"
|
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
|
-
|
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.
|
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 ||
|
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
|
-
|
210
|
-
|
211
|
-
|
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
|
-
@
|
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 =
|
26
|
+
def self.safety_assured
|
27
|
+
previous_value = safe
|
18
28
|
begin
|
19
|
-
|
29
|
+
self.safe = true
|
20
30
|
yield
|
21
31
|
ensure
|
22
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
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 {
|
110
|
+
retry_lock_timeouts { perform_method(method, *args, &block) }
|
90
111
|
else
|
91
|
-
|
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
|
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
|
-
|
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
|