diff options
author | Aaron Patterson <aaron.patterson@gmail.com> | 2014-06-04 11:45:53 -0700 |
---|---|---|
committer | Aaron Patterson <aaron.patterson@gmail.com> | 2014-06-04 11:45:53 -0700 |
commit | 1ce0cc0d38c9f5058cb484612fab625db823c4e4 (patch) | |
tree | 7c856d0c2f6534c349ff2e81a01046acaa868c31 /activerecord | |
parent | 3f7e482aed8f67137c6c8382f6c558d7e856b341 (diff) | |
parent | b8f586a094c104006d29a87fee0d8b48d0af2d14 (diff) | |
download | rails-1ce0cc0d38c9f5058cb484612fab625db823c4e4.tar.gz rails-1ce0cc0d38c9f5058cb484612fab625db823c4e4.tar.bz2 rails-1ce0cc0d38c9f5058cb484612fab625db823c4e4.zip |
Merge branch 'master' of github.com:rails/rails
* 'master' of github.com:rails/rails:
For our build, stick with mail 2.5.x for now
Correct result, previously showing wrong result
Collapse PG default extractoin of most types to single regex
Change wording of explanation about precision & scale of decimal numbers [ci skip]
Cleaned up duplicated CHANGELOG entry [ci skip]
reuse available belongs_to? method
Convert StrongParameters cache to a hash. This fixes an unbounded memory leak demonstrated on @tenderlove's latest blog post:
Partially revert deprecation of *_filter
Pluralize params
Add default_i18n_subject to the guides
Fix regression on eager loading association based on SQL query rather than existing column.
Relax mail gem constraint from ~> 2.5.4 to ~> 2.5, >= 2.5.4
Keep column defaults in type cast form
Return a null column when no column exists for an attribute
Refactor XML serializer to not depend on `serialized_attributes`
Test the serialized types of virtual columns in XML
implement ActiveRecord::Base#pretty_print + changelog
Remove duplicated HashWithIndifferentAccess#with_indifferent_access.
Diffstat (limited to 'activerecord')
22 files changed, 228 insertions, 82 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 02fa504c78..4f0b1a76df 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,18 @@ +* Fix regression on eager loading association based on SQL query rather than + existing column. + + Fixes #15480. + + *Lauro Caetano*, *Carlos Antonio da Silva* + +* Return a null column from `column_for_attribute` when no column exists. + + *Sean Griffin* + +* Implemented ActiveRecord::Base#pretty_print to work with PP. + + *Ethan* + * Preserve type when dumping PostgreSQL point, bit, bit varying and money columns. @@ -577,12 +592,11 @@ *arthurnn* -* Passing an Active Record object to `find` is now deprecated. Call `.id` - on the object first. - * Passing an Active Record object to `find` or `exists?` is now deprecated. Call `.id` on the object first. + *Aaron Patterson* + * Only use BINARY for MySQL case sensitive uniqueness check when column has a case insensitive collation. *Ryuta Kamizono* diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 35ad512537..954128064d 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -116,7 +116,7 @@ module ActiveRecord end def target_reflection_has_associated_record? - !(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?) + !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?) end def update_through_counter?(method) diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 63773bd5e1..1b83700613 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -104,11 +104,13 @@ module ActiveRecord end def association_key_type - @klass.column_types[association_key_name.to_s].type + column = @klass.column_types[association_key_name.to_s] + column && column.type end def owner_key_type - @model.column_types[owner_key_name.to_s].type + column = @model.column_types[owner_key_name.to_s] + column && column.type end def load_slices(slices) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index f8a85b8a6f..fcf3b219d4 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -63,14 +63,13 @@ module ActiveRecord # Note: this does not capture all cases, for example it would be crazy to try to # properly support stale-checking for nested associations. def stale_state - if through_reflection.macro == :belongs_to + if through_reflection.belongs_to? owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s end end def foreign_key_present? - through_reflection.macro == :belongs_to && - !owner[through_reflection.foreign_key].nil? + through_reflection.belongs_to? && !owner[through_reflection.foreign_key].nil? end def ensure_mutable diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index b6520b9b3d..e56a4cc805 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -350,10 +350,12 @@ module ActiveRecord # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> # # person.column_for_attribute(:nothing) - # # => nil + # # => #<ActiveRecord::ConnectionAdapters::Column:0xXXX @name=nil, @sql_type=nil, @cast_type=#<Type::Value>, ...> def column_for_attribute(name) - # FIXME: should this return a null object for columns that don't exist? - self.class.columns_hash[name.to_s] + name = name.to_s + self.class.columns_hash.fetch(name) do + ConnectionAdapters::Column.new(name, nil, Type::Value.new) + end end # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, @@ -438,16 +440,16 @@ module ActiveRecord # Filters the primary keys and readonly attributes from the attribute names. def attributes_for_update(attribute_names) - attribute_names.select do |name| - column_for_attribute(name) && !readonly_attribute?(name) + attribute_names.reject do |name| + readonly_attribute?(name) end end # Filters out the primary keys, from the attribute names, when the primary # key is to be generated (e.g. the id attribute has no value). def attributes_for_create(attribute_names) - attribute_names.select do |name| - column_for_attribute(name) && !(pk_attribute?(name) && id.nil?) + attribute_names.reject do |name| + pk_attribute?(name) && id.nil? end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 6cd4e43ddd..4e32b78e34 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -94,8 +94,7 @@ module ActiveRecord end def _field_changed?(attr, old, value) - column = column_for_attribute(attr) || Type::Value.new - column.changed?(old, value) + column_for_attribute(attr).changed?(old, value) end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index c3e601a208..5203b30462 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -72,18 +72,20 @@ module ActiveRecord @attributes.delete(attr_name) column = column_for_attribute(attr_name) + unless has_attribute?(attr_name) || self.class.columns_hash.key?(attr_name) + raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" + end + # If we're dealing with a binary column, write the data to the cache # so we don't attempt to typecast multiple times. - if column && column.binary? + if column.binary? @attributes[attr_name] = value end - if column && should_type_cast + if should_type_cast @raw_attributes[attr_name] = column.type_cast_for_write(value) - elsif !should_type_cast || @raw_attributes.has_key?(attr_name) - @raw_attributes[attr_name] = value else - raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" + @raw_attributes[attr_name] = value 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 117c0f0969..a9b3e9cfb9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -102,8 +102,8 @@ module ActiveRecord # * <tt>:index</tt> - # Create an index for the column. Can be either <tt>true</tt> or an options hash. # - # For clarity's sake: the precision is the number of significant digits, - # while the scale is the number of digits that can be stored following + # Note: The precision is the total number of significant digits + # and the scale is the number of digits that can be stored following # the decimal point. For example, the number 123.45 has a precision of 5 # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can # range from -999.99 to 999.99. diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 34054589d0..3b0dcbc6a7 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -52,7 +52,7 @@ module ActiveRecord end def extract_default(default) - type_cast_for_write(type_cast(default)) + type_cast(default) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 67570dad3c..0f20d52879 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -511,58 +511,16 @@ module ActiveRecord # makes this method very very slow. return default unless default - # TODO: The default extraction is related to the cast-type. - # we should probably make a type_map lookup and cast the default- - # expression accordingly - if oid.type == :enum && default =~ /\A'(.*)'::/ - return $1 - end - case default - when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m - $1 + # Quoted types + when /\A[\(B]?'(.*)'::/m + $1.gsub(/''/, "'") + # Boolean types + when 'true', 'false' + default # Numeric types when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ $1 - # Character types - when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m - $1.gsub(/''/, "'") - # Binary data types - when /\A'(.*)'::bytea\z/m - $1 - # Date/time types - when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ - $1 - when /\A'(.*)'::interval\z/ - $1 - # Boolean type - when 'true' - true - when 'false' - false - # Geometric types - when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ - $1 - # Network address types - when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ - $1 - # Bit string types - when /\AB'(.*)'::"?bit(?: varying)?"?\z/ - $1 - # XML type - when /\A'(.*)'::xml\z/m - $1 - # Arrays - when /\A'(.*)'::"?\D+"?\[\]\z/ - $1 - # Hstore - when /\A'(.*)'::hstore\z/ - $1 - # JSON - when /\A'(.*)'::json\z/ - $1 - when /\A'(.*)'::money\z/ - $1 # Object identifier types when /\A-?\d+\z/ $1 diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 1d75eeda69..d6849fef2e 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -249,7 +249,7 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil, options = {}) - defaults = self.class.column_defaults.dup + defaults = self.class.raw_column_defaults.dup defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } @raw_attributes = defaults @@ -432,6 +432,29 @@ module ActiveRecord "#<#{self.class} #{inspection}>" end + # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record` + # when pp is required. + def pretty_print(pp) + pp.object_address_group(self) do + if defined?(@attributes) && @attributes + column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } + pp.seplist(column_names, proc { pp.text ',' }) do |column_name| + column_value = read_attribute(column_name) + pp.breakable ' ' + pp.group(1) do + pp.text column_name + pp.text ':' + pp.breakable + pp.pp column_value + end + end + else + pp.breakable ' ' + pp.text 'not initialized' + end + end + end + # Returns a hash of the given methods with their names as keys and returned values as values. def slice(*methods) Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index f0ddf202c3..baf2b5fbf8 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -241,6 +241,14 @@ module ActiveRecord @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }] end + # Returns a hash where the keys are the column names and the values + # are the default values suitable for use in `@raw_attriubtes` + def raw_column_defaults # :nodoc: + @raw_column_defauts ||= Hash[column_defaults.map { |name, default| + [name, columns_hash[name].type_cast_for_write(default)] + }] + end + # Returns an array of column names as strings. def column_names @column_names ||= columns.map { |column| column.name } @@ -285,6 +293,7 @@ module ActiveRecord @arel_engine = nil @column_defaults = nil + @raw_column_defauts = nil @column_names = nil @column_types = nil @content_columns = nil diff --git a/activerecord/lib/active_record/properties.rb b/activerecord/lib/active_record/properties.rb index 5fd51e09fa..48ee42aaca 100644 --- a/activerecord/lib/active_record/properties.rb +++ b/activerecord/lib/active_record/properties.rb @@ -113,6 +113,7 @@ module ActiveRecord @columns_hash = nil @column_types = nil @column_defaults = nil + @raw_column_defaults = nil @column_names = nil @content_columns = nil end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 9ee6422329..11ab1b4595 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -278,7 +278,7 @@ module ActiveRecord if group_attrs.first.respond_to?(:to_sym) association = @klass._reflect_on_association(group_attrs.first.to_sym) - associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations + associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations group_fields = Array(associated ? association.foreign_key : group_attrs) else group_fields = group_attrs diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 1a766093d0..019fe2218e 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -180,12 +180,12 @@ module ActiveRecord #:nodoc: class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class - type = if klass.serialized_attributes.key?(name) + column = klass.columns_hash[name] || Type::Value.new + + type = if column.serialized? super - elsif klass.columns_hash.key?(name) - klass.columns_hash[name].type else - NilClass + column.type end { :text => :string, diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 4bd4486b41..910067666a 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1239,6 +1239,10 @@ class EagerAssociationTest < ActiveRecord::TestCase } end + test "including association based on sql condition and no database column" do + assert_equal pets(:parrot), Owner.including_last_pet.first.last_pet + end + test "include instance dependent associations is deprecated" do message = "association scope 'posts_with_signature' is" assert_deprecated message do diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index 2a52bf574c..715d92af99 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -1,6 +1,8 @@ require 'cases/helper' require 'models/person' require 'models/topic' +require 'pp' +require 'active_support/core_ext/string/strip' class NonExistentTable < ActiveRecord::Base; end @@ -30,4 +32,70 @@ class CoreTest < ActiveRecord::TestCase def test_inspect_class_without_table assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect end + + def test_pretty_print_new + topic = Topic.new + actual = '' + PP.pp(topic, StringIO.new(actual)) + expected = <<-PRETTY.strip_heredoc + #<Topic:0xXXXXXX + id: nil, + title: nil, + author_name: nil, + author_email_address: "test@test.com", + written_on: nil, + bonus_time: nil, + last_read: nil, + content: nil, + important: nil, + approved: true, + replies_count: 0, + unique_replies_count: 0, + parent_id: nil, + parent_title: nil, + type: nil, + group: nil, + created_at: nil, + updated_at: nil> + PRETTY + assert actual.start_with?(expected.split('XXXXXX').first) + assert actual.end_with?(expected.split('XXXXXX').last) + end + + def test_pretty_print_persisted + topic = topics(:first) + actual = '' + PP.pp(topic, StringIO.new(actual)) + expected = <<-PRETTY.strip_heredoc + #<Topic:0x\\w+ + id: 1, + title: "The First Topic", + author_name: "David", + author_email_address: "david@loudthinking.com", + written_on: 2003-07-16 14:28:11 UTC, + bonus_time: 2000-01-01 14:28:00 UTC, + last_read: Thu, 15 Apr 2004, + content: "Have a nice day", + important: nil, + approved: false, + replies_count: 1, + unique_replies_count: 0, + parent_id: nil, + parent_title: nil, + type: nil, + group: nil, + created_at: [^,]+, + updated_at: [^,>]+> + PRETTY + assert_match(/\A#{expected}\z/, actual) + end + + def test_pretty_print_uninitialized + topic = Topic.allocate + actual = '' + PP.pp(topic, StringIO.new(actual)) + expected = "#<Topic:XXXXXX not initialized>\n" + assert actual.start_with?(expected.split('XXXXXX').first) + assert actual.end_with?(expected.split('XXXXXX').last) + end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index f885a8cbc0..92144bc802 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -206,6 +206,11 @@ if current_adapter?(:PostgreSQLAdapter) assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parse after updating default using '::text' since postgreSQL will add parens to the default in db" end + def test_default_containing_quote_and_colons + @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'" + assert_equal "foo'::bar", Default.new.string_col + end + teardown do @connection.schema_search_path = @old_search_path Default.reset_column_information diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index df4183c065..987c55ebc2 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -616,6 +616,34 @@ class DirtyTest < ActiveRecord::TestCase end end + test "defaults with type that implements `type_cast_for_write`" do + type = Class.new(ActiveRecord::Type::Value) do + def type_cast(value) + value.to_i + end + + def type_cast_for_write(value) + value.to_s + end + + alias type_cast_for_database type_cast_for_write + end + + model_class = Class.new(ActiveRecord::Base) do + self.table_name = 'numeric_data' + property :foo, type.new, default: 1 + end + + model = model_class.new + assert_not model.foo_changed? + + model = model_class.new(foo: 1) + assert_not model.foo_changed? + + model = model_class.new(foo: '1') + assert_not model.foo_changed? + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index e6603f28be..b3c02d29cb 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -80,6 +80,25 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal :integer, @first.column_for_attribute("id").type end + def test_non_existent_columns_return_null_object + column = @first.column_for_attribute("attribute_that_doesnt_exist") + assert_equal "attribute_that_doesnt_exist", column.name + assert_equal nil, column.sql_type + assert_equal nil, column.type + assert_not column.number? + assert_not column.text? + assert_not column.binary? + end + + def test_non_existent_columns_are_identity_types + column = @first.column_for_attribute("attribute_that_doesnt_exist") + object = Object.new + + assert_equal object, column.type_cast(object) + assert_equal object, column.type_cast_for_write(object) + assert_equal object, column.type_cast_for_database(object) + end + def test_reflection_klass_for_nested_class_name reflection = MacroReflection.new(:company, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) assert_nothing_raised do diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 1a690c01a6..c34e7d5a30 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -416,8 +416,9 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase def test_should_support_aliased_attributes xml = Author.select("name as firstname").to_xml - array = Hash.from_xml(xml)['authors'] - assert_equal array.size, array.select { |author| author.has_key? 'firstname' }.size + Author.all.each do |author| + assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml + end end def test_array_to_xml_including_has_many_association diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index cf24502d3a..2e3a9a3681 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -3,6 +3,18 @@ class Owner < ActiveRecord::Base has_many :pets, -> { order 'pets.name desc' } has_many :toys, :through => :pets + belongs_to :last_pet, class_name: 'Pet' + scope :including_last_pet, -> { + select(%q[ + owners.*, ( + select p.pet_id from pets p + where p.owner_id = owners.owner_id + order by p.name desc + limit 1 + ) as last_pet_id + ]).includes(:last_pet) + } + after_commit :execute_blocks def blocks |