diff options
Diffstat (limited to 'activerecord')
14 files changed, 216 insertions, 124 deletions
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 24231dc9e1..9530f134d0 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -77,7 +77,11 @@ module ActiveRecord end def with_type(type) - self.class.new(name, value_before_type_cast, type, original_attribute) + if changed_in_place? + with_value_from_user(value).with_type(type) + else + self.class.new(name, value_before_type_cast, type, original_attribute) + end end def type_cast(*) @@ -108,6 +112,22 @@ module ActiveRecord [self.class, name, value_before_type_cast, type].hash end + def init_with(coder) + @name = coder["name"] + @value_before_type_cast = coder["value_before_type_cast"] + @type = coder["type"] + @original_attribute = coder["original_attribute"] + @value = coder["value"] if coder.map.key?("value") + end + + def encode_with(coder) + coder["name"] = name + coder["value_before_type_cast"] = value_before_type_cast if value_before_type_cast + coder["type"] = type if type + coder["original_attribute"] = original_attribute if original_attribute + coder["value"] = value if defined?(@value) + end + protected attr_reader :original_attribute @@ -185,6 +205,8 @@ module ActiveRecord end class Uninitialized < Attribute # :nodoc: + UNINITIALIZED_ORIGINAL_VALUE = Object.new + def initialize(name, type) super(name, nil, type) end @@ -195,12 +217,20 @@ module ActiveRecord end end + def original_value + UNINITIALIZED_ORIGINAL_VALUE + end + def value_for_database end def initialized? false end + + def with_type(type) + self.class.new(name, type) + end end private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index be581ac2a9..720d5f8b7c 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -1,7 +1,10 @@ require 'active_record/attribute_set/builder' +require 'active_record/attribute_set/yaml_encoder' module ActiveRecord class AttributeSet # :nodoc: + delegate :each_value, to: :attributes + def initialize(attributes) @attributes = attributes end diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index 3bd7c7997b..24a255efc1 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -22,7 +22,7 @@ module ActiveRecord end class LazyAttributeHash # :nodoc: - delegate :transform_values, :each_key, to: :materialize + delegate :transform_values, :each_key, :each_value, to: :materialize def initialize(types, values, additional_types) @types = types diff --git a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb new file mode 100644 index 0000000000..6208048231 --- /dev/null +++ b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb @@ -0,0 +1,39 @@ +module ActiveRecord + class AttributeSet + # Attempts to do more intelligent YAML dumping of an + # ActiveRecord::AttributeSet to reduce the size of the resulting string + class YAMLEncoder + def initialize(default_types) + @default_types = default_types + end + + def encode(attribute_set, coder) + coder['concise_attributes'] = attribute_set.each_value.map do |attr| + if attr.type.equal?(default_types[attr.name]) + attr.with_type(nil) + else + attr + end + end + end + + def decode(coder) + if coder['attributes'] + coder['attributes'] + else + attributes_hash = Hash[coder['concise_attributes'].map do |attr| + if attr.type.nil? + attr = attr.with_type(default_types[attr.name]) + end + [attr.name, attr] + end] + AttributeSet.new(attributes_hash) + end + end + + protected + + attr_reader :default_types + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index bbb0e9249d..8dbafc5a4b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -51,11 +51,12 @@ module ActiveRecord options[:primary_key] != default_primary_key end - def defined_for?(options_or_to_table = {}) - if options_or_to_table.is_a?(Hash) - options_or_to_table.all? {|key, value| options[key].to_s == value.to_s } + def defined_for?(to_table_ord = nil, to_table: nil, **options) + if to_table_ord + self.to_table == to_table_ord.to_s else - to_table == options_or_to_table.to_s + (to_table.nil? || to_table.to_s == self.to_table) && + options.all? { |k, v| self.options[k].to_s == v.to_s } end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index d45ca9b24b..de337b24d6 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -338,7 +338,7 @@ module ActiveRecord # post.title # => 'hello world' def init_with(coder) coder = LegacyYamlAdapter.convert(self.class, coder) - @attributes = coder['attributes'] + @attributes = self.class.yaml_encoder.decode(coder) init_internals @@ -404,11 +404,9 @@ module ActiveRecord # Post.new.encode_with(coder) # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) - # FIXME: Remove this when we better serialize attributes - coder['raw_attributes'] = attributes_before_type_cast - coder['attributes'] = @attributes + self.class.yaml_encoder.encode(@attributes, coder) coder['new_record'] = new_record? - coder['active_record_yaml_version'] = 1 + coder['active_record_yaml_version'] = 2 end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ diff --git a/activerecord/lib/active_record/legacy_yaml_adapter.rb b/activerecord/lib/active_record/legacy_yaml_adapter.rb index 89dee58423..c7683f68c7 100644 --- a/activerecord/lib/active_record/legacy_yaml_adapter.rb +++ b/activerecord/lib/active_record/legacy_yaml_adapter.rb @@ -4,7 +4,7 @@ module ActiveRecord return coder unless coder.is_a?(Psych::Coder) case coder["active_record_yaml_version"] - when 1 then coder + when 1, 2 then coder else if coder["attributes"].is_a?(AttributeSet) Rails420.convert(klass, coder) diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index f691a8319d..7996c32bbc 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -267,6 +267,10 @@ module ActiveRecord @attribute_types ||= Hash.new(Type::Value.new) end + def yaml_encoder # :nodoc: + @yaml_encoder ||= AttributeSet::YAMLEncoder.new(attribute_types) + end + # Returns the type of the attribute with the given name, after applying # all modifiers. This method is the only valid source of information for # anything related to the types of a model's attributes. This method will @@ -375,6 +379,7 @@ module ActiveRecord @columns = nil @columns_hash = nil @attribute_names = nil + @yaml_encoder = nil direct_descendants.each do |descendant| descendant.send(:reload_schema_from_cache) end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index f3ec2b98d3..fe2425b845 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -3,98 +3,92 @@ require 'bigdecimal' require 'yaml' require 'securerandom' -module ActiveRecord - module ConnectionAdapters - class SQLite3Adapter - class QuotingTest < ActiveRecord::SQLite3TestCase - def setup - @conn = ActiveRecord::Base.connection - end - - def test_type_cast_binary_encoding_without_logger - @conn.extend(Module.new { def logger; end }) - binary = SecureRandom.hex - expected = binary.dup.encode!(Encoding::UTF_8) - assert_equal expected, @conn.type_cast(binary) - end - - def test_type_cast_symbol - assert_equal 'foo', @conn.type_cast(:foo) - end - - def test_type_cast_date - date = Date.today - expected = @conn.quoted_date(date) - assert_equal expected, @conn.type_cast(date) - end - - def test_type_cast_time - time = Time.now - expected = @conn.quoted_date(time) - assert_equal expected, @conn.type_cast(time) - end - - def test_type_cast_numeric - assert_equal 10, @conn.type_cast(10) - assert_equal 2.2, @conn.type_cast(2.2) - end - - def test_type_cast_nil - assert_equal nil, @conn.type_cast(nil) - end - - def test_type_cast_true - assert_equal 't', @conn.type_cast(true) - end - - def test_type_cast_false - assert_equal 'f', @conn.type_cast(false) - end - - def test_type_cast_bigdecimal - bd = BigDecimal.new '10.0' - assert_equal bd.to_f, @conn.type_cast(bd) - end - - def test_type_cast_unknown_should_raise_error - obj = Class.new.new - assert_raise(TypeError) { @conn.type_cast(obj) } - end - - def test_type_cast_object_which_responds_to_quoted_id - quoted_id_obj = Class.new { - def quoted_id - "'zomg'" - end - - def id - 10 - end - }.new - assert_equal 10, @conn.type_cast(quoted_id_obj) - - quoted_id_obj = Class.new { - def quoted_id - "'zomg'" - end - }.new - assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } - end - - def test_quoting_binary_strings - value = "hello".encode('ascii-8bit') - type = Type::String.new - - assert_equal "'hello'", @conn.quote(type.serialize(value)) - end - - def test_quoted_time_returns_date_qualified_time - value = ::Time.utc(2000, 1, 1, 12, 30, 0, 999999) - type = Type::Time.new - - assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value)) - end +class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase + def setup + @conn = ActiveRecord::Base.connection + end + + def test_type_cast_binary_encoding_without_logger + @conn.extend(Module.new { def logger; end }) + binary = SecureRandom.hex + expected = binary.dup.encode!(Encoding::UTF_8) + assert_equal expected, @conn.type_cast(binary) + end + + def test_type_cast_symbol + assert_equal 'foo', @conn.type_cast(:foo) + end + + def test_type_cast_date + date = Date.today + expected = @conn.quoted_date(date) + assert_equal expected, @conn.type_cast(date) + end + + def test_type_cast_time + time = Time.now + expected = @conn.quoted_date(time) + assert_equal expected, @conn.type_cast(time) + end + + def test_type_cast_numeric + assert_equal 10, @conn.type_cast(10) + assert_equal 2.2, @conn.type_cast(2.2) + end + + def test_type_cast_nil + assert_equal nil, @conn.type_cast(nil) + end + + def test_type_cast_true + assert_equal 't', @conn.type_cast(true) + end + + def test_type_cast_false + assert_equal 'f', @conn.type_cast(false) + end + + def test_type_cast_bigdecimal + bd = BigDecimal.new '10.0' + assert_equal bd.to_f, @conn.type_cast(bd) + end + + def test_type_cast_unknown_should_raise_error + obj = Class.new.new + assert_raise(TypeError) { @conn.type_cast(obj) } + end + + def test_type_cast_object_which_responds_to_quoted_id + quoted_id_obj = Class.new { + def quoted_id + "'zomg'" + end + + def id + 10 end - end + }.new + assert_equal 10, @conn.type_cast(quoted_id_obj) + + quoted_id_obj = Class.new { + def quoted_id + "'zomg'" + end + }.new + assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } + end + + def test_quoting_binary_strings + value = "hello".encode('ascii-8bit') + type = ActiveRecord::Type::String.new + + assert_equal "'hello'", @conn.quote(type.serialize(value)) + end + + def test_quoted_time_returns_date_qualified_time + value = ::Time.utc(2000, 1, 1, 12, 30, 0, 999999) + type = ActiveRecord::Type::Time.new + + assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value)) end end diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index 559b951109..24cc6875ab 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -1,24 +1,20 @@ require 'cases/helper' -module ActiveRecord::ConnectionAdapters - class SQLite3Adapter - class StatementPoolTest < ActiveRecord::SQLite3TestCase - if Process.respond_to?(:fork) - def test_cache_is_per_pid +class SQLite3StatementPoolTest < ActiveRecord::SQLite3TestCase + if Process.respond_to?(:fork) + def test_cache_is_per_pid - cache = StatementPool.new(10) - cache['foo'] = 'bar' - assert_equal 'bar', cache['foo'] + cache = ActiveRecord::ConnectionAdapters::SQLite3Adapter::StatementPool.new(10) + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] - pid = fork { - lookup = cache['foo']; - exit!(!lookup) - } + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } - Process.waitpid pid - assert $?.success?, 'process should exit successfully' - end - end + Process.waitpid pid + assert $?.success?, 'process should exit successfully' end end end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index a24a4fc6a4..b1b8639696 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -242,5 +242,12 @@ module ActiveRecord attribute.with_value_from_user(1) end end + + test "with_type preserves mutations" do + attribute = Attribute.from_database(:foo, "", Type::Value.new) + attribute.value << "1" + + assert_equal 1, attribute.with_type(Type::Integer.new).value + end end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index a3f8d26100..7ad9ff1f57 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -734,6 +734,17 @@ class DirtyTest < ActiveRecord::TestCase assert_equal "arr", pirate.catchphrase end + test "attributes assigned but not selected are dirty" do + person = Person.select(:id).first + refute person.changed? + + person.first_name = "Sean" + assert person.changed? + + person.first_name = nil + assert person.changed? + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index b8307d6665..5b5307489a 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -41,10 +41,8 @@ class I18nValidationTest < ActiveRecord::TestCase [ "given custom message", {:message => "custom"}, {:message => "custom"}], [ "given if condition", {:if => lambda { true }}, {}], [ "given unless condition", {:unless => lambda { false }}, {}], - [ "given option that is not reserved", {:format => "jpg"}, {:format => "jpg" }] - # TODO Add :on case, but below doesn't work, because then the validation isn't run for some reason - # even when using .save instead .valid? - # [ "given on condition", {on: :save}, {}] + [ "given option that is not reserved", {:format => "jpg"}, {:format => "jpg" }], + [ "given on condition", {on: [:create, :update] }, {}] ] COMMON_CASES.each do |name, validation_options, generate_message_options| diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 56909a8630..d1c9a00786 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -109,6 +109,16 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_equal("Have a nice day", topic.content) end + def test_yaml_encoding_keeps_mutations + author = Author.first + author.name = "Sean" + dumped = YAML.load(YAML.dump(author)) + + assert_equal "Sean", dumped.name + assert_equal author.name_was, dumped.name_was + assert_equal author.changes, dumped.changes + end + private def yaml_fixture(file_name) |