Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

- Added `add_foreign_key_safely` method
- Added `add_null_constraint_safely` and `remove_null_constraint_safely` methods
- Added `add_column_safely` method
- Added `backfill_column_safely` method
- Added `change_column_safely` method
- Added `statement_timeout` and `lock_timeout` functionality

## 0.5.1 (2019-12-17)
Expand Down
36 changes: 19 additions & 17 deletions lib/strong_migrations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require "active_support"

# modules
require "strong_migrations/util"
require "strong_migrations/checker"
require "strong_migrations/database_tasks"
require "strong_migrations/migration"
Expand All @@ -25,28 +26,15 @@ class << self
self.error_messages = {
add_column_default:
"Adding a column with a non-null default causes the entire table to be rewritten.
Instead, add the column without a default value, then change the default.
Instead, add the column without a default value, backfill and then change the default.

class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
def up
%{add_command}
%{change_command}
end

def down
%{remove_command}
end
end

Then backfill the existing rows in the Rails console or a separate migration with disable_ddl_transaction!.

class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
disable_ddl_transaction!

def change
%{code}
%{command}
end
end%{append}",
end",

add_column_json:
"There's no equality operator for the json column type, which can
Expand All @@ -61,7 +49,21 @@ def change
3. Backfill data from the old column to the new column
4. Move reads from the old column to the new column
5. Stop writing to the old column
6. Drop the old column",
6. Drop the old column

To achieve this, you can use:

class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
disable_ddl_transaction!

def up
%{up_command}
end

def down
%{down_command}
end
end",

remove_column: "ActiveRecord caches attributes which causes problems
when removing columns. Be sure to ignore the column%{column_suffix}:
Expand Down
62 changes: 24 additions & 38 deletions lib/strong_migrations/checker.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module StrongMigrations
class Checker
include Util

attr_accessor :direction

def initialize(migration)
Expand Down Expand Up @@ -81,35 +83,39 @@ def perform(method, *args)
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."
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
command: command_str("add_column_safely", [table, column, type, options])
end

if type.to_s == "json" && postgresql?
raise_error :add_column_json
end
when :change_column
table, column, type = args
table, column, type, options = args
options ||= {}

safe = false
found_column = connection.columns(table).find { |c| c.name.to_s == column.to_s }
raise StrongMigrations::Error, "Column '#{column}' of relation '#{table}' does not exist" unless found_column

# 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
safe = found_column.type == :string
end

unless safe
down_options = {}
options.each do |option, value|
if value != found_column.send(option)
down_options[option] = found_column.send(option)
end
end
previous_type = found_column.type

raise_error :change_column,
up_command: command_str("change_column_safely", [table, column, type, options]),
down_command: command_str("change_column_safely", [table, column, previous_type, down_options])
end
raise_error :change_column unless safe
when :create_table
table, options = args
options ||= {}
Expand Down Expand Up @@ -186,10 +192,6 @@ def set_timeouts

private

def connection
@migration.connection
end

def version
@migration.version
end
Expand All @@ -202,22 +204,6 @@ def version_safe?
version && version <= StrongMigrations.start_after
end

def postgresql?
%w(PostgreSQL PostGIS).include?(connection.adapter_name)
end

def postgresql_version
@postgresql_version ||= begin
target_version = StrongMigrations.target_postgresql_version
if target_version && defined?(Rails) && (Rails.env.development? || Rails.env.test?)
# we only need major version right now
target_version.to_i * 10000
else
connection.execute("SHOW server_version_num").first["server_version_num"].to_i
end
end
end

def raise_error(message_key, header: nil, **vars)
return unless StrongMigrations.check_enabled?(message_key, version: version)

Expand All @@ -238,7 +224,7 @@ def raise_error(message_key, header: nil, **vars)

def constraint_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) }
code = quote_identifiers(statement, identifiers)
"safety_assured do\n execute '#{code}' \n end"
end

Expand Down
Loading