pg_ha_migrations 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|