module Flydata module TableDef class MysqlTableDef TYPE_MAP_M2F = { 'bigint' => 'int8', 'binary' => 'binary', 'blob' => 'varbinary(65535)', 'bool' => 'int1', 'boolean' => 'int1', 'char' => 'varchar', 'date' => 'date', 'datetime' => 'datetime', 'dec' => 'numeric', 'decimal' => 'numeric', 'double' => 'float8', 'double precision' => 'float8', 'enum' => 'enum', 'fixed' => 'numeric', 'float' => 'float4', 'int' => 'int4', 'integer' => 'int4', 'longblob' => 'varbinary(4294967295)', 'longtext' => 'text', 'mediumblob' => 'varbinary(16777215)', 'mediumint' => 'int3', 'mediumtext' => 'text', 'numeric' => 'numeric', 'smallint' => 'int2', 'text' => 'text', 'time' => 'time', 'timestamp' => 'datetime', 'tinyblob' => 'varbinary(255)', 'tinyint' => 'int1', 'tinytext' => 'text', 'varbinary' => 'varbinary', 'varchar' => 'varchar', } def self.convert_to_flydata_type(type) TYPE_MAP_M2F.each do |mysql_type, flydata_type| if /^#{mysql_type}\(|^#{mysql_type}$/.match(type) ret_type = type.gsub(/^#{mysql_type}/, flydata_type) ret_type = check_and_set_varchar_length(ret_type, mysql_type, flydata_type) return ret_type end end nil end # 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 def self.check_and_set_varchar_length(type, mysql_type, flydata_type) 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 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