2014-06-07 15:46:22 -04:00
|
|
|
module ActiveRecord
|
|
|
|
class Attribute # :nodoc:
|
|
|
|
class << self
|
2014-06-22 18:57:40 -04:00
|
|
|
def from_database(name, value, type)
|
|
|
|
FromDatabase.new(name, value, type)
|
2014-06-07 15:46:22 -04:00
|
|
|
end
|
|
|
|
|
2014-06-22 18:57:40 -04:00
|
|
|
def from_user(name, value, type)
|
|
|
|
FromUser.new(name, value, type)
|
2014-06-07 15:46:22 -04:00
|
|
|
end
|
2014-06-22 17:38:55 -04:00
|
|
|
|
`update_column` take ruby-land input, not database-land input
In the case of serialized columns, we would expect the unserialized
value as input, not the serialized value. The original issue which made
this distinction, #14163, introduced a bug. If you passed serialized
input to the method, it would double serialize when it was sent to the
database. You would see the wrong input upon reloading, or get an error
if you had a specific type on the serialized column.
To put it another way, `update_column` is a special case of
`update_all`, which would take `['a']` and not `['a'].to_yaml`, but you
would not pass data from `params` to it.
Fixes #18037
2014-12-16 17:19:47 -05:00
|
|
|
def with_cast_value(name, value, type)
|
|
|
|
WithCastValue.new(name, value, type)
|
|
|
|
end
|
|
|
|
|
2014-06-22 18:57:40 -04:00
|
|
|
def null(name)
|
|
|
|
Null.new(name)
|
|
|
|
end
|
|
|
|
|
|
|
|
def uninitialized(name, type)
|
|
|
|
Uninitialized.new(name, type)
|
2014-06-22 17:38:55 -04:00
|
|
|
end
|
2014-06-07 15:46:22 -04:00
|
|
|
end
|
|
|
|
|
2014-06-22 18:57:40 -04:00
|
|
|
attr_reader :name, :value_before_type_cast, :type
|
2014-06-07 15:46:22 -04:00
|
|
|
|
|
|
|
# This method should not be called directly.
|
|
|
|
# Use #from_database or #from_user
|
2014-06-22 18:57:40 -04:00
|
|
|
def initialize(name, value_before_type_cast, type)
|
|
|
|
@name = name
|
2014-06-07 15:46:22 -04:00
|
|
|
@value_before_type_cast = value_before_type_cast
|
|
|
|
@type = type
|
|
|
|
end
|
|
|
|
|
|
|
|
def value
|
|
|
|
# `defined?` is cheaper than `||=` when we get back falsy values
|
2014-07-12 20:30:49 -04:00
|
|
|
@value = original_value unless defined?(@value)
|
2014-06-07 15:46:22 -04:00
|
|
|
@value
|
|
|
|
end
|
|
|
|
|
2014-07-12 20:30:49 -04:00
|
|
|
def original_value
|
|
|
|
type_cast(value_before_type_cast)
|
|
|
|
end
|
|
|
|
|
2014-06-07 15:46:22 -04:00
|
|
|
def value_for_database
|
2015-02-17 15:35:23 -05:00
|
|
|
type.serialize(value)
|
2014-06-07 15:46:22 -04:00
|
|
|
end
|
|
|
|
|
2014-06-16 13:20:55 -04:00
|
|
|
def changed_from?(old_value)
|
|
|
|
type.changed?(old_value, value, value_before_type_cast)
|
|
|
|
end
|
|
|
|
|
|
|
|
def changed_in_place_from?(old_value)
|
2015-01-20 16:09:53 -05:00
|
|
|
has_been_read? && type.changed_in_place?(old_value, value)
|
2014-06-16 13:20:55 -04:00
|
|
|
end
|
|
|
|
|
2014-06-22 18:25:40 -04:00
|
|
|
def with_value_from_user(value)
|
2014-06-22 18:57:40 -04:00
|
|
|
self.class.from_user(name, value, type)
|
2014-06-22 18:25:40 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def with_value_from_database(value)
|
2014-06-22 18:57:40 -04:00
|
|
|
self.class.from_database(name, value, type)
|
2014-06-22 18:25:40 -04:00
|
|
|
end
|
|
|
|
|
`update_column` take ruby-land input, not database-land input
In the case of serialized columns, we would expect the unserialized
value as input, not the serialized value. The original issue which made
this distinction, #14163, introduced a bug. If you passed serialized
input to the method, it would double serialize when it was sent to the
database. You would see the wrong input upon reloading, or get an error
if you had a specific type on the serialized column.
To put it another way, `update_column` is a special case of
`update_all`, which would take `['a']` and not `['a'].to_yaml`, but you
would not pass data from `params` to it.
Fixes #18037
2014-12-16 17:19:47 -05:00
|
|
|
def with_cast_value(value)
|
|
|
|
self.class.with_cast_value(name, value, type)
|
|
|
|
end
|
|
|
|
|
Attribute assignment and type casting has nothing to do with columns
It's finally finished!!!!!!! The reason the Attributes API was kept
private in 4.2 was due to some publicly visible implementation details.
It was previously implemented by overloading `columns` and
`columns_hash`, to make them return column objects which were modified
with the attribute information.
This meant that those methods LIED! We didn't change the database
schema. We changed the attribute information on the class. That is
wrong! It should be the other way around, where schema loading just
calls the attributes API for you. And now it does!
Yes, this means that there is nothing that happens in automatic schema
loading that you couldn't manually do yourself. (There's still some
funky cases where we hit the connection adapter that I need to handle,
before we can turn off automatic schema detection entirely.)
There were a few weird test failures caused by this that had to be
fixed. The main source came from the fact that the attribute methods are
now defined in terms of `attribute_names`, which has a clause like
`return [] unless table_exists?`. I don't *think* this is an issue,
since the only place this caused failures were in a fake adapter which
didn't override `table_exists?`.
Additionally, there were a few cases where tests were failing because a
migration was run, but the model was not reloaded. I'm not sure why
these started failing from this change, I might need to clear an
additional cache in `reload_schema_from_cache`. Again, since this is not
normal usage, and it's expected that `reset_column_information` will be
called after the table is modified, I don't think it's a problem.
Still, test failures that were unrelated to the change are worrying, and
I need to dig into them further.
Finally, I spent a lot of time debugging issues with the mutex used in
`define_attribute_methods`. I think we can just remove that method
entirely, and define the attribute methods *manually* in the call to
`define_attribute`, which would simplify the code *tremendously*.
Ok. now to make this damn thing public, and work on moving it up to
Active Model.
2015-01-30 16:03:36 -05:00
|
|
|
def with_type(type)
|
|
|
|
self.class.new(name, value_before_type_cast, type)
|
|
|
|
end
|
|
|
|
|
2014-07-12 20:30:49 -04:00
|
|
|
def type_cast(*)
|
2014-06-07 15:46:22 -04:00
|
|
|
raise NotImplementedError
|
|
|
|
end
|
|
|
|
|
2014-06-22 17:38:55 -04:00
|
|
|
def initialized?
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
2015-01-14 19:08:25 -05:00
|
|
|
def came_from_user?
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2015-01-20 16:09:53 -05:00
|
|
|
def has_been_read?
|
|
|
|
defined?(@value)
|
|
|
|
end
|
|
|
|
|
2014-08-15 15:37:53 -04:00
|
|
|
def ==(other)
|
|
|
|
self.class == other.class &&
|
|
|
|
name == other.name &&
|
|
|
|
value_before_type_cast == other.value_before_type_cast &&
|
|
|
|
type == other.type
|
|
|
|
end
|
2015-01-27 15:42:02 -05:00
|
|
|
alias eql? ==
|
|
|
|
|
|
|
|
def hash
|
|
|
|
[self.class, name, value_before_type_cast, type].hash
|
|
|
|
end
|
2014-08-15 15:37:53 -04:00
|
|
|
|
2014-06-07 15:46:22 -04:00
|
|
|
protected
|
|
|
|
|
|
|
|
def initialize_dup(other)
|
|
|
|
if defined?(@value) && @value.duplicable?
|
|
|
|
@value = @value.dup
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-06-24 06:30:21 -04:00
|
|
|
class FromDatabase < Attribute # :nodoc:
|
2014-06-07 15:46:22 -04:00
|
|
|
def type_cast(value)
|
2015-02-17 13:29:51 -05:00
|
|
|
type.deserialize(value)
|
2014-06-07 15:46:22 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-06-24 06:30:21 -04:00
|
|
|
class FromUser < Attribute # :nodoc:
|
2014-06-07 15:46:22 -04:00
|
|
|
def type_cast(value)
|
2015-02-17 15:39:42 -05:00
|
|
|
type.cast(value)
|
2014-06-07 15:46:22 -04:00
|
|
|
end
|
2015-01-14 19:08:25 -05:00
|
|
|
|
|
|
|
def came_from_user?
|
|
|
|
true
|
|
|
|
end
|
2014-06-07 15:46:22 -04:00
|
|
|
end
|
2014-06-16 13:20:55 -04:00
|
|
|
|
`update_column` take ruby-land input, not database-land input
In the case of serialized columns, we would expect the unserialized
value as input, not the serialized value. The original issue which made
this distinction, #14163, introduced a bug. If you passed serialized
input to the method, it would double serialize when it was sent to the
database. You would see the wrong input upon reloading, or get an error
if you had a specific type on the serialized column.
To put it another way, `update_column` is a special case of
`update_all`, which would take `['a']` and not `['a'].to_yaml`, but you
would not pass data from `params` to it.
Fixes #18037
2014-12-16 17:19:47 -05:00
|
|
|
class WithCastValue < Attribute # :nodoc:
|
|
|
|
def type_cast(value)
|
|
|
|
value
|
|
|
|
end
|
|
|
|
|
|
|
|
def changed_in_place_from?(old_value)
|
|
|
|
false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-06-22 18:57:40 -04:00
|
|
|
class Null < Attribute # :nodoc:
|
|
|
|
def initialize(name)
|
|
|
|
super(name, nil, Type::Value.new)
|
2014-06-22 18:25:40 -04:00
|
|
|
end
|
2014-06-22 17:38:55 -04:00
|
|
|
|
2014-06-22 18:25:40 -04:00
|
|
|
def value
|
|
|
|
nil
|
2014-06-22 17:38:55 -04:00
|
|
|
end
|
2014-06-26 11:36:40 -04:00
|
|
|
|
Attribute assignment and type casting has nothing to do with columns
It's finally finished!!!!!!! The reason the Attributes API was kept
private in 4.2 was due to some publicly visible implementation details.
It was previously implemented by overloading `columns` and
`columns_hash`, to make them return column objects which were modified
with the attribute information.
This meant that those methods LIED! We didn't change the database
schema. We changed the attribute information on the class. That is
wrong! It should be the other way around, where schema loading just
calls the attributes API for you. And now it does!
Yes, this means that there is nothing that happens in automatic schema
loading that you couldn't manually do yourself. (There's still some
funky cases where we hit the connection adapter that I need to handle,
before we can turn off automatic schema detection entirely.)
There were a few weird test failures caused by this that had to be
fixed. The main source came from the fact that the attribute methods are
now defined in terms of `attribute_names`, which has a clause like
`return [] unless table_exists?`. I don't *think* this is an issue,
since the only place this caused failures were in a fake adapter which
didn't override `table_exists?`.
Additionally, there were a few cases where tests were failing because a
migration was run, but the model was not reloaded. I'm not sure why
these started failing from this change, I might need to clear an
additional cache in `reload_schema_from_cache`. Again, since this is not
normal usage, and it's expected that `reset_column_information` will be
called after the table is modified, I don't think it's a problem.
Still, test failures that were unrelated to the change are worrying, and
I need to dig into them further.
Finally, I spent a lot of time debugging issues with the mutex used in
`define_attribute_methods`. I think we can just remove that method
entirely, and define the attribute methods *manually* in the call to
`define_attribute`, which would simplify the code *tremendously*.
Ok. now to make this damn thing public, and work on moving it up to
Active Model.
2015-01-30 16:03:36 -05:00
|
|
|
def with_type(type)
|
|
|
|
self.class.with_cast_value(name, nil, type)
|
|
|
|
end
|
|
|
|
|
2014-06-26 11:36:40 -04:00
|
|
|
def with_value_from_database(value)
|
|
|
|
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
|
|
|
|
end
|
|
|
|
alias_method :with_value_from_user, :with_value_from_database
|
2014-06-22 17:38:55 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
class Uninitialized < Attribute # :nodoc:
|
2014-06-22 18:57:40 -04:00
|
|
|
def initialize(name, type)
|
|
|
|
super(name, nil, type)
|
2014-06-22 17:38:55 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def value
|
2014-06-22 18:57:40 -04:00
|
|
|
if block_given?
|
|
|
|
yield name
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def value_for_database
|
2014-06-22 17:38:55 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def initialized?
|
|
|
|
false
|
2014-06-16 13:20:55 -04:00
|
|
|
end
|
|
|
|
end
|
2015-01-27 15:08:00 -05:00
|
|
|
private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
|
2014-06-07 15:46:22 -04:00
|
|
|
end
|
|
|
|
end
|