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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 140b74a0133d3db034a54fc178e4f95e53fbdec0c569dc2ade222efc96b83a6b
4
- data.tar.gz: f4aadca5f15f85849b975630bdb937e3eef00498c0e806f6ca7cd8112d859fd4
3
+ metadata.gz: c2afb67b7b25f7608d3d77d266703a23b5585fcfe006d8442a3da42547979f8d
4
+ data.tar.gz: 502347f1d82a120f93694bdffb3cc9d434573bb2e6c14f75f61c5dc1b43c723d
5
5
  SHA512:
6
- metadata.gz: 2f79d4dbd4342e45af9799f4d303c8ad8af68e8aa24fc5e86667547ec2a4c8ce95e123a6b3f0eb1851573d7a1bebd89a6795f4afffd4a319a0a9f0d73293f3ee
7
- data.tar.gz: a784f11afffd45d827d7135d1f8fb822979ba851f6d8b54e09025ff9144f32f5091dce8e080901bac51237be74894571c6d007eddf373e3007de4886f5391b54
6
+ metadata.gz: bb821b724c1d55150415e117c1509f407edadcb209312ee869b230563b59e55e64072371631b9a75f59e84bbc7af0bdde05e5e5e872ad0dc738917d28ea5c49f
7
+ data.tar.gz: eb503dd38ecc6ad8877e4a2c506b6503f5a00ac661f8adbc4962d6802009c278091d4a7236dff4c83d64885405d468a9275fd4601e869d3dffac709c3c39cc13
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.6.7 (2020-05-13)
2
+
3
+ - Improved comments in initializer
4
+ - Fixed string timeouts for Postgres
5
+
1
6
  ## 0.6.6 (2020-05-08)
2
7
 
3
8
  - Added warnings for missing and long lock timeouts
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
- Some operations rarely cause issues in practice, but can be checked if desired. Enable checks with:
566
+ ### Removing an index non-concurrently
595
567
 
596
- ```ruby
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, start_after: 20170101000000)
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 automatically (to update planner statistics) after an index is added
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
@@ -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, :helpers
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
- if helpers?
200
- raise_error :change_column_null_postgresql_helper,
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
- raise_error :change_column_null_postgresql,
207
- add_constraint_code: constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column]),
208
- validate_constraint_code: constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
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 helpers?
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.to_i * 1000)}"
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.to_i * 1000)}"
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] WARNING: No lock timeout set. This is dangerous."
366
+ warn "[strong_migrations] DANGER: No lock timeout set"
373
367
  elsif lock_timeout_sec > limit
374
- warn "[strong_migrations] WARNING: Lock timeout is longer than #{limit} seconds: #{lock_timeout}. This is dangerous."
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] WARNING: Lock timeout is longer than #{limit} seconds: #{lock_timeout}. This is dangerous."
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 helpers?
405
- StrongMigrations.helpers
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)
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.6.6"
2
+ VERSION = "0.6.7"
3
3
  end
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.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-08 00:00:00.000000000 Z
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