strong_migrations 0.6.6 → 0.6.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +8 -40
- data/lib/generators/strong_migrations/templates/initializer.rb.tt +3 -2
- data/lib/strong_migrations.rb +2 -37
- data/lib/strong_migrations/checker.rb +21 -21
- data/lib/strong_migrations/version.rb +1 -1
- metadata +2 -3
- data/lib/strong_migrations/migration_helpers.rb +0 -117
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c2afb67b7b25f7608d3d77d266703a23b5585fcfe006d8442a3da42547979f8d
|
4
|
+
data.tar.gz: 502347f1d82a120f93694bdffb3cc9d434573bb2e6c14f75f61c5dc1b43c723d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb821b724c1d55150415e117c1509f407edadcb209312ee869b230563b59e55e64072371631b9a75f59e84bbc7af0bdde05e5e5e872ad0dc738917d28ea5c49f
|
7
|
+
data.tar.gz: eb503dd38ecc6ad8877e4a2c506b6503f5a00ac661f8adbc4962d6802009c278091d4a7236dff4c83d64885405d468a9275fd4601e869d3dffac709c3c39cc13
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -21,6 +21,7 @@ gem 'strong_migrations'
|
|
21
21
|
And run:
|
22
22
|
|
23
23
|
```sh
|
24
|
+
bundle install
|
24
25
|
rails generate strong_migrations:install
|
25
26
|
```
|
26
27
|
|
@@ -40,8 +41,7 @@ Potentially dangerous operations:
|
|
40
41
|
|
41
42
|
Postgres-specific checks:
|
42
43
|
|
43
|
-
- [adding an index non-concurrently](#adding-an-index)
|
44
|
-
- [removing an index non-concurrently](#removing-an-index)
|
44
|
+
- [adding an index non-concurrently](#adding-an-index-non-concurrently)
|
45
45
|
- [adding a reference](#adding-a-reference)
|
46
46
|
- [adding a foreign key](#adding-a-foreign-key)
|
47
47
|
- [adding a json column](#adding-a-json-column)
|
@@ -257,6 +257,8 @@ class CreateUsers < ActiveRecord::Migration[6.0]
|
|
257
257
|
end
|
258
258
|
```
|
259
259
|
|
260
|
+
If you intend to drop an existing table, run `drop_table` first.
|
261
|
+
|
260
262
|
### Using change_column_null with a default value
|
261
263
|
|
262
264
|
#### Bad
|
@@ -297,7 +299,7 @@ class ExecuteSQL < ActiveRecord::Migration[6.0]
|
|
297
299
|
end
|
298
300
|
```
|
299
301
|
|
300
|
-
### Adding an index
|
302
|
+
### Adding an index non-concurrently
|
301
303
|
|
302
304
|
#### Bad
|
303
305
|
|
@@ -333,36 +335,6 @@ With [gindex](https://github.com/ankane/gindex), you can generate an index migra
|
|
333
335
|
rails g index table column
|
334
336
|
```
|
335
337
|
|
336
|
-
### Removing an index
|
337
|
-
|
338
|
-
Note: This check is [opt-in](#opt-in-checks).
|
339
|
-
|
340
|
-
#### Bad
|
341
|
-
|
342
|
-
In Postgres, removing an index non-concurrently locks the table for a brief period.
|
343
|
-
|
344
|
-
```ruby
|
345
|
-
class RemoveSomeIndexFromUsers < ActiveRecord::Migration[6.0]
|
346
|
-
def change
|
347
|
-
remove_index :users, :some_column
|
348
|
-
end
|
349
|
-
end
|
350
|
-
```
|
351
|
-
|
352
|
-
#### Good
|
353
|
-
|
354
|
-
Remove indexes concurrently.
|
355
|
-
|
356
|
-
```ruby
|
357
|
-
class RemoveSomeIndexFromUsers < ActiveRecord::Migration[6.0]
|
358
|
-
disable_ddl_transaction!
|
359
|
-
|
360
|
-
def change
|
361
|
-
remove_index :users, column: :some_column, algorithm: :concurrently
|
362
|
-
end
|
363
|
-
end
|
364
|
-
```
|
365
|
-
|
366
338
|
### Adding a reference
|
367
339
|
|
368
340
|
#### Bad
|
@@ -591,16 +563,12 @@ Note: Since `remove_column` always requires a `safety_assured` block, it’s not
|
|
591
563
|
|
592
564
|
## Opt-in Checks
|
593
565
|
|
594
|
-
|
566
|
+
### Removing an index non-concurrently
|
595
567
|
|
596
|
-
|
597
|
-
StrongMigrations.enable_check(:remove_index)
|
598
|
-
```
|
599
|
-
|
600
|
-
To start a check only after a specific migration, use:
|
568
|
+
Postgres supports removing indexes concurrently, but removing them non-concurrently shouldn’t be an issue for most applications. You can enable this check with:
|
601
569
|
|
602
570
|
```ruby
|
603
|
-
StrongMigrations.enable_check(:remove_index
|
571
|
+
StrongMigrations.enable_check(:remove_index)
|
604
572
|
```
|
605
573
|
|
606
574
|
## Disable Checks
|
@@ -1,12 +1,13 @@
|
|
1
1
|
# Mark existing migrations as safe
|
2
2
|
StrongMigrations.start_after = <%= start_after %>
|
3
3
|
|
4
|
-
# Set timeouts
|
4
|
+
# Set timeouts for migrations
|
5
5
|
# If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user
|
6
6
|
StrongMigrations.lock_timeout = 10.seconds
|
7
7
|
StrongMigrations.statement_timeout = 1.hour
|
8
8
|
|
9
|
-
# Analyze tables
|
9
|
+
# Analyze tables after indexes are added
|
10
|
+
# Outdated statistics can sometimes hurt performance
|
10
11
|
StrongMigrations.auto_analyze = true
|
11
12
|
|
12
13
|
# Add custom checks
|
data/lib/strong_migrations.rb
CHANGED
@@ -5,7 +5,6 @@ require "active_support"
|
|
5
5
|
require "strong_migrations/checker"
|
6
6
|
require "strong_migrations/database_tasks"
|
7
7
|
require "strong_migrations/migration"
|
8
|
-
require "strong_migrations/migration_helpers"
|
9
8
|
require "strong_migrations/version"
|
10
9
|
|
11
10
|
# integrations
|
@@ -18,7 +17,7 @@ module StrongMigrations
|
|
18
17
|
class << self
|
19
18
|
attr_accessor :auto_analyze, :start_after, :checks, :error_messages,
|
20
19
|
:target_postgresql_version, :target_mysql_version, :target_mariadb_version,
|
21
|
-
:enabled_checks, :lock_timeout, :statement_timeout
|
20
|
+
:enabled_checks, :lock_timeout, :statement_timeout
|
22
21
|
attr_writer :lock_timeout_limit
|
23
22
|
end
|
24
23
|
self.auto_analyze = false
|
@@ -182,19 +181,6 @@ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
182
181
|
end
|
183
182
|
end",
|
184
183
|
|
185
|
-
change_column_null_postgresql_helper:
|
186
|
-
"Setting NOT NULL on a column requires an AccessExclusiveLock,
|
187
|
-
which is expensive on large tables. Instead, we can use a constraint and
|
188
|
-
validate it in a separate step with a more agreeable RowShareLock.
|
189
|
-
|
190
|
-
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
191
|
-
disable_ddl_transaction!
|
192
|
-
|
193
|
-
def change
|
194
|
-
%{command}
|
195
|
-
end
|
196
|
-
end",
|
197
|
-
|
198
184
|
change_column_null_mysql:
|
199
185
|
"Setting NOT NULL on an existing column is not safe with your database engine.",
|
200
186
|
|
@@ -213,23 +199,9 @@ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
213
199
|
def change
|
214
200
|
%{validate_foreign_key_code}
|
215
201
|
end
|
216
|
-
end"
|
217
|
-
|
218
|
-
add_foreign_key_helper:
|
219
|
-
"New foreign keys are validated by default. This acquires an AccessExclusiveLock,
|
220
|
-
which is expensive on large tables. Instead, we can validate it in a separate step
|
221
|
-
with a more agreeable RowShareLock.
|
222
|
-
|
223
|
-
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
224
|
-
disable_ddl_transaction!
|
225
|
-
|
226
|
-
def change
|
227
|
-
%{command}
|
228
|
-
end
|
229
|
-
end",
|
202
|
+
end"
|
230
203
|
}
|
231
204
|
self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
|
232
|
-
self.helpers = false
|
233
205
|
|
234
206
|
# private
|
235
207
|
def self.developer_env?
|
@@ -263,13 +235,6 @@ end",
|
|
263
235
|
false
|
264
236
|
end
|
265
237
|
end
|
266
|
-
|
267
|
-
# def self.enable_helpers
|
268
|
-
# unless helpers
|
269
|
-
# ActiveRecord::Migration.include(StrongMigrations::MigrationHelpers)
|
270
|
-
# self.helpers = true
|
271
|
-
# end
|
272
|
-
# end
|
273
238
|
end
|
274
239
|
|
275
240
|
ActiveSupport.on_load(:active_record) do
|
@@ -196,17 +196,12 @@ Then add the foreign key in separate migrations."
|
|
196
196
|
table, column, null, default = args
|
197
197
|
if !null
|
198
198
|
if postgresql?
|
199
|
-
|
200
|
-
|
201
|
-
command: command_str(:add_null_constraint_safely, [table, column])
|
202
|
-
else
|
203
|
-
# match https://github.com/nullobject/rein
|
204
|
-
constraint_name = "#{table}_#{column}_null"
|
199
|
+
# match https://github.com/nullobject/rein
|
200
|
+
constraint_name = "#{table}_#{column}_null"
|
205
201
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
end
|
202
|
+
raise_error :change_column_null_postgresql,
|
203
|
+
add_constraint_code: constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column]),
|
204
|
+
validate_constraint_code: constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
|
210
205
|
elsif mysql? || mariadb?
|
211
206
|
raise_error :change_column_null_mysql
|
212
207
|
elsif !default.nil?
|
@@ -222,10 +217,7 @@ Then add the foreign key in separate migrations."
|
|
222
217
|
validate = options.fetch(:validate, true) || ActiveRecord::VERSION::STRING < "5.2"
|
223
218
|
|
224
219
|
if postgresql? && validate
|
225
|
-
if
|
226
|
-
raise_error :add_foreign_key_helper,
|
227
|
-
command: command_str(:add_foreign_key_safely, [from_table, to_table, options])
|
228
|
-
elsif ActiveRecord::VERSION::STRING < "5.2"
|
220
|
+
if ActiveRecord::VERSION::STRING < "5.2"
|
229
221
|
# fk name logic from rails
|
230
222
|
primary_key = options[:primary_key] || "id"
|
231
223
|
column = options[:column] || "#{to_table.to_s.singularize}_id"
|
@@ -250,8 +242,10 @@ Then add the foreign key in separate migrations."
|
|
250
242
|
|
251
243
|
result = yield
|
252
244
|
|
245
|
+
# outdated statistics + a new index can hurt performance of existing queries
|
253
246
|
if StrongMigrations.auto_analyze && direction == :up && method == :add_index
|
254
247
|
if postgresql?
|
248
|
+
# TODO remove verbose in 0.7.0
|
255
249
|
connection.execute "ANALYZE VERBOSE #{connection.quote_table_name(args[0].to_s)}"
|
256
250
|
elsif mariadb? || mysql?
|
257
251
|
connection.execute "ANALYZE TABLE #{connection.quote_table_name(args[0].to_s)}"
|
@@ -267,7 +261,7 @@ Then add the foreign key in separate migrations."
|
|
267
261
|
if StrongMigrations.statement_timeout
|
268
262
|
statement =
|
269
263
|
if postgresql?
|
270
|
-
"SET statement_timeout TO #{connection.quote(StrongMigrations.statement_timeout
|
264
|
+
"SET statement_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.statement_timeout))}"
|
271
265
|
elsif mysql?
|
272
266
|
"SET max_execution_time = #{connection.quote(StrongMigrations.statement_timeout.to_i * 1000)}"
|
273
267
|
elsif mariadb?
|
@@ -282,7 +276,7 @@ Then add the foreign key in separate migrations."
|
|
282
276
|
if StrongMigrations.lock_timeout
|
283
277
|
statement =
|
284
278
|
if postgresql?
|
285
|
-
"SET lock_timeout TO #{connection.quote(StrongMigrations.lock_timeout
|
279
|
+
"SET lock_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.lock_timeout))}"
|
286
280
|
elsif mysql? || mariadb?
|
287
281
|
"SET lock_wait_timeout = #{connection.quote(StrongMigrations.lock_timeout)}"
|
288
282
|
else
|
@@ -369,22 +363,24 @@ Then add the foreign key in separate migrations."
|
|
369
363
|
lock_timeout = connection.select_all("SHOW lock_timeout").first["lock_timeout"]
|
370
364
|
lock_timeout_sec = timeout_to_sec(lock_timeout)
|
371
365
|
if lock_timeout_sec == 0
|
372
|
-
warn "[strong_migrations]
|
366
|
+
warn "[strong_migrations] DANGER: No lock timeout set"
|
373
367
|
elsif lock_timeout_sec > limit
|
374
|
-
warn "[strong_migrations]
|
368
|
+
warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
|
375
369
|
end
|
376
370
|
elsif mysql? || mariadb?
|
377
371
|
lock_timeout = connection.select_all("SHOW VARIABLES LIKE 'lock_wait_timeout'").first["Value"]
|
378
372
|
if lock_timeout.to_i > limit
|
379
|
-
warn "[strong_migrations]
|
373
|
+
warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
|
380
374
|
end
|
381
375
|
end
|
382
376
|
@lock_timeout_checked = true
|
383
377
|
end
|
384
378
|
end
|
385
379
|
|
380
|
+
# units: https://www.postgresql.org/docs/current/config-setting.html
|
386
381
|
def timeout_to_sec(timeout)
|
387
382
|
suffixes = {
|
383
|
+
"us" => 0.001,
|
388
384
|
"ms" => 1,
|
389
385
|
"s" => 1000,
|
390
386
|
"min" => 1000 * 60,
|
@@ -401,8 +397,12 @@ Then add the foreign key in separate migrations."
|
|
401
397
|
timeout_ms / 1000.0
|
402
398
|
end
|
403
399
|
|
404
|
-
def
|
405
|
-
|
400
|
+
def postgresql_timeout(timeout)
|
401
|
+
if timeout.is_a?(String)
|
402
|
+
timeout
|
403
|
+
else
|
404
|
+
timeout.to_i * 1000
|
405
|
+
end
|
406
406
|
end
|
407
407
|
|
408
408
|
def raise_error(message_key, header: nil, append: nil, **vars)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: strong_migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2020-05-
|
13
|
+
date: 2020-05-13 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -115,7 +115,6 @@ files:
|
|
115
115
|
- lib/strong_migrations/checker.rb
|
116
116
|
- lib/strong_migrations/database_tasks.rb
|
117
117
|
- lib/strong_migrations/migration.rb
|
118
|
-
- lib/strong_migrations/migration_helpers.rb
|
119
118
|
- lib/strong_migrations/railtie.rb
|
120
119
|
- lib/strong_migrations/version.rb
|
121
120
|
- lib/tasks/strong_migrations.rake
|
@@ -1,117 +0,0 @@
|
|
1
|
-
module StrongMigrations
|
2
|
-
module MigrationHelpers
|
3
|
-
def add_foreign_key_safely(from_table, to_table, **options)
|
4
|
-
ensure_postgresql(__method__)
|
5
|
-
ensure_not_in_transaction(__method__)
|
6
|
-
|
7
|
-
reversible do |dir|
|
8
|
-
dir.up do
|
9
|
-
if ActiveRecord::VERSION::STRING >= "5.2"
|
10
|
-
add_foreign_key(from_table, to_table, options.merge(validate: false))
|
11
|
-
validate_foreign_key(from_table, to_table)
|
12
|
-
else
|
13
|
-
options = connection.foreign_key_options(from_table, to_table, options)
|
14
|
-
fk_name, column, primary_key = options.values_at(:name, :column, :primary_key)
|
15
|
-
primary_key ||= "id"
|
16
|
-
|
17
|
-
statement = ["ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)"]
|
18
|
-
statement << on_delete_update_statement(:delete, options[:on_delete]) if options[:on_delete]
|
19
|
-
statement << on_delete_update_statement(:update, options[:on_update]) if options[:on_update]
|
20
|
-
statement << "NOT VALID"
|
21
|
-
|
22
|
-
safety_assured do
|
23
|
-
execute quote_identifiers(statement.join(" "), [from_table, fk_name, column, to_table, primary_key])
|
24
|
-
execute quote_identifiers("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name])
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
dir.down do
|
30
|
-
remove_foreign_key(from_table, to_table)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def add_null_constraint_safely(table_name, column_name, name: nil)
|
36
|
-
ensure_postgresql(__method__)
|
37
|
-
ensure_not_in_transaction(__method__)
|
38
|
-
|
39
|
-
reversible do |dir|
|
40
|
-
dir.up do
|
41
|
-
name ||= null_constraint_name(table_name, column_name)
|
42
|
-
|
43
|
-
safety_assured do
|
44
|
-
execute quote_identifiers("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table_name, name, column_name])
|
45
|
-
execute quote_identifiers("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table_name, name])
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
dir.down do
|
50
|
-
remove_null_constraint_safely(table_name, column_name)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
# removing constraints is safe, but this method is safe to reverse as well
|
56
|
-
def remove_null_constraint_safely(table_name, column_name, name: nil)
|
57
|
-
# could also ensure in transaction so it can be reversed
|
58
|
-
# but that's more of a concern for a reversible migrations check
|
59
|
-
ensure_postgresql(__method__)
|
60
|
-
|
61
|
-
reversible do |dir|
|
62
|
-
dir.up do
|
63
|
-
name ||= null_constraint_name(table_name, column_name)
|
64
|
-
|
65
|
-
safety_assured do
|
66
|
-
execute quote_identifiers("ALTER TABLE %s DROP CONSTRAINT %s", [table_name, name])
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
dir.down do
|
71
|
-
add_null_constraint_safely(table_name, column_name)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
private
|
77
|
-
|
78
|
-
def ensure_postgresql(method_name)
|
79
|
-
raise StrongMigrations::Error, "`#{method_name}` is intended for Postgres only" unless postgresql?
|
80
|
-
end
|
81
|
-
|
82
|
-
def postgresql?
|
83
|
-
%w(PostgreSQL PostGIS).include?(connection.adapter_name)
|
84
|
-
end
|
85
|
-
|
86
|
-
def ensure_not_in_transaction(method_name)
|
87
|
-
if connection.transaction_open?
|
88
|
-
raise StrongMigrations::Error, "Cannot run `#{method_name}` inside a transaction. Use `disable_ddl_transaction` to disable the transaction."
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
# match https://github.com/nullobject/rein
|
93
|
-
def null_constraint_name(table_name, column_name)
|
94
|
-
"#{table_name}_#{column_name}_null"
|
95
|
-
end
|
96
|
-
|
97
|
-
def on_delete_update_statement(delete_or_update, action)
|
98
|
-
on = delete_or_update.to_s.upcase
|
99
|
-
|
100
|
-
case action
|
101
|
-
when :nullify
|
102
|
-
"ON #{on} SET NULL"
|
103
|
-
when :cascade
|
104
|
-
"ON #{on} CASCADE"
|
105
|
-
when :restrict
|
106
|
-
"ON #{on} RESTRICT"
|
107
|
-
else
|
108
|
-
# same error message as Active Record
|
109
|
-
raise "'#{action}' is not supported for :on_update or :on_delete.\nSupported values are: :nullify, :cascade, :restrict"
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def quote_identifiers(statement, identifiers)
|
114
|
-
statement % identifiers.map { |v| connection.quote_table_name(v) }
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|