lib/strong_migrations/migration.rb in strong_migrations-0.3.1 vs lib/strong_migrations/migration.rb in strong_migrations-0.4.0
- old
+ new
@@ -13,12 +13,10 @@
super
end
def method_missing(method, *args, &block)
unless @safe || ENV["SAFETY_ASSURED"] || is_a?(ActiveRecord::Schema) || @direction == :down || version_safe?
- ar5 = ActiveRecord::VERSION::MAJOR >= 5
-
case method
when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
columns =
case method
when :remove_timestamps
@@ -34,11 +32,11 @@
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"
+ code = "self.ignored_columns = #{columns.inspect}"
raise_error :remove_column,
model: args[0].to_s.classify,
code: code,
command: command_str(method, args),
@@ -63,25 +61,34 @@
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)
+ code: backfill_code(table, column, default),
+ append: append
end
if type.to_s == "json" && postgresql?
- if postgresql_version >= 90400
- raise_error :add_column_json
- else
- raise_error :add_column_json_legacy,
- model: table.to_s.classify,
- table: connection.quote_table_name(table.to_s)
- end
+ raise_error :add_column_json
end
when :change_column
table, column, type = args
safe = false
@@ -101,11 +108,11 @@
(@new_tables ||= []) << table.to_s
when :add_reference, :add_belongs_to
table, reference, options = args
options ||= {}
- index_value = options.fetch(:index, ar5)
+ 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)]),
@@ -117,10 +124,36 @@
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
@@ -150,19 +183,29 @@
end
def raise_error(message_key, header: nil, **vars)
message = StrongMigrations.error_messages[message_key] || "Missing message"
- 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"
+ 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
+ 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]
@@ -177,14 +220,10 @@
"#{command} #{str_args.join(", ")}"
end
def backfill_code(table, column, default)
model = table.to_s.classify
- 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
+ "#{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