require 'active_record/connection_adapters/abstract_adapter'
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter < AbstractAdapter
module OID
class Type
def type; end
def simplified_type(sql_type); type end
def infinity(options = {})
::Float::INFINITY * (options[:negative] ? -1 : 1)
end
end
class Identity < Type
def type_cast(value)
value
end
end
class String < Type
def type; :string end
def type_cast(value)
return if value.nil?
value.to_s
end
end
class SpecializedString < OID::String
def type; @type end
def initialize(type)
@type = type
end
end
class Text < OID::String
def type; :text end
end
class Bit < Type
def type; :string end
def type_cast(value)
if ::String === value
ConnectionAdapters::PostgreSQLColumn.string_to_bit value
else
value
end
end
end
class Bytea < Type
def type; :binary end
def type_cast(value)
return if value.nil?
PGconn.unescape_bytea value
end
end
class Money < Type
def type; :decimal end
def type_cast(value)
return if value.nil?
return value unless ::String === value
# Because money output is formatted according to the locale, there are two
# cases to consider (note the decimal separators):
# (1) $12,345,678.12
# (2) $12.345.678,12
# Negative values are represented as follows:
# (3) -$2.55
# (4) ($2.55)
value.sub!(/^\((.+)\)$/, '-\1') # (4)
case value
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
value.gsub!(/[^-\d.]/, '')
when /^-?\D+[\d.]+,\d{2}$/ # (2)
value.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
end
ConnectionAdapters::Column.value_to_decimal value
end
end
class Vector < Type
attr_reader :delim, :subtype
# +delim+ corresponds to the `typdelim` column in the pg_types
# table. +subtype+ is derived from the `typelem` column in the
# pg_types table.
def initialize(delim, subtype)
@delim = delim
@subtype = subtype
end
# FIXME: this should probably split on +delim+ and use +subtype+
# to cast the values. Unfortunately, the current Rails behavior
# is to just return the string.
def type_cast(value)
value
end
end
class Point < Type
def type; :string end
def type_cast(value)
if ::String === value
ConnectionAdapters::PostgreSQLColumn.string_to_point value
else
value
end
end
end
class Array < Type
def type; @subtype.type end
attr_reader :subtype
def initialize(subtype)
@subtype = subtype
end
def type_cast(value)
if ::String === value
ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype
else
value
end
end
end
class Range < Type
attr_reader :subtype
def simplified_type(sql_type); sql_type.to_sym end
def initialize(subtype)
@subtype = subtype
end
def extract_bounds(value)
from, to = value[1..-2].split(',')
{
from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
exclude_start: (value[0] == '('),
exclude_end: (value[-1] == ')')
}
end
def infinity?(value)
value.respond_to?(:infinite?) && value.infinite?
end
def type_cast_single(value)
infinity?(value) ? value : @subtype.type_cast(value)
end
def type_cast(value)
return if value.nil? || value == 'empty'
return value if value.is_a?(::Range)
extracted = extract_bounds(value)
from = type_cast_single extracted[:from]
to = type_cast_single extracted[:to]
if !infinity?(from) && extracted[:exclude_start]
if from.respond_to?(:succ)
from = from.succ
ActiveSupport::Deprecation.warn <<-MESSAGE
Excluding the beginning of a Range is only partialy supported through `#succ`.
This is not reliable and will be removed in the future.
MESSAGE
else
raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')"
end
end
::Range.new(from, to, extracted[:exclude_end])
end
end
class Integer < Type
def type; :integer end
def type_cast(value)
return if value.nil?
ConnectionAdapters::Column.value_to_integer value
end
end
class Boolean < Type
def type; :boolean end
def type_cast(value)
return if value.nil?
ConnectionAdapters::Column.value_to_boolean value
end
end
class Timestamp < Type
def type; :timestamp; end
def simplified_type(sql_type)
:datetime
end
def type_cast(value)
return if value.nil?
# FIXME: probably we can improve this since we know it is PG
# specific
ConnectionAdapters::PostgreSQLColumn.string_to_time value
end
end
class Date < Type
def type; :date; end
def type_cast(value)
return if value.nil?
# FIXME: probably we can improve this since we know it is PG
# specific
ConnectionAdapters::Column.value_to_date value
end
end
class Time < Type
def type; :time end
def type_cast(value)
return if value.nil?
# FIXME: probably we can improve this since we know it is PG
# specific
ConnectionAdapters::Column.string_to_dummy_time value
end
end
class Float < Type
def type; :float end
def type_cast(value)
case value
when nil; nil
when 'Infinity'; ::Float::INFINITY
when '-Infinity'; -::Float::INFINITY
when 'NaN'; ::Float::NAN
else
value.to_f
end
end
end
class Decimal < Type
def type; :decimal end
def type_cast(value)
return if value.nil?
ConnectionAdapters::Column.value_to_decimal value
end
def infinity(options = {})
BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
end
end
class Enum < Type
def type; :enum end
def type_cast(value)
value.to_s
end
end
class Hstore < Type
def type; :hstore end
def type_cast_for_write(value)
ConnectionAdapters::PostgreSQLColumn.hstore_to_string value
end
def type_cast(value)
return if value.nil?
ConnectionAdapters::PostgreSQLColumn.string_to_hstore value
end
def accessor
ActiveRecord::Store::StringKeyedHashAccessor
end
end
class Cidr < Type
def type; :cidr end
def type_cast(value)
return if value.nil?
ConnectionAdapters::PostgreSQLColumn.string_to_cidr value
end
end
class Inet < Cidr
def type; :inet end
end
class Json < Type
def type; :json end
def type_cast_for_write(value)
ConnectionAdapters::PostgreSQLColumn.json_to_string value
end
def type_cast(value)
return if value.nil?
ConnectionAdapters::PostgreSQLColumn.string_to_json value
end
def accessor
ActiveRecord::Store::StringKeyedHashAccessor
end
end
class Uuid < Type
def type; :uuid end
def type_cast(value)
value.presence
end
end
class TypeMap
def initialize
@mapping = {}
end
def []=(oid, type)
@mapping[oid] = type
end
def [](oid)
@mapping[oid]
end
def clear
@mapping.clear
end
def key?(oid)
@mapping.key? oid
end
def fetch(ftype, fmod)
# The type for the numeric depends on the width of the field,
# so we'll do something special here.
#
# When dealing with decimal columns:
#
# places after decimal = fmod - 4 & 0xffff
# places before decimal = (fmod - 4) >> 16 & 0xffff
if ftype == 1700 && (fmod - 4 & 0xffff).zero?
ftype = 23
end
@mapping.fetch(ftype) { |oid| yield oid, fmod }
end
end
# When the PG adapter connects, the pg_type table is queried. The
# key of this hash maps to the `typname` column from the table.
# type_map is then dynamically built with oids as the key and type
# objects as values.
NAMES = Hash.new { |h,k| # :nodoc:
h[k] = OID::Identity.new
}
# Register an OID type named +name+ with a typecasting object in
# +type+. +name+ should correspond to the `typname` column in
# the `pg_type` table.
def self.register_type(name, type)
NAMES[name] = type
end
# Alias the +old+ type to the +new+ type.
def self.alias_type(new, old)
NAMES[new] = NAMES[old]
end
# Is +name+ a registered type?
def self.registered_type?(name)
NAMES.key? name
end
register_type 'int2', OID::Integer.new
alias_type 'int4', 'int2'
alias_type 'int8', 'int2'
alias_type 'oid', 'int2'
register_type 'numeric', OID::Decimal.new
register_type 'float4', OID::Float.new
alias_type 'float8', 'float4'
register_type 'text', OID::Text.new
register_type 'varchar', OID::String.new
alias_type 'char', 'varchar'
alias_type 'name', 'varchar'
alias_type 'bpchar', 'varchar'
register_type 'bool', OID::Boolean.new
register_type 'bit', OID::Bit.new
alias_type 'varbit', 'bit'
register_type 'timestamp', OID::Timestamp.new
alias_type 'timestamptz', 'timestamp'
register_type 'date', OID::Date.new
register_type 'time', OID::Time.new
register_type 'money', OID::Money.new
register_type 'bytea', OID::Bytea.new
register_type 'point', OID::Point.new
register_type 'hstore', OID::Hstore.new
register_type 'json', OID::Json.new
register_type 'cidr', OID::Cidr.new
register_type 'inet', OID::Inet.new
register_type 'uuid', OID::Uuid.new
register_type 'xml', SpecializedString.new(:xml)
register_type 'tsvector', SpecializedString.new(:tsvector)
register_type 'macaddr', SpecializedString.new(:macaddr)
register_type 'citext', SpecializedString.new(:citext)
register_type 'ltree', SpecializedString.new(:ltree)
# FIXME: why are we keeping these types as strings?
alias_type 'interval', 'varchar'
alias_type 'path', 'varchar'
alias_type 'line', 'varchar'
alias_type 'polygon', 'varchar'
alias_type 'circle', 'varchar'
alias_type 'lseg', 'varchar'
alias_type 'box', 'varchar'
end
end
end
end