ActiveRecordでinteger型の項目をマイグレして、int型の最大値以上の数を入れて保存しようとすると、モデル側で以下のエラーが出ます。
ActiveModel::RangeError: 2147483648 is out of range for ActiveModel::Type::Integer with limit 4
最大値のエラーなのでエラー自体には納得なんですが、この手のエラーはバリデーションでerrorsに格納してほしいなーと思い、ソースコードを読んでみました。
新規作成時はdatabase_statement.rbのinsertメソッドが呼び出され、
- ActiveRecord::ConnectionAdapters::DatabaseStatement#to_sql
- BindCollector#compile
- ActiveRecord::ConnectionAdapters::Quoting#prepare_binds_for_database
- ActiveRecord::Attribute#value_for_database
とメソッドが呼ばれていきます。
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
sql, binds, pk, sequence_name = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds)
value = exec_insert(sql, name, binds, pk, sequence_name)
id_value || last_inserted_id(value)
end
# Converts an arel AST to SQL
def to_sql(arel, binds = [])
if arel.respond_to?(:ast)
collected = visitor.accept(arel.ast, collector)
collected.compile(binds.dup, self)
else
arel
end
end
class BindCollector < Arel::Collectors::Bind
def compile(bvs, conn)
casted_binds = conn.prepare_binds_for_database(bvs)
super(casted_binds.map { |value| conn.quote(value) })
end
end
def prepare_binds_for_database(binds) # :nodoc:
binds.map(&:value_for_database)
end
def value_for_database
type.serialize(value)
end
binds変数にはQueryAttributeの配列が入ります。int型の項目の場合はQueryAttributeオブジェクトのtypeにはType::Integer型のオブジェクトが入ります。このType::Integerのserializeでrangeチェックをしていて、range外の場合はRangeErrorがraiseされます。
DEFAULT_LIMIT = 4
def serialize(value)
result = cast(value)
if result
ensure_in_range(result)
end
result
end
def ensure_in_range(value)
unless range.cover?(value)
raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}"
end
end
def initialize(*)
super
@range = min_value...max_value
end
def max_value
1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign
end
def min_value
-max_value
end
def _limit
self.limit || DEFAULT_LIMIT
end
MySQLの場合は以下のTypeMapの設定によってMySQLの型からRubyオブジェクトのType::XXXに変換されます。intの場合はlimit: 4、bigintの場合はlimit: 8のように、項目によってrangeの桁数が変わります。MySQLの項目型の判定は正規表現マッチを使っています。
def initialize_type_map(m) # :nodoc:
super
register_class_with_limit m, %r(char)i, MysqlString
m.register_type %r(tinytext)i, Type::Text.new(limit: 2**8 - 1)
m.register_type %r(tinyblob)i, Type::Binary.new(limit: 2**8 - 1)
m.register_type %r(text)i, Type::Text.new(limit: 2**16 - 1)
m.register_type %r(blob)i, Type::Binary.new(limit: 2**16 - 1)
m.register_type %r(mediumtext)i, Type::Text.new(limit: 2**24 - 1)
m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1)
m.register_type %r(longtext)i, Type::Text.new(limit: 2**32 - 1)
m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1)
m.register_type %r(^float)i, Type::Float.new(limit: 24)
m.register_type %r(^double)i, Type::Float.new(limit: 53)
m.register_type %r(^json)i, MysqlJson.new
register_integer_type m, %r(^bigint)i, limit: 8
register_integer_type m, %r(^int)i, limit: 4
register_integer_type m, %r(^mediumint)i, limit: 3
register_integer_type m, %r(^smallint)i, limit: 2
register_integer_type m, %r(^tinyint)i, limit: 1
m.register_type %r(^tinyint\(1\))i, Type::Boolean.new if emulate_booleans
m.alias_type %r(year)i, 'integer'
m.alias_type %r(bit)i, 'binary'
m.register_type(%r(enum)i) do |sql_type|
limit = sql_type[/^enum\((.+)\)/i, 1]
.split(',').map{|enum| enum.strip.length - 2}.max
MysqlString.new(limit: limit)
end
m.register_type(%r(^set)i) do |sql_type|
limit = sql_type[/^set\((.+)\)/i, 1]
.split(',').map{|set| set.strip.length - 1}.sum - 1
MysqlString.new(limit: limit)
end
end
項目の型はSHOW FULL FIELDS FROM
で取得しています。
def column_definitions(table_name) # :nodoc:
execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result|
each_hash(result)
end
end