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 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