require 'flydata-core/table_def/base' module FlydataCore module TableDef class PostgresqlTableDef < Base VALUE_CONVERTERS = {} TYPE_MAP_P2F = { 'bigint' => {type: 'int8'}, 'int8' => {type: 'int8'}, 'bigserial' => {type: 'serial8'}, 'serial8' => {type: 'serial8'}, 'bit' => {type: 'bit', width_attrs:["character_maximum_length"]}, 'bit varying' => {type: 'varbit', width_attrs:["character_maximum_length"]}, 'varbit' => {type: 'varbit', width_attrs:["character_maximum_length"]}, 'boolean' => {type: 'boolean'}, # 'box' 'bytea' => {type: 'bytea'}, 'character' => {type: 'varchar', width_attrs:["character_octet_length"], def_width:[1]}, 'character varying' => {type: 'varchar', width_attrs:["character_octet_length"], def_width:[1]}, 'varchar' => {type: 'varchar', width_attrs:["character_octet_length"], def_width:[1]}, # 'cidr' # 'circle' 'date' => {type: 'date'}, 'double precision' => {type: 'float8'}, 'float8' => {type: 'float8'}, 'integer' => {type: 'int4'}, 'int' => {type: 'int4'}, 'int4' => {type: 'int4'}, # 'interval' # 'json' # 'line' # 'lseg' # 'macaddr' 'money' => {type: 'money', width_attrs:["numeric_precision", "numeric_scale"]}, 'numeric' => {type: 'numeric', width_attrs:["numeric_precision", "numeric_scale"] #can be no width values }, # 'path' # 'pg_lsn' # 'point' # 'polygon' 'real' => {type: 'float4'}, 'float4' => {type: 'float4'}, 'smallint' => {type: 'int2'}, 'int2' => {type: 'int2'}, 'smallserial' => {type: 'serial2'}, 'serial2' => {type: 'serial2'}, 'serial' => {type: 'serial4'}, 'serial4' => {type: 'serial4'}, 'text' => {type: 'text'}, 'time' => {type: 'time'}, 'time without time zone' => {type: 'time'}, 'time with time zone' => {type: 'timetz'}, 'timetz' => {type: 'timetz'}, 'timestamp' => {type: 'datetime'}, 'timestamp without time zone' => {type: 'datetime'}, 'timestamp with time zone' => {type: 'datetimetz'}, 'timestamptz' => {type: 'datetimetz'}, :default => {type: '_unsupported'}, # 'tsquery' # 'tsvector' # 'txid_snapshot' # 'uuid' # 'xml' #--- PosgreSQL automatically converts following data types to another data type --- # 'decimal' (converted to 'numeric') # 'bool' ('boolean') # 'char' ('character') } def self.convert_to_flydata_type(information_schema_columns) pg_type = information_schema_columns["data_type"] raise "Unknown PostgreSQL type or internal error. type:#{pg_type}" unless pg_type unless TYPE_MAP_P2F.has_key?(pg_type) pg_type = :default end type_hash = TYPE_MAP_P2F[pg_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, pg_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] } 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 = {} # SourcePostgresql uses UTF8 client encoding no matter what the server # encoding is. default_charset = 'UTF_8' default_charset_postgresql = '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.inspect column_def[column[:column]] = coldef iscol_arr.each do |iscol| # gather information for unique keys that this column belongs to. if iscol['is_primary'] == 'f' && iscol['is_unique'] == 't' && iscol['is_valid'] == 't' && iscol['is_partial'] == 'f' unique_keys_hash[iscol['index_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_postgresql, 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"] == "NO" # index column: information_schema_column["is_primary"] == "f" # regular column: information_schema_column["is_primary"] == nil 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 def self.to_boolean(col_value) return nil if col_value.nil? # Catch all possible literal for boolean type in PostgreSQL. # 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 PostgreSQL boolean type:`#{col_value}`" end end end end