strong_migrations 0.6.6 → 0.6.7
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 +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
|