diff options
15 files changed, 149 insertions, 64 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index b59d24f88d..e614458375 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,13 @@ +* Respect precision option for arrays of timestamps. + + Fixes #27514. + + *Sean Griffin* + +* Optimize slow model instantiation when using STI and `store_full_sti_class = false` option. + + *Konstantin Lazarev* + * Add `touch` option to counter cache modifying methods. Works when updating, resetting, incrementing and decrementing counters: diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index e20b65e43c..b0e1391cb9 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -226,6 +226,11 @@ module ActiveRecord super end + def changed?(*) + emit_warning_if_needed("changed?", "saved_changes?") + super + end + def changed(*) emit_warning_if_needed("changed", "saved_changes.keys") super diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 1c3d10c15d..01cd1e5446 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -67,8 +67,8 @@ module ActiveRecord @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) - if version < "5.0.0" - raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.0." + if version < "5.1.10" + raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.1.10." end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index 8ccdd69b1d..e1a75f8e5e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -5,6 +5,8 @@ module ActiveRecord class Array < Type::Value # :nodoc: include Type::Helpers::Mutable + Data = Struct.new(:encoder, :values) # :nodoc: + attr_reader :subtype, :delimiter delegate :type, :user_input_in_time_zone, :limit, :precision, :scale, to: :subtype @@ -17,8 +19,11 @@ module ActiveRecord end def deserialize(value) - if value.is_a?(::String) + case value + when ::String type_cast_array(@pg_decoder.decode(value), :deserialize) + when Data + deserialize(value.values) else super end @@ -33,11 +38,8 @@ module ActiveRecord def serialize(value) if value.is_a?(::Array) - result = @pg_encoder.encode(type_cast_array(value, :serialize)) - if encoding = determine_encoding_of_strings(value) - result.force_encoding(encoding) - end - result + casted_values = type_cast_array(value, :serialize) + Data.new(@pg_encoder, casted_values) else super end @@ -58,6 +60,10 @@ module ActiveRecord value.map(&block) end + def changed_in_place?(raw_old_value, new_value) + deserialize(raw_old_value) != new_value + end + private def type_cast_array(value, method) @@ -67,13 +73,6 @@ module ActiveRecord @subtype.public_send(method, value) end end - - def determine_encoding_of_strings(value) - case value - when ::Array then determine_encoding_of_strings(value.first) - when ::String then value.encoding - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index b5031d890f..3783925954 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -92,6 +92,8 @@ module ActiveRecord else super end + when OID::Array::Data + _quote(encode_array(value)) else super end @@ -106,10 +108,37 @@ module ActiveRecord { value: value.to_s, format: 1 } when OID::Xml::Data, OID::Bit::Data value.to_s + when OID::Array::Data + encode_array(value) else super end end + + def encode_array(array_data) + encoder = array_data.encoder + values = type_cast_array(array_data.values) + + result = encoder.encode(values) + if encoding = determine_encoding_of_strings_in_array(values) + result.force_encoding(encoding) + end + result + end + + def determine_encoding_of_strings_in_array(value) + case value + when ::Array then determine_encoding_of_strings_in_array(value.first) + when ::String then value.encoding + end + end + + def type_cast_array(values) + case values + when ::Array then values.map { |item| type_cast_array(item) } + else _type_cast(values) + end + end end end end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index a1d4f47372..fbdaeaae51 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -130,16 +130,26 @@ module ActiveRecord store_full_sti_class ? name : name.demodulize end + def inherited(subclass) + subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new) + super + end + protected # Returns the class type of the record using the current module as a prefix. So descendants of # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. def compute_type(type_name) - if type_name.match(/^::/) + if type_name.start_with?("::".freeze) # If the type is prefixed with a scope operator then we assume that # the type_name is an absolute reference. ActiveSupport::Dependencies.constantize(type_name) else + type_candidate = @_type_candidates_cache[type_name] + if type_candidate && type_constant = ActiveSupport::Dependencies.safe_constantize(type_candidate) + return type_constant + end + # Build a list of candidates to search for candidates = [] name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" } @@ -147,7 +157,10 @@ module ActiveRecord candidates.each do |candidate| constant = ActiveSupport::Dependencies.safe_constantize(candidate) - return constant if candidate == constant.to_s + if candidate == constant.to_s + @_type_candidates_cache[type_name] = candidate + return constant + end end raise NameError.new("uninitialized constant #{candidates.first}", candidates.first) diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 982a91782f..c78c6178ff 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -21,6 +21,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase t.datetime :datetimes, array: true t.hstore :hstores, array: true t.decimal :decimals, array: true, default: [], precision: 10, scale: 2 + t.timestamp :timestamps, array: true, default: [], precision: 6 end end PgArray.reset_column_information @@ -213,7 +214,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase x = PgArray.create!(tags: tags) x.reload - assert_equal x.tags_before_type_cast, PgArray.type_for_attribute("tags").serialize(tags) + refute x.changed? end def test_quoting_non_standard_delimiters @@ -221,9 +222,10 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ",") semicolon_delim = oid::Array.new(ActiveRecord::Type::String.new, ";") + conn = PgArray.connection - assert_equal %({"hello,",world;}), comma_delim.serialize(strings) - assert_equal %({hello,;"world;"}), semicolon_delim.serialize(strings) + assert_equal %({"hello,",world;}), conn.type_cast(comma_delim.serialize(strings)) + assert_equal %({hello,;"world;"}), conn.type_cast(semicolon_delim.serialize(strings)) end def test_mutate_array @@ -319,6 +321,15 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase assert_equal [arrays_of_utf8_strings], @type.deserialize(@type.serialize([arrays_of_utf8_strings])) end + def test_precision_is_respected_on_timestamp_columns + time = Time.now.change(usec: 123) + record = PgArray.create!(timestamps: [time]) + + assert_equal 123, record.timestamps.first.usec + record.reload + assert_equal 123, record.timestamps.first.usec + end + private def assert_cycle(field, array) # test creation diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb index bd45a9daa0..784d77a8d1 100644 --- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -19,7 +19,7 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase big_array = [123456789123456789] assert_raises(ActiveModel::RangeError) { int_array.serialize(big_array) } - assert_equal "{123456789123456789}", bigint_array.serialize(big_array) + assert_equal "{123456789123456789}", @connection.type_cast(bigint_array.serialize(big_array)) end test "range types correctly respect registration of subtypes" do diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index dd40f38b5a..a611cc208c 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -4,6 +4,7 @@ require "models/author" require "models/topic" require "models/reply" require "models/category" +require "models/categorization" require "models/company" require "models/customer" require "models/developer" @@ -33,8 +34,6 @@ class SecondAbstractClass < FirstAbstractClass self.abstract_class = true end class Photo < SecondAbstractClass; end -class Category < ActiveRecord::Base; end -class Categorization < ActiveRecord::Base; end class Smarts < ActiveRecord::Base; end class CreditCard < ActiveRecord::Base class PinNumber < ActiveRecord::Base @@ -45,8 +44,6 @@ class CreditCard < ActiveRecord::Base class Brand < Category; end end class MasterCreditCard < ActiveRecord::Base; end -class Post < ActiveRecord::Base; end -class Computer < ActiveRecord::Base; end class NonExistentTable < ActiveRecord::Base; end class TestOracleDefault < ActiveRecord::Base; end @@ -56,12 +53,6 @@ end class Weird < ActiveRecord::Base; end -class Boolean < ActiveRecord::Base - def has_fun - super - end -end - class LintTest < ActiveRecord::TestCase include ActiveModel::Lint::Tests diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 9ad4664567..e570e9ac1d 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -58,21 +58,21 @@ class InheritanceTest < ActiveRecord::TestCase end def test_compute_type_success - assert_equal Author, ActiveRecord::Base.send(:compute_type, "Author") + assert_equal Author, Company.send(:compute_type, "Author") end def test_compute_type_nonexistent_constant e = assert_raises NameError do - ActiveRecord::Base.send :compute_type, "NonexistentModel" + Company.send :compute_type, "NonexistentModel" end - assert_equal "uninitialized constant ActiveRecord::Base::NonexistentModel", e.message - assert_equal "ActiveRecord::Base::NonexistentModel", e.name + assert_equal "uninitialized constant Company::NonexistentModel", e.message + assert_equal "Company::NonexistentModel", e.name end def test_compute_type_no_method_error ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise NoMethodError }) do assert_raises NoMethodError do - ActiveRecord::Base.send :compute_type, "InvalidModel" + Company.send :compute_type, "InvalidModel" end end end @@ -90,7 +90,7 @@ class InheritanceTest < ActiveRecord::TestCase ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise e }) do exception = assert_raises NameError do - ActiveRecord::Base.send :compute_type, "InvalidModel" + Company.send :compute_type, "InvalidModel" end assert_equal error.message, exception.message end @@ -99,7 +99,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_compute_type_argument_error ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise ArgumentError }) do assert_raises ArgumentError do - ActiveRecord::Base.send :compute_type, "InvalidModel" + Company.send :compute_type, "InvalidModel" end end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index a90058e8bb..0ef51272b9 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -100,7 +100,13 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_klass_for_nested_class_name - reflection = ActiveRecord::Reflection.create(:has_many, nil, nil, { class_name: "MyApplication::Business::Company" }, ActiveRecord::Base) + reflection = ActiveRecord::Reflection.create( + :has_many, + nil, + nil, + { class_name: "MyApplication::Business::Company" }, + Customer + ) assert_nothing_raised do assert_equal MyApplication::Business::Company, reflection.klass end diff --git a/activerecord/test/models/boolean.rb b/activerecord/test/models/boolean.rb index 7bae22e5f9..0da228aac2 100644 --- a/activerecord/test/models/boolean.rb +++ b/activerecord/test/models/boolean.rb @@ -1,2 +1,5 @@ class Boolean < ActiveRecord::Base + def has_fun + super + end end diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 03d3daecc8..6e68935f9b 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -709,55 +709,73 @@ class Book < ApplicationRecord end ``` -By default, Active Record doesn't know about the connection between these associations. This can lead to two copies of an object getting out of sync: +Active Record will attempt to automatically identify that these two models share a bi-directional association based on the association name. In this way, Active Record will only load one copy of the `Author` object, making your application more efficient and preventing inconsistent data: ```ruby a = Author.first b = a.books.first a.first_name == b.author.first_name # => true -a.first_name = 'Manny' -a.first_name == b.author.first_name # => false +a.first_name = 'David' +a.first_name == b.author.first_name # => true ``` -This happens because `a` and `b.author` are two different in-memory representations of the same data, and neither one is automatically refreshed from changes to the other. Active Record provides the `:inverse_of` option so that you can inform it of these relations: +Active Record supports automatic identification for most associations with standard names. However, Active Record will not automatically identify bi-directional associations that contain any of the following options: + +* `:conditions` +* `:through` +* `:polymorphic` +* `:class_name` +* `:foreign_key` + +For example, consider the following model declarations: ```ruby class Author < ApplicationRecord - has_many :books, inverse_of: :author + has_many :books end class Book < ApplicationRecord - belongs_to :author, inverse_of: :books + belongs_to :writer, class_name: 'Author', foreign_key: 'author_id' end ``` -With these changes, Active Record will only load one copy of the author object, preventing inconsistencies and making your application more efficient: +Active Record will no longer automatically recognize the bi-directional association: ```ruby a = Author.first b = a.books.first -a.first_name == b.author.first_name # => true -a.first_name = 'Manny' -a.first_name == b.author.first_name # => true +a.first_name == b.writer.first_name # => true +a.first_name = 'David' +a.first_name == b.writer.first_name # => false +``` + +Active Record provides the `:inverse_of` option so you can explicitly declare bi-directional associations: + +```ruby +class Author < ApplicationRecord + has_many :books, inverse_of: 'writer' +end + +class Book < ApplicationRecord + belongs_to :writer, class_name: 'Author', foreign_key: 'author_id' +end ``` -There are a few limitations to `inverse_of` support: +By including the `:inverse_of` option in the `has_many` association declaration, Active Record will now recognize the bi-directional association: + +```ruby +a = Author.first +b = a.books.first +a.first_name == b.writer.first_name # => true +a.first_name = 'David' +a.first_name == b.writer.first_name # => true +``` + +There are a few limitations to `:inverse_of` support: * They do not work with `:through` associations. * They do not work with `:polymorphic` associations. * They do not work with `:as` associations. -* For `belongs_to` associations, `has_many` inverse associations are ignored. - -Every association will attempt to automatically find the inverse association -and set the `:inverse_of` option heuristically (based on the association name). -Most associations with standard names will be supported. However, associations -that contain the following options will not have their inverses set -automatically: - -* `:conditions` -* `:through` -* `:polymorphic` -* `:foreign_key` Detailed Association Reference ------------------------------ diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml index a2b2a64ba6..8bc8735a8e 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml @@ -1,4 +1,4 @@ -# MySQL. Versions 5.0 and up are supported. +# MySQL. Versions 5.1.10 and up are supported. # # Install the MySQL driver: # gem install activerecord-jdbcmysql-adapter diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml index d987cf303b..269af1470d 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml @@ -1,4 +1,4 @@ -# MySQL. Versions 5.0 and up are supported. +# MySQL. Versions 5.1.10 and up are supported. # # Install the MySQL driver # gem install mysql2 |