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