require 'flydata-core/table_def/base' module FlydataCore module TableDef class OracleTableDef < Base VALUE_CONVERTERS = {} #TODO: Check supported data format one by one TYPE_MAP_O2F = { 'NUMBER' => {type: 'int8'}, 'BIGINT' => {type: 'int8'}, 'INT8' => {type: 'int8'}, 'SERIAL8' => {type: 'serial8'}, 'CHARACTER' => {type: 'varchar', width_attrs:["MAX_LENGTH"], def_width:[1]}, 'CHARACTER varying' => {type: 'varchar', width_attrs:["MAX_LENGTH"], def_width:[1]}, 'VARCHAR' => {type: 'varchar', width_attrs:["MAX_LENGTH"], def_width:[1]}, 'VARCHAR2' => {type: 'varchar', width_attrs:["MAX_LENGTH"], def_width:[1]}, 'DATE' => {type: 'date'}, 'DOUBLE PRECISION' => {type: 'float8'}, 'FLOAT8' => {type: 'float8'}, 'INTEGER' => {type: 'int4'}, 'INT' => {type: 'int4'}, 'INT4' => {type: 'int4'}, 'NUMERIC' => {type: 'numeric', width_attrs:["PRECISION", "SCALE"] #can be no width values }, 'REAL' => {type: 'float4'}, 'FLOAT4' => {type: 'float4'}, 'SMALLINT' => {type: 'int2'}, 'INT2' => {type: 'int2'}, 'SMALLSERIAL' => {type: 'serial2'}, 'TEXT' => {type: 'text'}, 'TIMESTAMP' => {type: 'datetime'}, :default => {type: '_unsupported'}, } def self.convert_to_flydata_type(information_schema_columns) ora_type = information_schema_columns["COLUMN_DATA_TYPE"] raise "Unknown Oracle type or internal error. type:#{ora_type}" unless ora_type unless TYPE_MAP_O2F.has_key?(ora_type) ora_type = :default end type_hash = TYPE_MAP_O2F[ora_type] flydata_type = type_hash[:type] ret_type = flydata_type width_values = get_width_values(information_schema_columns, type_hash) if width_values ret_type += "(#{width_values.join(",")})" end if type_hash[:override] ret_type = type_hash[:override].call(ret_type, ora_type, flydata_type) end ret_type end private def self.get_width_values(information_schema_columns, type_hash) values = [] if type_hash.has_key?(:width_attrs) values = type_hash[:width_attrs].collect{|attr| information_schema_columns[attr].to_i } end if type_hash.has_key?(:def_width) if values.nil? || values.size != type_hash[:def_width].size raise "The number of the default values must match the number of width attributes. column:#{information_schema_columns[:column_name]} type:#{type_hash[:type]} def_width:#{type_hash[:def_width].inspect} width_attrs:#{type_hash[:width_attrs].inspect}" end values = values.each_with_index.collect {|v, i| v ? v : type_hash[:def_width][i]} end values.pop until values.empty? || values.last # remove trailing nil if values.any?{|v| v.nil?} raise "nil value is not allowed" end values.empty? ? nil : values end def self._create(information_schema_columns, options) table_def = information_schema_columns.collect {|iscol| iscol.first}.inspect table_name = nil columns = [] column_def = {} # TODO: Set UTF-8 to NLS_LANG whne creating a connection # Otherwise the user will see "Warning: NLS_LANG is not set. fallback to US7ASCII" # # SourceOracle uses UTF8 client encoding no matter what the server # encoding is. default_charset = 'UTF_8' default_charset_oracle = 'UTF8' comment = nil unique_keys_hash = Hash.new {|h, k| h[k] = []} information_schema_columns.each do |iscol_arr| # An iscol_arr represents a column. Column information in all elements in iscol_arr is the same. # Only difference between elements is index information. iscol = iscol_arr.first column = parse_one_column_def(iscol) if table_name unless table_name == column[:table] raise "Table name must match through all columns. Got `#{table_name}` and `#{column[:table]}`" end else table_name = column[:table] end columns << column coldef = iscol.dup.tap{|c| c.each{|k, v| c[k] = v.to_i if v.kind_of?(BigDecimal) } }.inspect column_def[column[:column]] = coldef # TODO: Handle unique keys #iscol_arr.each do |iscol| # # gather information for unique keys that this column belongs to. # if iscol['IS_PRIMARY'] == 'f' && iscol['IS_UNIQUE'] == 't' # unique_keys_hash[iscol['CONSTRAINT_NAME'].to_sym] << iscol['COLUMN_NAME'] # end #end end unique_keys = unique_keys_hash.values [table_def, table_name, columns, column_def, unique_keys, default_charset, default_charset_oracle, comment] end def self.parse_one_column_def(information_schema_column) column = {} column[:table] = information_schema_column["TABLE_NAME"] column[:column] = information_schema_column["COLUMN_NAME"] column[:type] = convert_to_flydata_type(information_schema_column) column[:not_null] = true if information_schema_column["IS_NULLABLE"] == "N" column[:primary_key] = true if information_schema_column["IS_PRIMARY"] == "t" column[:default] = case column[:type] when 'boolean' to_boolean(information_schema_column["COLUMN_DEFAULT"]) when '_unsupported' # omit default value nil else information_schema_column["COLUMN_DEFAULT"] # TODO nil handling end column end #TODO: Revisit how to handle boolean default value def self.to_boolean(col_value) return nil if col_value.nil? # Catch all possible literal for boolean type in Oracle. # Actual col_value coming in here is: # 'true' or 'false' for default value # 't' or 'f' for column value return true if col_value.to_s =~ /^(t|true|y|yes|on|1)$/i return false if col_value.to_s =~ /^(f|false|n|no|off|0)$/i raise "Invalid default value for Oracle boolean type:`#{col_value}`" end end end end