strong_migrations 1.8.0 → 2.3.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 +38 -0
- data/LICENSE.txt +1 -1
- data/README.md +91 -93
- data/lib/generators/strong_migrations/install_generator.rb +3 -7
- data/lib/strong_migrations/adapters/abstract_adapter.rb +5 -10
- data/lib/strong_migrations/adapters/mariadb_adapter.rb +1 -1
- data/lib/strong_migrations/adapters/mysql_adapter.rb +9 -4
- data/lib/strong_migrations/adapters/postgresql_adapter.rb +24 -21
- data/lib/strong_migrations/checker.rb +70 -4
- data/lib/strong_migrations/checks.rb +80 -80
- data/lib/strong_migrations/error_messages.rb +17 -11
- data/lib/strong_migrations/migration.rb +2 -1
- data/lib/strong_migrations/{database_tasks.rb → migration_context.rb} +20 -3
- data/lib/strong_migrations/migrator.rb +6 -4
- data/lib/strong_migrations/safe_methods.rb +59 -38
- 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
|
@@ -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
|
|
@@ -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
|
|
@@ -42,7 +42,7 @@ module StrongMigrations
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def add_column_default_safe?
|
|
45
|
-
|
|
45
|
+
true
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def change_type_safe?(table, column, type, options, existing_column, existing_type)
|
|
@@ -95,7 +95,7 @@ module StrongMigrations
|
|
|
95
95
|
"timestamp" => "timestamp without time zone",
|
|
96
96
|
"timestamptz" => "timestamp with time zone"
|
|
97
97
|
}
|
|
98
|
-
maybe_safe = type_map.
|
|
98
|
+
maybe_safe = type_map.value?(existing_type) && precision >= existing_precision
|
|
99
99
|
|
|
100
100
|
if maybe_safe
|
|
101
101
|
new_type = type.to_s == "datetime" ? datetime_type : type.to_s
|
|
@@ -103,7 +103,7 @@ module StrongMigrations
|
|
|
103
103
|
# resolve with fallback
|
|
104
104
|
new_type = type_map[new_type] || new_type
|
|
105
105
|
|
|
106
|
-
safe = new_type == existing_type ||
|
|
106
|
+
safe = new_type == existing_type || time_zone == "UTC"
|
|
107
107
|
end
|
|
108
108
|
when "time"
|
|
109
109
|
precision = options[:precision] || options[:limit] || 6
|
|
@@ -127,19 +127,19 @@ module StrongMigrations
|
|
|
127
127
|
safe
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
# TODO remove when Active Record < 7.1 is no longer supported
|
|
131
|
+
def index_invalid?(table_name, index_name)
|
|
131
132
|
query = <<~SQL
|
|
132
133
|
SELECT
|
|
133
|
-
|
|
134
|
-
pg_get_constraintdef(oid) AS def
|
|
134
|
+
indisvalid
|
|
135
135
|
FROM
|
|
136
|
-
|
|
136
|
+
pg_index
|
|
137
137
|
WHERE
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
indrelid = to_regclass(#{connection.quote(connection.quote_table_name(table_name))}) AND
|
|
139
|
+
indexrelid = to_regclass(#{connection.quote(connection.quote_table_name(index_name))}) AND
|
|
140
|
+
indisvalid = false
|
|
141
141
|
SQL
|
|
142
|
-
select_all(query.squish).
|
|
142
|
+
select_all(query.squish).any?
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
def writes_blocked?
|
|
@@ -173,6 +173,15 @@ module StrongMigrations
|
|
|
173
173
|
["primary_key", "serial", "bigserial"]
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
+
def max_constraint_name_length
|
|
177
|
+
63
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def constraints(table, column)
|
|
181
|
+
# TODO improve column check
|
|
182
|
+
connection.check_constraints(table).select { |c| /\b#{Regexp.escape(column.to_s)}\b/.match?(c.expression) }
|
|
183
|
+
end
|
|
184
|
+
|
|
176
185
|
private
|
|
177
186
|
|
|
178
187
|
def set_timeout(setting, timeout)
|
|
@@ -210,15 +219,9 @@ module StrongMigrations
|
|
|
210
219
|
end
|
|
211
220
|
|
|
212
221
|
def datetime_type
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
# no need to support custom datetime_types
|
|
217
|
-
connection.class.datetime_type
|
|
218
|
-
else
|
|
219
|
-
# https://github.com/rails/rails/issues/21126#issuecomment-327895275
|
|
220
|
-
:datetime
|
|
221
|
-
end
|
|
222
|
+
# https://github.com/rails/rails/pull/41084
|
|
223
|
+
# no need to support custom datetime_types
|
|
224
|
+
key = connection.class.datetime_type
|
|
222
225
|
|
|
223
226
|
# could be timestamp, timestamp without time zone, timestamp with time zone, etc
|
|
224
227
|
connection.class.const_get(:NATIVE_DATABASE_TYPES).fetch(key).fetch(:name)
|
|
@@ -11,10 +11,16 @@ module StrongMigrations
|
|
|
11
11
|
|
|
12
12
|
def initialize(migration)
|
|
13
13
|
@migration = migration
|
|
14
|
+
reset
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reset
|
|
14
18
|
@new_tables = []
|
|
15
19
|
@new_columns = []
|
|
16
20
|
@timeouts_set = false
|
|
17
21
|
@committed = false
|
|
22
|
+
@transaction_disabled = false
|
|
23
|
+
@skip_retries = false
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
def self.safety_assured
|
|
@@ -27,7 +33,10 @@ module StrongMigrations
|
|
|
27
33
|
end
|
|
28
34
|
end
|
|
29
35
|
|
|
30
|
-
def perform(method, *args)
|
|
36
|
+
def perform(method, *args, &block)
|
|
37
|
+
return yield if skip?
|
|
38
|
+
|
|
39
|
+
check_adapter
|
|
31
40
|
check_version_supported
|
|
32
41
|
set_timeouts
|
|
33
42
|
check_lock_timeout
|
|
@@ -96,9 +105,9 @@ module StrongMigrations
|
|
|
96
105
|
# TODO figure out how to handle methods that generate multiple statements
|
|
97
106
|
# like add_reference(table, ref, index: {algorithm: :concurrently})
|
|
98
107
|
# lock timeout after first statement will cause retry to fail
|
|
99
|
-
retry_lock_timeouts {
|
|
108
|
+
retry_lock_timeouts { perform_method(method, *args, &block) }
|
|
100
109
|
else
|
|
101
|
-
|
|
110
|
+
perform_method(method, *args, &block)
|
|
102
111
|
end
|
|
103
112
|
|
|
104
113
|
# outdated statistics + a new index can hurt performance of existing queries
|
|
@@ -109,6 +118,13 @@ module StrongMigrations
|
|
|
109
118
|
result
|
|
110
119
|
end
|
|
111
120
|
|
|
121
|
+
def perform_method(method, *args)
|
|
122
|
+
if StrongMigrations.remove_invalid_indexes && direction == :up && method == :add_index && postgresql?
|
|
123
|
+
remove_invalid_index_if_needed(*args)
|
|
124
|
+
end
|
|
125
|
+
yield
|
|
126
|
+
end
|
|
127
|
+
|
|
112
128
|
def retry_lock_timeouts(check_committed: false)
|
|
113
129
|
retries = 0
|
|
114
130
|
begin
|
|
@@ -129,8 +145,22 @@ module StrongMigrations
|
|
|
129
145
|
version && version <= StrongMigrations.start_after
|
|
130
146
|
end
|
|
131
147
|
|
|
148
|
+
def skip?
|
|
149
|
+
StrongMigrations.skipped_databases.map(&:to_s).include?(db_config_name)
|
|
150
|
+
end
|
|
151
|
+
|
|
132
152
|
private
|
|
133
153
|
|
|
154
|
+
def check_adapter
|
|
155
|
+
return if defined?(@adapter_checked)
|
|
156
|
+
|
|
157
|
+
if adapter.instance_of?(Adapters::AbstractAdapter)
|
|
158
|
+
warn "[strong_migrations] Unsupported adapter: #{connection.adapter_name}. Use StrongMigrations.skip_database(#{db_config_name.to_sym.inspect}) to silence this warning."
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
@adapter_checked = true
|
|
162
|
+
end
|
|
163
|
+
|
|
134
164
|
def check_version_supported
|
|
135
165
|
return if defined?(@version_checked)
|
|
136
166
|
|
|
@@ -200,12 +230,48 @@ module StrongMigrations
|
|
|
200
230
|
@migration.connection
|
|
201
231
|
end
|
|
202
232
|
|
|
233
|
+
def db_config_name
|
|
234
|
+
connection.pool.db_config.name
|
|
235
|
+
end
|
|
236
|
+
|
|
203
237
|
def retry_lock_timeouts?(method)
|
|
204
238
|
(
|
|
205
239
|
StrongMigrations.lock_timeout_retries > 0 &&
|
|
206
240
|
!in_transaction? &&
|
|
207
|
-
method != :transaction
|
|
241
|
+
method != :transaction &&
|
|
242
|
+
!@skip_retries
|
|
208
243
|
)
|
|
209
244
|
end
|
|
245
|
+
|
|
246
|
+
def without_retries
|
|
247
|
+
previous_value = @skip_retries
|
|
248
|
+
begin
|
|
249
|
+
@skip_retries = true
|
|
250
|
+
yield
|
|
251
|
+
ensure
|
|
252
|
+
@skip_retries = previous_value
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# REINDEX INDEX CONCURRENTLY leaves a new invalid index if it fails, so use remove_index instead
|
|
257
|
+
def remove_invalid_index_if_needed(*args)
|
|
258
|
+
options = args.extract_options!
|
|
259
|
+
|
|
260
|
+
# ensures has same options as existing index
|
|
261
|
+
# check args to avoid errors with index_exists?
|
|
262
|
+
return unless args.size == 2 && connection.index_exists?(*args, **options.merge(valid: false))
|
|
263
|
+
|
|
264
|
+
table, columns = args
|
|
265
|
+
index_name = options.fetch(:name, connection.index_name(table, columns))
|
|
266
|
+
|
|
267
|
+
# valid option is ignored for Active Record < 7.1, so check name as well
|
|
268
|
+
return if ar_version < 7.1 && !adapter.index_invalid?(table, index_name)
|
|
269
|
+
|
|
270
|
+
@migration.say("Attempting to remove invalid index")
|
|
271
|
+
without_retries do
|
|
272
|
+
# TODO pass index schema for extra safety?
|
|
273
|
+
@migration.remove_index(table, **{name: index_name}.merge(options.slice(:algorithm)))
|
|
274
|
+
end
|
|
275
|
+
end
|
|
210
276
|
end
|
|
211
277
|
end
|
|
@@ -10,7 +10,7 @@ module StrongMigrations
|
|
|
10
10
|
if !new_table?(table)
|
|
11
11
|
if postgresql? && options[:validate] != false
|
|
12
12
|
add_options = options.merge(validate: false)
|
|
13
|
-
name = options[:name] ||
|
|
13
|
+
name = options[:name] || connection.check_constraint_options(table, expression, options)[:name]
|
|
14
14
|
validate_options = {name: name}
|
|
15
15
|
|
|
16
16
|
if StrongMigrations.safe_by_default
|
|
@@ -39,27 +39,20 @@ module StrongMigrations
|
|
|
39
39
|
#
|
|
40
40
|
# Also, Active Record has special case for uuid columns that allows function default values
|
|
41
41
|
# https://github.com/rails/rails/blob/v7.0.3.1/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb#L92-L93
|
|
42
|
-
if
|
|
42
|
+
if !default.nil? && (!adapter.add_column_default_safe? || (volatile = (postgresql? && type.to_s == "uuid" && default.to_s.include?("()") && adapter.default_volatile?(default))))
|
|
43
43
|
if options[:null] == false
|
|
44
44
|
options = options.except(:null)
|
|
45
45
|
append = "\n\nThen add the NOT NULL constraint in separate migrations."
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
change_command: command_str("change_column_default", [table, column, default]),
|
|
57
|
-
remove_command: command_str("remove_column", [table, column]),
|
|
58
|
-
code: backfill_code(table, column, default, volatile),
|
|
59
|
-
append: append,
|
|
60
|
-
rewrite_blocks: adapter.rewrite_blocks,
|
|
61
|
-
default_type: (volatile ? "volatile" : "non-null")
|
|
62
|
-
end
|
|
48
|
+
raise_error :add_column_default,
|
|
49
|
+
add_command: command_str("add_column", [table, column, type, options.except(:default)]),
|
|
50
|
+
change_command: command_str("change_column_default", [table, column, default]),
|
|
51
|
+
remove_command: command_str("remove_column", [table, column]),
|
|
52
|
+
code: backfill_code(table, column, default, volatile),
|
|
53
|
+
append: append,
|
|
54
|
+
rewrite_blocks: adapter.rewrite_blocks,
|
|
55
|
+
default_type: (volatile ? "volatile" : "non-null")
|
|
63
56
|
elsif default.is_a?(Proc) && postgresql?
|
|
64
57
|
# adding a column with a VOLATILE default is not safe
|
|
65
58
|
# https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES
|
|
@@ -217,17 +210,43 @@ module StrongMigrations
|
|
|
217
210
|
end
|
|
218
211
|
|
|
219
212
|
raise_error :change_column, rewrite_blocks: adapter.rewrite_blocks unless safe
|
|
213
|
+
|
|
214
|
+
# constraints must be rechecked
|
|
215
|
+
# Postgres recommends dropping constraints before and adding them back
|
|
216
|
+
# https://www.postgresql.org/docs/current/ddl-alter.html#DDL-ALTER-COLUMN-TYPE
|
|
217
|
+
if postgresql?
|
|
218
|
+
constraints = adapter.constraints(table, column)
|
|
219
|
+
if constraints.any?
|
|
220
|
+
change_commands = []
|
|
221
|
+
constraints.each do |c|
|
|
222
|
+
change_commands << command_str(:remove_check_constraint, [table, c.expression, {name: c.name}])
|
|
223
|
+
end
|
|
224
|
+
change_commands << command_str(:change_column, args + [options])
|
|
225
|
+
constraints.each do |c|
|
|
226
|
+
change_commands << command_str(:add_check_constraint, [table, c.expression, {name: c.name, validate: false}])
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
validate_commands = []
|
|
230
|
+
constraints.each do |c|
|
|
231
|
+
validate_commands << command_str(:validate_check_constraint, [table, {name: c.name}])
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
raise_error :change_column_constraint,
|
|
235
|
+
change_column_code: change_commands.join("\n "),
|
|
236
|
+
validate_constraint_code: validate_commands.join("\n ")
|
|
237
|
+
end
|
|
238
|
+
end
|
|
220
239
|
end
|
|
221
240
|
|
|
222
241
|
def check_change_column_default(*args)
|
|
223
242
|
table, column, _default_or_changes = args
|
|
224
243
|
|
|
225
244
|
# just check ActiveRecord::Base, even though can override on model
|
|
226
|
-
partial_inserts =
|
|
245
|
+
partial_inserts = ActiveRecord::Base.partial_inserts
|
|
227
246
|
|
|
228
247
|
if partial_inserts && !new_column?(table, column)
|
|
229
248
|
raise_error :change_column_default,
|
|
230
|
-
config:
|
|
249
|
+
config: "partial_inserts"
|
|
231
250
|
end
|
|
232
251
|
end
|
|
233
252
|
|
|
@@ -235,60 +254,40 @@ module StrongMigrations
|
|
|
235
254
|
table, column, null, default = args
|
|
236
255
|
if !null
|
|
237
256
|
if postgresql?
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if safe_with_check_constraint
|
|
241
|
-
safe = adapter.constraints(table).any? { |c| c["def"] == "CHECK ((#{column} IS NOT NULL))" || c["def"] == "CHECK ((#{connection.quote_column_name(column)} IS NOT NULL))" }
|
|
242
|
-
end
|
|
257
|
+
constraints = connection.check_constraints(table)
|
|
258
|
+
safe = constraints.any? { |c| c.options[:validate] && (c.expression == "#{column} IS NOT NULL" || c.expression == "#{connection.quote_column_name(column)} IS NOT NULL") }
|
|
243
259
|
|
|
244
260
|
unless safe
|
|
261
|
+
expression = "#{quote_column_if_needed(column)} IS NOT NULL"
|
|
262
|
+
|
|
245
263
|
# match https://github.com/nullobject/rein
|
|
246
264
|
constraint_name = "#{table}_#{column}_null"
|
|
265
|
+
if adapter.max_constraint_name_length && constraint_name.bytesize > adapter.max_constraint_name_length
|
|
266
|
+
constraint_name = connection.check_constraint_options(table, expression, {})[:name]
|
|
247
267
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
constraint_methods = ar_version >= 6.1
|
|
253
|
-
|
|
254
|
-
validate_constraint_code =
|
|
255
|
-
if constraint_methods
|
|
256
|
-
String.new(command_str(:validate_check_constraint, [table, {name: constraint_name}]))
|
|
257
|
-
else
|
|
258
|
-
String.new(safety_assured_str(validate_code))
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
if safe_with_check_constraint
|
|
262
|
-
change_args = [table, column, null]
|
|
263
|
-
|
|
264
|
-
validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}"
|
|
265
|
-
|
|
266
|
-
if constraint_methods
|
|
267
|
-
validate_constraint_code << "\n #{command_str(:remove_check_constraint, [table, {name: constraint_name}])}"
|
|
268
|
-
else
|
|
269
|
-
validate_constraint_code << "\n #{safety_assured_str(remove_code)}"
|
|
268
|
+
# avoid collision with Active Record naming for safe_by_default
|
|
269
|
+
if StrongMigrations.safe_by_default
|
|
270
|
+
constraint_name = constraint_name.sub("rails", "strong_migrations")
|
|
270
271
|
end
|
|
271
272
|
end
|
|
272
273
|
|
|
274
|
+
add_args = [table, expression, {name: constraint_name, validate: false}]
|
|
275
|
+
validate_args = [table, {name: constraint_name}]
|
|
276
|
+
change_args = [table, column, null]
|
|
277
|
+
remove_args = [table, {name: constraint_name}]
|
|
278
|
+
|
|
273
279
|
if StrongMigrations.safe_by_default
|
|
274
|
-
safe_change_column_null(
|
|
280
|
+
safe_change_column_null(add_args, validate_args, change_args, remove_args, table, column, default, constraints)
|
|
275
281
|
throw :safe
|
|
276
282
|
end
|
|
277
283
|
|
|
278
|
-
add_constraint_code =
|
|
279
|
-
if constraint_methods
|
|
280
|
-
command_str(:add_check_constraint, [table, "#{quote_column_if_needed(column)} IS NOT NULL", {name: constraint_name, validate: false}])
|
|
281
|
-
else
|
|
282
|
-
safety_assured_str(add_code)
|
|
283
|
-
end
|
|
284
|
+
add_constraint_code = command_str(:add_check_constraint, add_args)
|
|
284
285
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
"def change\n #{validate_constraint_code}\n end"
|
|
291
|
-
end
|
|
286
|
+
up_code = String.new(command_str(:validate_check_constraint, validate_args))
|
|
287
|
+
up_code << "\n #{command_str(:change_column_null, change_args)}"
|
|
288
|
+
up_code << "\n #{command_str(:remove_check_constraint, remove_args)}"
|
|
289
|
+
down_code = "#{add_constraint_code}\n #{command_str(:change_column_null, [table, column, true])}"
|
|
290
|
+
validate_constraint_code = "def up\n #{up_code}\n end\n\n def down\n #{down_code}\n end"
|
|
292
291
|
|
|
293
292
|
raise_error :change_column_null_postgresql,
|
|
294
293
|
add_constraint_code: add_constraint_code,
|
|
@@ -337,29 +336,28 @@ module StrongMigrations
|
|
|
337
336
|
columns =
|
|
338
337
|
case method
|
|
339
338
|
when :remove_timestamps
|
|
340
|
-
[
|
|
339
|
+
[:created_at, :updated_at]
|
|
341
340
|
when :remove_column
|
|
342
|
-
[args[1]
|
|
341
|
+
[args[1]]
|
|
343
342
|
when :remove_columns
|
|
344
|
-
# Active Record 6.1+ supports options
|
|
345
343
|
if args.last.is_a?(Hash)
|
|
346
|
-
args[1..-2]
|
|
344
|
+
args[1..-2]
|
|
347
345
|
else
|
|
348
|
-
args[1..-1]
|
|
346
|
+
args[1..-1]
|
|
349
347
|
end
|
|
350
348
|
else
|
|
351
349
|
options = args[2] || {}
|
|
352
350
|
reference = args[1]
|
|
353
351
|
cols = []
|
|
354
|
-
cols << "#{reference}_type" if options[:polymorphic]
|
|
355
|
-
cols << "#{reference}_id"
|
|
352
|
+
cols << "#{reference}_type".to_sym if options[:polymorphic]
|
|
353
|
+
cols << "#{reference}_id".to_sym
|
|
356
354
|
cols
|
|
357
355
|
end
|
|
358
356
|
|
|
359
|
-
code = "self.ignored_columns += #{columns.inspect}"
|
|
357
|
+
code = "self.ignored_columns += #{columns.map(&:to_s).inspect}"
|
|
360
358
|
|
|
361
359
|
raise_error :remove_column,
|
|
362
|
-
model: args[0]
|
|
360
|
+
model: model_name(args[0]),
|
|
363
361
|
code: code,
|
|
364
362
|
command: command_str(method, args),
|
|
365
363
|
column_suffix: columns.size > 1 ? "s" : ""
|
|
@@ -373,12 +371,6 @@ module StrongMigrations
|
|
|
373
371
|
# avoid suggesting extra (invalid) args
|
|
374
372
|
args = args[0..1] unless StrongMigrations.safe_by_default
|
|
375
373
|
|
|
376
|
-
# Active Record < 6.1 only supports two arguments (including options)
|
|
377
|
-
if args.size == 2 && ar_version < 6.1
|
|
378
|
-
# arg takes precedence over option
|
|
379
|
-
options[:column] = args.pop
|
|
380
|
-
end
|
|
381
|
-
|
|
382
374
|
if StrongMigrations.safe_by_default
|
|
383
375
|
safe_remove_index(*args, **options)
|
|
384
376
|
throw :safe
|
|
@@ -433,7 +425,7 @@ module StrongMigrations
|
|
|
433
425
|
message = message + append if append
|
|
434
426
|
|
|
435
427
|
vars[:migration_name] = @migration.class.name
|
|
436
|
-
vars[:migration_suffix] =
|
|
428
|
+
vars[:migration_suffix] = migration_suffix
|
|
437
429
|
vars[:base_model] = "ApplicationRecord"
|
|
438
430
|
|
|
439
431
|
# escape % not followed by {
|
|
@@ -474,20 +466,20 @@ module StrongMigrations
|
|
|
474
466
|
end
|
|
475
467
|
|
|
476
468
|
def backfill_code(table, column, default, function = false)
|
|
477
|
-
model = table
|
|
469
|
+
model = model_name(table)
|
|
478
470
|
if function
|
|
479
471
|
# update_all(column: Arel.sql(default)) also works in newer versions of Active Record
|
|
480
472
|
update_expr = "#{quote_column_if_needed(column)} = #{default}"
|
|
481
|
-
"#{model}.unscoped.in_batches do |relation| \n relation.where(#{column}: nil).update_all(#{update_expr.inspect})\n sleep(0.01)\n end"
|
|
473
|
+
"#{model}.unscoped.in_batches(of: 10000) do |relation| \n relation.where(#{column}: nil).update_all(#{update_expr.inspect})\n sleep(0.01)\n end"
|
|
482
474
|
else
|
|
483
|
-
"#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
|
|
475
|
+
"#{model}.unscoped.in_batches(of: 10000) do |relation| \n relation.where(#{column}: nil).update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
|
|
484
476
|
end
|
|
485
477
|
end
|
|
486
478
|
|
|
487
479
|
# only quote when needed
|
|
488
480
|
# important! only use for display purposes
|
|
489
481
|
def quote_column_if_needed(column)
|
|
490
|
-
|
|
482
|
+
/\A[a-z0-9_]+\z/.match?(column.to_s) ? column : connection.quote_column_name(column)
|
|
491
483
|
end
|
|
492
484
|
|
|
493
485
|
def new_table?(table)
|
|
@@ -497,5 +489,13 @@ module StrongMigrations
|
|
|
497
489
|
def new_column?(table, column)
|
|
498
490
|
new_table?(table) || @new_columns.include?([table.to_s, column.to_s])
|
|
499
491
|
end
|
|
492
|
+
|
|
493
|
+
def migration_suffix
|
|
494
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def model_name(table)
|
|
498
|
+
table.to_s.classify
|
|
499
|
+
end
|
|
500
500
|
end
|
|
501
501
|
end
|
|
@@ -25,16 +25,6 @@ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
|
25
25
|
end
|
|
26
26
|
end",
|
|
27
27
|
|
|
28
|
-
add_column_default_null:
|
|
29
|
-
"Adding a column with a null default blocks %{rewrite_blocks} while the entire table is rewritten.
|
|
30
|
-
Instead, add the column without a default value.
|
|
31
|
-
|
|
32
|
-
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
33
|
-
def change
|
|
34
|
-
%{command}
|
|
35
|
-
end
|
|
36
|
-
end",
|
|
37
|
-
|
|
38
28
|
add_column_default_callable:
|
|
39
29
|
"Strong Migrations does not support inspecting callable default values.
|
|
40
30
|
Please make really sure you're not calling a VOLATILE function,
|
|
@@ -70,6 +60,22 @@ while the entire table is rewritten. A safer approach is to:
|
|
|
70
60
|
change_column_with_not_null:
|
|
71
61
|
"Changing the type is safe, but setting NOT NULL is not.",
|
|
72
62
|
|
|
63
|
+
change_column_constraint: "Changing the type of a column that has check constraints blocks reads and writes
|
|
64
|
+
while every row is checked. Drop the check constraints on the column before
|
|
65
|
+
changing the type and add them back afterwards.
|
|
66
|
+
|
|
67
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
68
|
+
def change
|
|
69
|
+
%{change_column_code}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
74
|
+
def change
|
|
75
|
+
%{validate_constraint_code}
|
|
76
|
+
end
|
|
77
|
+
end",
|
|
78
|
+
|
|
73
79
|
remove_column: "Active Record caches attributes, which causes problems
|
|
74
80
|
when removing columns. Be sure to ignore the column%{column_suffix}:
|
|
75
81
|
|
|
@@ -157,7 +163,7 @@ you're doing is safe before proceeding, then wrap it in a safety_assured { ... }
|
|
|
157
163
|
create_table:
|
|
158
164
|
"The force option will destroy existing tables.
|
|
159
165
|
If this is intended, drop the existing table first.
|
|
160
|
-
|
|
166
|
+
In any case, remove the force option.",
|
|
161
167
|
|
|
162
168
|
execute:
|
|
163
169
|
"Strong Migrations does not support inspecting what happens inside an
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
module StrongMigrations
|
|
2
|
-
module
|
|
3
|
-
|
|
4
|
-
def migrate(*args)
|
|
2
|
+
module MigrationContext
|
|
3
|
+
def up(...)
|
|
5
4
|
super
|
|
6
5
|
rescue => e
|
|
6
|
+
strong_migrations_process_exception(e)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def down(...)
|
|
10
|
+
super
|
|
11
|
+
rescue => e
|
|
12
|
+
strong_migrations_process_exception(e)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run(...)
|
|
16
|
+
super
|
|
17
|
+
rescue => e
|
|
18
|
+
strong_migrations_process_exception(e)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def strong_migrations_process_exception(e)
|
|
7
24
|
if e.cause.is_a?(StrongMigrations::Error)
|
|
8
25
|
# strip cause and clean backtrace
|
|
9
26
|
def e.cause
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
module StrongMigrations
|
|
2
2
|
module Migrator
|
|
3
|
-
def ddl_transaction(migration,
|
|
3
|
+
def ddl_transaction(migration, ...)
|
|
4
4
|
return super unless StrongMigrations.lock_timeout_retries > 0 && use_transaction?(migration)
|
|
5
5
|
|
|
6
6
|
# handle MigrationProxy class
|
|
7
7
|
migration = migration.send(:migration) if migration.respond_to?(:migration, true)
|
|
8
8
|
|
|
9
|
-
# retry migration since the entire transaction needs to be rerun
|
|
10
9
|
checker = migration.send(:strong_migrations_checker)
|
|
10
|
+
return super if checker.skip?
|
|
11
|
+
|
|
12
|
+
# retry migration since the entire transaction needs to be rerun
|
|
11
13
|
checker.retry_lock_timeouts(check_committed: true) do
|
|
12
14
|
# failed transaction reverts timeout, so need to re-apply
|
|
13
|
-
checker.
|
|
15
|
+
checker.reset
|
|
14
16
|
|
|
15
|
-
super(migration,
|
|
17
|
+
super(migration, ...)
|
|
16
18
|
end
|
|
17
19
|
end
|
|
18
20
|
end
|