pg_ha_migrations 1.0.0 → 1.1.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/.pryrc +21 -0
- data/.travis.yml +3 -1
- data/README.md +33 -3
- data/lib/pg_ha_migrations/blocking_database_transactions_reporter.rb +1 -3
- data/lib/pg_ha_migrations/dependent_objects_checks.rb +61 -0
- data/lib/pg_ha_migrations/safe_statements.rb +13 -12
- data/lib/pg_ha_migrations/unsafe_statements.rb +85 -89
- data/lib/pg_ha_migrations/version.rb +1 -1
- data/lib/pg_ha_migrations.rb +19 -0
- data/pg_ha_migrations.gemspec +2 -1
- metadata +33 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b472247321efcc716bc1f7c4e497dcbe32058e4d2ce11d8f6d0d9fea7136755
|
4
|
+
data.tar.gz: 820bf96e3200deb2cffd30ba5534513600d05138f57f7812539cc8b33f96058a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b95318b6bf16ea1f5c9ec2514a6d6cccf912fc051caa9a954c58a038431d9afbbf01a23576f4ceaab1856922dc6e9aee5573743abb619f494c0f6023dcb217a9
|
7
|
+
data.tar.gz: 8bf47117312865cf57b51d200f3d131c266171a0f9899f39b8aa3f09aad50842555d93007a0b33a00873a7d67ec56a0f9b9027a028bf28338399939cd285ceda
|
data/.pryrc
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
if defined?(PryByebug)
|
2
|
+
Pry.commands.alias_command 'c', 'continue'
|
3
|
+
Pry.commands.alias_command 's', 'step'
|
4
|
+
Pry.commands.alias_command 'n', 'next'
|
5
|
+
Pry.commands.alias_command 'f', 'finish'
|
6
|
+
end
|
7
|
+
|
8
|
+
# https://github.com/pry/pry/issues/1275#issuecomment-131969510
|
9
|
+
# Prevent issue where text input does not display on screen in container after typing Ctrl-C in a pry repl
|
10
|
+
at_exit do
|
11
|
+
exit!(1)
|
12
|
+
end
|
13
|
+
|
14
|
+
trap('INT') do
|
15
|
+
begin
|
16
|
+
Pry.run_command "continue", :show_output => true, :target => Pry.current
|
17
|
+
rescue
|
18
|
+
exit
|
19
|
+
end
|
20
|
+
end
|
21
|
+
# End pry Ctrl-C workaround
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -48,11 +48,25 @@ and never use `def change`. We believe that this is the only safe approach in pr
|
|
48
48
|
|
49
49
|
### Migrations
|
50
50
|
|
51
|
-
|
51
|
+
There are two major classes of concerns we try to handle in the API:
|
52
52
|
|
53
|
-
|
53
|
+
- Database safety (e.g., long-held locks)
|
54
|
+
- Application safety (e.g., dropping columns the app uses)
|
54
55
|
|
55
|
-
|
56
|
+
We rename migration methods with prefixes denoting their safety level:
|
57
|
+
|
58
|
+
- `safe_*`: These methods check for both application and database safety concerns prefer concurrent operations where available, set low lock timeouts where appropriate, and decompose operations into multiple safe steps.
|
59
|
+
- `unsafe_*`: These methods are generally a direct dispatch to the native ActiveRecord migration method.
|
60
|
+
|
61
|
+
Calling the original migration methods without a prefix will raise an error.
|
62
|
+
|
63
|
+
The API is designed to be explicit yet remain flexible. There may be situations where invoking the `unsafe_*` method is preferred (or the only option available for definitionally unsafe operations).
|
64
|
+
|
65
|
+
While `unsafe_*` methods were historically (through 1.0) pure wrappers for invoking the native ActiveRecord migration method, there is a class of problems that we can't handle easily without breaking that design rule a bit. For example, dropping a column is unsafe from an application perspective, so we make the application safety concerns explicit by using an `unsafe_` prefix. Using `unsafe_remove_column` calls out the need to audit the application to confirm the migration won't break the application. Because there are no safe alternatives we don't define a `safe_remove_column` analogue. However there are still conditions we'd like to assert before dropping a column. For example, dropping an unused column that's used in one or more indexes may be safe from an application perspective, but the cascading drop of the index won't use a `CONCURRENT` operation to drop the dependent indexes and is therefore unsafe from a database perspective.
|
66
|
+
|
67
|
+
When `unsafe_*` migration methods support checks of this type you can bypass the checks by passing an `:allow_dependent_objects` key in the method's `options` hash containing an array of dependent object types you'd like to allow. Until 2.0 none of these checks will run by default, but you can opt-in by setting `config.check_for_dependent_objects = true` [in your configuration initializer](#configuration).
|
68
|
+
|
69
|
+
Similarly we believe the `force: true` option to ActiveRecord's `create_table` method is always unsafe, and therefore we disallow it even when calling `unsafe_create_table`. This option won't be enabled by default until 2.0, but you can opt-in by setting `config.allow_force_create_table = false` [in your configuration initializer](#configuration).
|
56
70
|
|
57
71
|
[Running multiple DDL statements inside a transaction acquires exclusive locks on all of the modified objects](https://medium.com/braintree-product-technology/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680#cc22). For that reason, this gem [disables DDL transactions](./lib/pg_ha_migrations.rb:8) by default. You can change this by resetting `ActiveRecord::Migration.disable_ddl_transaction` in your application.
|
58
72
|
|
@@ -195,6 +209,20 @@ Set maintenance work mem.
|
|
195
209
|
safe_set_maintenance_work_mem_gb 1
|
196
210
|
```
|
197
211
|
|
212
|
+
### Configuration
|
213
|
+
|
214
|
+
The gem can be configured in an initializer.
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
PgHaMigrations.configure do |config|
|
218
|
+
# ...
|
219
|
+
end
|
220
|
+
```
|
221
|
+
|
222
|
+
#### Available options
|
223
|
+
|
224
|
+
- `disable_default_migration_methods`: If true, the default implementations of DDL changes in `ActiveRecord::Migration` and the PostgreSQL adapter will be overridden by implementations that raise a `PgHaMigrations::UnsafeMigrationError`. Default: `true`
|
225
|
+
- `check_for_dependent_objects`: If true, some `unsafe_*` migration methods will raise a `PgHaMigrations::UnsafeMigrationError` if any dependent objects exist. Default: `false`
|
198
226
|
|
199
227
|
### Rake Tasks
|
200
228
|
|
@@ -220,6 +248,8 @@ Rake::Task["pg_ha_migrations:check_blocking_database_transactions"].enhance ["db
|
|
220
248
|
|
221
249
|
After checking out the repo, run `bin/setup` to install dependencies and start a postgres docker container. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
222
250
|
|
251
|
+
Running tests will automatically create a test database in the locally running Postgres server. You can find the connection parameters in `spec/spec_helper.rb`, but setting the environment variables `PGHOST`, `PGPORT`, `PGUSER`, and `PGPASSWORD` will override the defaults.
|
252
|
+
|
223
253
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
224
254
|
|
225
255
|
## Contributing
|
@@ -29,9 +29,7 @@ module PgHaMigrations
|
|
29
29
|
migrations in this deploy that will attempt to create additional
|
30
30
|
concurrent indexes on the same physical database (even if the table
|
31
31
|
being indexes is on another dimension) those migrations will not be
|
32
|
-
able to complete until the in-progress index creations finish
|
33
|
-
|
34
|
-
For more information, see #service-db in Slack.\n
|
32
|
+
able to complete until the in-progress index creations finish.\n
|
35
33
|
eos
|
36
34
|
report << "\n" # Blank line intentional
|
37
35
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module PgHaMigrations::DependentObjectsChecks
|
2
|
+
ALLOWED_TYPES_OPTIONS = [:indexes].freeze
|
3
|
+
|
4
|
+
ObjectDependency = Struct.new(:owner_type, :owner_name, :dependent_type, :dependent_name) do
|
5
|
+
def error_text
|
6
|
+
"#{dependent_type} '#{dependent_name}' depends on #{owner_type} '#{owner_name}'"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def dependent_objects_for_migration_method(method_name, arguments:)
|
11
|
+
allowed_types = arguments.last.is_a?(Hash) ? arguments.last[:allow_dependent_objects] || [] : []
|
12
|
+
|
13
|
+
if (invalid_allowed_types = allowed_types - ALLOWED_TYPES_OPTIONS).present?
|
14
|
+
raise ArgumentError, "Received invalid entries in allow_dependent_objects: #{invalid_allowed_types.inspect}"
|
15
|
+
end
|
16
|
+
|
17
|
+
dependent_objects = []
|
18
|
+
|
19
|
+
case method_name
|
20
|
+
when :remove_column
|
21
|
+
table_name = arguments[0]
|
22
|
+
column_name = arguments[1]
|
23
|
+
|
24
|
+
unless allowed_types.include?(:indexes)
|
25
|
+
# https://www.postgresql.org/docs/current/catalog-pg-depend.html
|
26
|
+
# deptype:
|
27
|
+
# - 'a' (DEPENDENCY_AUTO): the dependent object should be automatically
|
28
|
+
# dropped if the referenced object is dropped
|
29
|
+
indexes = ActiveRecord::Base.structs_from_sql(ObjectDependency, <<~SQL)
|
30
|
+
SELECT
|
31
|
+
'column' AS owner_type,
|
32
|
+
ref_attr.attname AS owner_name,
|
33
|
+
'index' AS dependent_type,
|
34
|
+
dep_class.relname AS dependent_name
|
35
|
+
FROM pg_catalog.pg_depend
|
36
|
+
JOIN pg_catalog.pg_class ref_class ON ref_class.oid = pg_depend.refobjid
|
37
|
+
JOIN pg_catalog.pg_attribute ref_attr ON ref_attr.attrelid = ref_class.oid
|
38
|
+
AND ref_attr.attnum = pg_depend.refobjsubid
|
39
|
+
JOIN pg_catalog.pg_class dep_class ON dep_class.oid = pg_depend.objid
|
40
|
+
WHERE pg_depend.deptype = 'a'
|
41
|
+
AND pg_depend.refclassid = 'pg_class'::regclass
|
42
|
+
-- This doesn't currently handle table names that are duplicative across schemas.
|
43
|
+
AND ref_class.relname = '#{PG::Connection.escape_string(table_name.to_s)}'
|
44
|
+
AND ref_attr.attname = '#{PG::Connection.escape_string(column_name.to_s)}'
|
45
|
+
AND pg_depend.classid = 'pg_class'::regclass
|
46
|
+
SQL
|
47
|
+
dependent_objects.concat(indexes)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def disallow_migration_method_if_dependent_objects!(method_name, arguments:)
|
53
|
+
dependent_objects = dependent_objects_for_migration_method(method_name, arguments: arguments)
|
54
|
+
|
55
|
+
if dependent_objects.present?
|
56
|
+
raise PgHaMigrations::UnsafeMigrationError, dependent_objects.map(&:error_text).join("; ")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
ActiveRecord::Migration.prepend(PgHaMigrations::DependentObjectsChecks)
|
@@ -1,5 +1,9 @@
|
|
1
1
|
module PgHaMigrations::SafeStatements
|
2
2
|
def safe_create_table(table, options={}, &block)
|
3
|
+
if options[:force]
|
4
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":force is NOT SAFE! Explicitly call unsafe_drop_table first if you want to recreate an existing table")
|
5
|
+
end
|
6
|
+
|
3
7
|
unsafe_create_table(table, options, &block)
|
4
8
|
end
|
5
9
|
|
@@ -38,20 +42,17 @@ module PgHaMigrations::SafeStatements
|
|
38
42
|
end
|
39
43
|
end
|
40
44
|
|
41
|
-
def safe_change_column_default(
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
45
|
+
def safe_change_column_default(table_name, column_name, default_value)
|
46
|
+
column = connection.send(:column_for, table_name, column_name)
|
47
|
+
|
48
|
+
if default_value.present? &&
|
49
|
+
!default_value.is_a?(Proc) &&
|
50
|
+
quote_default_expression(default_value, column) == "NULL"
|
51
|
+
raise PgHaMigrations::InvalidMigrationError, "Requested new default value of <#{default_value}>, but that casts to NULL for the type <#{column.type}>. Did you mean to you mean to use a Proc instead?"
|
47
52
|
end
|
48
53
|
|
49
|
-
safely_acquire_lock_for_table(
|
50
|
-
|
51
|
-
ALTER TABLE #{PG::Connection.quote_ident(table.to_s)}
|
52
|
-
ALTER COLUMN #{PG::Connection.quote_ident(column.to_s)}
|
53
|
-
SET DEFAULT #{escaped_value}
|
54
|
-
SQL
|
54
|
+
safely_acquire_lock_for_table(table_name) do
|
55
|
+
unsafe_change_column_default(table_name, column_name, default_value)
|
55
56
|
end
|
56
57
|
end
|
57
58
|
|
@@ -1,114 +1,110 @@
|
|
1
1
|
module PgHaMigrations::UnsafeStatements
|
2
|
-
def self.
|
3
|
-
define_method(
|
4
|
-
|
5
|
-
|
6
|
-
# UnsafeStatements). If don't find the method in the inheritance chain,
|
7
|
-
# we need to rely on the ActiveRecord::Migration#method_missing
|
8
|
-
# implementation since much of ActiveRecord::Migration's functionality
|
9
|
-
# is not implemented in real methods but rather by proxying.
|
10
|
-
#
|
11
|
-
# For example, ActiveRecord::Migration doesn't define #create_table.
|
12
|
-
# Instead ActiveRecord::Migration#method_missing proxies the method
|
13
|
-
# to the connection. However some migration compatibility version
|
14
|
-
# subclasses _do_ explicitly define #create_table, so we can't rely
|
15
|
-
# on only one way of finding the proper dispatch target.
|
16
|
-
|
17
|
-
# Exclude our `raise` guard implementations.
|
18
|
-
ancestors_without_unsafe_statements = self.class.ancestors - [PgHaMigrations::UnsafeStatements]
|
19
|
-
|
20
|
-
delegate_method = self.method(method_name)
|
21
|
-
candidate_method = delegate_method
|
22
|
-
|
23
|
-
# Find the first usable method in the ancestor chain
|
24
|
-
# or stop looking if there are no more possible
|
25
|
-
# implementations.
|
26
|
-
until candidate_method.nil? || ancestors_without_unsafe_statements.include?(candidate_method.owner)
|
27
|
-
candidate_method = candidate_method.super_method
|
2
|
+
def self.disable_or_delegate_default_method(method_name, error_message, allow_reentry_from_compatibility_module: false)
|
3
|
+
define_method(method_name) do |*args, &block|
|
4
|
+
if PgHaMigrations.config.check_for_dependent_objects
|
5
|
+
disallow_migration_method_if_dependent_objects!(method_name, arguments: args)
|
28
6
|
end
|
29
7
|
|
30
|
-
if
|
31
|
-
|
8
|
+
if PgHaMigrations.config.disable_default_migration_methods
|
9
|
+
# Most migration methods are only ever called by a migration and
|
10
|
+
# therefore aren't re-entrant or callable from another migration
|
11
|
+
# method, but `execute` is called directly by at least one of the
|
12
|
+
# implementations in `ActiveRecord::Migration::Compatibility` so
|
13
|
+
# we have to explicitly handle that case by allowing execution of
|
14
|
+
# the original implementation by its original name.
|
15
|
+
unless allow_reentry_from_compatibility_module && caller[0] =~ /lib\/active_record\/migration\/compatibility.rb/
|
16
|
+
raise PgHaMigrations::UnsafeMigrationError, error_message
|
17
|
+
end
|
32
18
|
end
|
33
19
|
|
34
|
-
|
35
|
-
# inheritance chain, use ActiveRecord::Migrations# method_missing
|
36
|
-
# otherwise use the method from the inheritance chain.
|
37
|
-
if delegate_method.owner == PgHaMigrations::UnsafeStatements
|
38
|
-
method_missing(method_name, *args, &block)
|
39
|
-
else
|
40
|
-
delegate_method.call(*args, &block)
|
41
|
-
end
|
20
|
+
execute_ancestor_statement(method_name, *args, &block)
|
42
21
|
end
|
43
22
|
end
|
44
23
|
|
45
|
-
delegate_unsafe_method_to_migration_base_class
|
46
|
-
|
47
|
-
|
48
|
-
|
24
|
+
def self.delegate_unsafe_method_to_migration_base_class(method_name)
|
25
|
+
define_method("unsafe_#{method_name}") do |*args, &block|
|
26
|
+
if PgHaMigrations.config.check_for_dependent_objects
|
27
|
+
disallow_migration_method_if_dependent_objects!(method_name, arguments: args)
|
28
|
+
end
|
49
29
|
|
50
|
-
|
51
|
-
|
52
|
-
raise PgHaMigrations::UnsafeMigrationError.new(":add_column is NOT SAFE! Use safe_add_column instead")
|
30
|
+
execute_ancestor_statement(method_name, *args, &block)
|
31
|
+
end
|
53
32
|
end
|
54
33
|
|
34
|
+
delegate_unsafe_method_to_migration_base_class :add_column
|
55
35
|
delegate_unsafe_method_to_migration_base_class :change_table
|
56
|
-
def change_table(name, options={})
|
57
|
-
raise PgHaMigrations::UnsafeMigrationError.new(":change_table is NOT SAFE! Use a combination of safe and explicit unsafe migration methods instead")
|
58
|
-
end
|
59
|
-
|
60
36
|
delegate_unsafe_method_to_migration_base_class :drop_table
|
61
|
-
def drop_table(name)
|
62
|
-
raise PgHaMigrations::UnsafeMigrationError.new(":drop_table is NOT SAFE! Explicitly call :unsafe_drop_table to proceed")
|
63
|
-
end
|
64
|
-
|
65
37
|
delegate_unsafe_method_to_migration_base_class :rename_table
|
66
|
-
def rename_table(old_name, new_name)
|
67
|
-
raise PgHaMigrations::UnsafeMigrationError.new(":rename_table is NOT SAFE! Explicitly call :unsafe_rename_table to proceed")
|
68
|
-
end
|
69
|
-
|
70
38
|
delegate_unsafe_method_to_migration_base_class :rename_column
|
71
|
-
def rename_column(table_name, column_name, new_column_name)
|
72
|
-
raise PgHaMigrations::UnsafeMigrationError.new(":rename_column is NOT SAFE! Explicitly call :unsafe_rename_column to proceed")
|
73
|
-
end
|
74
|
-
|
75
39
|
delegate_unsafe_method_to_migration_base_class :change_column
|
76
|
-
|
77
|
-
raise PgHaMigrations::UnsafeMigrationError.new(":change_column is NOT SAFE! Use a combination of safe and explicit unsafe migration methods instead")
|
78
|
-
end
|
79
|
-
|
80
|
-
def change_column_null(table_name, column_name, null, default = nil)
|
81
|
-
raise PgHaMigrations::UnsafeMigrationError.new(<<-EOS.strip_heredoc)
|
82
|
-
:change_column_null is NOT (guaranteed to be) SAFE! Either use :safe_make_column_nullable or explicitly call :unsafe_make_column_not_nullable to proceed
|
83
|
-
EOS
|
84
|
-
end
|
85
|
-
|
40
|
+
delegate_unsafe_method_to_migration_base_class :change_column_default
|
86
41
|
delegate_unsafe_method_to_migration_base_class :remove_column
|
87
|
-
def remove_column(table_name, column_name, type, options={})
|
88
|
-
raise PgHaMigrations::UnsafeMigrationError.new(":remove_column is NOT SAFE! Explicitly call :unsafe_remove_column to proceed")
|
89
|
-
end
|
90
|
-
|
91
42
|
delegate_unsafe_method_to_migration_base_class :add_index
|
92
|
-
def add_index(table_name, column_names, options={})
|
93
|
-
raise PgHaMigrations::UnsafeMigrationError.new(":add_index is NOT SAFE! Use safe_add_concurrent_index instead")
|
94
|
-
end
|
95
|
-
|
96
43
|
delegate_unsafe_method_to_migration_base_class :execute
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
44
|
+
delegate_unsafe_method_to_migration_base_class :remove_index
|
45
|
+
delegate_unsafe_method_to_migration_base_class :add_foreign_key
|
46
|
+
|
47
|
+
disable_or_delegate_default_method :create_table, ":create_table is NOT SAFE! Use safe_create_table instead"
|
48
|
+
disable_or_delegate_default_method :add_column, ":add_column is NOT SAFE! Use safe_add_column instead"
|
49
|
+
disable_or_delegate_default_method :change_table, ":change_table is NOT SAFE! Use a combination of safe and explicit unsafe migration methods instead"
|
50
|
+
disable_or_delegate_default_method :drop_table, ":drop_table is NOT SAFE! Explicitly call :unsafe_drop_table to proceed"
|
51
|
+
disable_or_delegate_default_method :rename_table, ":rename_table is NOT SAFE! Explicitly call :unsafe_rename_table to proceed"
|
52
|
+
disable_or_delegate_default_method :rename_column, ":rename_column is NOT SAFE! Explicitly call :unsafe_rename_column to proceed"
|
53
|
+
disable_or_delegate_default_method :change_column, ":change_column is NOT SAFE! Use a combination of safe and explicit unsafe migration methods instead"
|
54
|
+
disable_or_delegate_default_method :change_column_default, ":change_column_default is NOT SAFE! Use safe_change_column_default instead"
|
55
|
+
disable_or_delegate_default_method :change_column_null, ":change_column_null is NOT (guaranteed to be) SAFE! Either use :safe_make_column_nullable or explicitly call :unsafe_make_column_not_nullable to proceed"
|
56
|
+
disable_or_delegate_default_method :remove_column, ":remove_column is NOT SAFE! Explicitly call :unsafe_remove_column to proceed"
|
57
|
+
disable_or_delegate_default_method :add_index, ":add_index is NOT SAFE! Use safe_add_concurrent_index instead"
|
58
|
+
disable_or_delegate_default_method :execute, ":execute is NOT SAFE! Explicitly call :unsafe_execute to proceed", allow_reentry_from_compatibility_module: true
|
59
|
+
disable_or_delegate_default_method :remove_index, ":remove_index is NOT SAFE! Use safe_remove_concurrent_index instead for Postgres 9.6 databases; Explicitly call :unsafe_remove_index to proceed on Postgres 9.1"
|
60
|
+
disable_or_delegate_default_method :add_foreign_key, ":add_foreign_key is NOT SAFE! Explicitly call :unsafe_add_foreign_key"
|
61
|
+
|
62
|
+
def unsafe_create_table(table, options={}, &block)
|
63
|
+
if options[:force] && !PgHaMigrations.config.allow_force_create_table
|
64
|
+
raise PgHaMigrations::UnsafeMigrationError.new(":force is NOT SAFE! Explicitly call unsafe_drop_table first if you want to recreate an existing table")
|
102
65
|
end
|
103
|
-
end
|
104
66
|
|
105
|
-
|
106
|
-
def remove_index(table_name, options={})
|
107
|
-
raise PgHaMigrations::UnsafeMigrationError.new(":remove_index is NOT SAFE! Use safe_remove_concurrent_index instead for Postgres 9.6 databases; Explicitly call :unsafe_remove_index to proceed on Postgres 9.1")
|
67
|
+
execute_ancestor_statement(:create_table, table, options, &block)
|
108
68
|
end
|
109
69
|
|
110
|
-
|
111
|
-
|
112
|
-
|
70
|
+
def execute_ancestor_statement(method_name, *args, &block)
|
71
|
+
# Dispatching here is a bit complicated: we need to execute the method
|
72
|
+
# belonging to the first member of the inheritance chain (besides
|
73
|
+
# UnsafeStatements). If don't find the method in the inheritance chain,
|
74
|
+
# we need to rely on the ActiveRecord::Migration#method_missing
|
75
|
+
# implementation since much of ActiveRecord::Migration's functionality
|
76
|
+
# is not implemented in real methods but rather by proxying.
|
77
|
+
#
|
78
|
+
# For example, ActiveRecord::Migration doesn't define #create_table.
|
79
|
+
# Instead ActiveRecord::Migration#method_missing proxies the method
|
80
|
+
# to the connection. However some migration compatibility version
|
81
|
+
# subclasses _do_ explicitly define #create_table, so we can't rely
|
82
|
+
# on only one way of finding the proper dispatch target.
|
83
|
+
|
84
|
+
# Exclude our `raise` guard implementations.
|
85
|
+
ancestors_without_unsafe_statements = self.class.ancestors - [PgHaMigrations::UnsafeStatements]
|
86
|
+
|
87
|
+
delegate_method = self.method(method_name)
|
88
|
+
candidate_method = delegate_method
|
89
|
+
|
90
|
+
# Find the first usable method in the ancestor chain
|
91
|
+
# or stop looking if there are no more possible
|
92
|
+
# implementations.
|
93
|
+
until candidate_method.nil? || ancestors_without_unsafe_statements.include?(candidate_method.owner)
|
94
|
+
candidate_method = candidate_method.super_method
|
95
|
+
end
|
96
|
+
|
97
|
+
if candidate_method
|
98
|
+
delegate_method = candidate_method
|
99
|
+
end
|
100
|
+
|
101
|
+
# If we failed to find a concrete implementation from the
|
102
|
+
# inheritance chain, use ActiveRecord::Migrations# method_missing
|
103
|
+
# otherwise use the method from the inheritance chain.
|
104
|
+
if delegate_method.owner == PgHaMigrations::UnsafeStatements
|
105
|
+
method_missing(method_name, *args, &block)
|
106
|
+
else
|
107
|
+
delegate_method.call(*args, &block)
|
108
|
+
end
|
113
109
|
end
|
114
110
|
end
|
data/lib/pg_ha_migrations.rb
CHANGED
@@ -5,6 +5,24 @@ require "active_record/migration"
|
|
5
5
|
require "relation_to_struct"
|
6
6
|
|
7
7
|
module PgHaMigrations
|
8
|
+
Config = Struct.new(
|
9
|
+
:disable_default_migration_methods,
|
10
|
+
:check_for_dependent_objects,
|
11
|
+
:allow_force_create_table
|
12
|
+
)
|
13
|
+
|
14
|
+
def self.config
|
15
|
+
@config ||= Config.new(
|
16
|
+
true,
|
17
|
+
false,
|
18
|
+
true
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.configure
|
23
|
+
yield config
|
24
|
+
end
|
25
|
+
|
8
26
|
LOCK_TIMEOUT_SECONDS = 5
|
9
27
|
LOCK_FAILURE_RETRY_DELAY_MULTLIPLIER = 5
|
10
28
|
|
@@ -25,6 +43,7 @@ require "pg_ha_migrations/blocking_database_transactions"
|
|
25
43
|
require "pg_ha_migrations/blocking_database_transactions_reporter"
|
26
44
|
require "pg_ha_migrations/unsafe_statements"
|
27
45
|
require "pg_ha_migrations/safe_statements"
|
46
|
+
require "pg_ha_migrations/dependent_objects_checks"
|
28
47
|
require "pg_ha_migrations/allowed_versions"
|
29
48
|
require "pg_ha_migrations/railtie"
|
30
49
|
require "pg_ha_migrations/hacks/disable_ddl_transaction"
|
data/pg_ha_migrations.gemspec
CHANGED
@@ -34,7 +34,8 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.add_development_dependency "rspec", "~> 3.0"
|
35
35
|
spec.add_development_dependency "pg"
|
36
36
|
spec.add_development_dependency "db-query-matchers", "~> 0.9.0"
|
37
|
-
|
37
|
+
spec.add_development_dependency 'pry'
|
38
|
+
spec.add_development_dependency 'pry-byebug'
|
38
39
|
|
39
40
|
spec.add_dependency "rails", ">= 5.0", "< 5.3"
|
40
41
|
spec.add_dependency "relation_to_struct"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg_ha_migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- celeen
|
@@ -14,7 +14,7 @@ authors:
|
|
14
14
|
autorequire:
|
15
15
|
bindir: exe
|
16
16
|
cert_chain: []
|
17
|
-
date: 2019-
|
17
|
+
date: 2019-09-05 00:00:00.000000000 Z
|
18
18
|
dependencies:
|
19
19
|
- !ruby/object:Gem::Dependency
|
20
20
|
name: bundler
|
@@ -86,6 +86,34 @@ dependencies:
|
|
86
86
|
- - "~>"
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: 0.9.0
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: pry
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: pry-byebug
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
89
117
|
- !ruby/object:Gem::Dependency
|
90
118
|
name: rails
|
91
119
|
requirement: !ruby/object:Gem::Requirement
|
@@ -129,6 +157,7 @@ extensions: []
|
|
129
157
|
extra_rdoc_files: []
|
130
158
|
files:
|
131
159
|
- ".gitignore"
|
160
|
+
- ".pryrc"
|
132
161
|
- ".rspec"
|
133
162
|
- ".ruby-version"
|
134
163
|
- ".travis.yml"
|
@@ -143,6 +172,7 @@ files:
|
|
143
172
|
- lib/pg_ha_migrations/allowed_versions.rb
|
144
173
|
- lib/pg_ha_migrations/blocking_database_transactions.rb
|
145
174
|
- lib/pg_ha_migrations/blocking_database_transactions_reporter.rb
|
175
|
+
- lib/pg_ha_migrations/dependent_objects_checks.rb
|
146
176
|
- lib/pg_ha_migrations/hacks/disable_ddl_transaction.rb
|
147
177
|
- lib/pg_ha_migrations/railtie.rb
|
148
178
|
- lib/pg_ha_migrations/safe_statements.rb
|
@@ -169,8 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
169
199
|
- !ruby/object:Gem::Version
|
170
200
|
version: '0'
|
171
201
|
requirements: []
|
172
|
-
|
173
|
-
rubygems_version: 2.7.8
|
202
|
+
rubygems_version: 3.0.3
|
174
203
|
signing_key:
|
175
204
|
specification_version: 4
|
176
205
|
summary: Enforces DDL/migration safety in Ruby on Rails project with an emphasis on
|