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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +39 -17
- data/lib/strong_migrations.rb +52 -6
- data/lib/strong_migrations/checker.rb +110 -27
- data/lib/strong_migrations/database_tasks.rb +1 -1
- data/lib/strong_migrations/migration_helpers.rb +117 -0
- data/lib/strong_migrations/version.rb +1 -1
- metadata +4 -4
- data/lib/strong_migrations/unsafe_migration.rb +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 858552e31803f119fa4df371348a710c20db26107ad55f66a553192fddefac47
|
4
|
+
data.tar.gz: a7646d2969c0051e9362103b037f6e46d0a878d521aa63e7811b5b23818cdb64
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
149
|
+
def up
|
148
150
|
User.unscoped.in_batches do |relation|
|
149
151
|
relation.update_all some_column: "default_value"
|
150
|
-
sleep(0.
|
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.
|
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
|
-
##
|
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
|
-
|
633
|
+
## Target Version
|
628
634
|
|
629
|
-
|
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
|
-
```
|
632
|
-
|
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
|
-
|
643
|
+
For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
|
636
644
|
|
637
|
-
|
645
|
+
## Timeouts
|
638
646
|
|
639
|
-
|
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.
|
652
|
+
StrongMigrations.statement_timeout = 1.hour
|
653
|
+
StrongMigrations.lock_timeout = 10.seconds
|
643
654
|
```
|
644
655
|
|
645
|
-
|
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
|
687
|
+
To get started with development:
|
666
688
|
|
667
689
|
```sh
|
668
690
|
git clone https://github.com/ankane/strong_migrations.git
|
data/lib/strong_migrations.rb
CHANGED
@@ -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/
|
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,
|
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
|
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: "
|
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
|
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 >=
|
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
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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 &&
|
189
|
-
|
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
|
-
|
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
|
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
|
-
|
222
|
-
target_version.to_i * 10000
|
301
|
+
target_version.to_s
|
223
302
|
else
|
224
|
-
|
303
|
+
yield
|
225
304
|
end
|
226
|
-
|
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.
|
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)
|
@@ -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
|
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.
|
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:
|
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.
|
125
|
+
rubygems_version: 3.1.2
|
126
126
|
signing_key:
|
127
127
|
specification_version: 4
|
128
128
|
summary: Catch unsafe migrations in development
|