diff options
Diffstat (limited to 'activerecord')
59 files changed, 612 insertions, 262 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index efa4abfa4d..39c8769814 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,26 @@ +* Deprecate `serialized_attributes` without replacement. You can access its + behavior by going through the column's type object. + + *Sean Griffin* + +* Correctly extract IPv6 addresses from `DATABASE_URI`: the square brackets + are part of the URI structure, not the actual host. + + Fixes #15705. + + *Andy Bakun*, *Aaron Stone* + +* Ensure both parent IDs are set on join records when both sides of a + through association are new. + + *Sean Griffin* + +* `ActiveRecord::Dirty` now detects in-place changes to mutable values. + Serialized attributes on ActiveRecord models will no longer save when + unchanged. Fixes #8328. + + Sean Griffin + * Fixed automatic maintaining test schema to properly handle sql structure schema format. @@ -400,7 +423,7 @@ *Eric Chahin* -* `sanitize_sql_like` helper method to escape a string for safe use in a SQL +* `sanitize_sql_like` helper method to escape a string for safe use in an SQL LIKE statement. Example: @@ -436,7 +459,7 @@ *Lauro Caetano* * Calling `delete_all` on an unloaded `CollectionProxy` no longer - generates a SQL statement containing each id of the collection: + generates an SQL statement containing each id of the collection: Before: diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index f856c482d6..53fa132219 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -31,6 +31,7 @@ require 'active_record/version' module ActiveRecord extend ActiveSupport::Autoload + autoload :Attribute autoload :Base autoload :Callbacks autoload :Core 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 da9b125fd6..175019a72b 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -82,12 +82,16 @@ module ActiveRecord @through_records[record.object_id] ||= begin ensure_mutable - through_record = through_association.build through_scope_attributes + through_record = through_association.build(*options_for_through_record) through_record.send("#{source_reflection.name}=", record) through_record end end + def options_for_through_record + [through_scope_attributes] + end + def through_scope_attributes scope.where_values_hash(through_association.reflection.name.to_s).except!(through_association.reflection.foreign_key) end @@ -180,7 +184,11 @@ module ActiveRecord def through_records_for(record) attributes = construct_join_attributes(record) candidates = Array.wrap(through_association.target) - candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes } + candidates.find_all do |c| + attributes.all? do |key, value| + c.public_send(key) == value + end + end end def delete_through_records(records) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index fcf3b219d4..f00fef8b9e 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -41,12 +41,16 @@ module ActiveRecord def construct_join_attributes(*records) ensure_mutable - join_attributes = { - source_reflection.foreign_key => - records.map { |record| - record.send(source_reflection.association_primary_key(reflection.klass)) - } - } + if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key + join_attributes = { source_reflection.name => records } + else + join_attributes = { + source_reflection.foreign_key => + records.map { |record| + record.send(source_reflection.association_primary_key(reflection.klass)) + } + } + end if options[:source_type] join_attributes[source_reflection.foreign_type] = diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb new file mode 100644 index 0000000000..f78cde23c5 --- /dev/null +++ b/activerecord/lib/active_record/attribute.rb @@ -0,0 +1,56 @@ +module ActiveRecord + class Attribute # :nodoc: + class << self + def from_database(value, type) + FromDatabase.new(value, type) + end + + def from_user(value, type) + FromUser.new(value, type) + end + end + + attr_reader :value_before_type_cast, :type + + # This method should not be called directly. + # Use #from_database or #from_user + def initialize(value_before_type_cast, type) + @value_before_type_cast = value_before_type_cast + @type = type + end + + def value + # `defined?` is cheaper than `||=` when we get back falsy values + @value = type_cast(value_before_type_cast) unless defined?(@value) + @value + end + + def value_for_database + type.type_cast_for_database(value) + end + + def type_cast + raise NotImplementedError + end + + protected + + def initialize_dup(other) + if defined?(@value) && @value.duplicable? + @value = @value.dup + end + end + + class FromDatabase < Attribute + def type_cast(value) + type.type_cast_from_database(value) + end + end + + class FromUser < Attribute + def type_cast(value) + type.type_cast_from_user(value) + end + end + end +end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index e626227e7e..b4d75d6556 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -156,16 +156,6 @@ module ActiveRecord end end - def find_generated_attribute_method(method_name) # :nodoc: - klass = self - until klass == Base - gen_methods = klass.generated_attribute_methods - return gen_methods.instance_method(method_name) if method_defined_within?(method_name, gen_methods, Object) - klass = klass.superclass - end - nil - end - # Returns +true+ if +attribute+ is an attribute method and table exists, # +false+ otherwise. # @@ -216,25 +206,6 @@ module ActiveRecord end end - # If we haven't generated any methods yet, generate them, then - # see if we've created the method we're looking for. - def method_missing(method, *args, &block) # :nodoc: - self.class.define_attribute_methods - if respond_to_without_attributes?(method) - # make sure to invoke the correct attribute method, as we might have gotten here via a `super` - # call in a overwritten attribute method - if attribute_method = self.class.find_generated_attribute_method(method) - # this is probably horribly slow, but should only happen at most once for a given AR class - attribute_method.bind(self).call(*args, &block) - else - return super unless respond_to_missing?(method, true) - send(method, *args, &block) - end - else - super - end - end - # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt> # which will all return +true+. It also define the attribute methods if they have @@ -252,18 +223,14 @@ module ActiveRecord # person.respond_to('age?') # => true # person.respond_to(:nothing) # => false def respond_to?(name, include_private = false) + return false unless super name = name.to_s - self.class.define_attribute_methods - result = super - - # If the result is false the answer is false. - return false unless result # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. - # We check defined?(@raw_attributes) not to issue warnings if called on objects that + # We check defined?(@attributes) not to issue warnings if called on objects that # have been allocated but not yet initialized. - if defined?(@raw_attributes) && @raw_attributes.any? && self.class.column_names.include?(name) + if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name) return has_attribute?(name) end @@ -280,7 +247,7 @@ module ActiveRecord # person.has_attribute?('age') # => true # person.has_attribute?(:nothing) # => false def has_attribute?(attr_name) - @raw_attributes.has_key?(attr_name.to_s) + @attributes.has_key?(attr_name.to_s) end # Returns an array of names for the attributes available on this object. @@ -292,7 +259,7 @@ module ActiveRecord # person.attribute_names # # => ["id", "created_at", "updated_at", "name", "age"] def attribute_names - @raw_attributes.keys + @attributes.keys end # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. @@ -400,11 +367,10 @@ module ActiveRecord protected - def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc: - attribute_names.each do |name| - attributes[name] = clone_attribute_value(reader_method, name) + def clone_attributes # :nodoc: + @attributes.each_with_object({}) do |(name, attr), h| + h[name] = attr.dup end - attributes end def clone_attribute_value(reader_method, attribute_name) # :nodoc: @@ -424,7 +390,7 @@ module ActiveRecord def attribute_method?(attr_name) # :nodoc: # We check defined? because Syck calls respond_to? before actually calling initialize. - defined?(@raw_attributes) && @raw_attributes.include?(attr_name) + defined?(@attributes) && @attributes.include?(attr_name) end private @@ -465,9 +431,6 @@ module ActiveRecord end def typecasted_attribute_value(name) - # FIXME: we need @attributes to be used consistently. - # If the values stored in @attributes were already typecasted, this code - # could be simplified read_attribute(name) end end diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index 4365f5a1a1..d057f0941a 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -43,7 +43,9 @@ module ActiveRecord # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21" def read_attribute_before_type_cast(attr_name) - @raw_attributes[attr_name.to_s] + if attr = @attributes[attr_name.to_s] + attr.value_before_type_cast + end end # Returns a hash of attributes before typecasting and deserialization. @@ -57,7 +59,7 @@ module ActiveRecord # task.attributes_before_type_cast # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast - @raw_attributes + @attributes.each_with_object({}) { |(k, v), h| h[k] = v.value_before_type_cast } end private diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index be438aba95..6a5c057384 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -38,12 +38,39 @@ module ActiveRecord end end - def initialize_dup(other) # :nodoc: - super - init_changed_attributes - end + def initialize_dup(other) # :nodoc: + super + init_changed_attributes + end + + def changed? + super || changed_in_place.any? + end + + def changed + super | changed_in_place + end + + def attribute_changed?(attr_name, options = {}) + result = super + # We can't change "from" something in place. Only setters can define + # "from" and "to" + result ||= changed_in_place?(attr_name) unless options.key?(:from) + result + end + + def changes_applied + super + store_original_raw_attributes + end + + def reset_changes + super + original_raw_attributes.clear + end + + private - private def initialize_internals_callback super init_changed_attributes @@ -65,11 +92,20 @@ module ActiveRecord old_value = old_attribute_value(attr) - result = super(attr, value) + result = super + store_original_raw_attribute(attr) save_changed_attribute(attr, old_value) result end + def raw_write_attribute(attr, value) + attr = attr.to_s + + result = super + original_raw_attributes[attr] = value + result + end + def save_changed_attribute(attr, old_value) if attribute_changed?(attr) changed_attributes.delete(attr) unless _field_changed?(attr, old_value) @@ -105,6 +141,41 @@ module ActiveRecord raw_value = read_attribute_before_type_cast(attr) column_for_attribute(attr).changed?(old_value, new_value, raw_value) end + + def changed_in_place + self.class.attribute_names.select do |attr_name| + changed_in_place?(attr_name) + end + end + + def changed_in_place?(attr_name) + type = type_for_attribute(attr_name) + old_value = original_raw_attribute(attr_name) + value = read_attribute(attr_name) + type.changed_in_place?(old_value, value) + end + + def original_raw_attribute(attr_name) + original_raw_attributes.fetch(attr_name) do + read_attribute_before_type_cast(attr_name) + end + end + + def original_raw_attributes + @original_raw_attributes ||= {} + end + + def store_original_raw_attribute(attr_name) + type = type_for_attribute(attr_name) + value = type.type_cast_for_database(read_attribute(attr_name)) + original_raw_attributes[attr_name] = value + end + + def store_original_raw_attributes + attribute_names.each do |attr| + store_original_raw_attribute(attr) + end + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 7b7e37884b..8c1cc128f7 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -82,25 +82,16 @@ module ActiveRecord # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) - # If it's cached, just return it - # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829. name = attr_name.to_s - @attributes[name] || @attributes.fetch(name) { - column = @column_types_override[name] if @column_types_override - column ||= @column_types[name] - - return @raw_attributes.fetch(name) { - if name == 'id' && self.class.primary_key != name - read_attribute(self.class.primary_key) - end - } unless column - - value = @raw_attributes.fetch(name) { - return block_given? ? yield(name) : nil - } - - @attributes[name] = column.type_cast_from_database(value) - } + @attributes.fetch(name) { + if name == 'id' + return read_attribute(self.class.primary_key) + elsif block_given? && self.class.columns_hash.key?(name) + return yield(name) + else + return nil + end + }.value end private diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 60debb7d18..734d94865a 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -3,20 +3,7 @@ module ActiveRecord module Serialization extend ActiveSupport::Concern - included do - # Returns a hash of all the attributes that have been specified for - # serialization as keys and their class restriction as values. - class_attribute :serialized_attributes, instance_accessor: false - self.serialized_attributes = {} - end - module ClassMethods - ## - # :method: serialized_attributes - # - # Returns a hash of all the attributes that have been specified for - # serialization as keys and their class restriction as values. - # If you have an attribute that needs to be saved to the database as an # object, and retrieved as the same object, then specify the name of that # attribute using this method and it will be handled automatically. The @@ -50,8 +37,6 @@ module ActiveRecord # serialize :preferences, Hash # end def serialize(attr_name, class_name_or_coder = Object) - include Behavior - coder = if [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } class_name_or_coder else @@ -61,25 +46,18 @@ module ActiveRecord decorate_attribute_type(attr_name, :serialize) do |type| Type::Serialized.new(type, coder) end - - # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy - # has its own hash of own serialized attributes - self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) - end - end - - - # This is only added to the model when serialize is called, which - # ensures we do not make things slower when serialization is not used. - module Behavior # :nodoc: - extend ActiveSupport::Concern - - def should_record_timestamps? - super || (self.record_timestamps && (attributes.keys & self.class.serialized_attributes.keys).present?) end - def keys_for_partial_write - super | (attributes.keys & self.class.serialized_attributes.keys) + def serialized_attributes + ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc) + `serialized_attributes` is deprecated without replacement, and will + be removed in Rails 5.0. + WARNING + @serialized_attributes ||= Hash[ + columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c| + [c.name, c.cast_type.coder] + } + ] end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index b72a6219b0..246a2cd8ba 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -69,22 +69,19 @@ module ActiveRecord def write_attribute_with_type_cast(attr_name, value, should_type_cast) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - @attributes.delete(attr_name) - column = type_for_attribute(attr_name) + type = type_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.binary? - @attributes[attr_name] = value - elsif should_type_cast - @attributes[attr_name] = column.type_cast_from_user(value) + if should_type_cast + @attributes[attr_name] = Attribute.from_user(value, type) + else + @attributes[attr_name] = Attribute.from_database(value, type) end - @raw_attributes[attr_name] = value + value end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 7ff5001796..e8ce00d92b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -193,7 +193,7 @@ module ActiveRecord # * You are creating a nested (savepoint) transaction # # The mysql, mysql2 and postgresql adapters support setting the transaction - # isolation level. However, support is disabled for mysql versions below 5, + # isolation level. However, support is disabled for MySQL versions below 5, # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] # which means the isolation level gets persisted outside the transaction. def transaction(options = {}) @@ -338,8 +338,8 @@ module ActiveRecord end # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work - # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in - # an UPDATE statement, so in the mysql adapters we redefine this to do that. + # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in + # an UPDATE statement, so in the MySQL adapters we redefine this to do that. def join_to_update(update, select) #:nodoc: key = update.key subselect = subquery_for(key, select) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index ffa6af6d99..22823a8c58 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -788,7 +788,7 @@ module ActiveRecord return option_strings end - # Overridden by the mysql adapter for supporting index lengths + # Overridden by the MySQL adapter for supporting index lengths def quoted_columns_for_index(column_names, options = {}) option_strings = Hash[column_names.map {|name| [name, '']}] 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 200b773172..a9b97d5919 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -75,7 +75,7 @@ module ActiveRecord end def has_default? - return false if blob_or_text_column? #mysql forbids defaults on blob and text columns + return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns super end @@ -213,7 +213,7 @@ module ActiveRecord Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) end - # Must return the Mysql error number from the exception, if the exception has an + # Must return the MySQL error number from the exception, if the exception has an # error number. def error_number(exception) # :nodoc: raise NotImplementedError diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 72c6990ba5..d629fca911 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -16,7 +16,7 @@ module ActiveRecord attr_reader :name, :cast_type, :null, :sql_type, :default_function delegate :type, :precision, :scale, :limit, :klass, :accessor, - :text?, :number?, :binary?, :serialized?, :changed?, + :text?, :number?, :binary?, :changed?, :type_cast_from_user, :type_cast_from_database, :type_cast_for_database, :type_cast_for_schema, to: :cast_type diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index b79d1a4458..2fcb085ab2 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -85,7 +85,7 @@ module ActiveRecord "password" => uri.password, "port" => uri.port, "database" => database_from_path, - "host" => uri.host }) + "host" => uri.hostname }) end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 909bba8c7d..252b82643d 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -409,7 +409,7 @@ module ActiveRecord stmt.execute(*type_casted_binds.map { |_, val| val }) rescue Mysql::Error => e # Older versions of MySQL leave the prepared statement in a bad - # place when an error occurs. To support older mysql versions, we + # place when an error occurs. To support older MySQL versions, we # need to close the statement and delete the statement from the # cache. stmt.close 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 87817fe8d1..4e7d472d97 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -12,12 +12,16 @@ module ActiveRecord def type_cast_from_database(value) if value.is_a?(::String) - type_cast_array(parse_pg_array(value)) + type_cast_array(parse_pg_array(value), :type_cast_from_database) else super end end + def type_cast_from_user(value) + type_cast_array(value, :type_cast_from_user) + end + # Loads pg_array_parser if available. String parsing can be # performed quicker by a native extension, which will not create # a large amount of Ruby objects that will need to be garbage @@ -32,11 +36,11 @@ module ActiveRecord private - def type_cast_array(value) + def type_cast_array(value, method) if value.is_a?(::Array) - value.map { |item| type_cast_array(item) } + value.map { |item| type_cast_array(item, method) } else - @subtype.type_cast_from_database(value) + @subtype.public_send(method, value) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb index 36c53d8732..89b203d2b1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -3,8 +3,9 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Bytea < Type::Binary - def cast_value(value) - PGconn.unescape_bytea value + def type_cast_from_database(value) + return if value.nil? + PGconn.unescape_bytea(super) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index 88de816d4f..0dd4b65333 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -3,14 +3,12 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Hstore < Type::Value + include Type::Mutable + def type :hstore end - def type_cast_from_user(value) - type_cast_from_database(type_cast_for_database(value)) - end - def type_cast_from_database(value) ConnectionAdapters::PostgreSQLColumn.string_to_hstore(value) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index b4fed1bcab..d1347c7bb5 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -3,14 +3,12 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Json < Type::Value + include Type::Mutable + def type :json end - def type_cast_from_user(value) - type_cast_from_database(type_cast_for_database(value)) - end - def type_cast_from_database(value) ConnectionAdapters::PostgreSQLColumn.string_to_json(value) end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 86b4cc90ea..d39e5fddfe 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -249,16 +249,19 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil, options = {}) - defaults = self.class.raw_column_defaults.dup - defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } + defaults = {} + self.class.raw_column_defaults.each do |k, v| + default = v.duplicable? ? v.dup : v + defaults[k] = Attribute.from_database(default, type_for_attribute(k)) + end - @raw_attributes = defaults - @column_types_override = nil + @attributes = defaults @column_types = self.class.column_types init_internals initialize_internals_callback + self.class.define_attribute_methods # +options+ argument is only needed to make protected_attributes gem easier to hook. # Remove it when we drop support to this gem. init_attributes(attributes, options) if attributes @@ -278,13 +281,11 @@ module ActiveRecord # post.init_with('attributes' => { 'title' => 'hello world' }) # post.title # => 'hello world' def init_with(coder) - @raw_attributes = coder['raw_attributes'] - @column_types_override = coder['column_types'] + @attributes = coder['attributes'] @column_types = self.class.column_types init_internals - @attributes = coder['attributes'] if coder['attributes'] @new_record = coder['new_record'] self.class.define_attribute_methods @@ -323,12 +324,9 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: - cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - - @raw_attributes = cloned_attributes - @raw_attributes[self.class.primary_key] = nil - @attributes = other.clone_attributes(:read_attribute) - @attributes[self.class.primary_key] = nil + pk = self.class.primary_key + @attributes = other.clone_attributes + @attributes[pk] = Attribute.from_database(nil, type_for_attribute(pk)) run_callbacks(:initialize) unless _initialize_callbacks.empty? @@ -354,9 +352,9 @@ module ActiveRecord # Post.new.encode_with(coder) # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) - coder['raw_attributes'] = @raw_attributes + # FIXME: Remove this when we better serialize attributes + coder['raw_attributes'] = attributes_before_type_cast coder['attributes'] = @attributes - coder['column_types'] = @column_types_override coder['new_record'] = new_record? end @@ -391,13 +389,13 @@ module ActiveRecord # accessible, even on destroyed records, but cloned models will not be # frozen. def freeze - @raw_attributes = @raw_attributes.clone.freeze + @attributes = @attributes.clone.freeze self end # Returns +true+ if the attributes hash has been frozen. def frozen? - @raw_attributes.frozen? + @attributes.frozen? end # Allows sort on objects @@ -426,9 +424,9 @@ module ActiveRecord # Returns the contents of the record as a nicely formatted string. def inspect - # We check defined?(@raw_attributes) not to issue warnings if the object is + # We check defined?(@attributes) not to issue warnings if the object is # allocated but not initialized. - inspection = if defined?(@raw_attributes) && @raw_attributes + inspection = if defined?(@attributes) && @attributes self.class.column_names.collect { |name| if has_attribute?(name) "#{name}: #{attribute_for_inspect(name)}" @@ -527,11 +525,10 @@ module ActiveRecord def init_internals pk = self.class.primary_key - @raw_attributes[pk] = nil unless @raw_attributes.key?(pk) + @attributes[pk] ||= Attribute.from_database(nil, type_for_attribute(pk)) @aggregation_cache = {} @association_cache = {} - @attributes = {} @readonly = false @destroyed = false @marked_for_destruction = false @@ -554,7 +551,7 @@ module ActiveRecord def thaw if frozen? - @raw_attributes = @raw_attributes.dup + @attributes = @attributes.dup end end end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 2ccb1b0702..52c70977ef 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -30,17 +30,18 @@ module ActiveRecord class SerializationTypeMismatch < ActiveRecordError end - # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt> - # misses adapter field). + # Raised when adapter not specified on connection (or configuration file + # +config/database.yml+ misses adapter field). class AdapterNotSpecified < ActiveRecordError end - # Raised when Active Record cannot find database adapter specified in <tt>config/database.yml</tt> or programmatically. + # Raised when Active Record cannot find database adapter specified in + # +config/database.yml+ or programmatically. class AdapterNotFound < ActiveRecordError end - # Raised when connection to the database could not been established (for example when <tt>connection=</tt> - # is given a nil object). + # Raised when connection to the database could not been established (for + # example when +connection=+ is given a nil object). class ConnectionNotEstablished < ActiveRecordError end @@ -82,19 +83,17 @@ module ActiveRecord class InvalidForeignKey < WrappedDatabaseException end - # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example, - # when using +find+ method) - # does not match number of expected variables. + # Raised when number of bind variables in statement given to +:condition+ key + # (for example, when using +find+ method) does not match number of expected + # values supplied. # - # For example, in + # For example, when there are two placeholders with only one value supplied: # # Location.where("lat = ? AND lng = ?", 53.7362) - # - # two placeholders are given but only one variable to fill them. class PreparedStatementInvalid < ActiveRecordError end - # Raised when a given database does not exist + # Raised when a given database does not exist. class NoDatabaseError < StatementInvalid end @@ -102,7 +101,8 @@ module ActiveRecord # instantiation, for example, when two users edit the same wiki page and one starts editing and saves # the page before the other. # - # Read more about optimistic locking in ActiveRecord::Locking module RDoc. + # Read more about optimistic locking in ActiveRecord::Locking module + # documentation. class StaleObjectError < ActiveRecordError attr_reader :record, :attempted_action @@ -114,8 +114,9 @@ module ActiveRecord end - # Raised when association is being configured improperly or - # user tries to use offset and limit together with has_many or has_and_belongs_to_many associations. + # Raised when association is being configured improperly or user tries to use + # offset and limit together with +has_many+ or +has_and_belongs_to_many+ + # associations. class ConfigurationError < ActiveRecordError end @@ -153,7 +154,8 @@ module ActiveRecord class Rollback < ActiveRecordError end - # Raised when attribute has a name reserved by Active Record (when attribute has name of one of Active Record instance methods). + # Raised when attribute has a name reserved by Active Record (when attribute + # has name of one of Active Record instance methods). class DangerousAttributeError < ActiveRecordError end @@ -171,7 +173,7 @@ module ActiveRecord end # Raised when an error occurred while doing a mass assignment to an attribute through the - # <tt>attributes=</tt> method. The exception has an +attribute+ property that is the name of the + # +attributes=+ method. The exception has an +attribute+ property that is the name of the # offending attribute. class AttributeAssignmentError < ActiveRecordError attr_reader :exception, :attribute diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 95fcbbe99a..4cd5f92207 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -15,9 +15,10 @@ module ActiveRecord # They are stored in YAML files, one file per model, which are placed in the directory # appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically # configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>). - # The fixture file ends with the <tt>.yml</tt> file extension (Rails example: - # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a fixture file looks - # like this: + # The fixture file ends with the +.yml+ file extension, for example: + # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). + # + # The format of a fixture file looks like this: # # rubyonrails: # id: 1 @@ -61,8 +62,8 @@ module ActiveRecord # end # end # - # By default, <tt>test_helper.rb</tt> will load all of your fixtures into your test database, - # so this test will succeed. + # By default, +test_helper.rb+ will load all of your fixtures into your test + # database, so this test will succeed. # # The testing environment will automatically load the all fixtures into the database before each # test. To ensure consistent data, the environment deletes the fixtures before running the load. @@ -375,8 +376,8 @@ module ActiveRecord # == Support for YAML defaults # # You can set and reuse defaults in your fixtures YAML file. - # This is the same technique used in the <tt>database.yml</tt> file - # to specify defaults: + # This is the same technique used in the +database.yml+ file to specify + # defaults: # # DEFAULTS: &DEFAULTS # created_on: <%= 3.weeks.ago.to_s(:db) %> @@ -392,7 +393,8 @@ module ActiveRecord # Any fixture labeled "DEFAULTS" is safely ignored. class FixtureSet #-- - # An instance of FixtureSet is normally stored in a single YAML file and possibly in a folder with the same name. + # An instance of FixtureSet is normally stored in a single YAML file and + # possibly in a folder with the same name. #++ MAX_ID = 2 ** 30 - 1 diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 481e5c17e4..01c001e692 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -366,16 +366,19 @@ module ActiveRecord # This class is used to verify that all migrations have been run before # loading a web page if config.active_record.migration_error is set to :page_load class CheckPending - def initialize(app) + def initialize(app, connection = Base.connection) @app = app + @connection = connection @last_check = 0 end def call(env) - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i - if @last_check < mtime - ActiveRecord::Migration.check_pending! - @last_check = mtime + if @connection.supports_migrations? + mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + if @last_check < mtime + ActiveRecord::Migration.check_pending!(@connection) + @last_check = mtime + end end @app.call(env) end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 3ab8ec4836..5c744762d9 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -48,11 +48,12 @@ module ActiveRecord # how this "single-table" inheritance mapping is implemented. def instantiate(attributes, column_types = {}) klass = discriminate_class_for_record(attributes) - klass.allocate.init_with( - 'raw_attributes' => attributes, - 'column_types' => column_types, - 'new_record' => false, - ) + + attributes = attributes.each_with_object({}) do |(name, value), h| + type = column_types.fetch(name) { klass.type_for_attribute(name) } + h[name] = Attribute.from_database(value, type) + end + klass.allocate.init_with('attributes' => attributes, 'new_record' => false) end private @@ -182,7 +183,6 @@ module ActiveRecord # So any change to the attributes in either instance will affect the other. def becomes(klass) became = klass.new - became.instance_variable_set("@raw_attributes", @raw_attributes) became.instance_variable_set("@attributes", @attributes) became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes) became.instance_variable_set("@new_record", new_record?) @@ -399,11 +399,9 @@ module ActiveRecord self.class.unscoped { self.class.find(id) } end - @raw_attributes.update(fresh_object.instance_variable_get('@raw_attributes')) + @attributes.update(fresh_object.instance_variable_get('@attributes')) - @column_types = self.class.column_types - @column_types_override = fresh_object.instance_variable_get('@column_types_override') - @attributes = {} + @column_types = self.class.column_types self end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 95a3c70c93..51c96373ee 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -487,7 +487,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # returns either nil or the inverse association name that it finds. def automatic_inverse_of if can_find_inverse_of_automatically?(self) - inverse_name = ActiveSupport::Inflector.underscore(active_record.name).to_sym + inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name).to_sym begin reflection = klass._reflect_on_association(inverse_name) diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index ae53f66d7a..8c29c69a9b 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -95,7 +95,7 @@ module ActiveRecord @hash_rows ||= begin # We freeze the strings to prevent them getting duped when - # used as keys in ActiveRecord::Base's @raw_attributes hash + # used as keys in ActiveRecord::Base's @attributes hash columns = @columns.map { |c| c.dup.freeze } @rows.map { |row| # In the past we used Hash[columns.zip(row)] diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 1aa93ffbb3..ff70cbed0f 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -107,7 +107,7 @@ module ActiveRecord end.join(', ') end - # Sanitizes a +string+ so that it is safe to use within a sql + # Sanitizes a +string+ so that it is safe to use within an SQL # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%" def sanitize_sql_like(string, escape_character = "\\") pattern = Regexp.union(escape_character, "%", "_") diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 019fe2218e..c2484d02ed 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -182,11 +182,7 @@ module ActiveRecord #:nodoc: klass = @serializable.class column = klass.columns_hash[name] || Type::Value.new - type = if column.serialized? - super - else - column.type - end + type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type { :text => :string, :time => :datetime }[type] || type diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index dedbbd3585..649f316ed8 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -6,7 +6,7 @@ module ActiveRecord # <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates # logic behind common tasks used to manage database and migrations. # - # The tasks defined here are used in rake tasks provided by Active Record. + # The tasks defined here are used with Rake tasks provided by Active Record. # # In order to use DatabaseTasks, a few config values need to be set. All the needed # config values are set by Rails already, so it's necessary to do it only if you @@ -14,7 +14,6 @@ module ActiveRecord # (in such case after configuring the database tasks, you can also use the rake tasks # defined in Active Record). # - # # The possible config values are: # # * +env+: current environment (like Rails.env). diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index c755831e6d..644c4852b9 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -124,7 +124,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; end def root_password - $stdout.print "Please provide the root password for your mysql installation\n>" + $stdout.print "Please provide the root password for your MySQL installation\n>" $stdin.gets.strip end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index e9b827886a..f1384e0bb2 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,3 +1,4 @@ +require 'active_record/type/mutable' require 'active_record/type/numeric' require 'active_record/type/time_value' require 'active_record/type/value' diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb new file mode 100644 index 0000000000..64cf4b9b93 --- /dev/null +++ b/activerecord/lib/active_record/type/mutable.rb @@ -0,0 +1,16 @@ +module ActiveRecord + module Type + module Mutable + def type_cast_from_user(value) + type_cast_from_database(type_cast_for_database(value)) + end + + # +raw_old_value+ will be the `_before_type_cast` version of the + # value (likely a string). +new_value+ will be the current, type + # cast value. + def changed_in_place?(raw_old_value, new_value) # :nodoc: + raw_old_value != type_cast_for_database(new_value) + end + end + end +end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index e6d84eadc0..42bbed7103 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,6 +1,8 @@ module ActiveRecord module Type class Serialized < SimpleDelegator # :nodoc: + include Mutable + attr_reader :subtype, :coder def initialize(subtype, coder) @@ -17,10 +19,6 @@ module ActiveRecord end end - def type_cast_from_user(value) - type_cast_from_database(type_cast_for_database(value)) - end - def type_cast_for_database(value) return if value.nil? unless is_default_value?(value) @@ -28,10 +26,6 @@ module ActiveRecord end end - def serialized? - true - end - def accessor ActiveRecord::Store::IndifferentHashAccessor end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index efcdab1c0e..875fb98c4b 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -44,10 +44,6 @@ module ActiveRecord false end - def serialized? - false - end - def klass # :nodoc: end @@ -60,6 +56,10 @@ module ActiveRecord old_value != new_value end + def changed_in_place?(*) # :nodoc: + false + end + private # Takes an input from the database, or from attribute setters, # and casts it to a type appropriate for this object. This method diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 66750deb68..0b1e3295cc 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -97,8 +97,13 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_type_cast_integers x = PgArray.new(ratings: ['1', '2']) - assert x.save! - assert_equal(['1', '2'], x.ratings) + + assert_equal([1, 2], x.ratings) + + x.save! + x.reload + + assert_equal([1, 2], x.ratings) end def test_select_with_strings diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 0b48fe9af8..42c68cdae7 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -84,7 +84,7 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase class FullAddressType < ActiveRecord::Type::Value def type; :full_address end - def type_cast(value) + def type_cast_from_database(value) if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/ FullAddress.new($1, $2) end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index a25c9cb5e4..83b495d600 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -163,6 +163,15 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_equal "GMT", y.timezone end + def test_changes_in_place + hstore = Hstore.create!(settings: { 'one' => 'two' }) + hstore.settings['three'] = 'four' + hstore.save! + hstore.reload + + assert_equal 'four', hstore.settings['three'] + end + def test_gen1 assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 3ee8839823..a3400a5a19 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -165,4 +165,24 @@ class PostgresqlJSONTest < ActiveRecord::TestCase JsonDataType.update_all payload: { } assert_equal({ }, json.reload.payload) end + + def test_changes_in_place + json = JsonDataType.new + assert_not json.changed? + + json.payload = { 'one' => 'two' } + assert json.changed? + assert json.payload_changed? + + json.save! + assert_not json.changed? + + json.payload['three'] = 'four' + assert json.payload_changed? + + json.save! + json.reload + + assert json.payload['three'] = 'four' + end end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 2e62189e7a..8641584c0c 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -330,6 +330,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert post.single_people.include?(person) end + def test_both_parent_ids_set_when_saving_new + post = Post.new(title: 'Hello', body: 'world') + person = Person.new(first_name: 'Sean') + + post.people = [person] + post.save + + assert post.id + assert person.id + assert_equal post.id, post.readers.first.post_id + assert_equal person.id, post.readers.first.person_id + end + def test_delete_association assert_queries(2){posts(:welcome);people(:michael); } diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index a2725441b3..089cb0a3a2 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -45,6 +45,20 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_equal clubs(:moustache_club), new_member.club end + def test_creating_association_sets_both_parent_ids_for_new + member = Member.new(name: 'Sean Griffin') + club = Club.new(name: 'Da Club') + + member.club = club + + member.save! + + assert member.id + assert club.id + assert_equal member.id, member.current_membership.member_id + assert_equal club.id, member.current_membership.club_id + end + def test_replace_target_record new_club = Club.create(:name => "Marx Bros") @member.club = new_club diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index a674a39d65..60df4e14dd 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -100,6 +100,17 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase assert_respond_to club_reflection, :has_inverse? assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically" end + + def test_polymorphic_relationships_should_still_not_have_inverses_when_non_polymorphic_relationship_has_the_same_name + man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse) + face_reflection = Face.reflect_on_association(:man) + + assert_respond_to face_reflection, :has_inverse? + assert face_reflection.has_inverse?, "For this test, the non-polymorphic association must have an inverse" + + assert_respond_to man_reflection, :has_inverse? + assert !man_reflection.has_inverse?, "The target of a polymorphic association should not find an inverse automatically" + end end class InverseAssociationTests < ActiveRecord::TestCase diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index a366caf875..f832ca3451 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -451,10 +451,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_declared_suffixed_attribute_method_affects_respond_to_and_method_missing - topic = @target.new(:title => 'Budget') %w(_default _title_default _it! _candidate= able?).each do |suffix| @target.class_eval "def attribute#{suffix}(*args) args end" @target.attribute_method_suffix suffix + topic = @target.new(:title => 'Budget') meth = "title#{suffix}" assert topic.respond_to?(meth) @@ -465,10 +465,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_declared_affixed_attribute_method_affects_respond_to_and_method_missing - topic = @target.new(:title => 'Budget') [['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix| @target.class_eval "def #{prefix}attribute#{suffix}(*args) args end" @target.attribute_method_affix({ :prefix => prefix, :suffix => suffix }) + topic = @target.new(:title => 'Budget') meth = "#{prefix}title#{suffix}" assert topic.respond_to?(meth) diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb new file mode 100644 index 0000000000..57dd2e9a5e --- /dev/null +++ b/activerecord/test/cases/attribute_test.rb @@ -0,0 +1,103 @@ +require 'cases/helper' +require 'minitest/mock' + +module ActiveRecord + class AttributeTest < ActiveRecord::TestCase + setup do + @type = MiniTest::Mock.new + end + + teardown do + assert @type.verify + end + + test "from_database + read type casts from database" do + @type.expect(:type_cast_from_database, 'type cast from database', ['a value']) + attribute = Attribute.from_database('a value', @type) + + type_cast_value = attribute.value + + assert_equal 'type cast from database', type_cast_value + end + + test "from_user + read type casts from user" do + @type.expect(:type_cast_from_user, 'type cast from user', ['a value']) + attribute = Attribute.from_user('a value', @type) + + type_cast_value = attribute.value + + assert_equal 'type cast from user', type_cast_value + end + + test "reading memoizes the value" do + @type.expect(:type_cast_from_database, 'from the database', ['whatever']) + attribute = Attribute.from_database('whatever', @type) + + type_cast_value = attribute.value + second_read = attribute.value + + assert_equal 'from the database', type_cast_value + assert_same type_cast_value, second_read + end + + test "reading memoizes falsy values" do + @type.expect(:type_cast_from_database, false, ['whatever']) + attribute = Attribute.from_database('whatever', @type) + + attribute.value + attribute.value + end + + test "read_before_typecast returns the given value" do + attribute = Attribute.from_database('raw value', @type) + + raw_value = attribute.value_before_type_cast + + assert_equal 'raw value', raw_value + end + + test "from_database + read_for_database type casts to and from database" do + @type.expect(:type_cast_from_database, 'read from database', ['whatever']) + @type.expect(:type_cast_for_database, 'ready for database', ['read from database']) + attribute = Attribute.from_database('whatever', @type) + + type_cast_for_database = attribute.value_for_database + + assert_equal 'ready for database', type_cast_for_database + end + + test "from_user + read_for_database type casts from the user to the database" do + @type.expect(:type_cast_from_user, 'read from user', ['whatever']) + @type.expect(:type_cast_for_database, 'ready for database', ['read from user']) + attribute = Attribute.from_user('whatever', @type) + + type_cast_for_database = attribute.value_for_database + + assert_equal 'ready for database', type_cast_for_database + end + + test "duping dups the value" do + @type.expect(:type_cast_from_database, 'type cast', ['a value']) + attribute = Attribute.from_database('a value', @type) + + value_from_orig = attribute.value + value_from_clone = attribute.dup.value + value_from_orig << ' foo' + + assert_equal 'type cast foo', value_from_orig + assert_equal 'type cast', value_from_clone + end + + test "duping does not dup the value if it is not dupable" do + @type.expect(:type_cast_from_database, false, ['a value']) + attribute = Attribute.from_database('a value', @type) + + assert_same attribute.value, attribute.dup.value + end + + test "duping does not eagerly type cast if we have not yet type cast" do + attribute = Attribute.from_database('a value', @type) + attribute.dup + end + end +end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 541f74f21e..8f83cf7cb4 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1488,15 +1488,14 @@ class BasicsTest < ActiveRecord::TestCase attrs = topic.attributes.dup attrs.delete 'id' - typecast = Class.new { - def type_cast_from_database value + typecast = Class.new(ActiveRecord::Type::Value) { + def type_cast value "t.lo" end } types = { 'author_name' => typecast.new } - topic = Topic.allocate.init_with 'raw_attributes' => attrs, - 'column_types' => types + topic = Topic.instantiate(attrs, types) assert_equal 't.lo', topic.author_name end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index b41b95309b..ccf2be369d 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -21,7 +21,7 @@ unless current_adapter?(:DB2Adapter) name = binary.name - # Mysql adapter doesn't properly encode things, so we have to do it + # MySQL adapter doesn't properly encode things, so we have to do it if current_adapter?(:MysqlAdapter) name.force_encoding(Encoding::UTF_8) end diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index da852aaa02..e1b2804a18 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -144,6 +144,18 @@ module ActiveRecord assert_equal nil, actual[:test] end + def test_database_url_with_ipv6_host_and_port + ENV['DATABASE_URL'] = "postgres://[::1]:5454/foo" + + config = {} + actual = resolve_config(config) + expected = { "adapter" => "postgresql", + "database" => "foo", + "host" => "::1", + "port" => 5454 } + assert_equal expected, actual["default_env"] + end + def test_url_sub_key_with_database_url ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO" diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 87f24e32b2..5d6601a881 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -445,11 +445,20 @@ class DirtyTest < ActiveRecord::TestCase def test_save_should_store_serialized_attributes_even_with_partial_writes with_partial_writes(Topic) do topic = Topic.create!(:content => {:a => "a"}) + + assert_not topic.changed? + topic.content[:b] = "b" - #assert topic.changed? # Known bug, will fail + + assert topic.changed? + topic.save! + + assert_not topic.changed? assert_equal "b", topic.content[:b] + topic.reload + assert_equal "b", topic.content[:b] end end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 984d1c2597..93adbdd05b 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -62,7 +62,7 @@ module ActiveRecord # Do a manual insertion if current_adapter?(:OracleAdapter) connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)" - elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before mysql 5.0.3 decimals stored as strings + elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')" elsif current_adapter?(:PostgreSQLAdapter) connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb index 84224e6e4c..319d3e1af3 100644 --- a/activerecord/test/cases/migration/logger_test.rb +++ b/activerecord/test/cases/migration/logger_test.rb @@ -3,7 +3,7 @@ require "cases/helper" module ActiveRecord class Migration class LoggerTest < ActiveRecord::TestCase - # mysql can't roll back ddl changes + # MySQL can't roll back ddl changes self.use_transactional_fixtures = false Migration = Struct.new(:name, :version) do diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb new file mode 100644 index 0000000000..eff000e1a4 --- /dev/null +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -0,0 +1,50 @@ +require 'cases/helper' +require "minitest/mock" + +module ActiveRecord + class Migration + class PendingMigrationsTest < ActiveRecord::TestCase + def setup + super + @connection = MiniTest::Mock.new + @app = MiniTest::Mock.new + @pending = CheckPending.new(@app, @connection) + @pending.instance_variable_set :@last_check, -1 # Force checking + end + + def teardown + super + assert @connection.verify + assert @app.verify + end + + def test_errors_if_pending + @connection.expect :supports_migrations?, true + + ActiveRecord::Migrator.stub :needs_migration?, true do + assert_raise ActiveRecord::PendingMigrationError do + @pending.call(nil) + end + end + end + + def test_checks_if_supported + @connection.expect :supports_migrations?, true + @app.expect :call, nil, [:foo] + + ActiveRecord::Migrator.stub :needs_migration?, false do + @pending.call(:foo) + end + end + + def test_doesnt_check_if_unsupported + @connection.expect :supports_migrations?, false + @app.expect :call, nil, [:foo] + + ActiveRecord::Migrator.stub :needs_migration?, true do + @pending.call(:foo) + end + end + end + end +end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index bc21eef919..28341d0b42 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -250,11 +250,9 @@ class PersistenceTest < ActiveRecord::TestCase end def test_create_columns_not_equal_attributes - topic = Topic.allocate.init_with( - 'raw_attributes' => { - 'title' => 'Another New Topic', - 'does_not_exist' => 'test' - } + topic = Topic.instantiate( + 'title' => 'Another New Topic', + 'does_not_exist' => 'test' ) assert_nothing_raised { topic.save } end @@ -300,10 +298,7 @@ class PersistenceTest < ActiveRecord::TestCase topic.title = "Still another topic" topic.save - topic_reloaded = Topic.allocate - topic_reloaded.init_with( - 'raw_attributes' => topic.attributes.merge('does_not_exist' => 'test') - ) + topic_reloaded = Topic.instantiate(topic.attributes.merge('does_not_exist' => 'test')) topic_reloaded.title = 'A New Topic' assert_nothing_raised { topic_reloaded.save } end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 7dd1f10ce9..3f52e80e11 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -69,6 +69,14 @@ class SerializationTest < ActiveRecord::TestCase ActiveRecord::Base.include_root_in_json = original_root_in_json end + def test_read_attribute_for_serialization_with_format_without_method_missing + klazz = Class.new(ActiveRecord::Base) + klazz.table_name = 'books' + + book = klazz.new + assert_nil book.read_attribute_for_serialization(:format) + end + def test_read_attribute_for_serialization_with_format_after_init klazz = Class.new(ActiveRecord::Base) klazz.table_name = 'books' diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 7d1c240638..186a1a2ade 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -22,7 +22,9 @@ class SerializedAttributeTest < ActiveRecord::TestCase end def test_list_of_serialized_attributes - assert_equal %w(content), Topic.serialized_attributes.keys + assert_deprecated do + assert_equal %w(content), Topic.serialized_attributes.keys + end end def test_serialized_attribute @@ -36,12 +38,6 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal(myobj, topic.content) end - def test_serialized_attribute_init_with - topic = Topic.allocate - topic.init_with('raw_attributes' => { 'content' => '--- foo' }) - assert_equal 'foo', topic.content - end - def test_serialized_attribute_in_base_class Topic.serialize("content", Hash) @@ -213,7 +209,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase t = Topic.create(content: "first") assert_equal("first", t.content) - t.update_column(:content, Topic.serialized_attributes["content"].dump("second")) + t.update_column(:content, Topic.type_for_attribute('content').type_cast_for_database("second")) assert_equal("second", t.content) end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index f841b1c983..e9cdf94c99 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -189,7 +189,6 @@ class StoreTest < ActiveRecord::TestCase assert_equal @john, loaded second_dump = YAML.dump(loaded) - assert_equal dumped, second_dump assert_equal @john, YAML.load(second_dump) end end diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index edb75d333f..3d7f0626e2 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -1,6 +1,7 @@ class Face < ActiveRecord::Base belongs_to :man, :inverse_of => :face belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face + belongs_to :polymorphic_man_without_inverse, :polymorphic => true # These is a "broken" inverse_of for the purposes of testing belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb index f4d127730c..a26491ce61 100644 --- a/activerecord/test/models/man.rb +++ b/activerecord/test/models/man.rb @@ -1,6 +1,7 @@ class Man < ActiveRecord::Base has_one :face, :inverse_of => :man has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man + has_one :polymorphic_face_without_inverse, :class_name => 'Face', :as => :polymorphic_man_without_inverse has_many :interests, :inverse_of => :man has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man # These are "broken" inverse_of associations for the purposes of testing diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 98fd0b6803..a7c7fc70bc 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -775,6 +775,8 @@ ActiveRecord::Schema.define do t.integer :man_id t.integer :polymorphic_man_id t.string :polymorphic_man_type + t.integer :polymorphic_man_without_inverse_id + t.string :polymorphic_man_without_inverse_type t.integer :horrible_polymorphic_man_id t.string :horrible_polymorphic_man_type end |