module Flydata module TableDef class MysqlTableDef # Check and set the varchar(char) size which is converted from # length to byte size. # On Mysql the record size of varchar(char) is a length of characters. # ex) varchar(6) on mysql -> varchar(18) on flydata PROC_override_varchar = ->(type, mysql_type, flydata_type) do return type unless %w(char varchar).include?(mysql_type) if type =~ /\((\d+)\)/ # expect 3 byte UTF-8 character "#{flydata_type}(#{$1.to_i * 3})" else raise "Invalid varchar type. It must be a bug... type:#{type}" end end PROC_override_varbinary = ->(type, mysql_type, flydata_type) do return type unless %w(binary varbinary).include?(mysql_type) if type =~ /\((\d+)\)/ # expect 2 bytes for each original byte + 2 bytes for the prefix # ex) 4E5DFF => "0x4e5dff" "#{flydata_type}(#{$1.to_i * 2 + 2})" else raise "Invalid varbinary type. It must be a bug... type:#{type}" end end TYPE_MAP_M2F = { 'bigint' => {type: 'int8'}, 'binary' => {type: 'binary', override: PROC_override_varbinary}, 'blob' => {type: 'varbinary(65535)'}, 'bool' => {type: 'int1'}, 'boolean' => {type: 'int1'}, 'char' => {type: 'varchar', override: PROC_override_varchar}, 'date' => {type: 'date'}, 'datetime' => {type: 'datetime'}, 'dec' => {type: 'numeric'}, 'decimal' => {type: 'numeric'}, 'double' => {type: 'float8'}, 'double precision' => {type: 'float8'}, 'enum' => {type: 'enum'}, 'fixed' => {type: 'numeric'}, 'float' => {type: 'float4'}, 'int' => {type: 'int4'}, 'integer' => {type: 'int4'}, 'longblob' => {type: 'varbinary(4294967295)'}, 'longtext' => {type: 'text'}, 'mediumblob' => {type: 'varbinary(16777215)'}, 'mediumint' => {type: 'int3'}, 'mediumtext' => {type: 'text'}, 'numeric' => {type: 'numeric'}, 'smallint' => {type: 'int2'}, 'text' => {type: 'text'}, 'time' => {type: 'time'}, 'timestamp' => {type: 'datetime'}, 'tinyblob' => {type: 'varbinary(255)'}, 'tinyint' => {type: 'int1'}, 'tinytext' => {type: 'text'}, 'varbinary' => {type: 'varbinary', override: PROC_override_varbinary}, 'varchar' => {type: 'varchar', override: PROC_override_varchar}, } def self.convert_to_flydata_type(type) TYPE_MAP_M2F.each do |mysql_type, type_hash| flydata_type = type_hash[:type] if /^#{mysql_type}\(|^#{mysql_type}$/.match(type) ret_type = type.gsub(/^#{mysql_type}/, flydata_type) if type_hash[:override] ret_type = type_hash[:override].call(ret_type, mysql_type, flydata_type) end return ret_type end end nil end def self.create(io) params = _create(io) params ? self.new(*params) : nil end def initialize(table_def, table_name, columns, default_charset, comment) @table_def = table_def @table_name = table_name @columns = columns @default_charset = default_charset @comment = comment end def self._create(io) table_def = '' table_name = nil columns = [] default_charset = nil comment = nil position = :before_create_table io.each_line do |line| case position when :before_create_table if line =~ /CREATE TABLE `(.*?)`/ position = :in_create_table table_name = $1 table_def += line.chomp next end when :in_create_table table_def += line.chomp stripped_line = line.strip # `col_smallint` smallint(6) DEFAULT NULL, if stripped_line.start_with?('`') columns << parse_one_column_def(line) # PRIMARY KEY (`id`) elsif stripped_line.start_with?("PRIMARY KEY") parse_key(line, columns) #) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='test table'; elsif stripped_line.start_with?(')') default_charset = $1 if line =~ /DEFAULT CHARSET\s*=\s*([^\s]+)/ comment = $1 if /COMMENT='((?:\\'|[^'])*)'/.match(line) position = :after_create_table elsif stripped_line.start_with?("KEY") # index creation. No action required. elsif stripped_line.start_with?("CONSTRAINT") # constraint definition. No acction required. elsif stripped_line.start_with?("UNIQUE KEY") parse_key(line, columns, :unique) else $stderr.puts "Unknown table definition. Skip. (#{line})" end when :after_create_table unless columns.any? {|column| column[:primary_key]} raise TableDefError, {error: "no primary key defined", table: table_name} end break end end position == :after_create_table ? [table_def, table_name, columns, default_charset, comment] : nil end attr_reader :columns, :table_name def to_flydata_tabledef tabledef = { table_name: @table_name, columns: @columns, } tabledef[:default_charset] = @default_charset if @default_charset tabledef[:comment] = @comment if @comment tabledef end def self.parse_one_column_def(query) line = query.strip line = line[0..-2] if line.end_with?(',') pos = 0 cond = :column_name column = {} while pos < line.length case cond when :column_name #`column_name` ... pos = line.index(' ', 1) column[:column] = if line[0] == '`' line[1..pos-2] else line[0..pos-1] end cond = :column_type pos += 1 when :column_type #... formattype(,,,) ... pos += 1 until line[pos] != ' ' start_pos = pos pos += 1 until line[pos].nil? || line[pos] =~ /\s|\(/ # meta if line[pos] == '(' #TODO: implement better parser pos = line.index(')', pos) pos += 1 end # type type = line[start_pos..pos-1] column[:type] = convert_to_flydata_type(type) cond = :options when :options column[:type] += ' unsigned' if line =~ /unsigned/i column[:auto_increment] = true if line =~ /AUTO_INCREMENT/i column[:not_null] = true if line =~ /NOT NULL/i column[:unique] = true if line =~ /UNIQUE/i if /DEFAULT\s+((?:[^'\s]+\b)|(?:'(?:\\'|[^'])*'))/i.match(line) val = $1 val = val.slice(1..-1) if val.start_with?("'") val = val.slice(0..-2) if val.end_with?("'") column[:default] = val == "NULL" ? nil : val end if /COMMENT\s+'(((?:\\'|[^'])*))'/i.match(line) column[:comment] = $1 end if block_given? column = yield(column, query, pos) end break else raise "Invalid condition. It must be a bug..." end end column end private def self.parse_key(line, columns, type = :primary_key) line = /\((?:`.*?`(?:\(.*?\))?(?:,\s*)?)+\)/.match(line)[0] keys = line.scan(/`(.*?)`/).collect{|item| item[0]} keys.each do |key| column = columns.detect {|column| column[:column] === key } raise "Key #{key} must exist in the definition " if column.nil? column[type] = true end end end end end