strong_migrations 1.0.0 → 1.3.0

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: a22e260ec0e3e09954c65535725a3ddc4439b9cf94182a21cf5963e12d8485fe
4
- data.tar.gz: 865633da561d77e615df16a9a00f8524a95bf73edc5590e3932855aaf544d4f7
3
+ metadata.gz: 4da88a3161110bc4e7fd6cc989ea51e0db2e32681c8026db11ac74c763f253b3
4
+ data.tar.gz: 583cd8b1e9665842ff6017c6d7242dcbfd9583157327b33ec023d5357c4e799f
5
5
  SHA512:
6
- metadata.gz: f9b4df7a67aae2c8e1f7c58327b0e7e298261a33d9810dff4b5634bee3e000f1b26e64ad7b3997c8fc6c1cfef3a5cbeabe35ecbe508ed902679e3c9a720ecfb8
7
- data.tar.gz: f4a7ecb6e64c40d1be1025a978387b94b9c2e47897cc25568c32dc32043292bc8fe14c9e6471108a4d84a1b1704459707f4fcf8a25227b7a86d8bcf2237dc19d
6
+ metadata.gz: 68356a9f5966f33a2a43f35a5f74ce21943798e4d85eb1c52710808dbac0efab6659d67a22d89aa4360cfc4f87e0dce6312e16f3a71dc0a739e87bb335d42f84
7
+ data.tar.gz: 67038470a20b75fd3dfb8642fea4775aedb08bff959255a44aa92271e2214300e9140f3d7cce21eae83e81ea314739c06999ce8d46a72f76f75f675e6c8914fb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## 1.3.0 (2022-08-30)
2
+
3
+ - Added check for `add_column` with `uuid` type and volatile default value
4
+
5
+ ## 1.2.0 (2022-06-10)
6
+
7
+ - Added check for index corruption with Postgres 14.0 to 14.3
8
+
9
+ ## 1.1.0 (2022-06-08)
10
+
11
+ - Added check for `force` option with `create_join_table`
12
+ - Improved errors for extra arguments
13
+ - Fixed ignoring extra arguments with `safe_by_default`
14
+ - Fixed missing options with `remove_index` and `safe_by_default`
15
+
1
16
  ## 1.0.0 (2022-03-21)
2
17
 
3
18
  New safe operations with MySQL and MariaDB
data/README.md CHANGED
@@ -135,7 +135,7 @@ class AddSomeColumnToUsers < ActiveRecord::Migration[7.0]
135
135
  end
136
136
  ```
137
137
 
138
- In Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+, this no longer requires a table rewrite and is safe.
138
+ In Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+, this no longer requires a table rewrite and is safe (except for volatile functions like `gen_random_uuid()`).
139
139
 
140
140
  #### Good
141
141
 
@@ -681,7 +681,7 @@ Disable specific checks with:
681
681
  StrongMigrations.disable_check(:add_index)
682
682
  ```
683
683
 
684
- Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
684
+ Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations/error_messages.rb) for the list of keys.
685
685
 
686
686
  ## Down Migrations / Rollbacks
687
687
 
@@ -13,13 +13,8 @@ module StrongMigrations
13
13
  @version ||= begin
14
14
  target_version(StrongMigrations.target_postgresql_version) do
15
15
  version = select_all("SHOW server_version_num").first["server_version_num"].to_i
16
- if version >= 100000
17
- # major version for 10+
18
- "#{version / 10000}"
19
- else
20
- # major and minor version for < 10
21
- "#{version / 10000}.#{(version % 10000) / 100}"
22
- end
16
+ # major and minor version
17
+ "#{version / 10000}.#{(version % 10000)}"
23
18
  end
24
19
  end
25
20
  end
@@ -77,7 +72,7 @@ module StrongMigrations
77
72
  # but there doesn't seem to be a way to set/modify it
78
73
  # https://wiki.postgresql.org/wiki/What%27s_new_in_PostgreSQL_9.2#Reduce_ALTER_TABLE_rewrites
79
74
  when "numeric", "decimal"
80
- # numeric and decimal are equivalent and can be used interchangably
75
+ # numeric and decimal are equivalent and can be used interchangeably
81
76
  safe = ["numeric", "decimal"].include?(existing_type) &&
82
77
  (
83
78
  (
@@ -160,6 +155,20 @@ module StrongMigrations
160
155
  select_all(query.squish).any?
161
156
  end
162
157
 
158
+ # only check in non-developer environments (where actual server version is used)
159
+ def index_corruption?
160
+ server_version >= Gem::Version.new("14.0") &&
161
+ server_version < Gem::Version.new("14.4") &&
162
+ !StrongMigrations.developer_env?
163
+ end
164
+
165
+ # default to true if unsure
166
+ def default_volatile?(default)
167
+ name = default.to_s.delete_suffix("()")
168
+ rows = select_all("SELECT provolatile FROM pg_proc WHERE proname = #{connection.quote(name)}").to_a
169
+ rows.empty? || rows.any? { |r| r["provolatile"] == "v" }
170
+ end
171
+
163
172
  private
164
173
 
165
174
  def set_timeout(setting, timeout)
@@ -33,29 +33,31 @@ module StrongMigrations
33
33
  # see checks.rb for methods
34
34
  case method
35
35
  when :add_check_constraint
36
- check_add_check_constraint(args)
36
+ check_add_check_constraint(*args)
37
37
  when :add_column
38
- check_add_column(args)
38
+ check_add_column(*args)
39
39
  when :add_foreign_key
40
- check_add_foreign_key(args)
40
+ check_add_foreign_key(*args)
41
41
  when :add_index
42
- check_add_index(args)
42
+ check_add_index(*args)
43
43
  when :add_reference, :add_belongs_to
44
- check_add_reference(method, args)
44
+ check_add_reference(method, *args)
45
45
  when :change_column
46
- check_change_column(args)
46
+ check_change_column(*args)
47
47
  when :change_column_null
48
- check_change_column_null(args)
48
+ check_change_column_null(*args)
49
49
  when :change_table
50
50
  check_change_table
51
+ when :create_join_table
52
+ check_create_join_table(*args)
51
53
  when :create_table
52
- check_create_table(args)
54
+ check_create_table(*args)
53
55
  when :execute
54
56
  check_execute
55
57
  when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
56
- check_remove_column(method, args)
58
+ check_remove_column(method, *args)
57
59
  when :remove_index
58
- check_remove_index(args)
60
+ check_remove_index(*args)
59
61
  when :rename_column
60
62
  check_rename_column
61
63
  when :rename_table
@@ -3,9 +3,9 @@ module StrongMigrations
3
3
  module Checks
4
4
  private
5
5
 
6
- def check_add_check_constraint(args)
7
- table, expression, options = args
8
- options ||= {}
6
+ def check_add_check_constraint(*args)
7
+ options = args.extract_options!
8
+ table, expression = args
9
9
 
10
10
  if !new_table?(table)
11
11
  if postgresql? && options[:validate] != false
@@ -14,7 +14,7 @@ module StrongMigrations
14
14
  validate_options = {name: name}
15
15
 
16
16
  if StrongMigrations.safe_by_default
17
- safe_add_check_constraint(table, expression, add_options, validate_options)
17
+ safe_add_check_constraint(*args, add_options, validate_options)
18
18
  throw :safe
19
19
  end
20
20
 
@@ -27,12 +27,14 @@ module StrongMigrations
27
27
  end
28
28
  end
29
29
 
30
- def check_add_column(args)
31
- table, column, type, options = args
32
- options ||= {}
30
+ def check_add_column(*args)
31
+ options = args.extract_options!
32
+ table, column, type = args
33
33
  default = options[:default]
34
34
 
35
- if !default.nil? && !adapter.add_column_default_safe?
35
+ # Active Record has special case for uuid columns that allows function default values
36
+ # https://github.com/rails/rails/blob/v7.0.3.1/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb#L92-L93
37
+ if !default.nil? && (!adapter.add_column_default_safe? || (volatile = (postgresql? && type.to_s == "uuid" && default.to_s.include?("()") && adapter.default_volatile?(default))))
36
38
  if options[:null] == false
37
39
  options = options.except(:null)
38
40
  append = "
@@ -44,9 +46,10 @@ Then add the NOT NULL constraint in separate migrations."
44
46
  add_command: command_str("add_column", [table, column, type, options.except(:default)]),
45
47
  change_command: command_str("change_column_default", [table, column, default]),
46
48
  remove_command: command_str("remove_column", [table, column]),
47
- code: backfill_code(table, column, default),
49
+ code: backfill_code(table, column, default, volatile),
48
50
  append: append,
49
- rewrite_blocks: adapter.rewrite_blocks
51
+ rewrite_blocks: adapter.rewrite_blocks,
52
+ default_type: (volatile ? "volatile" : "non-null")
50
53
  elsif default.is_a?(Proc) && postgresql?
51
54
  # adding a column with a VOLATILE default is not safe
52
55
  # https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES
@@ -73,14 +76,14 @@ Then add the NOT NULL constraint in separate migrations."
73
76
  #
74
77
  # note: adding foreign_keys with create_table is fine
75
78
  # since the table is always guaranteed to be empty
76
- def check_add_foreign_key(args)
77
- from_table, to_table, options = args
78
- options ||= {}
79
+ def check_add_foreign_key(*args)
80
+ options = args.extract_options!
81
+ from_table, to_table = args
79
82
 
80
83
  validate = options.fetch(:validate, true)
81
84
  if postgresql? && validate
82
85
  if StrongMigrations.safe_by_default
83
- safe_add_foreign_key(from_table, to_table, options)
86
+ safe_add_foreign_key(*args, **options)
84
87
  throw :safe
85
88
  end
86
89
 
@@ -90,19 +93,24 @@ Then add the NOT NULL constraint in separate migrations."
90
93
  end
91
94
  end
92
95
 
93
- def check_add_index(args)
94
- table, columns, options = args
95
- options ||= {}
96
+ def check_add_index(*args)
97
+ options = args.extract_options!
98
+ table, columns = args
96
99
 
97
100
  if columns.is_a?(Array) && columns.size > 3 && !options[:unique]
98
101
  raise_error :add_index_columns, header: "Best practice"
99
102
  end
100
103
 
104
+ # safe_by_default goes through this path as well
105
+ if postgresql? && options[:algorithm] == :concurrently && adapter.index_corruption?
106
+ raise_error :add_index_corruption
107
+ end
108
+
101
109
  # safe to add non-concurrently to new tables (even after inserting data)
102
110
  # since the table won't be in use by the application
103
111
  if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
104
112
  if StrongMigrations.safe_by_default
105
- safe_add_index(table, columns, options)
113
+ safe_add_index(*args, **options)
106
114
  throw :safe
107
115
  end
108
116
 
@@ -110,9 +118,9 @@ Then add the NOT NULL constraint in separate migrations."
110
118
  end
111
119
  end
112
120
 
113
- def check_add_reference(method, args)
114
- table, reference, options = args
115
- options ||= {}
121
+ def check_add_reference(method, *args)
122
+ options = args.extract_options!
123
+ table, reference = args
116
124
 
117
125
  if postgresql?
118
126
  index_value = options.fetch(:index, true)
@@ -127,7 +135,7 @@ Then add the NOT NULL constraint in separate migrations."
127
135
  end
128
136
 
129
137
  if StrongMigrations.safe_by_default
130
- safe_add_reference(table, reference, options)
138
+ safe_add_reference(*args, **options)
131
139
  throw :safe
132
140
  end
133
141
 
@@ -148,9 +156,9 @@ Then add the foreign key in separate migrations."
148
156
  end
149
157
  end
150
158
 
151
- def check_change_column(args)
152
- table, column, type, options = args
153
- options ||= {}
159
+ def check_change_column(*args)
160
+ options = args.extract_options!
161
+ table, column, type = args
154
162
 
155
163
  safe = false
156
164
  existing_column = connection.columns(table).find { |c| c.name.to_s == column.to_s }
@@ -168,7 +176,7 @@ Then add the foreign key in separate migrations."
168
176
  raise_error :change_column, rewrite_blocks: adapter.rewrite_blocks unless safe
169
177
  end
170
178
 
171
- def check_change_column_null(args)
179
+ def check_change_column_null(*args)
172
180
  table, column, null, default = args
173
181
  if !null
174
182
  if postgresql?
@@ -221,9 +229,17 @@ Then add the foreign key in separate migrations."
221
229
  safety_assured_str(add_code)
222
230
  end
223
231
 
232
+ validate_constraint_code =
233
+ if safe_with_check_constraint
234
+ down_code = "#{add_constraint_code}\n #{command_str(:change_column_null, [table, column, true])}"
235
+ "def up\n #{validate_constraint_code}\n end\n\n def down\n #{down_code}\n end"
236
+ else
237
+ "def change\n #{validate_constraint_code}\n end"
238
+ end
239
+
224
240
  raise_error :change_column_null_postgresql,
225
241
  add_constraint_code: add_constraint_code,
226
- validate_constraint_code: "def change\n #{validate_constraint_code}\n end"
242
+ validate_constraint_code: validate_constraint_code
227
243
  end
228
244
  elsif mysql? || mariadb?
229
245
  unless adapter.strict_mode?
@@ -242,13 +258,21 @@ Then add the foreign key in separate migrations."
242
258
  raise_error :change_table, header: "Possibly dangerous operation"
243
259
  end
244
260
 
245
- def check_create_table(args)
246
- table, options = args
247
- options ||= {}
261
+ def check_create_join_table(*args)
262
+ options = args.extract_options!
248
263
 
249
264
  raise_error :create_table if options[:force]
250
265
 
251
- # keep track of new tables of add_index check
266
+ # TODO keep track of new table of add_index check
267
+ end
268
+
269
+ def check_create_table(*args)
270
+ options = args.extract_options!
271
+ table, _ = args
272
+
273
+ raise_error :create_table if options[:force]
274
+
275
+ # keep track of new table of add_index check
252
276
  @new_tables << table.to_s
253
277
  end
254
278
 
@@ -256,7 +280,7 @@ Then add the foreign key in separate migrations."
256
280
  raise_error :execute, header: "Possibly dangerous operation"
257
281
  end
258
282
 
259
- def check_remove_column(method, args)
283
+ def check_remove_column(method, *args)
260
284
  columns =
261
285
  case method
262
286
  when :remove_timestamps
@@ -288,20 +312,26 @@ Then add the foreign key in separate migrations."
288
312
  column_suffix: columns.size > 1 ? "s" : ""
289
313
  end
290
314
 
291
- def check_remove_index(args)
292
- table, options = args
293
- unless options.is_a?(Hash)
294
- options = {column: options}
295
- end
296
- options ||= {}
315
+ def check_remove_index(*args)
316
+ options = args.extract_options!
317
+ table, _ = args
297
318
 
298
319
  if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
320
+ # avoid suggesting extra (invalid) args
321
+ args = args[0..1] unless StrongMigrations.safe_by_default
322
+
323
+ # Active Record < 6.1 only supports two arguments (including options)
324
+ if args.size == 2 && ar_version < 6.1
325
+ # arg takes precedence over option
326
+ options[:column] = args.pop
327
+ end
328
+
299
329
  if StrongMigrations.safe_by_default
300
- safe_remove_index(table, options)
330
+ safe_remove_index(*args, **options)
301
331
  throw :safe
302
332
  end
303
333
 
304
- raise_error :remove_index, command: command_str("remove_index", [table, options.merge(algorithm: :concurrently)])
334
+ raise_error :remove_index, command: command_str("remove_index", args + [options.merge(algorithm: :concurrently)])
305
335
  end
306
336
  end
307
337
 
@@ -390,9 +420,14 @@ Then add the foreign key in separate migrations."
390
420
  "#{command} #{str_args.join(", ")}"
391
421
  end
392
422
 
393
- def backfill_code(table, column, default)
423
+ def backfill_code(table, column, default, function = false)
394
424
  model = table.to_s.classify
395
- "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
425
+ if function
426
+ # update_all(column: Arel.sql(default)) also works in newer versions of Active Record
427
+ "#{model}.unscoped.in_batches do |relation| \n relation.where(#{column}: nil).update_all(\"#{column} = #{default}\")\n sleep(0.01)\n end"
428
+ else
429
+ "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
430
+ end
396
431
  end
397
432
 
398
433
  def new_table?(table)
@@ -1,7 +1,7 @@
1
1
  module StrongMigrations
2
2
  self.error_messages = {
3
3
  add_column_default:
4
- "Adding a column with a non-null default blocks %{rewrite_blocks} while the entire table is rewritten.
4
+ "Adding a column with a %{default_type} default blocks %{rewrite_blocks} while the entire table is rewritten.
5
5
  Instead, add the column without a default value, then change the default.
6
6
 
7
7
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
@@ -128,6 +128,11 @@ end",
128
128
  "Adding a non-unique index with more than three columns rarely improves performance.
129
129
  Instead, start an index with columns that narrow down the results the most.",
130
130
 
131
+ add_index_corruption:
132
+ "Adding an index concurrently can cause silent data corruption in Postgres 14.0 to 14.3.
133
+ Upgrade Postgres before adding new indexes, or wrap this step in a safety_assured { ... } block
134
+ to accept the risk.",
135
+
131
136
  change_table:
132
137
  "Strong Migrations does not support inspecting what happens inside a
133
138
  change_table block, so cannot help you here. Please make really sure that what
@@ -5,22 +5,22 @@ module StrongMigrations
5
5
  end
6
6
 
7
7
  # TODO check if invalid index with expected name exists and remove if needed
8
- def safe_add_index(table, columns, options)
8
+ def safe_add_index(*args, **options)
9
9
  disable_transaction
10
- @migration.add_index(table, columns, **options.merge(algorithm: :concurrently))
10
+ @migration.add_index(*args, **options.merge(algorithm: :concurrently))
11
11
  end
12
12
 
13
- def safe_remove_index(table, options)
13
+ def safe_remove_index(*args, **options)
14
14
  disable_transaction
15
- @migration.remove_index(table, **options.merge(algorithm: :concurrently))
15
+ @migration.remove_index(*args, **options.merge(algorithm: :concurrently))
16
16
  end
17
17
 
18
- def safe_add_reference(table, reference, options)
18
+ def safe_add_reference(table, reference, *args, **options)
19
19
  @migration.reversible do |dir|
20
20
  dir.up do
21
21
  disable_transaction
22
22
  foreign_key = options.delete(:foreign_key)
23
- @migration.add_reference(table, reference, **options)
23
+ @migration.add_reference(table, reference, *args, **options)
24
24
  if foreign_key
25
25
  # same as Active Record
26
26
  name =
@@ -43,10 +43,10 @@ module StrongMigrations
43
43
  end
44
44
  end
45
45
 
46
- def safe_add_foreign_key(from_table, to_table, options)
46
+ def safe_add_foreign_key(from_table, to_table, *args, **options)
47
47
  @migration.reversible do |dir|
48
48
  dir.up do
49
- @migration.add_foreign_key(from_table, to_table, **options.merge(validate: false))
49
+ @migration.add_foreign_key(from_table, to_table, *args, **options.merge(validate: false))
50
50
  disable_transaction
51
51
  @migration.validate_foreign_key(from_table, to_table)
52
52
  end
@@ -56,10 +56,10 @@ module StrongMigrations
56
56
  end
57
57
  end
58
58
 
59
- def safe_add_check_constraint(table, expression, add_options, validate_options)
59
+ def safe_add_check_constraint(table, expression, *args, add_options, validate_options)
60
60
  @migration.reversible do |dir|
61
61
  dir.up do
62
- @migration.add_check_constraint(table, expression, **add_options)
62
+ @migration.add_check_constraint(table, expression, *args, **add_options)
63
63
  disable_transaction
64
64
  @migration.validate_check_constraint(table, **validate_options)
65
65
  end
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "1.0.0"
2
+ VERSION = "1.3.0"
3
3
  end
@@ -1,5 +1,5 @@
1
1
  namespace :strong_migrations do
2
- # https://www.pgrs.net/2008/03/13/alphabetize-schema-rb-columns/
2
+ # https://www.pgrs.net/2008/03/12/alphabetize-schema-rb-columns/
3
3
  task :alphabetize_columns do
4
4
  $stderr.puts "Dumping schema"
5
5
  ActiveRecord::Base.logger.level = Logger::INFO
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: 1.0.0
4
+ version: 1.3.0
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: 2022-03-21 00:00:00.000000000 Z
13
+ date: 2022-08-30 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord