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 +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
|
[](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
|