strong_migrations 0.5.1 → 0.6.0

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: '04239dfae50098f46f93b7d09d8fa8ab767372f9b52a58db25902eb88e449e56'
4
- data.tar.gz: 5a7c529bde98344e6277dc6ab4b915f14c445468c0936998dab0da3376a6b953
3
+ metadata.gz: 858552e31803f119fa4df371348a710c20db26107ad55f66a553192fddefac47
4
+ data.tar.gz: a7646d2969c0051e9362103b037f6e46d0a878d521aa63e7811b5b23818cdb64
5
5
  SHA512:
6
- metadata.gz: adc856d7c80a2ecf601efac5170ce365269c6ac474fb33b390f00362e9068c676221b29c065b791d8730dd4c44937b12ab0c13f65b8bd92656a47182dd1cbf2a
7
- data.tar.gz: 838d682a173a8d69da91702f9373d21d8d769179fc5520b6b2a18ab55f91e2970584022cbdfe1492b8cc5f33e9602f9fd6d445ae4a2b17f4b25d3a629225de45
6
+ metadata.gz: 3127959b63c1f846a3bd4caf213fa1edc52ad3dc5fdec3d45fde1ffb8028f5e0a2c5b3fb4db2a696ffb26862262d2e866acc2ccb1eba80600fea2e5c16b43f22
7
+ data.tar.gz: 14a4fc2f946cfa05bfe86b2fcc390633691c354495f83eb9382ec8a0dfad7be0b72c1fdf813ea76522981b73336d2cd3cf15d2b3bd5a8004eed3d87d5b7e0327
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## 0.6.0 (2020-01-24)
2
+
3
+ - Added `statement_timeout` and `lock_timeout`
4
+ - Adding a column with a non-null default value is safe in MySQL 8.0.12+ and MariaDB 10.3.2+
5
+ - Added `change_column_null` check for MySQL and MariaDB
6
+ - Added `auto_analyze` for MySQL and MariaDB
7
+ - Added `target_mysql_version` and `target_mariadb_version`
8
+ - Switched to `up` for backfilling
9
+
1
10
  ## 0.5.1 (2019-12-17)
2
11
 
3
12
  - Fixed migration name in error messages
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Catch unsafe migrations in development
4
4
 
5
+ Supports for PostgreSQL, MySQL, and MariaDB
6
+
5
7
  :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
6
8
 
7
9
  [![Build Status](https://travis-ci.org/ankane/strong_migrations.svg?branch=master)](https://travis-ci.org/ankane/strong_migrations)
@@ -86,7 +88,7 @@ end
86
88
 
87
89
  ### Adding a column with a default value
88
90
 
89
- Note: This operation is safe in Postgres 11+.
91
+ Note: This operation is safe in Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+
90
92
 
91
93
  #### Bad
92
94
 
@@ -144,10 +146,10 @@ There are three keys to backfilling safely: batching, throttling, and running it
144
146
  class BackfillSomeColumn < ActiveRecord::Migration[6.0]
145
147
  disable_ddl_transaction!
146
148
 
147
- def change
149
+ def up
148
150
  User.unscoped.in_batches do |relation|
149
151
  relation.update_all some_column: "default_value"
150
- sleep(0.1) # throttle
152
+ sleep(0.01) # throttle
151
153
  end
152
154
  end
153
155
  end
@@ -181,7 +183,13 @@ class AddSomeIndexToUsers < ActiveRecord::Migration[6.0]
181
183
  end
182
184
  ```
183
185
 
184
- If you forget `disable_ddl_transaction!`, the migration will fail. Also, note that indexes on new tables (those created in the same migration) don’t require this. Check out [gindex](https://github.com/ankane/gindex) to quickly generate index migrations without memorizing the syntax.
186
+ If you forget `disable_ddl_transaction!`, the migration will fail. Also, note that indexes on new tables (those created in the same migration) don’t require this.
187
+
188
+ With [gindex](https://github.com/ankane/gindex), you can generate an index migration instantly with:
189
+
190
+ ```sh
191
+ rails g index table column
192
+ ```
185
193
 
186
194
  ### Adding a reference
187
195
 
@@ -614,9 +622,7 @@ StrongMigrations.error_messages[:add_column_default] = "Your custom instructions
614
622
 
615
623
  Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
616
624
 
617
- ## Postgres-Specific Features
618
-
619
- ### Analyze Tables
625
+ ## Analyze Tables
620
626
 
621
627
  Analyze tables automatically (to update planner statistics) after an index is added. Create an initializer with:
622
628
 
@@ -624,25 +630,41 @@ Analyze tables automatically (to update planner statistics) after an index is ad
624
630
  StrongMigrations.auto_analyze = true
625
631
  ```
626
632
 
627
- ### Lock Timeout
633
+ ## Target Version
628
634
 
629
- It’s a good idea to set a lock timeout for the database user that runs migrations. This way, if migrations can’t acquire a lock in a timely manner, other statements won’t be stuck behind it. Here’s a great explanation of [how lock queues work](https://www.citusdata.com/blog/2018/02/15/when-postgresql-blocks/).
635
+ If your development database version is different from production, you can specify the production version so the right checks are run in development.
630
636
 
631
- ```sql
632
- ALTER ROLE myuser SET lock_timeout = '10s';
637
+ ```ruby
638
+ StrongMigrations.target_postgresql_version = "10"
639
+ StrongMigrations.target_mysql_version = "8.0.12"
640
+ StrongMigrations.target_mariadb_version = "10.3.2"
633
641
  ```
634
642
 
635
- There’s also [a gem](https://github.com/gocardless/activerecord-safer_migrations) you can use for this.
643
+ For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
636
644
 
637
- ### Target Version
645
+ ## Timeouts
638
646
 
639
- If your development database version is different from production, you can specify the production version so the right checks are run in development.
647
+ It’s a good idea to set a long statement timeout and a short lock timeout for migrations. This way, migrations can run for a while, and if a migration can’t acquire a lock in a timely manner, other statements won’t be stuck behind it.
648
+
649
+ You can use:
640
650
 
641
651
  ```ruby
642
- StrongMigrations.target_postgresql_version = 10 # or 9.6, etc
652
+ StrongMigrations.statement_timeout = 1.hour
653
+ StrongMigrations.lock_timeout = 10.seconds
643
654
  ```
644
655
 
645
- For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
656
+ Or set the timeouts directly on the database user that runs migrations. For Postgres, use:
657
+
658
+ ```sql
659
+ ALTER ROLE myuser SET statement_timeout = '1h';
660
+ ALTER ROLE myuser SET lock_timeout = '10s';
661
+ ```
662
+
663
+ Note: If you use PgBouncer in transaction mode, you must set timeouts on the database user.
664
+
665
+ ## Permissions
666
+
667
+ We recommend using a [separate database user](https://ankane.org/postgres-users) for migrations when possible so you don’t need to grant your app user permission to alter tables.
646
668
 
647
669
  ## Additional Reading
648
670
 
@@ -662,7 +684,7 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
662
684
  - Write, clarify, or fix documentation
663
685
  - Suggest or add new features
664
686
 
665
- To get started with development and testing:
687
+ To get started with development:
666
688
 
667
689
  ```sh
668
690
  git clone https://github.com/ankane/strong_migrations.git
@@ -1,15 +1,24 @@
1
+ # dependencies
1
2
  require "active_support"
2
3
 
4
+ # modules
3
5
  require "strong_migrations/checker"
4
6
  require "strong_migrations/database_tasks"
5
7
  require "strong_migrations/migration"
6
- require "strong_migrations/railtie" if defined?(Rails)
7
- require "strong_migrations/unsafe_migration"
8
+ require "strong_migrations/migration_helpers"
8
9
  require "strong_migrations/version"
9
10
 
11
+ # integrations
12
+ require "strong_migrations/railtie" if defined?(Rails)
13
+
10
14
  module StrongMigrations
15
+ class Error < StandardError; end
16
+ class UnsafeMigration < Error; end
17
+
11
18
  class << self
12
- attr_accessor :auto_analyze, :start_after, :checks, :error_messages, :target_postgresql_version, :enabled_checks
19
+ attr_accessor :auto_analyze, :start_after, :checks, :error_messages,
20
+ :target_postgresql_version, :target_mysql_version, :target_mariadb_version,
21
+ :enabled_checks, :lock_timeout, :statement_timeout, :helpers
13
22
  end
14
23
  self.auto_analyze = false
15
24
  self.start_after = 0
@@ -35,7 +44,7 @@ Then backfill the existing rows in the Rails console or a separate migration wit
35
44
  class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
36
45
  disable_ddl_transaction!
37
46
 
38
- def change
47
+ def up
39
48
  %{code}
40
49
  end
41
50
  end%{append}",
@@ -55,7 +64,7 @@ table and indexes to be rewritten. A safer approach is to:
55
64
  5. Stop writing to the old column
56
65
  6. Drop the old column",
57
66
 
58
- remove_column: "ActiveRecord caches attributes which causes problems
67
+ remove_column: "Active Record caches attributes which causes problems
59
68
  when removing columns. Be sure to ignore the column%{column_suffix}:
60
69
 
61
70
  class %{model} < %{base_model}
@@ -150,7 +159,7 @@ Rails console or a separate migration with disable_ddl_transaction!.
150
159
  class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
151
160
  disable_ddl_transaction!
152
161
 
153
- def change
162
+ def up
154
163
  %{code}
155
164
  end
156
165
  end",
@@ -172,6 +181,22 @@ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
172
181
  end
173
182
  end",
174
183
 
184
+ change_column_null_postgresql_helper:
185
+ "Setting NOT NULL on a column requires an AccessExclusiveLock,
186
+ which is expensive on large tables. Instead, we can use a constraint and
187
+ validate it in a separate step with a more agreeable RowShareLock.
188
+
189
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
190
+ disable_ddl_transaction!
191
+
192
+ def change
193
+ %{command}
194
+ end
195
+ end",
196
+
197
+ change_column_null_mysql:
198
+ "Setting NOT NULL on an existing column is not safe with your database engine.",
199
+
175
200
  add_foreign_key:
176
201
  "New foreign keys are validated by default. This acquires an AccessExclusiveLock,
177
202
  which is expensive on large tables. Instead, validate it in a separate migration
@@ -188,8 +213,22 @@ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
188
213
  %{validate_foreign_key_code}
189
214
  end
190
215
  end",
216
+
217
+ add_foreign_key_helper:
218
+ "New foreign keys are validated by default. This acquires an AccessExclusiveLock,
219
+ which is expensive on large tables. Instead, we can validate it in a separate step
220
+ with a more agreeable RowShareLock.
221
+
222
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
223
+ disable_ddl_transaction!
224
+
225
+ def change
226
+ %{command}
227
+ end
228
+ end",
191
229
  }
192
230
  self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
231
+ self.helpers = false
193
232
 
194
233
  def self.add_check(&block)
195
234
  checks << block
@@ -211,6 +250,13 @@ end",
211
250
  false
212
251
  end
213
252
  end
253
+
254
+ # def self.enable_helpers
255
+ # unless helpers
256
+ # ActiveRecord::Migration.include(StrongMigrations::MigrationHelpers)
257
+ # self.helpers = true
258
+ # end
259
+ # end
214
260
  end
215
261
 
216
262
  ActiveSupport.on_load(:active_record) do
@@ -6,6 +6,7 @@ module StrongMigrations
6
6
  @migration = migration
7
7
  @new_tables = []
8
8
  @safe = false
9
+ @timeouts_set = false
9
10
  end
10
11
 
11
12
  def safety_assured
@@ -19,6 +20,8 @@ module StrongMigrations
19
20
  end
20
21
 
21
22
  def perform(method, *args)
23
+ set_timeouts
24
+
22
25
  unless safe?
23
26
  case method
24
27
  when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
@@ -77,7 +80,7 @@ module StrongMigrations
77
80
  options ||= {}
78
81
  default = options[:default]
79
82
 
80
- if !default.nil? && !(postgresql? && postgresql_version >= 110000)
83
+ if !default.nil? && !((postgresql? && postgresql_version >= Gem::Version.new("11")) || (mysql? && mysql_version >= Gem::Version.new("8.0.12")) || (mariadb? && mariadb_version >= Gem::Version.new("10.3.2")))
81
84
 
82
85
  if options[:null] == false
83
86
  options = options.except(:null)
@@ -139,12 +142,19 @@ Then add the NOT NULL constraint."
139
142
  table, column, null, default = args
140
143
  if !null
141
144
  if postgresql?
142
- # match https://github.com/nullobject/rein
143
- constraint_name = "#{table}_#{column}_null"
144
-
145
- raise_error :change_column_null_postgresql,
146
- add_constraint_code: constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column]),
147
- validate_constraint_code: constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
145
+ if helpers?
146
+ raise_error :change_column_null_postgresql_helper,
147
+ command: command_str(:add_null_constraint_safely, [table, column])
148
+ else
149
+ # match https://github.com/nullobject/rein
150
+ constraint_name = "#{table}_#{column}_null"
151
+
152
+ raise_error :change_column_null_postgresql,
153
+ add_constraint_code: constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column]),
154
+ validate_constraint_code: constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
155
+ end
156
+ elsif mysql? || mariadb?
157
+ raise_error :change_column_null_mysql
148
158
  elsif !default.nil?
149
159
  raise_error :change_column_null,
150
160
  code: backfill_code(table, column, default)
@@ -153,18 +163,15 @@ Then add the NOT NULL constraint."
153
163
  when :add_foreign_key
154
164
  from_table, to_table, options = args
155
165
  options ||= {}
156
- validate = options.fetch(:validate, true)
157
-
158
- if postgresql?
159
- if ActiveRecord::VERSION::STRING >= "5.2"
160
- if validate
161
- raise_error :add_foreign_key,
162
- add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
163
- validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
164
- end
165
- else
166
- # always validated before 5.2
167
166
 
167
+ # always validated before 5.2
168
+ validate = options.fetch(:validate, true) || ActiveRecord::VERSION::STRING < "5.2"
169
+
170
+ if postgresql? && validate
171
+ if helpers?
172
+ raise_error :add_foreign_key_helper,
173
+ command: command_str(:add_foreign_key_safely, [from_table, to_table, options])
174
+ elsif ActiveRecord::VERSION::STRING < "5.2"
168
175
  # fk name logic from rails
169
176
  primary_key = options[:primary_key] || "id"
170
177
  column = options[:column] || "#{to_table.to_s.singularize}_id"
@@ -174,6 +181,10 @@ Then add the NOT NULL constraint."
174
181
  raise_error :add_foreign_key,
175
182
  add_foreign_key_code: constraint_str("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) NOT VALID", [from_table, fk_name, column, to_table, primary_key]),
176
183
  validate_foreign_key_code: constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name])
184
+ else
185
+ raise_error :add_foreign_key,
186
+ add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
187
+ validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
177
188
  end
178
189
  end
179
190
  end
@@ -185,13 +196,51 @@ Then add the NOT NULL constraint."
185
196
 
186
197
  result = yield
187
198
 
188
- if StrongMigrations.auto_analyze && direction == :up && postgresql? && method == :add_index
189
- connection.execute "ANALYZE VERBOSE #{connection.quote_table_name(args[0].to_s)}"
199
+ if StrongMigrations.auto_analyze && direction == :up && method == :add_index
200
+ if postgresql?
201
+ connection.execute "ANALYZE VERBOSE #{connection.quote_table_name(args[0].to_s)}"
202
+ elsif mariadb? || mysql?
203
+ connection.execute "ANALYZE TABLE #{connection.quote_table_name(args[0].to_s)}"
204
+ end
190
205
  end
191
206
 
192
207
  result
193
208
  end
194
209
 
210
+ def set_timeouts
211
+ if !@timeouts_set
212
+ if StrongMigrations.statement_timeout
213
+ statement =
214
+ if postgresql?
215
+ "SET statement_timeout TO #{connection.quote(StrongMigrations.statement_timeout)}"
216
+ elsif mysql?
217
+ "SET max_execution_time = #{connection.quote(StrongMigrations.statement_timeout.to_i * 1000)}"
218
+ elsif mariadb?
219
+ "SET max_statement_time = #{connection.quote(StrongMigrations.statement_timeout)}"
220
+ else
221
+ raise StrongMigrations::Error, "Statement timeout not supported for this database"
222
+ end
223
+
224
+ connection.select_all(statement)
225
+ end
226
+
227
+ if StrongMigrations.lock_timeout
228
+ statement =
229
+ if postgresql?
230
+ "SET lock_timeout TO #{connection.quote(StrongMigrations.lock_timeout)}"
231
+ elsif mysql? || mariadb?
232
+ "SET lock_wait_timeout = #{connection.quote(StrongMigrations.lock_timeout)}"
233
+ else
234
+ raise StrongMigrations::Error, "Lock timeout not supported for this database"
235
+ end
236
+
237
+ connection.select_all(statement)
238
+ end
239
+
240
+ @timeouts_set = true
241
+ end
242
+ end
243
+
195
244
  private
196
245
 
197
246
  def connection
@@ -211,19 +260,53 @@ Then add the NOT NULL constraint."
211
260
  end
212
261
 
213
262
  def postgresql?
214
- %w(PostgreSQL PostGIS).include?(connection.adapter_name)
263
+ connection.adapter_name =~ /postg/i # PostgreSQL, PostGIS
215
264
  end
216
265
 
217
266
  def postgresql_version
218
267
  @postgresql_version ||= begin
219
- target_version = StrongMigrations.target_postgresql_version
268
+ target_version(StrongMigrations.target_postgresql_version) do
269
+ connection.select_all("SHOW server_version").first["server_version"]
270
+ end
271
+ end
272
+ end
273
+
274
+ def mysql?
275
+ connection.adapter_name =~ /mysql/i && !connection.try(:mariadb?)
276
+ end
277
+
278
+ def mysql_version
279
+ @mysql_version ||= begin
280
+ target_version(StrongMigrations.target_mysql_version) do
281
+ connection.select_all("SELECT VERSION()").first["VERSION()"].split("-").first
282
+ end
283
+ end
284
+ end
285
+
286
+ def mariadb?
287
+ connection.adapter_name =~ /mysql/i && connection.try(:mariadb?)
288
+ end
289
+
290
+ def mariadb_version
291
+ @mariadb_version ||= begin
292
+ target_version(StrongMigrations.target_mariadb_version) do
293
+ connection.select_all("SELECT VERSION()").first["VERSION()"].split("-").first
294
+ end
295
+ end
296
+ end
297
+
298
+ def target_version(target_version)
299
+ version =
220
300
  if target_version && defined?(Rails) && (Rails.env.development? || Rails.env.test?)
221
- # we only need major version right now
222
- target_version.to_i * 10000
301
+ target_version.to_s
223
302
  else
224
- connection.execute("SHOW server_version_num").first["server_version_num"].to_i
303
+ yield
225
304
  end
226
- end
305
+ Gem::Version.new(version)
306
+ end
307
+
308
+ def helpers?
309
+ StrongMigrations.helpers
227
310
  end
228
311
 
229
312
  def raise_error(message_key, header: nil, **vars)
@@ -275,7 +358,7 @@ Then add the NOT NULL constraint."
275
358
 
276
359
  def backfill_code(table, column, default)
277
360
  model = table.to_s.classify
278
- "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.1)\n end"
361
+ "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
279
362
  end
280
363
 
281
364
  def new_table?(table)
@@ -3,7 +3,7 @@ module StrongMigrations
3
3
  def migrate
4
4
  super
5
5
  rescue => e
6
- if e.cause.is_a?(StrongMigrations::UnsafeMigration)
6
+ if e.cause.is_a?(StrongMigrations::Error)
7
7
  # strip cause and clean backtrace
8
8
  def e.cause
9
9
  nil
@@ -0,0 +1,117 @@
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
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.5.1"
2
+ VERSION = "0.6.0"
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.5.1
4
+ version: 0.6.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: 2019-12-18 00:00:00.000000000 Z
13
+ date: 2020-01-24 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -99,8 +99,8 @@ files:
99
99
  - lib/strong_migrations/checker.rb
100
100
  - lib/strong_migrations/database_tasks.rb
101
101
  - lib/strong_migrations/migration.rb
102
+ - lib/strong_migrations/migration_helpers.rb
102
103
  - lib/strong_migrations/railtie.rb
103
- - lib/strong_migrations/unsafe_migration.rb
104
104
  - lib/strong_migrations/version.rb
105
105
  - lib/tasks/strong_migrations.rake
106
106
  homepage: https://github.com/ankane/strong_migrations
@@ -122,7 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
122
  - !ruby/object:Gem::Version
123
123
  version: '0'
124
124
  requirements: []
125
- rubygems_version: 3.0.3
125
+ rubygems_version: 3.1.2
126
126
  signing_key:
127
127
  specification_version: 4
128
128
  summary: Catch unsafe migrations in development
@@ -1,4 +0,0 @@
1
- module StrongMigrations
2
- class UnsafeMigration < StandardError
3
- end
4
- end