require 'open3' require 'shellwords' require 'flydata-core/table_def/mysql_table_def' module FlydataCore module Mysql class CommandGenerator DEFAULT_OPTION = "--default-character-set=utf8 --protocol=tcp" DEFAULT_MYSQL_OPTION = "#{DEFAULT_OPTION} --skip-auto-rehash" def self.default_cmd_option(command) case command when 'mysql' DEFAULT_MYSQL_OPTION else #mysqldump DEFAULT_OPTION end end # Generate mysql/mysqldump command with options # options must be hash # - command # mysql(default) | mysqldump # - host # - port # - username # - password # - database # - tables # array # - ssl_ca # - custom_option # string def self.generate_mysql_cmd(option) raise ArgumentError.new("option must be hash.") unless option.kind_of?(Hash) option = convert_keys_to_sym(option) command = option[:command] ? option[:command] : 'mysql' host = option[:host] ? "-h #{option[:host].shellescape}" : nil port = option[:port] ? "-P #{option[:port].to_s.shellescape}" : nil username = option[:username] ? "-u#{option[:username].shellescape}" : nil password = if !(option[:password].to_s.empty?) "-p#{option[:password].shellescape}" else nil end database = option[:database].shellescape if option[:database] tables = option[:tables] ? option[:tables].collect{|t| t.shellescape}.join(' ') : nil ssl_ca = option[:ssl_ca] ? option[:ssl_ca] : nil ssl_cipher = option[:ssl_cipher] ? option[:ssl_cipher] : nil default_option = option[:no_default_option] ? "" : default_cmd_option(command) default_option += " --ssl-ca #{ssl_ca}" if ssl_ca default_option += " --ssl-cipher=#{ssl_cipher}" unless ssl_cipher.to_s.empty? default_option = nil if default_option == '' custom_option = option[:custom_option] [command, host, port, username, password, default_option, custom_option, database, tables].compact.join(' ') end # DDL_DUMP_CMD_TEMPLATE = "MYSQL_PWD=\"%s\" mysqldump --protocol=tcp -d -h %s -P %s -u %s %s %s" def self.generate_mysql_ddl_dump_cmd(option) opt = option.dup opt[:command] = 'mysqldump' opt[:custom_option] = '-d' generate_mysql_cmd(opt) end # MYSQL_DUMP_CMD_TEMPLATE = "MYSQL_PWD=\"%s\" mysqldump --default-character-set=utf8 --protocol=tcp -h %s -P %s -u%s --skip-lock-tables --single-transaction --hex-blob %s %s %s" def self.generate_mysqldump_with_master_data_cmd(option) opt = option.dup opt[:command] = 'mysqldump' opt[:custom_option] = '--skip-lock-tables --single-transaction --hex-blob --flush-logs --master-data=2' generate_mysql_cmd(opt) end def self.generate_mysqldump_without_master_data_cmd(option) opt = option.dup opt[:command] = 'mysqldump' opt[:custom_option] = '--skip-lock-tables --single-transaction --hex-blob' if opt[:result_file] opt[:custom_option] << " --result-file=#{opt[:result_file]}" end generate_mysql_cmd(opt) end def self.generate_mysql_show_grants_cmd(option) opt = option.dup opt[:command] = 'mysql' opt[:custom_option] = '-e "SHOW GRANTS;"' generate_mysql_cmd(opt) end def self.each_mysql_tabledef(tables, options, &block) tables = tables.clone missing_tables = [] begin if tables.to_s == '' || tables.to_s == '[]' raise ArgumentError, "tables is nil or empty" end _each_mysql_tabledef(tables, options, &block) rescue TableMissingError => e tables.delete e.table missing_tables << e.table return missing_tables if tables.empty? retry end missing_tables end private class TableMissingError < RuntimeError def initialize(message, table) super(message) @table = table end attr_reader :table end def self._each_mysql_tabledef(tables, option) command = generate_mysql_ddl_dump_cmd(option.merge(tables: tables)) create_opt = {} if option.has_key?(:skip_primary_key_check) create_opt[:skip_primary_key_check] = option[:skip_primary_key_check] end Open3.popen3(command) do |stdin, stdout, stderr| stdin.close stdout.set_encoding("utf-8", "utf-8") # mysqldump output must be in UTF-8 create_flydata_ctl_table = true while !stdout.eof? begin mysql_tabledef = FlydataCore::TableDef::MysqlTableDef.create(stdout, create_opt) break if mysql_tabledef.nil? yield(mysql_tabledef, nil) rescue FlydataCore::TableDefError=> e yield(nil, e) end end errors = "" while !stderr.eof? line = stderr.gets.gsub('mysqldump: ', '') case line when /Couldn't find table: "([^"]+)"/ missing_table = $1 raise TableMissingError.new(line, missing_table) when /Warning: Using a password on the command line interface can be insecure./ # Ignore else errors << line end end raise errors unless errors.empty? end end def self.convert_keys_to_sym(hash) hash.inject(hash.dup) do |ret, (k, v)| if k.kind_of?(String) && ret[k.to_sym].nil? ret[k.to_sym] = v end ret end end end end end