diff options
Diffstat (limited to 'activerecord/lib/active_record/connection_adapters')
26 files changed, 464 insertions, 602 deletions
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index f54fcc4040..117c0f0969 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -262,6 +262,7 @@ module ActiveRecord alias :belongs_to :references def new_column_definition(name, type, options) # :nodoc: + type = aliased_types[type] || type column = create_column_definition name, type limit = options.fetch(:limit) do native[type][:limit] if native[type].is_a?(Hash) @@ -292,6 +293,12 @@ module ActiveRecord def native @native end + + def aliased_types + HashWithIndifferentAccess.new( + timestamp: :datetime, + ) + end end class AlterTable # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 8a7a869eec..0c55dbb037 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -382,7 +382,7 @@ module ActiveRecord m.alias_type %r(clob)i, 'text' m.register_type %r(date)i, Type::Date.new m.register_type %r(time)i, Type::Time.new - m.register_type %r(timestamp)i, Type::Timestamp.new + m.alias_type %r(timestamp)i, 'datetime' m.register_type %r(datetime)i, Type::DateTime.new m.alias_type %r(numeric)i, 'decimal' m.alias_type %r(number)i, 'decimal' diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 5eb2e86d48..86eb2a38d8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -163,7 +163,6 @@ module ActiveRecord :float => { :name => "float" }, :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, :time => { :name => "time" }, :date => { :name => "date" }, :binary => { :name => "blob" }, @@ -250,10 +249,9 @@ module ActiveRecord raise NotImplementedError end - # Overridden by the adapters to instantiate their specific Column type. def new_column(field, default, sql_type, null, collation, extra = "") # :nodoc: cast_type = lookup_cast_type(sql_type) - Column.new(field, default, cast_type, sql_type, null, collation, extra) + Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) end # Must return the Mysql error number from the exception, if the exception has an diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 0087c20b88..a718756b93 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -18,7 +18,7 @@ module ActiveRecord alias :encoded? :coder - delegate :type, to: :cast_type + delegate :type, :klass, :text?, :number?, :binary?, :type_cast_for_write, to: :cast_type # Instantiates a new column in the table. # @@ -43,79 +43,16 @@ module ActiveRecord @coder = nil end - # Returns +true+ if the column is either of type string or text. - def text? - type == :string || type == :text - end - - # Returns +true+ if the column is either of type integer, float or decimal. - def number? - type == :integer || type == :float || type == :decimal - end - def has_default? !default.nil? end - # Returns the Ruby class that corresponds to the abstract data type. - def klass - case type - when :integer then Fixnum - when :float then Float - when :decimal then BigDecimal - when :datetime, :timestamp, :time then Time - when :date then Date - when :text, :string, :binary then String - when :boolean then Object - end - end - - def binary? - type == :binary - end - - # Casts a Ruby value to something appropriate for writing to the database. - # Numeric columns will typecast boolean and string to appropriate numeric - # values. - def type_cast_for_write(value) - return value unless number? - - case value - when FalseClass - 0 - when TrueClass - 1 - when String - value.presence - else - value - end - end - # Casts value to an appropriate instance. def type_cast(value) - return nil if value.nil? - return coder.load(value) if encoded? - - klass = self.class - - case type - when :string, :text - case value - when TrueClass; "1" - when FalseClass; "0" - else - value.to_s - end - when :integer then klass.value_to_integer(value) - when :float then value.to_f - when :decimal then klass.value_to_decimal(value) - when :datetime, :timestamp then klass.string_to_time(value) - when :time then klass.string_to_dummy_time(value) - when :date then klass.value_to_date(value) - when :binary then klass.binary_to_string(value) - when :boolean then klass.value_to_boolean(value) - else value + if encoded? + coder.load(value) + else + cast_type.type_cast(value) end end @@ -131,133 +68,8 @@ module ActiveRecord type_cast(default) end - class << self - # Used to convert from BLOBs to Strings - def binary_to_string(value) - value - end - - def value_to_date(value) - if value.is_a?(String) - return nil if value.empty? - fast_string_to_date(value) || fallback_string_to_date(value) - elsif value.respond_to?(:to_date) - value.to_date - else - value - end - end - - def string_to_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - def string_to_dummy_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - dummy_time_string = "2000-01-01 #{string}" - - fast_string_to_time(dummy_time_string) || begin - time_hash = Date._parse(dummy_time_string) - return nil if time_hash[:hour].nil? - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) - end - end - - # convert something to a boolean - def value_to_boolean(value) - if value.is_a?(String) && value.empty? - nil - else - TRUE_VALUES.include?(value) - end - end - - # Used to convert values to integer. - # handle the case when an integer column is used to store boolean values - def value_to_integer(value) - case value - when TrueClass, FalseClass - value ? 1 : 0 - else - value.to_i rescue nil - end - end - - # convert something to a BigDecimal - def value_to_decimal(value) - # Using .class is faster than .is_a? and - # subclasses of BigDecimal will be handled - # in the else clause - if value.class == BigDecimal - value - elsif value.respond_to?(:to_d) - value.to_d - else - value.to_s.to_d - end - end - - protected - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 - end - - def new_date(year, mon, mday) - if year && year != 0 - Date.new(year, mon, mday) rescue nil - end - end - - def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) - # Treat 0000-00-00 00:00:00 as nil. - return nil if year.nil? || (year == 0 && mon == 0 && mday == 0) - - if offset - time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil - return nil unless time - - time -= offset - Base.default_timezone == :utc ? time : time.getlocal - else - Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - end - - def fast_string_to_date(string) - if string =~ Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ Format::ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def fallback_string_to_time(string) - time_hash = Date._parse(string) - time_hash[:sec_fraction] = microseconds(time_hash) - - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) - end - end - private - delegate :extract_scale, to: Type + delegate :extract_scale, to: :cast_type def extract_limit(sql_type) $1.to_i if sql_type =~ /\((.*)\)/ diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 2e39168374..0a14cdfe89 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -29,13 +29,6 @@ module ActiveRecord module ConnectionAdapters class Mysql2Adapter < AbstractMysqlAdapter - - class Column < AbstractMysqlAdapter::Column # :nodoc: - def adapter - Mysql2Adapter - end - end - ADAPTER_NAME = 'Mysql2' def initialize(connection, logger, connection_options, config) @@ -69,11 +62,6 @@ module ActiveRecord end end - def new_column(field, default, sql_type, null, collation, extra = "") # :nodoc: - cast_type = lookup_cast_type(sql_type) - Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) - end - def error_number(exception) exception.error_number if exception.respond_to?(:error_number) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index bf09bfe217..aa8a91ed39 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -66,35 +66,6 @@ module ActiveRecord # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. # class MysqlAdapter < AbstractMysqlAdapter - - class Column < AbstractMysqlAdapter::Column #:nodoc: - def self.string_to_time(value) - return super unless Mysql::Time === value - new_time( - value.year, - value.month, - value.day, - value.hour, - value.minute, - value.second, - value.second_part) - end - - def self.string_to_dummy_time(v) - return super unless Mysql::Time === v - new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part) - end - - def self.string_to_date(v) - return super unless Mysql::Time === v - new_date(v.year, v.month, v.day) - end - - def adapter - MysqlAdapter - end - end - ADAPTER_NAME = 'MySQL' class StatementPool < ConnectionAdapters::StatementPool @@ -156,11 +127,6 @@ module ActiveRecord end end - def new_column(field, default, sql_type, null, collation, extra = "") # :nodoc: - cast_type = lookup_cast_type(sql_type) - Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) - end - def error_number(exception) # :nodoc: exception.errno if exception.respond_to?(:errno) end @@ -296,126 +262,70 @@ module ActiveRecord @connection.insert_id end - module Fields - class Type - def type; end - - def type_cast_for_write(value) - value + module Fields # :nodoc: + class DateTime < Type::DateTime + def cast_value(value) + if Mysql::Time === value + new_time( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.second_part) + else + super + end end end - class Identity < Type - def type_cast(value); value; end - end - - class Integer < Type - def type_cast(value) - return if value.nil? - - value.to_i rescue value ? 1 : 0 - 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 mysql - # specific - ConnectionAdapters::Column.value_to_date value - end - end - - class DateTime < Type - def type; :datetime; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.string_to_time value + class Time < Type::Time + def cast_value(value) + if Mysql::Time === value + new_time( + 2000, + 01, + 01, + value.hour, + value.minute, + value.second, + value.second_part) + else + super + end end end - class Time < Type - def type; :time; end + class << self + TYPES = ConnectionAdapters::Type::HashLookupTypeMap.new # :nodoc: - def type_cast(value) - return if value.nil? + delegate :register_type, :alias_type, to: :TYPES - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.string_to_dummy_time value + def find_type(field) + if field.type == Mysql::Field::TYPE_TINY && field.length > 1 + TYPES.lookup(Mysql::Field::TYPE_LONG) + else + TYPES.lookup(field.type) + end end end - class Float < Type - def type; :float; end - - def type_cast(value) - return if value.nil? - - value.to_f - end - end - - class Decimal < Type - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_decimal value - end - end - - class Boolean < Type - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_boolean value - end - end - - TYPES = {} - - # Register an MySQL +type_id+ with a typecasting object in - # +type+. - def self.register_type(type_id, type) - TYPES[type_id] = type - end - - def self.alias_type(new, old) - TYPES[new] = TYPES[old] - end - - def self.find_type(field) - if field.type == Mysql::Field::TYPE_TINY && field.length > 1 - TYPES[Mysql::Field::TYPE_LONG] - else - TYPES.fetch(field.type) { Fields::Identity.new } - end - end - - register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new - register_type Mysql::Field::TYPE_LONG, Fields::Integer.new + register_type Mysql::Field::TYPE_TINY, Type::Boolean.new + register_type Mysql::Field::TYPE_LONG, Type::Integer.new alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG - register_type Mysql::Field::TYPE_VAR_STRING, Fields::Identity.new - register_type Mysql::Field::TYPE_BLOB, Fields::Identity.new - register_type Mysql::Field::TYPE_DATE, Fields::Date.new + register_type Mysql::Field::TYPE_DATE, Type::Date.new register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new register_type Mysql::Field::TYPE_TIME, Fields::Time.new - register_type Mysql::Field::TYPE_FLOAT, Fields::Float.new + register_type Mysql::Field::TYPE_FLOAT, Type::Float.new + end - Mysql::Field.constants.grep(/TYPE/).map { |class_name| - Mysql::Field.const_get class_name - }.reject { |const| TYPES.key? const }.each do |const| - register_type const, Fields::Identity.new - end + def initialize_type_map(m) # :nodoc: + super + m.register_type %r(datetime)i, Fields::DateTime.new + m.register_type %r(time)i, Fields::Time.new end def exec_without_stmt(sql, name = 'SQL') # :nodoc: @@ -433,7 +343,7 @@ module ActiveRecord fields << field_name if field.decimals > 0 - types[field_name] = Fields::Decimal.new + types[field_name] = Type::Decimal.new else types[field_name] = Fields.find_type field end @@ -449,7 +359,7 @@ module ActiveRecord end end - def execute_and_free(sql, name = nil) + def execute_and_free(sql, name = nil) # :nodoc: result = execute(sql, name) ret = yield result result.free @@ -462,7 +372,7 @@ module ActiveRecord end alias :create :insert_sql - def exec_delete(sql, name, binds) + def exec_delete(sql, name, binds) # :nodoc: affected_rows = 0 exec_query(sql, name, binds) do |n| diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index a14381acb6..0cbedb0987 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -13,19 +13,6 @@ module ActiveRecord string.split(',').map{ |v| Float(v) } end - def string_to_time(string) # :nodoc: - return string unless String === string - - case string - when 'infinity'; Float::INFINITY - when '-infinity'; -Float::INFINITY - when / BC$/ - super("-" + string.sub(/ BC$/, "")) - else - super - end - end - def string_to_bit(value) # :nodoc: case value when /^0x/i diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index 1dd8acc257..80c79642f3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -6,29 +6,20 @@ module ActiveRecord class PostgreSQLColumn < Column #:nodoc: attr_accessor :array - def initialize(name, default, oid_type, sql_type = nil, null = true) - @oid_type = oid_type + def initialize(name, default, cast_type, sql_type = nil, null = true) default_value = self.class.extract_value_from_default(default) if sql_type =~ /\[\]$/ @array = true - super(name, default_value, oid_type, sql_type[0..sql_type.length - 3], null) + super(name, default_value, cast_type, sql_type[0..sql_type.length - 3], null) else @array = false - super(name, default_value, oid_type, sql_type, null) + super(name, default_value, cast_type, sql_type, null) end @default_function = default if has_default_function?(default_value, default) end - def number? - !array && super - end - - def text? - !array && super - end - # :stopdoc: class << self include PostgreSQL::Cast @@ -112,26 +103,8 @@ module ActiveRecord end end - # Casts a Ruby value to something appropriate for writing to PostgreSQL. - # see ActiveRecord::ConnectionAdapters::Class#type_cast_for_write - # see ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::Type - def type_cast_for_write(value) - if @oid_type.respond_to?(:type_cast_for_write) - @oid_type.type_cast_for_write(value) - else - super - end - end - - def type_cast(value) - return if value.nil? - return super if encoded? - - @oid_type.type_cast value - end - def accessor - @oid_type.accessor + cast_type.accessor end private @@ -149,12 +122,6 @@ module ActiveRecord end end - # Extracts the scale from PostgreSQL-specific data types. - def extract_scale(sql_type) - # Money type has a fixed scale of 2. - sql_type =~ /^money/ ? 2 : super - end - # Extracts the precision from PostgreSQL-specific data types. def extract_precision(sql_type) if sql_type == 'money' diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 90bf6c6d1a..1d0384c3df 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -2,43 +2,25 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Type < Type::Value + module Infinity def infinity(options = {}) - ::Float::INFINITY * (options[:negative] ? -1 : 1) + options[:negative] ? -::Float::INFINITY : ::Float::INFINITY 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 + class SpecializedString < Type::String + attr_reader :type def initialize(type) @type = type end - end - class Text < OID::String - def type; :text end + def text? + false + end end - class Bit < Type - def type; :string end - + class Bit < Type::String def type_cast(value) if ::String === value ConnectionAdapters::PostgreSQLColumn.string_to_bit value @@ -48,20 +30,20 @@ module ActiveRecord end end - class Bytea < Type - def type; :binary end - - def type_cast(value) - return if value.nil? + class Bytea < Type::Binary + def cast_value(value) PGconn.unescape_bytea value end end - class Money < Type - def type; :decimal end + class Money < Type::Decimal + include Infinity - def type_cast(value) - return if value.nil? + def extract_scale(sql_type) + 2 + end + + def cast_value(value) return value unless ::String === value # Because money output is formatted according to the locale, there are two @@ -80,11 +62,11 @@ module ActiveRecord value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') end - ConnectionAdapters::Column.value_to_decimal value + super(value) end end - class Vector < Type + class Vector < Type::Value attr_reader :delim, :subtype # +delim+ corresponds to the `typdelim` column in the pg_types @@ -103,9 +85,7 @@ module ActiveRecord end end - class Point < Type - def type; :string end - + class Point < Type::String def type_cast(value) if ::String === value ConnectionAdapters::PostgreSQLColumn.string_to_point value @@ -115,10 +95,10 @@ module ActiveRecord end end - class Array < Type - def type; @subtype.type end - + class Array < Type::Value attr_reader :subtype + delegate :type, to: :subtype + def initialize(subtype) @subtype = subtype end @@ -132,7 +112,7 @@ module ActiveRecord end end - class Range < Type + class Range < Type::Value attr_reader :subtype, :type def initialize(subtype, type) @@ -158,8 +138,8 @@ module ActiveRecord infinity?(value) ? value : @subtype.type_cast(value) end - def type_cast(value) - return if value.nil? || value == 'empty' + def cast_value(value) + return if value == 'empty' return value if value.is_a?(::Range) extracted = extract_bounds(value) @@ -181,109 +161,77 @@ This is not reliable and will be removed in the future. end end - class Integer < Type - def type; :integer end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_integer value - end + class Integer < Type::Integer + include Infinity end - class Boolean < Type - def type; :boolean end - - def type_cast(value) - return if value.nil? + class DateTime < Type::DateTime + include Infinity - ConnectionAdapters::Column.value_to_boolean value + def cast_value(value) + if value.is_a?(::String) + case value + when 'infinity' then ::Float::INFINITY + when '-infinity' then -::Float::INFINITY + when / BC$/ + super("-" + value.sub(/ BC$/, "")) + else + super + end + else + value + end end end - class DateTime < Type - def 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 + class Date < Type::Date + include Infinity 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 + class Time < Type::Time + include Infinity 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 + class Float < Type::Float + include Infinity 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 + when nil then nil + when 'Infinity' then ::Float::INFINITY + when '-Infinity' then -::Float::INFINITY + when 'NaN' then ::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 - + class Decimal < Type::Decimal def infinity(options = {}) BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) end end - class Enum < Type - def type; :enum end + class Enum < Type::Value + def type + :enum + end def type_cast(value) value.to_s end end - class Hstore < Type - def type; :hstore end + class Hstore < Type::Value + 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? - + def cast_value(value) ConnectionAdapters::PostgreSQLColumn.string_to_hstore value end @@ -292,28 +240,32 @@ This is not reliable and will be removed in the future. end end - class Cidr < Type - def type; :cidr end - def type_cast(value) - return if value.nil? + class Cidr < Type::Value + def type + :cidr + end + def cast_value(value) ConnectionAdapters::PostgreSQLColumn.string_to_cidr value end end + class Inet < Cidr - def type; :inet end + def type + :inet + end end - class Json < Type - def type; :json end + class Json < Type::Value + 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? - + def cast_value(value) ConnectionAdapters::PostgreSQLColumn.string_to_json value end @@ -322,47 +274,13 @@ This is not reliable and will be removed in the future. 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 + class Uuid < Type::Value + def type + :uuid 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 } + def type_cast(value) + value.presence end end @@ -402,19 +320,19 @@ This is not reliable and will be removed in the future. end def register_array_type(row) - if subtype = @store[row['typelem'].to_i] + if subtype = @store.lookup(row['typelem'].to_i) register row['oid'], OID::Array.new(subtype) end end def register_range_type(row) - if subtype = @store[row['rngsubtype'].to_i] + if subtype = @store.lookup(row['rngsubtype'].to_i) register row['oid'], OID::Range.new(subtype, row['typname'].to_sym) end end def register_domain_type(row) - if base_type = @store[row["typbasetype"].to_i] + if base_type = @store.lookup(row["typbasetype"].to_i) register row['oid'], base_type else warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}." @@ -422,7 +340,7 @@ This is not reliable and will be removed in the future. end def register_composite_type(row) - if subtype = @store[row['typelem'].to_i] + if subtype = @store.lookup(row['typelem'].to_i) register row['oid'], OID::Vector.new(row['typdelim'], subtype) end end @@ -433,7 +351,7 @@ This is not reliable and will be removed in the future. raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil? return if @store.key?(oid) - @store[oid] = oid_type + @store.register_type(oid, oid_type) end end @@ -442,7 +360,7 @@ This is not reliable and will be removed in the future. # 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 + h[k] = Type::Value.new } # Register an OID type named +name+ with a typecasting object in @@ -469,12 +387,12 @@ This is not reliable and will be removed in the future. 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 + register_type 'text', Type::Text.new + register_type 'varchar', Type::String.new alias_type 'char', 'varchar' alias_type 'name', 'varchar' alias_type 'bpchar', 'varchar' - register_type 'bool', OID::Boolean.new + register_type 'bool', Type::Boolean.new register_type 'bit', OID::Bit.new alias_type 'varbit', 'bit' register_type 'timestamp', OID::DateTime.new diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 59e157744f..ed3e884455 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -216,7 +216,6 @@ module ActiveRecord float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "timestamp" }, - timestamp: { name: "timestamp" }, time: { name: "time" }, date: { name: "date" }, daterange: { name: "daterange" }, @@ -370,7 +369,7 @@ module ActiveRecord raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" end - @type_map = OID::TypeMap.new + @type_map = Type::HashLookupTypeMap.new initialize_type_map(type_map) @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true @@ -544,12 +543,29 @@ module ActiveRecord initialize_type_map(type_map, [oid]) end - type_map.fetch(oid, fmod) { + type_map.fetch(normalize_oid_type(oid, fmod)) { warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String." - type_map[oid] = OID::Identity.new + Type::Value.new.tap do |cast_type| + type_map.register_type(oid, cast_type) + end } end + def normalize_oid_type(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? + 23 + else + ftype + end + end + def initialize_type_map(type_map, oids = nil) if supports_ranges? query = <<-SQL diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 0330fa762c..a5e2619cb8 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -41,14 +41,12 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - class SQLite3Column < Column #:nodoc: - class << self - def binary_to_string(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - value + class SQLite3Binary < Type::Binary # :nodoc: + def cast_value(value) + if value.encoding != Encoding::ASCII_8BIT + value = value.force_encoding(Encoding::ASCII_8BIT) end + value end end @@ -69,7 +67,6 @@ module ActiveRecord float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "datetime" }, - timestamp: { name: "datetime" }, time: { name: "time" }, date: { name: "date" }, binary: { name: "blob" }, @@ -396,7 +393,7 @@ module ActiveRecord sql_type = field['type'] cast_type = lookup_cast_type(sql_type) - SQLite3Column.new(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0) + Column.new(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0) end end @@ -503,6 +500,12 @@ module ActiveRecord end protected + + def initialize_type_map(m) + super + m.register_type(/binary/i, SQLite3Binary.new) + end + def select(sql, name = nil, binds = []) #:nodoc: exec_query(sql, name, binds) end diff --git a/activerecord/lib/active_record/connection_adapters/type.rb b/activerecord/lib/active_record/connection_adapters/type.rb index 34b1e9e39e..763176cb2b 100644 --- a/activerecord/lib/active_record/connection_adapters/type.rb +++ b/activerecord/lib/active_record/connection_adapters/type.rb @@ -1,4 +1,7 @@ +require 'active_record/connection_adapters/type/numeric' +require 'active_record/connection_adapters/type/time_value' require 'active_record/connection_adapters/type/value' + require 'active_record/connection_adapters/type/binary' require 'active_record/connection_adapters/type/boolean' require 'active_record/connection_adapters/type/date' @@ -9,8 +12,9 @@ require 'active_record/connection_adapters/type/integer' require 'active_record/connection_adapters/type/string' require 'active_record/connection_adapters/type/text' require 'active_record/connection_adapters/type/time' -require 'active_record/connection_adapters/type/timestamp' + require 'active_record/connection_adapters/type/type_map' +require 'active_record/connection_adapters/type/hash_lookup_type_map' module ActiveRecord module ConnectionAdapters diff --git a/activerecord/lib/active_record/connection_adapters/type/binary.rb b/activerecord/lib/active_record/connection_adapters/type/binary.rb index 168d824d3d..4b2d1a66e0 100644 --- a/activerecord/lib/active_record/connection_adapters/type/binary.rb +++ b/activerecord/lib/active_record/connection_adapters/type/binary.rb @@ -5,6 +5,14 @@ module ActiveRecord def type :binary end + + def binary? + true + end + + def klass + ::String + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/boolean.rb b/activerecord/lib/active_record/connection_adapters/type/boolean.rb index 938d227632..2337bdd563 100644 --- a/activerecord/lib/active_record/connection_adapters/type/boolean.rb +++ b/activerecord/lib/active_record/connection_adapters/type/boolean.rb @@ -5,6 +5,16 @@ module ActiveRecord def type :boolean end + + private + + def cast_value(value) + if value == '' + nil + else + Column::TRUE_VALUES.include?(value) + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/date.rb b/activerecord/lib/active_record/connection_adapters/type/date.rb index 1632f3c8f4..1e7205fd0b 100644 --- a/activerecord/lib/active_record/connection_adapters/type/date.rb +++ b/activerecord/lib/active_record/connection_adapters/type/date.rb @@ -5,6 +5,39 @@ module ActiveRecord def type :date end + + def klass + ::Date + end + + private + + def cast_value(value) + if value.is_a?(::String) + return if value.empty? + fast_string_to_date(value) || fallback_string_to_date(value) + elsif value.respond_to?(:to_date) + value.to_date + else + value + end + end + + def fast_string_to_date(string) + if string =~ Column::Format::ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def new_date(year, mon, mday) + if year && year != 0 + ::Date.new(year, mon, mday) rescue nil + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/date_time.rb b/activerecord/lib/active_record/connection_adapters/type/date_time.rb index 1d7d3cfbbf..c34f4c5a53 100644 --- a/activerecord/lib/active_record/connection_adapters/type/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/type/date_time.rb @@ -1,12 +1,34 @@ -require 'active_record/connection_adapters/type/timestamp' - module ActiveRecord module ConnectionAdapters module Type - class DateTime < Timestamp # :nodoc: + class DateTime < Value # :nodoc: + include TimeValue + def type :datetime end + + private + + def cast_value(string) + return string unless string.is_a?(::String) + return if string.empty? + + fast_string_to_time(string) || fallback_string_to_time(string) + end + + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 + end + + def fallback_string_to_time(string) + time_hash = ::Date._parse(string) + time_hash[:sec_fraction] = microseconds(time_hash) + + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/decimal.rb b/activerecord/lib/active_record/connection_adapters/type/decimal.rb index 5b39ea9e2f..ac5af4b963 100644 --- a/activerecord/lib/active_record/connection_adapters/type/decimal.rb +++ b/activerecord/lib/active_record/connection_adapters/type/decimal.rb @@ -2,9 +2,25 @@ module ActiveRecord module ConnectionAdapters module Type class Decimal < Value # :nodoc: + include Numeric + def type :decimal end + + def klass + ::BigDecimal + end + + private + + def cast_value(value) + if value.respond_to?(:to_d) + value.to_d + else + value.to_s.to_d + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/float.rb b/activerecord/lib/active_record/connection_adapters/type/float.rb index 089169e7c9..51cfa5d86a 100644 --- a/activerecord/lib/active_record/connection_adapters/type/float.rb +++ b/activerecord/lib/active_record/connection_adapters/type/float.rb @@ -2,9 +2,21 @@ module ActiveRecord module ConnectionAdapters module Type class Float < Value # :nodoc: + include Numeric + def type :float end + + def klass + ::Float + end + + private + + def cast_value(value) + value.to_f + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/connection_adapters/type/hash_lookup_type_map.rb new file mode 100644 index 0000000000..8503d3ea1b --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/hash_lookup_type_map.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class HashLookupTypeMap < TypeMap # :nodoc: + delegate :key?, to: :@mapping + + def lookup(type) + @mapping.fetch(type, proc { default_value }).call(type) + end + + def fetch(type, &block) + @mapping.fetch(type, block).call(type) + end + + def alias_type(type, alias_type) + register_type(type) { lookup(alias_type) } + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/integer.rb b/activerecord/lib/active_record/connection_adapters/type/integer.rb index 5510a11bd4..8f3469434c 100644 --- a/activerecord/lib/active_record/connection_adapters/type/integer.rb +++ b/activerecord/lib/active_record/connection_adapters/type/integer.rb @@ -2,9 +2,25 @@ module ActiveRecord module ConnectionAdapters module Type class Integer < Value # :nodoc: + include Numeric + def type :integer end + + def klass + ::Fixnum + end + + private + + def cast_value(value) + case value + when true then 1 + when false then 0 + else value.to_i rescue nil + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/numeric.rb b/activerecord/lib/active_record/connection_adapters/type/numeric.rb new file mode 100644 index 0000000000..a3379831cb --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/numeric.rb @@ -0,0 +1,20 @@ +module ActiveRecord + module ConnectionAdapters + module Type + module Numeric # :nodoc: + def number? + true + end + + def type_cast_for_write(value) + case value + when true then 1 + when false then 0 + when ::String then value.presence + else super + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/string.rb b/activerecord/lib/active_record/connection_adapters/type/string.rb index 0feb4299f5..55f0e1ee1c 100644 --- a/activerecord/lib/active_record/connection_adapters/type/string.rb +++ b/activerecord/lib/active_record/connection_adapters/type/string.rb @@ -5,6 +5,24 @@ module ActiveRecord def type :string end + + def text? + true + end + + def klass + ::String + end + + private + + def cast_value(value) + case value + when true then "1" + when false then "0" + else value.to_s + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/time.rb b/activerecord/lib/active_record/connection_adapters/type/time.rb index a3a687a8ad..4dd201e3fe 100644 --- a/activerecord/lib/active_record/connection_adapters/type/time.rb +++ b/activerecord/lib/active_record/connection_adapters/type/time.rb @@ -2,9 +2,26 @@ module ActiveRecord module ConnectionAdapters module Type class Time < Value # :nodoc: + include TimeValue + def type :time end + + private + + def cast_value(value) + return value unless value.is_a?(::String) + return if value.empty? + + dummy_time_value = "2000-01-01 #{value}" + + fast_string_to_time(dummy_time_value) || begin + time_hash = ::Date._parse(dummy_time_value) + return if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/type/time_value.rb b/activerecord/lib/active_record/connection_adapters/type/time_value.rb new file mode 100644 index 0000000000..e9ca4adeda --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/time_value.rb @@ -0,0 +1,36 @@ +module ActiveRecord + module ConnectionAdapters + module Type + module TimeValue # :nodoc: + def klass + ::Time + end + + private + + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) + # Treat 0000-00-00 00:00:00 as nil. + return if year.nil? || (year == 0 && mon == 0 && mday == 0) + + if offset + time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return unless time + + time -= offset + Base.default_timezone == :utc ? time : time.getlocal + else + ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + end + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ Column::Format::ISO_DATETIME + microsec = ($7.to_r * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/timestamp.rb b/activerecord/lib/active_record/connection_adapters/type/timestamp.rb deleted file mode 100644 index 92bf0a1954..0000000000 --- a/activerecord/lib/active_record/connection_adapters/type/timestamp.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module Type - class Timestamp < Value # :nodoc: - def type - :timestamp - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/type/value.rb b/activerecord/lib/active_record/connection_adapters/type/value.rb index f7d7b9351b..289c27f6d4 100644 --- a/activerecord/lib/active_record/connection_adapters/type/value.rb +++ b/activerecord/lib/active_record/connection_adapters/type/value.rb @@ -3,6 +3,40 @@ module ActiveRecord module Type class Value # :nodoc: def type; end + + def extract_scale(sql_type) + Type.extract_scale(sql_type) + end + + def type_cast(value) + cast_value(value) unless value.nil? + end + + def type_cast_for_write(value) + value + end + + def text? + false + end + + def number? + false + end + + def binary? + false + end + + def klass + ::Object + end + + private + + def cast_value(value) + value + end end end end |