lib/strong_migrations/migration.rb in strong_migrations-0.4.0 vs lib/strong_migrations/migration.rb in strong_migrations-0.4.1

- old
+ new

@@ -1,229 +1,26 @@ module StrongMigrations module Migration - def safety_assured - previous_value = @safe - @safe = true - yield - ensure - @safe = previous_value + def initialize(*args) + super + @checker = StrongMigrations::Checker.new(self) end def migrate(direction) - @direction = direction + @checker.direction = direction super end - def method_missing(method, *args, &block) - unless @safe || ENV["SAFETY_ASSURED"] || is_a?(ActiveRecord::Schema) || @direction == :down || version_safe? - case method - when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to - columns = - case method - when :remove_timestamps - ["created_at", "updated_at"] - when :remove_column - [args[1].to_s] - when :remove_columns - args[1..-1].map(&:to_s) - else - options = args[2] || {} - reference = args[1] - cols = [] - cols << "#{reference}_type" if options[:polymorphic] - cols << "#{reference}_id" - cols - end - - code = "self.ignored_columns = #{columns.inspect}" - - raise_error :remove_column, - model: args[0].to_s.classify, - code: code, - command: command_str(method, args), - column_suffix: columns.size > 1 ? "s" : "" - when :change_table - raise_error :change_table, header: "Possibly dangerous operation" - when :rename_table - raise_error :rename_table - when :rename_column - raise_error :rename_column - when :add_index - table, columns, options = args - options ||= {} - - if columns.is_a?(Array) && columns.size > 3 && !options[:unique] - raise_error :add_index_columns, header: "Best practice" - end - if postgresql? && options[:algorithm] != :concurrently && !@new_tables.to_a.include?(table.to_s) - raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)]) - end - when :add_column - table, column, type, options = args - options ||= {} - default = options[:default] - - if !default.nil? && !(postgresql? && postgresql_version >= 110000) - - if options[:null] == false - options = options.except(:null) - append = " - -Then add the NOT NULL constraint. - -class %{migration_name}NotNull < ActiveRecord::Migration%{migration_suffix} - def change - #{command_str("change_column_null", [table, column, false])} - end -end" - end - - raise_error :add_column_default, - add_command: command_str("add_column", [table, column, type, options.except(:default)]), - change_command: command_str("change_column_default", [table, column, default]), - remove_command: command_str("remove_column", [table, column]), - code: backfill_code(table, column, default), - append: append - end - - if type.to_s == "json" && postgresql? - raise_error :add_column_json - end - when :change_column - table, column, type = args - - safe = false - # assume Postgres 9.1+ since previous versions are EOL - if postgresql? && type.to_s == "text" - found_column = connection.columns(table).find { |c| c.name.to_s == column.to_s } - safe = found_column && found_column.type == :string - end - raise_error :change_column unless safe - when :create_table - table, options = args - options ||= {} - - raise_error :create_table if options[:force] - - # keep track of new tables of add_index check - (@new_tables ||= []) << table.to_s - when :add_reference, :add_belongs_to - table, reference, options = args - options ||= {} - - index_value = options.fetch(:index, true) - if postgresql? && index_value - columns = options[:polymorphic] ? [:"#{reference}_type", :"#{reference}_id"] : :"#{reference}_id" - - raise_error :add_reference, - reference_command: command_str(method, [table, reference, options.merge(index: false)]), - index_command: command_str("add_index", [table, columns, {algorithm: :concurrently}]) - end - when :execute - raise_error :execute, header: "Possibly dangerous operation" - when :change_column_null - table, column, null, default = args - if !null && !default.nil? - raise_error :change_column_null, - code: backfill_code(table, column, default) - end - when :add_foreign_key - from_table, to_table, options = args - options ||= {} - validate = options.fetch(:validate, true) - - if postgresql? - if ActiveRecord::VERSION::STRING >= "5.2" - if validate - raise_error :add_foreign_key, - add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]), - validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table]) - end - else - # always validated before 5.2 - - # fk name logic from rails - primary_key = options[:primary_key] || "id" - column = options[:column] || "#{to_table.to_s.singularize}_id" - hashed_identifier = Digest::SHA256.hexdigest("#{from_table}_#{column}_fk").first(10) - fk_name = options[:name] || "fk_rails_#{hashed_identifier}" - - raise_error :add_foreign_key, - add_foreign_key_code: foreign_key_str("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) NOT VALID", [from_table, fk_name, column, to_table, primary_key]), - validate_foreign_key_code: foreign_key_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name]) - end - end - end - - StrongMigrations.checks.each do |check| - instance_exec(method, args, &check) - end + def method_missing(method, *args) + @checker.perform(method, *args) do + super end - - result = super - - if StrongMigrations.auto_analyze && @direction == :up && postgresql? && method == :add_index - connection.execute "ANALYZE VERBOSE #{connection.quote_table_name(args[0].to_s)}" - end - - result end - private - - def postgresql? - %w(PostgreSQL PostGIS).include?(connection.adapter_name) - end - - def postgresql_version - @postgresql_version ||= connection.execute("SHOW server_version_num").first["server_version_num"].to_i - end - - def version_safe? - version && version <= StrongMigrations.start_after - end - - def raise_error(message_key, header: nil, **vars) - message = StrongMigrations.error_messages[message_key] || "Missing message" - - vars[:migration_name] = self.class.name - vars[:migration_suffix] = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" - vars[:base_model] = "ApplicationRecord" - - # interpolate variables in appended code - if vars[:append] - vars[:append] = vars[:append].gsub(/%(?!{)/, "%%") % vars + def safety_assured + @checker.safety_assured do + yield end - - # escape % not followed by { - stop!(message.gsub(/%(?!{)/, "%%") % vars, header: header || "Dangerous operation detected") - end - - def foreign_key_str(statement, identifiers) - # not all identifiers are tables, but this method of quoting should be fine - code = statement % identifiers.map { |v| connection.quote_table_name(v) } - "safety_assured do\n execute '#{code}' \n end" - end - - def command_str(command, args) - str_args = args[0..-2].map { |a| a.inspect } - - # prettier last arg - last_arg = args[-1] - if last_arg.is_a?(Hash) - if last_arg.any? - str_args << last_arg.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") - end - else - str_args << last_arg.inspect - end - - "#{command} #{str_args.join(", ")}" - end - - def backfill_code(table, column, default) - model = table.to_s.classify - "#{model}.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.1)\n end" end def stop!(message, header: "Custom check") raise StrongMigrations::UnsafeMigration, "\n=== #{header} #strong_migrations ===\n\n#{message}\n" end