lib/strong_migrations/migration.rb in strong_migrations-0.2.3 vs lib/strong_migrations/migration.rb in strong_migrations-0.3.0

- old
+ new

@@ -12,75 +12,141 @@ @direction = direction super end def method_missing(method, *args, &block) + table = args[0].to_s + unless @safe || ENV["SAFETY_ASSURED"] || is_a?(ActiveRecord::Schema) || @direction == :down || version_safe? + ar5 = ActiveRecord::VERSION::MAJOR >= 5 + model = table.classify + case method - when :remove_column - raise_error :remove_column - when :remove_timestamps - raise_error :remove_column + 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 = ar5 ? "self.ignored_columns = #{columns.inspect}" : "def self.columns\n super.reject { |c| #{columns.inspect}.include?(c.name) }\n end" + + command = String.new("#{method} #{sym_str(table)}") + case method + when :remove_column, :remove_reference, :remove_belongs_to + command << ", #{sym_str(args[1])}#{options_str(args[2] || {})}" + when :remove_columns + columns.each do |c| + command << ", #{sym_str(c)}" + end + end + + raise_error :remove_column, + model: model, + code: code, + command: command, + column_suffix: columns.size > 1 ? "s" : "" when :change_table - raise_error :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 columns = args[1] options = args[2] || {} if columns.is_a?(Array) && columns.size > 3 && !options[:unique] - raise_error :add_index_columns + raise_error :add_index_columns, header: "Best practice" end - if postgresql? && options[:algorithm] != :concurrently && !@new_tables.to_a.include?(args[0].to_s) - raise_error :add_index + if postgresql? && options[:algorithm] != :concurrently && !@new_tables.to_a.include?(table) + raise_error :add_index, + table: sym_str(table), + column: column_str(columns), + options: options_str(options.except(:algorithm)) end when :add_column + column = args[1] type = args[2] options = args[3] || {} - raise_error :add_column_default unless options[:default].nil? + default = options[:default] + + if !default.nil? && !(postgresql? && postgresql_version >= 110000) + raise_error :add_column_default, + table: sym_str(table), + column: sym_str(column), + type: sym_str(type), + options: options_str(options.except(:default)), + default: default.inspect, + code: backfill_code(model, column, default) + end + if type.to_s == "json" && postgresql? if postgresql_version >= 90400 raise_error :add_column_json else - raise_error :add_column_json_legacy + raise_error :add_column_json_legacy, + model: model, + table: connection.quote_table_name(table) end end when :change_column safe = false # assume Postgres 9.1+ since previous versions are EOL if postgresql? && args[2].to_s == "text" - column = connection.columns(args[0]).find { |c| c.name.to_s == args[1].to_s } + column = connection.columns(table).find { |c| c.name.to_s == args[1].to_s } safe = column && column.type == :string end raise_error :change_column unless safe when :create_table options = args[1] || {} raise_error :create_table if options[:force] - (@new_tables ||= []) << args[0].to_s - when :add_reference + (@new_tables ||= []) << table + when :add_reference, :add_belongs_to options = args[2] || {} - index_value = options.fetch(:index, ActiveRecord::VERSION::MAJOR >= 5 ? true : false) + index_value = options.fetch(:index, ar5) if postgresql? && index_value - raise_error :add_reference + reference = args[1] + columns = [] + columns << "#{reference}_type" if options[:polymorphic] + columns << "#{reference}_id" + raise_error :add_reference, + command: method, + table: sym_str(table), + reference: sym_str(reference), + column: column_str(columns), + options: options_str(options.except(:index)) end when :execute - raise_error :execute + raise_error :execute, header: "Possibly dangerous operation" when :change_column_null - null = args[2] - default = args[3] + _, column, null, default = args if !null && !default.nil? - raise_error :change_column_null + raise_error :change_column_null, + code: backfill_code(model, column, default) end end + + StrongMigrations.checks.each do |check| + instance_exec(method, args, &check) + end end result = super if StrongMigrations.auto_analyze && @direction == :up && postgresql? && method == :add_index - connection.execute "ANALYZE VERBOSE #{connection.quote_table_name(args[0])}" + connection.execute "ANALYZE VERBOSE #{connection.quote_table_name(table)}" end result end @@ -96,20 +162,48 @@ def version_safe? version && version <= StrongMigrations.start_after end - def raise_error(message_key) - wait_message = ' - __ __ _____ _______ _ - \ \ / /\ |_ _|__ __| | - \ \ /\ / / \ | | | | | | - \ \/ \/ / /\ \ | | | | | | - \ /\ / ____ \ _| |_ | | |_| - \/ \/_/ \_\_____| |_| (_) #strong_migrations - -' + def raise_error(message_key, header: nil, **vars) message = StrongMigrations.error_messages[message_key] || "Missing message" - raise StrongMigrations::UnsafeMigration, "#{wait_message}#{message}\n" + + ar5 = ActiveRecord::VERSION::MAJOR >= 5 + vars[:migration_name] = self.class.name + vars[:migration_suffix] = ar5 ? "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" : "" + vars[:base_model] = ar5 ? "ApplicationRecord" : "ActiveRecord::Base" + + # escape % not followed by { + stop!(message.gsub(/%(?!{)/, "%%") % vars, header: header || "Dangerous operation detected") + end + + def sym_str(v) + v.to_sym.inspect + end + + def column_str(columns) + columns = Array(columns).map(&:to_sym) + columns = columns.first if columns.size == 1 + columns.inspect + end + + def options_str(options) + str = String.new("") + options.each do |k, v| + str << ", #{k}: #{v.inspect}" + end + str + end + + def backfill_code(model, column, default) + if ActiveRecord::VERSION::MAJOR >= 5 + "#{model}.in_batches.update_all #{column}: #{default.inspect}" + else + "#{model}.find_in_batches do |records|\n #{model}.where(id: records.map(&:id)).update_all #{column}: #{default.inspect}\n end" + end + end + + def stop!(message, header: "Custom check") + raise StrongMigrations::UnsafeMigration, "\n=== #{header} #strong_migrations ===\n\n#{message}\n" end end end