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