module PgHaMigrations::UnsafeStatements def self.disable_or_delegate_default_method(method_name, error_message, allow_reentry_from_compatibility_module: false) define_method(method_name) do |*args, &block| if PgHaMigrations.config.check_for_dependent_objects disallow_migration_method_if_dependent_objects!(method_name, arguments: args) end if PgHaMigrations.config.disable_default_migration_methods # Most migration methods are only ever called by a migration and # therefore aren't re-entrant or callable from another migration # method, but `execute` is called directly by at least one of the # implementations in `ActiveRecord::Migration::Compatibility` so # we have to explicitly handle that case by allowing execution of # the original implementation by its original name. unless allow_reentry_from_compatibility_module && caller[0] =~ /lib\/active_record\/migration\/compatibility.rb/ raise PgHaMigrations::UnsafeMigrationError, error_message end end execute_ancestor_statement(method_name, *args, &block) end ruby2_keywords method_name end def self.delegate_unsafe_method_to_migration_base_class(method_name) define_method("unsafe_#{method_name}") do |*args, &block| if PgHaMigrations.config.check_for_dependent_objects disallow_migration_method_if_dependent_objects!(method_name, arguments: args) end execute_ancestor_statement(method_name, *args, &block) end ruby2_keywords "unsafe_#{method_name}" end delegate_unsafe_method_to_migration_base_class :add_column delegate_unsafe_method_to_migration_base_class :change_table delegate_unsafe_method_to_migration_base_class :drop_table delegate_unsafe_method_to_migration_base_class :rename_table delegate_unsafe_method_to_migration_base_class :rename_column delegate_unsafe_method_to_migration_base_class :change_column delegate_unsafe_method_to_migration_base_class :change_column_default delegate_unsafe_method_to_migration_base_class :remove_column delegate_unsafe_method_to_migration_base_class :execute delegate_unsafe_method_to_migration_base_class :remove_index delegate_unsafe_method_to_migration_base_class :add_foreign_key delegate_unsafe_method_to_migration_base_class :remove_foreign_key disable_or_delegate_default_method :create_table, ":create_table is NOT SAFE! Use safe_create_table instead" disable_or_delegate_default_method :add_column, ":add_column is NOT SAFE! Use safe_add_column instead" disable_or_delegate_default_method :change_table, ":change_table is NOT SAFE! Use a combination of safe and explicit unsafe migration methods instead" disable_or_delegate_default_method :drop_table, ":drop_table is NOT SAFE! Explicitly call :unsafe_drop_table to proceed" disable_or_delegate_default_method :rename_table, ":rename_table is NOT SAFE! Explicitly call :unsafe_rename_table to proceed" disable_or_delegate_default_method :rename_column, ":rename_column is NOT SAFE! Explicitly call :unsafe_rename_column to proceed" disable_or_delegate_default_method :change_column, ":change_column is NOT SAFE! Use a combination of safe and explicit unsafe migration methods instead" disable_or_delegate_default_method :change_column_default, ":change_column_default is NOT SAFE! Use safe_change_column_default instead" 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" disable_or_delegate_default_method :remove_column, ":remove_column is NOT SAFE! Explicitly call :unsafe_remove_column to proceed" disable_or_delegate_default_method :add_index, ":add_index is NOT SAFE! Use safe_add_concurrent_index instead" disable_or_delegate_default_method :execute, ":execute is NOT SAFE! Explicitly call :unsafe_execute to proceed", allow_reentry_from_compatibility_module: true 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" disable_or_delegate_default_method :add_foreign_key, ":add_foreign_key is NOT SAFE! Explicitly call :unsafe_add_foreign_key" disable_or_delegate_default_method :remove_foreign_key, ":remove_foreign_key is NOT SAFE! Explicitly call :unsafe_remove_foreign_key" def unsafe_create_table(table, options={}, &block) if options[:force] && !PgHaMigrations.config.allow_force_create_table raise PgHaMigrations::UnsafeMigrationError.new(":force is NOT SAFE! Explicitly call unsafe_drop_table first if you want to recreate an existing table") end execute_ancestor_statement(:create_table, table, **options, &block) end def unsafe_add_index(table, column_names, options = {}) if ((ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 2) || ActiveRecord::VERSION::MAJOR > 5) && column_names.is_a?(String) && /\W/.match?(column_names) && options.key?(:opclass) raise PgHaMigrations::InvalidMigrationError, "ActiveRecord drops the :opclass option when supplying a string containing an expression or list of columns; instead either supply an array of columns or include the opclass in the string for each column" end execute_ancestor_statement(:add_index, table, column_names, **options) end ruby2_keywords def execute_ancestor_statement(method_name, *args, &block) # Dispatching here is a bit complicated: we need to execute the method # belonging to the first member of the inheritance chain (besides # UnsafeStatements). If don't find the method in the inheritance chain, # we need to rely on the ActiveRecord::Migration#method_missing # implementation since much of ActiveRecord::Migration's functionality # is not implemented in real methods but rather by proxying. # # For example, ActiveRecord::Migration doesn't define #create_table. # Instead ActiveRecord::Migration#method_missing proxies the method # to the connection. However some migration compatibility version # subclasses _do_ explicitly define #create_table, so we can't rely # on only one way of finding the proper dispatch target. # Exclude our `raise` guard implementations. ancestors_without_unsafe_statements = self.class.ancestors - [PgHaMigrations::UnsafeStatements] delegate_method = self.method(method_name) candidate_method = delegate_method # Find the first usable method in the ancestor chain # or stop looking if there are no more possible # implementations. until candidate_method.nil? || ancestors_without_unsafe_statements.include?(candidate_method.owner) candidate_method = candidate_method.super_method end if candidate_method delegate_method = candidate_method end # If we failed to find a concrete implementation from the # inheritance chain, use ActiveRecord::Migrations# method_missing # otherwise use the method from the inheritance chain. if delegate_method.owner == PgHaMigrations::UnsafeStatements method_missing(method_name, *args, &block) else delegate_method.call(*args, &block) end end end