2015-05-28 18:26:49 -04:00
|
|
|
require 'active_record/attribute/user_provided_default'
|
|
|
|
|
2014-05-23 14:24:52 -04:00
|
|
|
module ActiveRecord
|
2015-02-06 15:39:40 -05:00
|
|
|
# See ActiveRecord::Attributes::ClassMethods for documentation
|
2015-02-06 15:02:03 -05:00
|
|
|
module Attributes
|
2014-05-23 14:24:52 -04:00
|
|
|
extend ActiveSupport::Concern
|
|
|
|
|
2015-02-06 15:39:40 -05:00
|
|
|
# :nodoc:
|
2014-05-27 13:59:22 -04:00
|
|
|
Type = ActiveRecord::Type
|
2014-05-23 14:24:52 -04:00
|
|
|
|
2014-05-28 13:11:57 -04:00
|
|
|
included do
|
2015-02-02 13:02:50 -05:00
|
|
|
class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal:
|
|
|
|
self.attributes_to_define_after_schema_loads = {}
|
2014-05-28 13:11:57 -04:00
|
|
|
end
|
|
|
|
|
2015-02-06 15:02:03 -05:00
|
|
|
module ClassMethods
|
|
|
|
# Defines an attribute with a type on this model. It will override the
|
|
|
|
# type of existing attributes if needed. This allows control over how
|
|
|
|
# values are converted to and from SQL when assigned to a model. It also
|
|
|
|
# changes the behavior of values passed to
|
2015-02-06 15:39:40 -05:00
|
|
|
# ActiveRecord::QueryMethods#where. This will let you use
|
2015-02-06 15:02:03 -05:00
|
|
|
# your domain objects across much of Active Record, without having to
|
|
|
|
# rely on implementation details or monkey patching.
|
|
|
|
#
|
|
|
|
# +name+ The name of the methods to define attribute methods for, and the
|
|
|
|
# column which this will persist to.
|
2014-05-26 20:06:05 -04:00
|
|
|
#
|
2015-02-06 15:44:40 -05:00
|
|
|
# +cast_type+ A symbol such as +:string+ or +:integer+, or a type object
|
|
|
|
# to be used for this attribute. See the examples below for more
|
|
|
|
# information about providing custom type objects.
|
2014-05-26 20:06:05 -04:00
|
|
|
#
|
|
|
|
# ==== Options
|
2015-03-05 08:17:01 -05:00
|
|
|
#
|
2015-02-06 15:02:03 -05:00
|
|
|
# The following options are accepted:
|
|
|
|
#
|
|
|
|
# +default+ The default value to use when no value is provided. If this option
|
|
|
|
# is not passed, the previous default value (if any) will be used.
|
|
|
|
# Otherwise, the default will be +nil+.
|
2014-05-26 20:06:05 -04:00
|
|
|
#
|
2015-03-05 08:17:01 -05:00
|
|
|
# +array+ (PG only) specifies that the type should be an array (see the
|
|
|
|
# examples below).
|
2015-02-06 15:02:03 -05:00
|
|
|
#
|
2015-03-05 08:17:01 -05:00
|
|
|
# +range+ (PG only) specifies that the type should be a range (see the
|
|
|
|
# examples below).
|
2014-05-26 20:06:05 -04:00
|
|
|
#
|
2014-05-23 14:24:52 -04:00
|
|
|
# ==== Examples
|
|
|
|
#
|
2014-06-23 12:50:21 -04:00
|
|
|
# The type detected by Active Record can be overridden.
|
2014-05-23 14:24:52 -04:00
|
|
|
#
|
|
|
|
# # db/schema.rb
|
|
|
|
# create_table :store_listings, force: true do |t|
|
|
|
|
# t.decimal :price_in_cents
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # app/models/store_listing.rb
|
|
|
|
# class StoreListing < ActiveRecord::Base
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# store_listing = StoreListing.new(price_in_cents: '10.1')
|
|
|
|
#
|
|
|
|
# # before
|
|
|
|
# store_listing.price_in_cents # => BigDecimal.new(10.1)
|
|
|
|
#
|
|
|
|
# class StoreListing < ActiveRecord::Base
|
2015-02-06 13:05:38 -05:00
|
|
|
# attribute :price_in_cents, :integer
|
2014-05-23 14:24:52 -04:00
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # after
|
|
|
|
# store_listing.price_in_cents # => 10
|
|
|
|
#
|
2015-02-06 15:47:51 -05:00
|
|
|
# A default can also be provided.
|
|
|
|
#
|
|
|
|
# create_table :store_listings, force: true do |t|
|
|
|
|
# t.string :my_string, default: "original default"
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# StoreListing.new.my_string # => "original default"
|
|
|
|
#
|
|
|
|
# class StoreListing < ActiveRecord::Base
|
|
|
|
# attribute :my_string, :string, default: "new default"
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# StoreListing.new.my_string # => "new default"
|
|
|
|
#
|
2015-05-30 13:06:16 -04:00
|
|
|
# class Product < ActiveRecord::Base
|
|
|
|
# attribute :my_default_proc, :datetime, default: -> { Time.now }
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
|
|
|
|
# sleep 1
|
|
|
|
# Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600
|
|
|
|
#
|
2015-02-06 15:02:03 -05:00
|
|
|
# Attributes do not need to be backed by a database column.
|
|
|
|
#
|
|
|
|
# class MyModel < ActiveRecord::Base
|
|
|
|
# attribute :my_string, :string
|
|
|
|
# attribute :my_int_array, :integer, array: true
|
|
|
|
# attribute :my_float_range, :float, range: true
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# model = MyModel.new(
|
|
|
|
# my_string: "string",
|
|
|
|
# my_int_array: ["1", "2", "3"],
|
|
|
|
# my_float_range: "[1,3.5]",
|
|
|
|
# )
|
|
|
|
# model.attributes
|
|
|
|
# # =>
|
|
|
|
# {
|
|
|
|
# my_string: "string",
|
|
|
|
# my_int_array: [1, 2, 3],
|
|
|
|
# my_float_range: 1.0..3.5
|
|
|
|
# }
|
|
|
|
#
|
|
|
|
# ==== Creating Custom Types
|
|
|
|
#
|
|
|
|
# Users may also define their own custom types, as long as they respond
|
2015-03-05 08:17:01 -05:00
|
|
|
# to the methods defined on the value type. The method +deserialize+ or
|
|
|
|
# +cast+ will be called on your type object, with raw input from the
|
|
|
|
# database or from your controllers. See ActiveRecord::Type::Value for the
|
|
|
|
# expected API. It is recommended that your type objects inherit from an
|
|
|
|
# existing type, or from ActiveRecord::Type::Value
|
2014-05-23 14:24:52 -04:00
|
|
|
#
|
|
|
|
# class MoneyType < ActiveRecord::Type::Integer
|
2015-02-17 15:39:42 -05:00
|
|
|
# def cast(value)
|
2014-05-23 14:24:52 -04:00
|
|
|
# if value.include?('$')
|
|
|
|
# price_in_dollars = value.gsub(/\$/, '').to_f
|
2015-02-06 15:39:40 -05:00
|
|
|
# super(price_in_dollars * 100)
|
2014-05-23 14:24:52 -04:00
|
|
|
# else
|
2015-02-06 15:39:40 -05:00
|
|
|
# super
|
2014-05-23 14:24:52 -04:00
|
|
|
# end
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
2015-02-17 16:17:59 -05:00
|
|
|
# # config/initializers/types.rb
|
|
|
|
# ActiveRecord::Type.register(:money, MoneyType)
|
|
|
|
#
|
|
|
|
# # /app/models/store_listing.rb
|
2014-05-23 14:24:52 -04:00
|
|
|
# class StoreListing < ActiveRecord::Base
|
2015-02-17 16:17:59 -05:00
|
|
|
# attribute :price_in_cents, :money
|
2014-05-23 14:24:52 -04:00
|
|
|
# end
|
|
|
|
#
|
|
|
|
# store_listing = StoreListing.new(price_in_cents: '$10.00')
|
|
|
|
# store_listing.price_in_cents # => 1000
|
2015-02-06 15:02:03 -05:00
|
|
|
#
|
|
|
|
# For more details on creating custom types, see the documentation for
|
2015-02-17 16:17:59 -05:00
|
|
|
# ActiveRecord::Type::Value. For more details on registering your types
|
|
|
|
# to be referenced by a symbol, see ActiveRecord::Type.register. You can
|
|
|
|
# also pass a type object directly, in place of a symbol.
|
2015-02-06 15:02:03 -05:00
|
|
|
#
|
|
|
|
# ==== Querying
|
|
|
|
#
|
2015-02-06 15:39:40 -05:00
|
|
|
# When ActiveRecord::QueryMethods#where is called, it will
|
2015-02-06 15:02:03 -05:00
|
|
|
# use the type defined by the model class to convert the value to SQL,
|
2015-02-17 15:35:23 -05:00
|
|
|
# calling +serialize+ on your type object. For example:
|
2015-02-06 15:02:03 -05:00
|
|
|
#
|
|
|
|
# class Money < Struct.new(:amount, :currency)
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# class MoneyType < Type::Value
|
|
|
|
# def initialize(currency_converter)
|
|
|
|
# @currency_converter = currency_converter
|
|
|
|
# end
|
|
|
|
#
|
2015-02-17 13:29:51 -05:00
|
|
|
# # value will be the result of +deserialize+ or
|
2015-03-05 08:17:01 -05:00
|
|
|
# # +cast+. Assumed to be an instance of +Money+ in
|
2015-02-06 15:02:03 -05:00
|
|
|
# # this case.
|
2015-02-17 15:35:23 -05:00
|
|
|
# def serialize(value)
|
2015-02-08 06:23:46 -05:00
|
|
|
# value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
|
2015-02-06 15:02:03 -05:00
|
|
|
# value_in_bitcoins.amount
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
2015-02-17 16:17:59 -05:00
|
|
|
# ActiveRecord::Type.register(:money, MoneyType)
|
|
|
|
#
|
2015-02-06 15:02:03 -05:00
|
|
|
# class Product < ActiveRecord::Base
|
|
|
|
# currency_converter = ConversionRatesFromTheInternet.new
|
2015-02-17 16:17:59 -05:00
|
|
|
# attribute :price_in_bitcoins, :money, currency_converter
|
2015-02-06 15:02:03 -05:00
|
|
|
# end
|
|
|
|
#
|
|
|
|
# Product.where(price_in_bitcoins: Money.new(5, "USD"))
|
|
|
|
# # => SELECT * FROM products WHERE price_in_bitcoins = 0.02230
|
|
|
|
#
|
|
|
|
# Product.where(price_in_bitcoins: Money.new(5, "GBP"))
|
|
|
|
# # => SELECT * FROM products WHERE price_in_bitcoins = 0.03412
|
|
|
|
#
|
|
|
|
# ==== Dirty Tracking
|
|
|
|
#
|
|
|
|
# The type of an attribute is given the opportunity to change how dirty
|
|
|
|
# tracking is performed. The methods +changed?+ and +changed_in_place?+
|
2015-02-06 15:39:40 -05:00
|
|
|
# will be called from ActiveModel::Dirty. See the documentation for those
|
|
|
|
# methods in ActiveRecord::Type::Value for more details.
|
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 attribute(name, cast_type, **options)
|
2014-05-23 14:24:52 -04:00
|
|
|
name = name.to_s
|
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
|
|
|
reload_schema_from_cache
|
2014-05-23 14:24:52 -04:00
|
|
|
|
2015-02-06 13:05:38 -05:00
|
|
|
self.attributes_to_define_after_schema_loads =
|
|
|
|
attributes_to_define_after_schema_loads.merge(
|
|
|
|
name => [cast_type, options]
|
|
|
|
)
|
2014-05-23 14:24:52 -04:00
|
|
|
end
|
|
|
|
|
2015-02-06 15:02:03 -05:00
|
|
|
# This is the low level API which sits beneath +attribute+. It only
|
|
|
|
# accepts type objects, and will do its work immediately instead of
|
|
|
|
# waiting for the schema to load. Automatic schema detection and
|
2015-02-06 15:39:40 -05:00
|
|
|
# ClassMethods#attribute both call this under the hood. While this method
|
|
|
|
# is provided so it can be used by plugin authors, application code
|
|
|
|
# should probably use ClassMethods#attribute.
|
2015-02-06 15:02:03 -05:00
|
|
|
#
|
|
|
|
# +name+ The name of the attribute being defined. Expected to be a +String+.
|
|
|
|
#
|
2015-02-06 15:39:40 -05:00
|
|
|
# +cast_type+ The type object to use for this attribute.
|
2015-02-06 15:02:03 -05:00
|
|
|
#
|
|
|
|
# +default+ The default value to use when no value is provided. If this option
|
|
|
|
# is not passed, the previous default value (if any) will be used.
|
2015-05-30 13:06:16 -04:00
|
|
|
# Otherwise, the default will be +nil+. A proc can also be passed, and
|
|
|
|
# will be called once each time a new value is needed.
|
2015-02-06 15:02:03 -05:00
|
|
|
#
|
|
|
|
# +user_provided_default+ Whether the default value should be cast using
|
2015-02-17 15:39:42 -05:00
|
|
|
# +cast+ or +deserialize+.
|
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 define_attribute(
|
|
|
|
name,
|
|
|
|
cast_type,
|
|
|
|
default: NO_DEFAULT_PROVIDED,
|
|
|
|
user_provided_default: true
|
|
|
|
)
|
|
|
|
attribute_types[name] = cast_type
|
|
|
|
define_default_attribute(name, default, cast_type, from_user: user_provided_default)
|
2014-05-23 14:24:52 -04:00
|
|
|
end
|
|
|
|
|
2015-02-06 15:02:03 -05:00
|
|
|
def load_schema! # :nodoc:
|
2014-05-23 14:24:52 -04:00
|
|
|
super
|
2015-02-02 13:02:50 -05:00
|
|
|
attributes_to_define_after_schema_loads.each do |name, (type, options)|
|
2015-02-06 13:05:38 -05:00
|
|
|
if type.is_a?(Symbol)
|
2015-02-15 16:21:01 -05:00
|
|
|
type = ActiveRecord::Type.lookup(type, **options.except(:default))
|
2015-02-06 13:05:38 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
define_attribute(name, type, **options.slice(:default))
|
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
|
|
|
end
|
2014-05-23 14:24:52 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
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
|
|
|
NO_DEFAULT_PROVIDED = Object.new # :nodoc:
|
|
|
|
private_constant :NO_DEFAULT_PROVIDED
|
2014-05-28 13:32:00 -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 define_default_attribute(name, value, type, from_user:)
|
|
|
|
if value == NO_DEFAULT_PROVIDED
|
|
|
|
default_attribute = _default_attributes[name].with_type(type)
|
|
|
|
elsif from_user
|
2015-05-28 18:26:49 -04:00
|
|
|
default_attribute = Attribute::UserProvidedDefault.new(
|
|
|
|
name,
|
|
|
|
value,
|
|
|
|
type,
|
Persist user provided default values, even if unchanged
This is a usability change to fix a quirk from our definition of partial
writes. By default, we only persist changed attributes. When creating a
new record, this is assumed that the default values came from the
database. However, if the user provided a default, it will not be
persisted, since we didn't see it as "changed". Since this is a very
specific case, I wanted to isolate it with the other quirks that come
from user provided default values. The number of edge cases which are
presenting themselves are starting to make me wonder if we should just
remove the ability to assign a default, in favor of overriding
`initialize`. For the time being, this is required for the attributes
API to not have confusing behavior.
We had to delete one test, since this actually changes the meaning of
`.changed?` on Active Record models. It now specifically means
`changed_from_database?`. While I think this will make the attributes
API more ergonomic to use, it is a subtle change in definition (though
not a backwards incompatible one). We should probably figure out the
right place to document this. (Feel free to open a PR doing that if
you're reading this).
/cc @rafaelfranca @kirs @senny
This is an alternate implementation of #19921.
Close #19921.
[Sean Griffin & Kir Shatrov]
2015-05-28 18:33:38 -04:00
|
|
|
_default_attributes[name],
|
2015-05-28 18:26:49 -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
|
|
|
else
|
|
|
|
default_attribute = Attribute.from_database(name, value, type)
|
2014-10-31 18:06:14 -04:00
|
|
|
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
|
|
|
_default_attributes[name] = default_attribute
|
2014-10-31 18:06:14 -04:00
|
|
|
end
|
2014-05-23 14:24:52 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|