Skip to content

Instantly share code, notes, and snippets.

@aks
Created May 25, 2018 04:20
Show Gist options
  • Select an option

  • Save aks/f091b44c239ced43260cbfa93b85cf64 to your computer and use it in GitHub Desktop.

Select an option

Save aks/f091b44c239ced43260cbfa93b85cf64 to your computer and use it in GitHub Desktop.
Self-contained ruby script to manage rails migration versions directly: insert, remove, check, match, or names by versions or patterns
#!/usr/bin/env ruby
# migrations insert VERSION
# migrations remove VERSION
# migrations check VERSION [VERSION2]
# migrations match VER_PATTERN
# migrations names NAME_PATTERN
# migrations status [ up | down ]
#
# NOTE: This script relies on 'psql` to access and query the database, which means that DB access can be
# configured entirely with PGUSER, PGHOST, PGDATABASE, and PGPORT.
#
# The -D DIR option must be given if this command is run outside of the Rails application directory.
#
# This script is self-contained, relying entirely on the gems below, and no other local script files.
require 'thor'
require 'open3'
require 'active_support'
require 'active_support/core_ext'
require 'active_support/inflector'
class Migrations < Thor
DEFAULT_APP_DIR = '/data/procore/current'
class_option :verbose, aliases: '-v', type: :boolean, default: false, desc: "Be verbose"
class_option :norun, aliases: '-n', type: :boolean, default: false, desc: "No run -- don't make any changes"
class_option :debug, aliases: '-d', type: :boolean, default: false, desc: "Debug mode"
class_option :directory, aliases: '-D', type: :string, banner: "DIR", desc: "use DIR as the Rails application directory"
desc "check [VERSION] [VERSION2]", "Check a version or version range in schema_migrations"
option :status, aliases: '-s', type: :string, enum: %w[up down], desc: "filter results by STATUS"
def check(version1=nil, version2=nil)
set_application_directory
if version2.nil?
version1 = validate_version(version1)
check_one_version(version1)
else
check_version_range(version1, version2)
end
end
desc "match PATTERN", "Check migration versions matching PATTERN"
option :status, aliases: '-s', type: :string, enum: %w[up down], desc: "filter results by STATUS"
def match(pattern)
set_application_directory
match_and_show_migration_versions(pattern)
end
desc "names PATTERN", "List migrations with names matching PATTERN"
option :status, aliases: '-s', type: :string, enum: %w[up down], desc: "filter results by STATUS"
def names(pattern)
set_application_directory
match_and_show_migration_names(pattern)
end
desc "status STATUS", "List the migrations having STATUS (up|down)"
def status(status_name)
set_application_directory
show_selected_status_migrations(status_name)
end
desc "insert VERSION", "insert VERSION into schema_migrations"
def insert(version)
set_application_directory
version = validate_version(version)
(status, name) = get_version_info(version)
if status == :down
insert_version version
else
say "Migration #{version} - #{name} is already up", :red
end
end
map '-h' => :help
desc "remove VERSION", "remove VERSION from schema_migrations"
def remove(version)
set_application_directory
version = validate_version(version)
(status, name) = get_version_info(version)
if status == :up
remove_version(version)
else
say "Migration #{version} - #{name} is already down", :red
end
end
no_commands do
module StandardClassExtensions
refine Array do
def unwrap
size < 2 ? first : self
end
def wrap
self
end
end
refine Object do
def wrap
Array(self)
end
end
refine String do
def split_into_array
split(/\n/).map(&:strip).reject(&:blank?).unwrap
end
end
end
using StandardClassExtensions
#### check_one_version
def check_one_version(version)
(status, name) = get_version_info(version)
show_version_info(status, version, name)
end
def get_version_info(version)
status = db_version_status(version)
name = migration_version_name(version) || '-- no file --'
[status, name]
end
def db_version_status(version)
db_version_statuses[version] || :down
end
def set_db_version_status(version, status)
db_version_statuses[version] = status
end
#### check_version_range
def check_version_range(version1, version2)
version1 = '' if ['*', '-'].include?(version1)
version2 = '' if ['*', '-'].include?(version2)
version1 = validate_version(version1) if version1.present?
version2 = validate_version(version2) if version2.present?
db_versions =
filter_version_range(db_version_statuses.keys, version1, version2)
migration_versions =
filter_version_range(migration_versions_and_names.keys, version1, version2)
selected_versions = (db_versions | migration_versions).uniq.sort
status_sym = validate_status_name(options[:status])
selected_versions.select! { |version| db_version_status(version) == status_sym } if status_sym.present?
if selected_versions.size.nonzero?
show_migrations(selected_versions)
else
msg =
if status_sym.present?
"There are no #{status_sym} migration"
else
"There are no migration"
end
msg +=
if version1.present? && version2.present?
" versions between #{version1} and #{version2}"
elsif version1.present?
" versions after #{version1}"
elsif version2.present?
"s versions before #{version2}"
else
"s at all!"
end
say msg, :red
end
end
#### match_and_show_migration_versions
def match_and_show_migration_versions(pattern)
db_versions =
filter_versions_by_pattern(db_version_statuses.keys, pattern)
migration_versions =
filter_versions_by_pattern(migration_versions_and_names.keys, pattern)
selected_versions = (db_versions | migration_versions).uniq.sort
status_sym = validate_status_name(options[:status])
selected_versions.select! { |ver| db_version_status(ver) == status_sym } if status_sym.present?
if selected_versions.size.nonzero?
show_migrations(selected_versions)
else
status_str = ' ' + (status_sym.present? ? status_sym.to_s : '')
say "There are no#{status_str} migration versions matching '#{pattern}'", :red
end
end
#### match_and_show_migration_names
def match_and_show_migration_names(pattern)
versions = migration_versions_and_names.select { |_version, name| name =~ /#{pattern}/ }.keys.sort
status_sym = validate_status_name(options[:status])
versions.select! { |ver| db_version_status(ver) == status_sym } if status_sym.present?
if versions.size.nonzero?
show_migrations(versions)
else
status_str = ' ' + (status_sym.present? ? status_sym.to_s : '')
say "There are no#{status_str} migrations with names matching '#{pattern}'", :red
end
end
#### show_selected_status_migrations
def show_selected_status_migrations(status_name)
status_sym = validate_status_name(status_name)
db_versions = db_version_statuses.keys
migration_versions = migration_versions_and_names.keys
all_versions = (db_versions | migration_versions).uniq.sort
selected_versions = all_versions.select { |ver| db_version_status(ver) == status_sym }
if selected_versions.size.nonzero?
show_migrations(selected_versions)
else
say "There are no #{status_sym} migrations", :red
end
end
#### db_version_statuses
def db_version_statuses
@db_version_statuses ||= get_db_version_statuses
end
def get_db_version_statuses
versions = sql_query_rows(select_db_versions_sql)
Hash[versions.zip].transform_values! { |_x| :up }
end
def select_db_versions_sql
"SELECT version FROM schema_migrations ORDER BY 1;"
end
def migration_version_name(version)
migration_versions_and_names[version]
end
def migration_versions_and_names
@migration_versions_and_names ||= get_migration_versions_and_names
end
def get_migration_versions_and_names
Hash[Dir['db/migrate/*.rb'].map { |path| File.basename(path, '.rb').split('_', 2) }].tap do |hash|
hash.transform_values! { |name| name.gsub('_', ' ').capitalize }
end
end
STATUS_FORMAT = "%6s %-14s %s\n"
def show_version_info(status, version, name)
unless @header
printf STATUS_FORMAT, 'Status', 'Version', 'Name'
printf STATUS_FORMAT, '------', '-'*14, '-'*40
@header = true
end
printf STATUS_FORMAT, status.to_s.center(6), version, name
end
#### filter_version_range
def filter_version_range(versions, ver1, ver2)
versions.select do |version|
(ver1.blank? || version >= ver1) &&
(ver2.blank? || version <= ver2)
end
end
#### filter_versions_by_pattern
def filter_versions_by_pattern(versions, pattern)
versions.select { |version| version =~ /#{pattern}/ }
end
#### filter_names_by_pattern
def filter_names_by_pattern(version_name_hash, pattern)
version_name_hash.select { |_version, name| name =~ /#{pattern}/i }
end
### show_migrations
def show_migrations(versions)
versions.each do |version|
(status, name) = get_version_info(version)
show_version_info(status, version, name)
end
end
#### validate_version
def validate_version(version)
return version if version =~ /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}$/ # YYYYmmddHHMMSS
error "No VERSION given!" if version.nil? || version.blank? || !interactive?
return version if yes?("Migration '#{version}' is a non-standard format; use it anyway? [yN]")
error "Nothing done"
end
#### validate_status_name
def validate_status_name(status_name)
return nil if status_name.nil?
case status_name
when "up", "u" then :up
when "down", "dow", "do", "d" then :down
else error "Unknown status name: #{status_name}; should be 'up' or 'down'"
end
end
#### insert_version
def insert_version(version)
sql_update("INSERT INTO schema_migrations (version) VALUES ('#{version}');")
set_db_version_status(version, :up)
puts "Migration #{version} inserted"
check_one_version(version)
end
#### remove_version
def remove_version(version)
sql_update("DELETE FROM schema_migrations WHERE version = '#{version}';")
set_db_version_status(version, :down)
puts "Migration #{version} removed"
check_one_version(version)
end
#### Utilities
def set_application_directory
app_dir = options[:directory]
if app_dir && Dir.exist?(dir_path = File.expand_path(app_dir))
Dir.chdir(dir_path)
elsif Dir.exist?('db/migrate')
return
elsif Dir.exist?(DEFAULT_APP_DIR)
Dir.chdir(DEFAULT_APP_DIR)
else
error "Cannot find 'db/migrate' directory! Use -D to configre it."
end
end
def get_databases
sql_query_rows('psql -t -c "select datname from pg_catalog.pg_database;"')
end
def sql_update(sql)
output = run_command('psql -t', sql)
puts output unless output.blank?
puts @last_stderr if @last_stderr.present?
end
def sql_query(sql)
run_command('psql -t', sql)
end
def sql_query_rows(sql)
sql_query(sql).split_into_array
end
def run_command_with_array_results(command, stdin=nil)
split_text_into_lines_and_strip(run_command(command, stdin))
end
def run_command(command, stdin = nil)
@last_stderr = nil
out, err, _stat = Open3.capture3(command, stdin_data: stdin)
error err if err =~ /ERROR/
@last_stderr = err
out
end
def interactive?
$stdin.isatty && $stdout.isatty
end
def error(msg)
say msg, :red
exit 1
end
end
end
Migrations.start
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment