strong_migrations 0.5.1 → 0.6.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: '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