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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +100 -1
- data/LICENSE.txt +1 -1
- data/README.md +421 -220
- data/lib/generators/strong_migrations/install_generator.rb +4 -8
- 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 +115 -19
- data/lib/strong_migrations/checks.rb +225 -97
- data/lib/strong_migrations/error_messages.rb +120 -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 +12 -6
- 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.4"
|
|
28
28
|
else
|
|
29
|
-
"
|
|
29
|
+
"18"
|
|
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,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 =
|
|
26
|
+
def self.safety_assured
|
|
27
|
+
previous_value = safe
|
|
19
28
|
begin
|
|
20
|
-
|
|
29
|
+
self.safe = true
|
|
21
30
|
yield
|
|
22
31
|
ensure
|
|
23
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 {
|
|
112
|
+
retry_lock_timeouts { perform_method(method, *args, &block) }
|
|
93
113
|
else
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
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
|