-
Notifications
You must be signed in to change notification settings - Fork 21.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix decorated type with type_for_attribute
on the serialized attribute
#41139
Fix decorated type with type_for_attribute
on the serialized attribute
#41139
Conversation
It is a regression test for rails#41138.
…riginal-attribute-type" This reverts commit 79d0c17, reversing changes made to bc828f7. Fixes rails#41138.
@kamipo If this PR is the direction you prefer, then I am fine with that. However, to me, it seems like def encrypts(name)
attribute(name) { |subtype| EncryptedType.new(subtype: subtype) }
end But this would not work currently. We could make it work by implementing decorator stacking, as I mentioned in #39929 (comment). Something like: decorator = [prev_decorator, decorator].compact.reduce(&:>>) Also, I realize the |
21af293
to
70259f5
Compare
klass = Class.new(ActiveRecord::Base) do | ||
self.table_name = Topic.table_name | ||
store :content | ||
attribute(:content) { |subtype| EncryptedType.new(subtype: subtype) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've allowed multiple decorations which can be used by such like EncryptedType
in 70259f5.
prev_cast_type, prev_options, prev_decorator = attributes_to_define_after_schema_loads[name] | ||
case cast_type | ||
when Symbol | ||
type = cast_type | ||
cast_type = -> _ { Type.lookup(type, **options, adapter: Type.adapter_name_from(self)) } | ||
when nil | ||
if (prev_cast_type, prev_default = attributes_to_define_after_schema_loads[name]) | ||
default = prev_default if default == NO_DEFAULT_PROVIDED | ||
|
||
unless cast_type && prev_cast_type | ||
cast_type ||= prev_cast_type | ||
options = prev_options || options if options.empty? | ||
decorator ||= prev_decorator | ||
cast_type = if block_given? | ||
-> subtype { yield Proc === prev_cast_type ? prev_cast_type[subtype] : prev_cast_type } | ||
else | ||
prev_cast_type | ||
end | ||
else | ||
cast_type = block || -> subtype { subtype } | ||
end | ||
end | ||
|
||
self.attributes_to_define_after_schema_loads = attributes_to_define_after_schema_loads.merge( | ||
name => [cast_type, options, decorator] | ||
) | ||
self.attributes_to_define_after_schema_loads = | ||
attributes_to_define_after_schema_loads.merge(name => [cast_type, default]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we assume that attributes_to_define_after_schema_loads[name][0]
will now always be either a Proc
or nil
? If so, what about:
prev_type_proc, prev_default = attributes_to_define_after_schema_loads[name]
type_proc =
if cast_type
-> _ { Type.lookup(cast_type, **options, adapter: Type.adapter_name_from(self)) }
else
[prev_type_proc, block].compact.reduce { |f, g| -> subtype { g[f[subtype]] } }
end
default = prev_default if cast_type.nil? && default == NO_DEFAULT_PROVIDED
self.attributes_to_define_after_schema_loads =
attributes_to_define_after_schema_loads.merge(name => [type_proc, default])
The else
condition could be written more neatly as [prev_type_proc, block].compact.reduce(&:>>)
when we drop Ruby 2.5 support, or if we add a Proc#>>
polyfill.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cast_type
will always be Proc
or a type object, isn't nil
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i.e. attribute :attr_name, EncryptedType.new
should work as it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right! I forgot that case. So then perhaps:
prev_type_proc, prev_default = attributes_to_define_after_schema_loads[name]
type_proc =
case cast_type
when nil
[prev_type_proc, block].compact.reduce { |f, g| -> subtype { g[f[subtype]] } }
when Symbol
-> _ { Type.lookup(cast_type, **options, adapter: Type.adapter_name_from(self)) }
else
-> _ { cast_type }
end
default = prev_default if cast_type.nil? && default == NO_DEFAULT_PROVIDED
self.attributes_to_define_after_schema_loads =
attributes_to_define_after_schema_loads.merge(name => [type_proc, default])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default = prev_default if cast_type.nil? && default == NO_DEFAULT_PROVIDED
in the above will override database default to nil
accidentally, and [prev_type_proc, block].compact.reduce { |f, g| -> subtype { g[f[subtype]] } }
can be nil
.
Personally I'm not interested creating extra block especially when cast_type
is a symbol or a type object which are the most case, just to use Proc#>>
in a future.
(Wrapping a block for a symbol is to avoid bin/test
testing issue. I'll fix that in a later PR.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and
[prev_type_proc, block].compact.reduce { |f, g| -> subtype { g[f[subtype]] } }
can benil
.
Yes. It would require changing load_schema!
from
cast_type = cast_type[type_for_attribute(name)] if Proc === cast_type
to something like
cast_type = type_for_attribute(name)
cast_type = type_proc[cast_type] if type_proc
default = prev_default if cast_type.nil? && default == NO_DEFAULT_PROVIDED
in the above will override database default tonil
accidentally,
That could be addressed by adding a default value of [nil, NO_DEFAULT_PROVIDED]
to the attributes_to_define_after_schema_loads
Hash.
However, it seems like you have plans for this code, so let's do the way that you prefer! 👍
klass = Class.new(ActiveRecord::Base) do | ||
self.table_name = Topic.table_name | ||
store :content | ||
attribute :content, :encrypted, subtype: type_for_attribute(:content) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do users have to provide a subtype or can it be inferred automatically? Feels like attribute should automatically cascade the types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean that we should provide a way to inject a subtype to the type which has subtype
option?
rails/activerecord/test/cases/serialized_attribute_test.rb
Lines 395 to 400 in 70259f5
class EncryptedType < ActiveRecord::Type::Text | |
include ActiveModel::Type::Helpers::Mutable | |
attr_reader :subtype, :encryptor | |
def initialize(subtype: ActiveModel::Type::String.new) |
rails/activerecord/test/cases/serialized_attribute_test.rb
Lines 446 to 450 in 70259f5
klass = Class.new(ActiveRecord::Base) do | |
self.table_name = Topic.table_name | |
store :content | |
attribute(:content) { |subtype| EncryptedType.new(subtype: subtype) } | |
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what you mean by "automatically cascade the types".
In 6.1, there is no reliable way. If an attribute isn't decorated (no enum
, serialize
, store
are used), attribute(:content) { |subtype| EncryptedType.new(subtype: subtype) }
, and decorate_attribute_type("content") { |subtype| EncryptedType.new(subtype: subtype) }
works.
In main branch, attribute(:content) { |subtype| EncryptedType.new(subtype: subtype) }
works even if enum
, serialize
, store
are used on the attribute (decorate_attribute_type
is removed in the branch).
If we provide a way for decorator officially, I feel that it is better to provide the API separately (e.g. decorate_attribute
etc) from the attribute
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wups, sorry, I forgot to get back to this! I'd just like to be able to do:
store :content
attribute :content, :encrypted
without needing to pass the subtype
, attribute :content, :encrypted, subtype: type_for_attribute(:content)
.
If we provide a way for decorator officially, I feel that it is better to provide the API separately (e.g.
decorate_attribute
etc) from theattribute
.
That also sounds good 👍
Thank you both! |
Just an implementation for rails#41139 (comment).
The regression #41138 which is caused by #39929 is happened due to using
cast_type
(:encrypted
) withprev_decorator
(serialize
instore
).Since multiple decorations (
:encrypted
onstore
on the original attribute) were never officially supported, it was just a coincidence that it worked, but I don't want to break that in exchange of allowing to ommit a type on subclasses.IMO, even if a type on subclasses can be ommited, I'd not recommend to ommit the type on subclasses for devs.
It is a similar point of view with 94ba417#r41923157.
Fixes #41138.
cc @georgeclaghorn @kaspth @jonathanhefner