1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Merge pull request #39599 from kamipo/sg-immutable-strings-by-default

Add a setting to specify that all string columns should be immutable
This commit is contained in:
Ryuta Kamizono 2020-06-13 15:51:42 +09:00 committed by GitHub
commit 00c2d3e1e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 123 additions and 44 deletions

View file

@ -3,6 +3,12 @@
module ActiveModel
module Type
class ImmutableString < Value # :nodoc:
def initialize(**args)
@true = -(args.delete(:true)&.to_s || "t")
@false = -(args.delete(:false)&.to_s || "f")
super
end
def type
:string
end
@ -10,21 +16,19 @@ module ActiveModel
def serialize(value)
case value
when ::Numeric, ::Symbol, ActiveSupport::Duration then value.to_s
when true then "t"
when false then "f"
when true then @true
when false then @false
else super
end
end
private
def cast_value(value)
result = \
case value
when true then "t"
when false then "f"
else value.to_s
end
result.freeze
case value
when true then @true
when false then @false
else value.to_s.freeze
end
end
end
end

View file

@ -8,6 +8,11 @@ module ActiveModel
@registrations = []
end
def initialize_dup(other)
@registrations = @registrations.dup
super
end
def register(type_name, klass = nil, **options, &block)
unless block_given?
block = proc { |_, *args| klass.new(*args) }

View file

@ -11,12 +11,22 @@ module ActiveModel
end
end
def to_immutable_string
ImmutableString.new(
true: @true,
false: @false,
limit: limit,
precision: precision,
scale: scale,
)
end
private
def cast_value(value)
case value
when ::String then ::String.new(value)
when true then "t"
when false then "f"
when true then @true
when false then @false
else value.to_s
end
end

View file

@ -1,3 +1,10 @@
* Added the setting `ActiveRecord::Base.immutable_strings_by_default`, which
allows you to specify that all string columns should be frozen unless
otherwise specified. This will reduce memory pressure for applications which
do not generally mutate string properties of Active Record objects.
*Sean Griffin*
* Deprecate `map!` and `collect!` on `ActiveRecord::Result`.
*Ryuta Kamizono*

View file

@ -576,7 +576,10 @@ module ActiveRecord
def initialize_type_map(m = type_map)
super
register_class_with_limit m, %r(char)i, MysqlString
m.register_type(%r(char)i) do |sql_type|
limit = extract_limit(sql_type)
Type.lookup(:string, adapter: :mysql2, limit: limit)
end
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)
@ -596,11 +599,11 @@ module ActiveRecord
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.alias_type %r(year)i, "integer"
m.alias_type %r(bit)i, "binary"
m.register_type %r(^enum)i, MysqlString.new
m.register_type %r(^set)i, MysqlString.new
m.register_type %r(^enum)i, Type.lookup(:string, adapter: :mysql2)
m.register_type %r(^set)i, Type.lookup(:string, adapter: :mysql2)
end
def register_integer_type(mapping, key, **options)
@ -841,26 +844,12 @@ module ActiveRecord
full_version_string.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1]
end
class MysqlString < Type::String # :nodoc:
def serialize(value)
case value
when true then "1"
when false then "0"
else super
end
end
private
def cast_value(value)
case value
when true then "1"
when false then "0"
else super
end
end
ActiveRecord::Type.register(:immutable_string, adapter: :mysql2) do |_, **args|
Type::ImmutableString.new(true: "1", false: "0", **args)
end
ActiveRecord::Type.register(:string, adapter: :mysql2) do |_, **args|
Type::String.new(true: "1", false: "0", **args)
end
ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2)
ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2)
end
end

View file

@ -117,6 +117,15 @@ module ActiveRecord
# during an ordered finder call. Useful when the primary key is not an
# auto-incrementing integer, for example when it's a UUID. Records are subsorted
# by the primary key if it exists to ensure deterministic results.
##
# :singleton-method: immutable_strings_by_default=
# :call-seq: immutable_strings_by_default=(bool)
#
# Determines whether columns should infer their type as `:string` or
# `:immutable_string`. This setting does not affect the behavior of
# `attribute :foo, :string`. Defaults to false.
included do
mattr_accessor :primary_key_prefix_type, instance_writer: false
@ -126,6 +135,7 @@ module ActiveRecord
class_attribute :internal_metadata_table_name, instance_accessor: false, default: "ar_internal_metadata"
class_attribute :pluralize_table_names, instance_writer: false, default: true
class_attribute :implicit_order_column, instance_accessor: false
class_attribute :immutable_strings_by_default, instance_accessor: false
self.protected_environments = ["production"]
self.inheritance_column = "type"
@ -495,9 +505,11 @@ module ActiveRecord
columns_hash = columns_hash.except(*ignored_columns) unless ignored_columns.empty?
@columns_hash = columns_hash.freeze
@columns_hash.each do |name, column|
type = connection.lookup_cast_type_from_column(column)
type = _convert_type_from_options(type)
define_attribute(
name,
connection.lookup_cast_type_from_column(column),
type,
default: column.default,
user_provided_default: false
)
@ -546,6 +558,14 @@ module ActiveRecord
base_class.table_name
end
end
def _convert_type_from_options(type)
if immutable_strings_by_default && type.respond_to?(:to_immutable_string)
type.to_immutable_string
else
type
end
end
end
end
end

View file

@ -63,6 +63,7 @@ module ActiveRecord
Decimal = ActiveModel::Type::Decimal
Float = ActiveModel::Type::Float
Integer = ActiveModel::Type::Integer
ImmutableString = ActiveModel::Type::ImmutableString
String = ActiveModel::Type::String
Value = ActiveModel::Type::Value
@ -74,6 +75,7 @@ module ActiveRecord
register(:decimal, Type::Decimal, override: false)
register(:float, Type::Float, override: false)
register(:integer, Type::Integer, override: false)
register(:immutable_string, Type::ImmutableString, override: false)
register(:json, Type::Json, override: false)
register(:string, Type::String, override: false)
register(:text, Type::Text, override: false)

View file

@ -111,22 +111,24 @@ module ActiveRecord
test "overloading properties does not attribute method order" do
attribute_names = OverloadedType.attribute_names
assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), attribute_names
expected = OverloadedType.column_names + ["non_existent_decimal"]
assert_equal expected, attribute_names
end
test "caches are cleared" do
klass = Class.new(OverloadedType)
column_count = klass.columns.length
assert_equal 6, klass.attribute_types.length
assert_equal 6, klass.column_defaults.length
assert_equal 6, klass.attribute_names.length
assert_equal column_count + 1, klass.attribute_types.length
assert_equal column_count + 1, klass.column_defaults.length
assert_equal column_count + 1, klass.attribute_names.length
assert_not klass.attribute_types.include?("wibble")
klass.attribute :wibble, Type::Value.new
assert_equal 7, klass.attribute_types.length
assert_equal 7, klass.column_defaults.length
assert_equal 7, klass.attribute_names.length
assert_equal column_count + 2, klass.attribute_types.length
assert_equal column_count + 2, klass.column_defaults.length
assert_equal column_count + 2, klass.attribute_names.length
assert_includes klass.attribute_types, "wibble"
end
@ -292,5 +294,44 @@ module ActiveRecord
assert_equal 1, klass.new(no_type: 1).no_type
assert_equal "foo", klass.new(no_type: "foo").no_type
end
test "immutable_strings_by_default changes schema inference for string columns" do
with_immutable_strings do
OverloadedType.reset_column_information
immutable_string_type = Type.lookup(:immutable_string).class
assert_instance_of immutable_string_type, OverloadedType.type_for_attribute("inferred_string")
end
end
test "immutable_strings_by_default retains limit information" do
with_immutable_strings do
OverloadedType.reset_column_information
assert_equal 255, OverloadedType.type_for_attribute("inferred_string").limit
end
end
test "immutable_strings_by_default does not affect `attribute :foo, :string`" do
with_immutable_strings do
OverloadedType.reset_column_information
default_string_type = Type.lookup(:string).class
assert_instance_of default_string_type, OverloadedType.type_for_attribute("string_with_default")
end
end
test "serialize boolean for both string types" do
default_string_type = Type.lookup(:string)
immutable_string_type = Type.lookup(:immutable_string)
assert_equal default_string_type.serialize(true), immutable_string_type.serialize(true)
assert_equal default_string_type.serialize(false), immutable_string_type.serialize(false)
end
private
def with_immutable_strings
old_value = ActiveRecord::Base.immutable_strings_by_default
ActiveRecord::Base.immutable_strings_by_default = true
yield
ensure
ActiveRecord::Base.immutable_strings_by_default = old_value
end
end
end

View file

@ -588,7 +588,7 @@ end
class InheritanceAttributeMappingTest < ActiveRecord::TestCase
setup do
@old_registry = ActiveRecord::Type.registry
ActiveRecord::Type.registry = ActiveRecord::Type::AdapterSpecificRegistry.new
ActiveRecord::Type.registry = ActiveRecord::Type.registry.dup
ActiveRecord::Type.register :omg_sti, InheritanceAttributeMappingTest::OmgStiType
Company.delete_all
Sponsor.delete_all

View file

@ -1125,6 +1125,7 @@ ActiveRecord::Schema.define do
t.float :unoverloaded_float
t.string :overloaded_string_with_limit, limit: 255
t.string :string_with_default, default: "the original default"
t.string :inferred_string, limit: 255
end
create_table :users, force: true do |t|