Created
May 25, 2018 04:20
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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