diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2010-01-24 01:33:18 +0530 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2010-01-24 01:33:18 +0530 |
commit | efd0bd3b7390ebb8526b981169025f2860f6a113 (patch) | |
tree | 1cb3bcd8f9534f64e8a764af1010ce65303b42b0 /activerecord/lib | |
parent | f4571e3617ccd9cc9e5ee9f7431066bd80395e22 (diff) | |
parent | 8ff2fb6f3aa6140f5a8bd018d5919a8a1e707cda (diff) | |
download | rails-efd0bd3b7390ebb8526b981169025f2860f6a113.tar.gz rails-efd0bd3b7390ebb8526b981169025f2860f6a113.tar.bz2 rails-efd0bd3b7390ebb8526b981169025f2860f6a113.zip |
Merge remote branch 'mainstream/master'
Diffstat (limited to 'activerecord/lib')
29 files changed, 861 insertions, 1401 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d5b6d40514..cc0accf90e 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -45,7 +45,6 @@ module ActiveRecord autoload :AssociationPreload autoload :Associations autoload :AttributeMethods - autoload :Attributes autoload :AutosaveAssociation autoload :Relation @@ -53,14 +52,13 @@ module ActiveRecord autoload_under 'relation' do autoload :QueryMethods autoload :FinderMethods - autoload :CalculationMethods + autoload :Calculations autoload :PredicateBuilder autoload :SpawnMethods end autoload :Base autoload :Batches - autoload :Calculations autoload :Callbacks autoload :DynamicFinderMatch autoload :DynamicScopeMatch @@ -78,7 +76,6 @@ module ActiveRecord autoload :StateMachine autoload :Timestamp autoload :Transactions - autoload :Types autoload :Validations end @@ -96,28 +93,6 @@ module ActiveRecord end end - module Attributes - extend ActiveSupport::Autoload - - eager_autoload do - autoload :Aliasing - autoload :Store - autoload :Typecasting - end - end - - module Type - extend ActiveSupport::Autoload - - eager_autoload do - autoload :Number, 'active_record/types/number' - autoload :Object, 'active_record/types/object' - autoload :Serialize, 'active_record/types/serialize' - autoload :TimeWithZone, 'active_record/types/time_with_zone' - autoload :Unknown, 'active_record/types/unknown' - end - end - module Locking extend ActiveSupport::Autoload diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index a43c4d09d6..a5b06460fe 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -187,13 +187,12 @@ module ActiveRecord conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}" conditions << append_conditions(reflection, preload_options) - associated_records = reflection.klass.with_exclusive_scope do - reflection.klass.where([conditions, ids]). + associated_records = reflection.klass.unscoped.where([conditions, ids]). includes(options[:include]). joins("INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}"). select("#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id"). order(options[:order]).to_a - end + set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id') end @@ -341,9 +340,7 @@ module ActiveRecord conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}" conditions << append_conditions(reflection, preload_options) - associated_records = klass.with_exclusive_scope do - klass.where([conditions, ids]).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a - end + associated_records = klass.unscoped.where([conditions, ids]).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a set_association_single_records(id_map, reflection.name, associated_records, primary_key) end @@ -362,14 +359,16 @@ module ActiveRecord conditions << append_conditions(reflection, preload_options) - reflection.klass.with_exclusive_scope do - reflection.klass.select(preload_options[:select] || options[:select] || "#{table_name}.*"). - includes(preload_options[:include] || options[:include]). - where([conditions, ids]). - joins(options[:joins]). - group(preload_options[:group] || options[:group]). - order(preload_options[:order] || options[:order]) - end + find_options = { + :select => preload_options[:select] || options[:select] || "#{table_name}.*", + :include => preload_options[:include] || options[:include], + :conditions => [conditions, ids], + :joins => options[:joins], + :group => preload_options[:group] || options[:group], + :order => preload_options[:order] || options[:order] + } + + reflection.klass.unscoped.apply_finder_options(find_options).to_a end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index ebf1a41e85..57785b4c93 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1463,13 +1463,6 @@ module ActiveRecord after_destroy(method_name) end - def find_with_associations(options, join_dependency) - rows = select_all_rows(options, join_dependency) - join_dependency.instantiate(rows) - rescue ThrowResult - [] - end - # Creates before_destroy callback methods that nullify, delete or destroy # has_many associated objects, according to the defined :dependent rule. # @@ -1693,66 +1686,6 @@ module ActiveRecord reflection end - def select_all_rows(options, join_dependency) - connection.select_all( - construct_finder_sql_with_included_associations(options, join_dependency), - "#{name} Load Including Associations" - ) - end - - def construct_finder_arel_with_included_associations(options, join_dependency) - relation = scoped - - for association in join_dependency.join_associations - relation = association.join_relation(relation) - end - - relation = relation.apply_finder_options(options).select(column_aliases(join_dependency)) - - if !using_limitable_reflections?(join_dependency.reflections) && relation.limit_value - relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency)) - end - - relation = relation.except(:limit, :offset) unless using_limitable_reflections?(join_dependency.reflections) - - relation - end - - def construct_finder_sql_with_included_associations(options, join_dependency) - construct_finder_arel_with_included_associations(options, join_dependency).to_sql - end - - def construct_arel_limited_ids_condition(options, join_dependency) - if (ids_array = select_limited_ids_array(options, join_dependency)).empty? - raise ThrowResult - else - Arel::Predicates::In.new( - Arel::SqlLiteral.new("#{connection.quote_table_name table_name}.#{primary_key}"), - ids_array - ) - end - end - - def select_limited_ids_array(options, join_dependency) - connection.select_all( - construct_finder_sql_for_association_limiting(options, join_dependency), - "#{name} Load IDs For Limited Eager Loading" - ).collect { |row| row[primary_key] } - end - - def construct_finder_sql_for_association_limiting(options, join_dependency) - relation = scoped - - for association in join_dependency.join_associations - relation = association.join_relation(relation) - end - - relation = relation.apply_finder_options(options).except(:select) - relation = relation.select(connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", relation.order_values.join(", "))) - - relation.to_sql - end - def using_limitable_reflections?(reflections) reflections.collect(&:collection?).length.zero? end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index e9402d3547..9487d16123 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -176,14 +176,15 @@ module ActiveRecord # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the # descendant's +construct_sql+ method will have set :counter_sql automatically. # Otherwise, construct options and pass them with scope to the target class's +count+. - def count(*args) + def count(column_name = nil, options = {}) if @reflection.options[:counter_sql] @reflection.klass.count_by_sql(@counter_sql) else - column_name, options = @reflection.klass.scoped.send(:construct_count_options_from_args, *args) + column_name, options = nil, column_name if column_name.is_a?(Hash) + if @reflection.options[:uniq] # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. - column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all + column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" unless column_name options.merge!(:distinct => true) 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 74921241f7..a4e144f233 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -8,18 +8,25 @@ module ActiveRecord end def read_attribute_before_type_cast(attr_name) - _attributes.without_typecast[attr_name] + @attributes[attr_name] end # Returns a hash of attributes before typecasting and deserialization. def attributes_before_type_cast - _attributes.without_typecast + self.attribute_names.inject({}) do |attrs, name| + attrs[name] = read_attribute_before_type_cast(name) + attrs + end end private # Handle *_before_type_cast for method_missing. def attribute_before_type_cast(attribute_name) - read_attribute_before_type_cast(attribute_name) + if attribute_name == 'id' + read_attribute_before_type_cast(self.class.primary_key) + else + read_attribute_before_type_cast(attribute_name) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 0154ee35f8..a949d80120 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -8,7 +8,23 @@ module ActiveRecord end def query_attribute(attr_name) - _attributes.has?(attr_name) + unless value = read_attribute(attr_name) + false + else + column = self.class.columns_hash[attr_name] + if column.nil? + if Numeric === value || value !~ /[^0-9]/ + !value.to_i.zero? + else + return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) + !value.blank? + end + elsif column.number? + !value.zero? + else + !value.blank? + end + end end private @@ -19,5 +35,3 @@ module ActiveRecord end end end - - diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 97caec7744..3da3d9d8cc 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -37,7 +37,11 @@ module ActiveRecord protected def define_method_attribute(attr_name) - define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name]) + if self.serialized_attributes[attr_name] + define_read_method_for_serialized_attribute(attr_name) + else + define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name]) + end if attr_name == primary_key && attr_name != "id" define_read_method(:id, attr_name, columns_hash[attr_name]) @@ -45,12 +49,18 @@ module ActiveRecord end private + # Define read method for serialized attribute. + def define_read_method_for_serialized_attribute(attr_name) + generated_attribute_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__) + end # Define an attribute reader method. Cope with nil column. def define_read_method(symbol, attr_name, column) - access_code = "_attributes['#{attr_name}']" + cast_code = column.type_cast_code('v') if column + access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" + unless attr_name.to_s == self.primary_key.to_s - access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless _attributes.key?('#{attr_name}'); ") + access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") end if cache_attribute?(attr_name) @@ -63,7 +73,38 @@ module ActiveRecord # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) - _attributes[attr_name] + attr_name = attr_name.to_s + attr_name = self.class.primary_key if attr_name == 'id' + if !(value = @attributes[attr_name]).nil? + if column = column_for_attribute(attr_name) + if unserializable_attribute?(attr_name, column) + unserialize_attribute(attr_name) + else + column.type_cast(value) + end + else + value + end + else + nil + end + end + + # Returns true if the attribute is of a text column and marked for serialization. + def unserializable_attribute?(attr_name, column) + column.text? && self.class.serialized_attributes[attr_name] + end + + # Returns the unserialized object of the attribute. + def unserialize_attribute(attr_name) + unserialized_object = object_from_yaml(@attributes[attr_name]) + + if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil? + @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object + else + raise SerializationTypeMismatch, + "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}" + end end private diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 4ac0c7f608..a8e3e28a7a 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -12,20 +12,48 @@ module ActiveRecord end module ClassMethods - - def cache_attribute?(attr_name) - time_zone_aware?(attr_name) || super - end - protected + # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. + # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone. + def define_method_attribute(attr_name) + if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) + method_body = <<-EOV + def #{attr_name}(reload = false) + cached = @attributes_cache['#{attr_name}'] + return cached if cached && !reload + time = read_attribute('#{attr_name}') + @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time + end + EOV + generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__) + else + super + end + end - def time_zone_aware?(attr_name) - column = columns_hash[attr_name] - time_zone_aware_attributes && - !skip_time_zone_conversion_for_attributes.include?(attr_name.to_sym) && - [:datetime, :timestamp].include?(column.type) + # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. + # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. + def define_method_attribute=(attr_name) + if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) + method_body = <<-EOV + def #{attr_name}=(time) + unless time.acts_like?(:time) + time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time + end + time = time.in_time_zone rescue nil if time + write_attribute(:#{attr_name}, time) + end + EOV + generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__) + else + super + end end + private + def create_time_zone_conversion_attribute?(name, column) + time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 37eadbe0a9..e31acac050 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -17,9 +17,14 @@ module ActiveRecord # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float # columns are turned into +nil+. def write_attribute(attr_name, value) - attr_name = _attributes.unalias(attr_name) + attr_name = attr_name.to_s + attr_name = self.class.primary_key if attr_name == 'id' @attributes_cache.delete(attr_name) - _attributes[attr_name] = value + if (column = column_for_attribute(attr_name)) && column.number? + @attributes[attr_name] = convert_number_column_value(value) + else + @attributes[attr_name] = value + end end private diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb deleted file mode 100644 index e4d9e89821..0000000000 --- a/activerecord/lib/active_record/attributes.rb +++ /dev/null @@ -1,37 +0,0 @@ -module ActiveRecord - module Attributes - - # Returns true if the given attribute is in the attributes hash - def has_attribute?(attr_name) - _attributes.key?(attr_name) - end - - # Returns an array of names for the attributes available on this object sorted alphabetically. - def attribute_names - _attributes.keys.sort! - end - - # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. - def attributes - attributes = _attributes.dup - attributes.typecast! unless _attributes.frozen? - attributes.to_h - end - - protected - - # Not to be confused with the public #attributes method, which returns a typecasted Hash. - def _attributes - @attributes - end - - def initialize_attribute_store(merge_attributes = nil) - @attributes = ActiveRecord::Attributes::Store.new - @attributes.merge!(merge_attributes) if merge_attributes - @attributes.types.merge!(self.class.attribute_types) - @attributes.aliases.merge!('id' => self.class.primary_key) unless 'id' == self.class.primary_key - @attributes - end - - end -end diff --git a/activerecord/lib/active_record/attributes/aliasing.rb b/activerecord/lib/active_record/attributes/aliasing.rb deleted file mode 100644 index db77739d1f..0000000000 --- a/activerecord/lib/active_record/attributes/aliasing.rb +++ /dev/null @@ -1,42 +0,0 @@ -module ActiveRecord - module Attributes - module Aliasing - # Allows access to keys using aliased names. - # - # Example: - # class Attributes < Hash - # include Aliasing - # end - # - # attributes = Attributes.new - # attributes.aliases['id'] = 'fancy_primary_key' - # attributes['fancy_primary_key'] = 2020 - # - # attributes['id'] - # => 2020 - # - # Additionally, symbols are always aliases of strings: - # attributes[:fancy_primary_key] - # => 2020 - # - def [](key) - super(unalias(key)) - end - - def []=(key, value) - super(unalias(key), value) - end - - def aliases - @aliases ||= {} - end - - def unalias(key) - key = key.to_s - aliases[key] || key - end - - end - end -end - diff --git a/activerecord/lib/active_record/attributes/store.rb b/activerecord/lib/active_record/attributes/store.rb deleted file mode 100644 index 61109f4acc..0000000000 --- a/activerecord/lib/active_record/attributes/store.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveRecord - module Attributes - class Store < Hash - include ActiveRecord::Attributes::Typecasting - include ActiveRecord::Attributes::Aliasing - - # Attributes not mapped to a column are handled using Type::Unknown, - # which enables boolean typecasting for unmapped keys. - def types - @types ||= Hash.new(Type::Unknown.new) - end - - end - end -end diff --git a/activerecord/lib/active_record/attributes/typecasting.rb b/activerecord/lib/active_record/attributes/typecasting.rb deleted file mode 100644 index 56c32f9895..0000000000 --- a/activerecord/lib/active_record/attributes/typecasting.rb +++ /dev/null @@ -1,117 +0,0 @@ -module ActiveRecord - module Attributes - module Typecasting - # Typecasts values during access based on their key mapping to a Type. - # - # Example: - # class Attributes < Hash - # include Typecasting - # end - # - # attributes = Attributes.new - # attributes.types['comments_count'] = Type::Integer - # attributes['comments_count'] = '5' - # - # attributes['comments_count'] - # => 5 - # - # To support keys not mapped to a typecaster, add a default to types. - # attributes.types.default = Type::Unknown - # attributes['age'] = '25' - # attributes['age'] - # => '25' - # - # A valid type supports #cast, #precast, #boolean, and #appendable? methods. - # - def [](key) - value = super(key) - typecast_read(key, value) - end - - def []=(key, value) - super(key, typecast_write(key, value)) - end - - def to_h - hash = {} - hash.merge!(self) - hash - end - - def dup # :nodoc: - copy = super - copy.types = types.dup - copy - end - - # Provides a duplicate with typecasting disabled. - # - # Example: - # attributes = Attributes.new - # attributes.types['comments_count'] = Type::Integer - # attributes['comments_count'] = '5' - # - # attributes.without_typecast['comments_count'] - # => '5' - # - def without_typecast - dup.without_typecast! - end - - def without_typecast! - types.clear - self - end - - def typecast! - keys.each { |key| self[key] = self[key] } - self - end - - # Check if key has a value that typecasts to true. - # - # attributes = Attributes.new - # attributes.types['comments_count'] = Type::Integer - # - # attributes['comments_count'] = 0 - # attributes.has?('comments_count') - # => false - # - # attributes['comments_count'] = 1 - # attributes.has?('comments_count') - # => true - # - def has?(key) - value = self[key] - boolean_typecast(key, value) - end - - def types - @types ||= {} - end - - protected - - def types=(other_types) - @types = other_types - end - - def boolean_typecast(key, value) - value ? types[key].boolean(value) : false - end - - def typecast_read(key, value) - type = types[key] - value = type.cast(value) - self[key] = value if type.appendable? && !frozen? - - value - end - - def typecast_write(key, value) - types[key].precast(value) - end - - end - end -end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 06244d1132..12feef4849 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -556,122 +556,9 @@ module ActiveRecord #:nodoc: end alias :colorize_logging= :colorize_logging - # Find operates with four different retrieval approaches: - # - # * Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). - # If no record can be found for all of the listed ids, then RecordNotFound will be raised. - # * Find first - This will return the first record matched by the options used. These options can either be specific - # conditions or merely an order. If no record can be matched, +nil+ is returned. Use - # <tt>Model.find(:first, *args)</tt> or its shortcut <tt>Model.first(*args)</tt>. - # * Find last - This will return the last record matched by the options used. These options can either be specific - # conditions or merely an order. If no record can be matched, +nil+ is returned. Use - # <tt>Model.find(:last, *args)</tt> or its shortcut <tt>Model.last(*args)</tt>. - # * Find all - This will return all the records matched by the options used. - # If no records are found, an empty array is returned. Use - # <tt>Model.find(:all, *args)</tt> or its shortcut <tt>Model.all(*args)</tt>. - # - # All approaches accept an options hash as their last parameter. - # - # ==== Parameters - # - # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>[ "user_name = ?", username ]</tt>, or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro. - # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name". - # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. - # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause. - # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned. - # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip rows 0 through 4. - # * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed), - # named associations in the same form used for the <tt>:include</tt> option, which will perform an <tt>INNER JOIN</tt> on the associated table(s), - # or an array containing a mixture of both strings and named associations. - # If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns. - # Pass <tt>:readonly => false</tt> to override. - # * <tt>:include</tt> - Names associations that should be loaded alongside. The symbols named refer - # to already defined associations. See eager loading under Associations. - # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you, for example, want to do a join but not - # include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name"). - # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name - # of a database view). - # * <tt>:readonly</tt> - Mark the returned records read-only so they cannot be saved or updated. - # * <tt>:lock</tt> - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE". - # <tt>:lock => true</tt> gives connection's default exclusive lock, usually "FOR UPDATE". - # - # ==== Examples - # - # # find by id - # Person.find(1) # returns the object for ID = 1 - # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) - # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17) - # Person.find([1]) # returns an array for the object with ID = 1 - # Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC") - # - # Note that returned records may not be in the same order as the ids you - # provide since database rows are unordered. Give an explicit <tt>:order</tt> - # to ensure the results are sorted. - # - # ==== Examples - # - # # find first - # Person.find(:first) # returns the first object fetched by SELECT * FROM people - # Person.find(:first, :conditions => [ "user_name = ?", user_name]) - # Person.find(:first, :conditions => [ "user_name = :u", { :u => user_name }]) - # Person.find(:first, :order => "created_on DESC", :offset => 5) - # - # # find last - # Person.find(:last) # returns the last object fetched by SELECT * FROM people - # Person.find(:last, :conditions => [ "user_name = ?", user_name]) - # Person.find(:last, :order => "created_on DESC", :offset => 5) - # - # # find all - # Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people - # Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50) - # Person.find(:all, :conditions => { :friends => ["Bob", "Steve", "Fred"] } - # Person.find(:all, :offset => 10, :limit => 10) - # Person.find(:all, :include => [ :account, :friends ]) - # Person.find(:all, :group => "category") - # - # Example for find with a lock: Imagine two concurrent transactions: - # each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting - # in two saves of <tt>person.visits = 3</tt>. By locking the row, the second - # transaction has to wait until the first is finished; we get the - # expected <tt>person.visits == 4</tt>. - # - # Person.transaction do - # person = Person.find(1, :lock => true) - # person.visits += 1 - # person.save! - # end - def find(*args) - options = args.extract_options! - - relation = construct_finder_arel(options, current_scoped_methods) - - case args.first - when :first, :last, :all - relation.send(args.first) - else - relation.find(*args) - end - end - + delegate :find, :first, :last, :all, :destroy, :destroy_all, :exists?, :delete, :delete_all, :update, :update_all, :to => :scoped delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped - - # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the - # same arguments to this method as you can to <tt>find(:first)</tt>. - def first(*args) - find(:first, *args) - end - - # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the - # same arguments to this method as you can to <tt>find(:last)</tt>. - def last(*args) - find(:last, *args) - end - - # A convenience wrapper for <tt>find(:all, *args)</tt>. You can pass in all the - # same arguments to this method as you can to <tt>find(:all)</tt>. - def all(*args) - find(:all, *args) - end + delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped # Executes a custom SQL query against your database and returns all the results. The results will # be returned as an array with columns requested encapsulated as attributes of the model you call @@ -699,40 +586,6 @@ module ActiveRecord #:nodoc: connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) } end - # Returns true if a record exists in the table that matches the +id+ or - # conditions given, or false otherwise. The argument can take five forms: - # - # * Integer - Finds the record with this primary key. - # * String - Finds the record with a primary key corresponding to this - # string (such as <tt>'5'</tt>). - # * Array - Finds the record that matches these +find+-style conditions - # (such as <tt>['color = ?', 'red']</tt>). - # * Hash - Finds the record that matches these +find+-style conditions - # (such as <tt>{:color => 'red'}</tt>). - # * No args - Returns false if the table is empty, true otherwise. - # - # For more information about specifying conditions as a Hash or Array, - # see the Conditions section in the introduction to ActiveRecord::Base. - # - # Note: You can't pass in a condition as a string (like <tt>name = - # 'Jamie'</tt>), since it would be sanitized and then queried against - # the primary key column, like <tt>id = 'name = \'Jamie\''</tt>. - # - # ==== Examples - # Person.exists?(5) - # Person.exists?('5') - # Person.exists?(:name => "David") - # Person.exists?(['name LIKE ?', "%#{query}%"]) - # Person.exists? - def exists?(id_or_conditions = nil) - case id_or_conditions - when Array, Hash - where(id_or_conditions).exists? - else - scoped.exists?(id_or_conditions) - end - end - # Creates an object (or multiple objects) and saves it to the database, if validations pass. # The resulting object is returned whether the object was saved successfully to the database or not. # @@ -766,177 +619,6 @@ module ActiveRecord #:nodoc: end end - # Updates an object (or multiple objects) and saves it to the database, if validations pass. - # The resulting object is returned whether the object was saved successfully to the database or not. - # - # ==== Parameters - # - # * +id+ - This should be the id or an array of ids to be updated. - # * +attributes+ - This should be a hash of attributes to be set on the object, or an array of hashes. - # - # ==== Examples - # - # # Updating one record: - # Person.update(15, :user_name => 'Samuel', :group => 'expert') - # - # # Updating multiple records: - # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } - # Person.update(people.keys, people.values) - def update(id, attributes) - if id.is_a?(Array) - idx = -1 - id.collect { |one_id| idx += 1; update(one_id, attributes[idx]) } - else - object = find(id) - object.update_attributes(attributes) - object - end - end - - # Deletes the row with a primary key matching the +id+ argument, using a - # SQL +DELETE+ statement, and returns the number of rows deleted. Active - # Record objects are not instantiated, so the object's callbacks are not - # executed, including any <tt>:dependent</tt> association options or - # Observer methods. - # - # You can delete multiple rows at once by passing an Array of <tt>id</tt>s. - # - # Note: Although it is often much faster than the alternative, - # <tt>#destroy</tt>, skipping callbacks might bypass business logic in - # your application that ensures referential integrity or performs other - # essential jobs. - # - # ==== Examples - # - # # Delete a single row - # Todo.delete(1) - # - # # Delete multiple rows - # Todo.delete([2,3,4]) - def delete(id_or_array) - scoped.delete(id_or_array) - end - - # Destroy an object (or multiple objects) that has the given id, the object is instantiated first, - # therefore all callbacks and filters are fired off before the object is deleted. This method is - # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run. - # - # This essentially finds the object (or multiple objects) with the given id, creates a new object - # from the attributes, and then calls destroy on it. - # - # ==== Parameters - # - # * +id+ - Can be either an Integer or an Array of Integers. - # - # ==== Examples - # - # # Destroy a single object - # Todo.destroy(1) - # - # # Destroy multiple objects - # todos = [1,2,3] - # Todo.destroy(todos) - def destroy(id) - if id.is_a?(Array) - id.map { |one_id| destroy(one_id) } - else - find(id).destroy - end - end - - # Updates all records with details given if they match a set of conditions supplied, limits and order can - # also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the - # database. It does not instantiate the involved models and it does not trigger Active Record callbacks - # or validations. - # - # ==== Parameters - # - # * +updates+ - A string, array, or hash representing the SET part of an SQL statement. - # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. See conditions in the intro. - # * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage. - # - # ==== Examples - # - # # Update all customers with the given attributes - # Customer.update_all :wants_email => true - # - # # Update all books with 'Rails' in their title - # Book.update_all "author = 'David'", "title LIKE '%Rails%'" - # - # # Update all avatars migrated more than a week ago - # Avatar.update_all ['migrated_at = ?', Time.now.utc], ['migrated_at > ?', 1.week.ago] - # - # # Update all books that match our conditions, but limit it to 5 ordered by date - # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5 - def update_all(updates, conditions = nil, options = {}) - relation = unscoped - - relation = relation.where(conditions) if conditions - relation = relation.limit(options[:limit]) if options[:limit].present? - relation = relation.order(options[:order]) if options[:order].present? - - if current_scoped_methods && current_scoped_methods.limit_value.present? && current_scoped_methods.order_values.present? - # Only take order from scope if limit is also provided by scope, this - # is useful for updating a has_many association with a limit. - relation = current_scoped_methods.merge(relation) if current_scoped_methods - else - relation = current_scoped_methods.except(:limit, :order).merge(relation) if current_scoped_methods - end - - relation.update(sanitize_sql_for_assignment(updates)) - end - - # Destroys the records matching +conditions+ by instantiating each - # record and calling its +destroy+ method. Each object's callbacks are - # executed (including <tt>:dependent</tt> association options and - # +before_destroy+/+after_destroy+ Observer methods). Returns the - # collection of objects that were destroyed; each will be frozen, to - # reflect that no changes should be made (since they can't be - # persisted). - # - # Note: Instantiation, callback execution, and deletion of each - # record can be time consuming when you're removing many records at - # once. It generates at least one SQL +DELETE+ query per record (or - # possibly more, to enforce your callbacks). If you want to delete many - # rows quickly, without concern for their associations or callbacks, use - # +delete_all+ instead. - # - # ==== Parameters - # - # * +conditions+ - A string, array, or hash that specifies which records - # to destroy. If omitted, all records are destroyed. See the - # Conditions section in the introduction to ActiveRecord::Base for - # more information. - # - # ==== Examples - # - # Person.destroy_all("last_login < '2004-04-04'") - # Person.destroy_all(:status => "inactive") - def destroy_all(conditions = nil) - where(conditions).destroy_all - end - - # Deletes the records matching +conditions+ without instantiating the records first, and hence not - # calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that - # goes straight to the database, much more efficient than +destroy_all+. Be careful with relations - # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns - # the number of rows affected. - # - # ==== Parameters - # - # * +conditions+ - Conditions are specified the same way as with +find+ method. - # - # ==== Example - # - # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')") - # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else']) - # - # Both calls delete the affected posts all at once with a single DELETE statement. If you need to destroy dependent - # associations or call your <tt>before_*</tt> or +after_destroy+ callbacks, use the +destroy_all+ method instead. - def delete_all(conditions = nil) - where(conditions).delete_all - end - # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. # The use of this method should be restricted to complicated SQL queries that can't be executed # using the ActiveRecord::Calculations class methods. Look into those before using this. @@ -1224,6 +906,10 @@ module ActiveRecord #:nodoc: reset_table_name end + def quoted_table_name + @quoted_table_name ||= connection.quote_table_name(table_name) + end + def reset_table_name #:nodoc: base = base_class @@ -1241,6 +927,7 @@ module ActiveRecord #:nodoc: name = "#{table_name_prefix}#{contained}#{undecorated_table_name(base.name)}#{table_name_suffix}" end + @quoted_table_name = nil set_table_name(name) name end @@ -1487,20 +1174,6 @@ module ActiveRecord #:nodoc: store_full_sti_class ? name : name.demodulize end - # Merges conditions so that the result is a valid +condition+ - def merge_conditions(*conditions) - segments = [] - - conditions.each do |condition| - unless condition.blank? - sql = sanitize_sql(condition) - segments << sql unless sql.blank? - end - end - - "(#{segments.join(') AND (')})" unless segments.empty? - end - def unscoped @unscoped ||= Relation.new(self, arel_table) finder_needs_type_condition? ? @unscoped.where(type_condition) : @unscoped @@ -1527,7 +1200,7 @@ module ActiveRecord #:nodoc: def instantiate(record) object = find_sti_class(record[inheritance_column]).allocate - object.send(:initialize_attribute_store, record) + object.instance_variable_set(:'@attributes', record) object.instance_variable_set(:'@attributes_cache', {}) object.send(:_run_find_callbacks) @@ -1563,43 +1236,11 @@ module ActiveRecord #:nodoc: end def construct_finder_arel(options = {}, scope = nil) - relation = unscoped.apply_finder_options(options) + relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : unscoped.merge(options) relation = scope.merge(relation) if scope relation end - def construct_join(joins) - case joins - when Symbol, Hash, Array - if array_of_strings?(joins) - joins.join(' ') + " " - else - build_association_joins(joins) - end - when String - " #{joins} " - else - "" - end - end - - def build_association_joins(joins) - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, joins, nil) - relation = unscoped.table - join_dependency.join_associations.map { |association| - if (association_relation = association.relation).is_a?(Array) - [Arel::InnerJoin.new(relation, association_relation.first, *association.association_join.first).joins(relation), - Arel::InnerJoin.new(relation, association_relation.last, *association.association_join.last).joins(relation)].join() - else - Arel::InnerJoin.new(relation, association_relation, *association.association_join).joins(relation) - end - }.join(" ") - end - - def array_of_strings?(o) - o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)} - end - def type_condition sti_column = arel_table[inheritance_column] condition = sti_column.eq(sti_name) @@ -1762,11 +1403,8 @@ module ActiveRecord #:nodoc: relation = construct_finder_arel(method_scoping[:find] || {}) if current_scoped_methods && current_scoped_methods.create_with_value && method_scoping[:create] - scope_for_create = case action - when :merge + scope_for_create = if action == :merge current_scoped_methods.create_with_value.merge(method_scoping[:create]) - when :reverse_merge - method_scoping[:create].merge(current_scoped_methods.create_with_value) else method_scoping[:create] end @@ -1781,15 +1419,7 @@ module ActiveRecord #:nodoc: method_scoping = relation end - if current_scoped_methods - case action - when :merge - method_scoping = current_scoped_methods.merge(method_scoping) - when :reverse_merge - method_scoping = current_scoped_methods.except(:where).merge(method_scoping) - method_scoping = method_scoping.merge(current_scoped_methods.only(:where)) - end - end + method_scoping = current_scoped_methods.merge(method_scoping) if current_scoped_methods && action == :merge self.scoped_methods << method_scoping begin @@ -1820,7 +1450,8 @@ module ActiveRecord #:nodoc: end def scoped_methods #:nodoc: - Thread.current[:"#{self}_scoped_methods"] ||= self.default_scoping.dup + key = :"#{self}_scoped_methods" + Thread.current[key] = Thread.current[key].presence || self.default_scoping.dup end def current_scoped_methods #:nodoc: @@ -2033,7 +1664,7 @@ module ActiveRecord #:nodoc: # In both instances, valid attribute keys are determined by the column names of the associated table -- # hence you can't have attributes that aren't part of the table columns. def initialize(attributes = nil) - initialize_attribute_store(attributes_from_column_definition) + @attributes = attributes_from_column_definition @attributes_cache = {} @new_record = true ensure_proper_type @@ -2064,7 +1695,7 @@ module ActiveRecord #:nodoc: callback(:after_initialize) if respond_to_without_attributes?(:after_initialize) cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) cloned_attributes.delete(self.class.primary_key) - initialize_attribute_store(cloned_attributes) + @attributes = cloned_attributes clear_aggregation_cache @attributes_cache = {} @new_record = true @@ -2294,11 +1925,21 @@ module ActiveRecord #:nodoc: def reload(options = nil) clear_aggregation_cache clear_association_cache - _attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes')) + @attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes')) @attributes_cache = {} self end + # Returns true if the given attribute is in the attributes hash + def has_attribute?(attr_name) + @attributes.has_key?(attr_name.to_s) + end + + # Returns an array of names for the attributes available on this object sorted alphabetically. + def attribute_names + @attributes.keys.sort + end + # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). # (Alias for the protected read_attribute method). @@ -2480,7 +2121,7 @@ module ActiveRecord #:nodoc: def update(attribute_names = @attributes.keys) attributes_with_values = arel_attributes_values(false, false, attribute_names) return 0 if attributes_with_values.empty? - self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).update(attributes_with_values) + self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(attributes_with_values) end # Creates a record with values matching those of the instance attributes @@ -2632,7 +2273,7 @@ module ActiveRecord #:nodoc: end def instantiate_time_object(name, values) - if self.class.send(:time_zone_aware?, name) + if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) Time.zone.local(*values) else Time.time_with_datetime_fallback(@@default_timezone, *values) @@ -2704,10 +2345,6 @@ module ActiveRecord #:nodoc: hash.inject([]) { |list, pair| list << "#{pair.first} = #{pair.last}" }.join(", ") end - def self.quoted_table_name - self.connection.quote_table_name(self.table_name) - end - def quote_columns(quoter, hash) hash.inject({}) do |quoted, (name, value)| quoted[quoter.quote_column_name(name)] = value @@ -2719,6 +2356,22 @@ module ActiveRecord #:nodoc: comma_pair_list(quote_columns(quoter, hash)) end + def convert_number_column_value(value) + if value == false + 0 + elsif value == true + 1 + elsif value.is_a?(String) && value.blank? + nil + else + value + end + end + + def object_from_yaml(string) + return string unless string.is_a?(String) && string =~ /^---/ + YAML::load(string) rescue string + end end Base.class_eval do @@ -2733,7 +2386,6 @@ module ActiveRecord #:nodoc: include AttributeMethods::PrimaryKey include AttributeMethods::TimeZoneConversion include AttributeMethods::Dirty - include Attributes, Types include Callbacks, ActiveModel::Observing, Timestamp include Associations, AssociationPreload, NamedScope include ActiveModel::Conversion @@ -2742,7 +2394,7 @@ module ActiveRecord #:nodoc: # #save_with_autosave_associations to be wrapped inside a transaction. include AutosaveAssociation, NestedAttributes - include Aggregations, Transactions, Reflection, Batches, Calculations, Serialization + include Aggregations, Transactions, Reflection, Batches, Serialization end end diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb deleted file mode 100644 index 8a44dc7df1..0000000000 --- a/activerecord/lib/active_record/calculations.rb +++ /dev/null @@ -1,173 +0,0 @@ -module ActiveRecord - module Calculations #:nodoc: - extend ActiveSupport::Concern - - CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include, :from] - - module ClassMethods - # Count operates using three different approaches. - # - # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model. - # * Count using column: By passing a column name to count, it will return a count of all the rows for the model with supplied column present - # * Count using options will find the row count matched by the options used. - # - # The third approach, count using options, accepts an option hash as the only parameter. The options are: - # - # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base. - # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed) - # or named associations in the same form used for the <tt>:include</tt> option, which will perform an INNER JOIN on the associated table(s). - # If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns. - # Pass <tt>:readonly => false</tt> to override. - # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer - # to already defined associations. When using named associations, count returns the number of DISTINCT items for the model you're counting. - # See eager loading under Associations. - # * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). - # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join but not - # include the joined columns. - # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ... - # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name - # of a database view). - # - # Examples for counting all: - # Person.count # returns the total count of all people - # - # Examples for counting by column: - # Person.count(:age) # returns the total count of all people whose age is present in database - # - # Examples for count with options: - # Person.count(:conditions => "age > 26") - # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN. - # Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins. - # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id) - # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*') - # - # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. Use Person.count instead. - def count(*args) - case args.size - when 0 - construct_calculation_arel.count - when 1 - if args[0].is_a?(Hash) - options = args[0] - distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false - construct_calculation_arel(options).count(options[:select], :distinct => distinct) - else - construct_calculation_arel.count(args[0]) - end - when 2 - column_name, options = args - distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false - construct_calculation_arel(options).count(column_name, :distinct => distinct) - else - raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}" - end - rescue ThrowResult - 0 - end - - # Calculates the average value on a given column. The value is returned as - # a float, or +nil+ if there's no row. See +calculate+ for examples with - # options. - # - # Person.average('age') # => 35.8 - def average(column_name, options = {}) - calculate(:average, column_name, options) - end - - # Calculates the minimum value on a given column. The value is returned - # with the same data type of the column, or +nil+ if there's no row. See - # +calculate+ for examples with options. - # - # Person.minimum('age') # => 7 - def minimum(column_name, options = {}) - calculate(:minimum, column_name, options) - end - - # Calculates the maximum value on a given column. The value is returned - # with the same data type of the column, or +nil+ if there's no row. See - # +calculate+ for examples with options. - # - # Person.maximum('age') # => 93 - def maximum(column_name, options = {}) - calculate(:maximum, column_name, options) - end - - # Calculates the sum of values on a given column. The value is returned - # with the same data type of the column, 0 if there's no row. See - # +calculate+ for examples with options. - # - # Person.sum('age') # => 4562 - def sum(column_name, options = {}) - calculate(:sum, column_name, options) - end - - # This calculates aggregate values in the given column. Methods for count, sum, average, minimum, and maximum have been added as shortcuts. - # Options such as <tt>:conditions</tt>, <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query. - # - # There are two basic forms of output: - # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float for AVG, and the given column's type for everything else. - # * Grouped values: This returns an ordered hash of the values and groups them by the <tt>:group</tt> option. It takes either a column name, or the name - # of a belongs_to association. - # - # values = Person.maximum(:age, :group => 'last_name') - # puts values["Drake"] - # => 43 - # - # drake = Family.find_by_last_name('Drake') - # values = Person.maximum(:age, :group => :family) # Person belongs_to :family - # puts values[drake] - # => 43 - # - # values.each do |family, max_age| - # ... - # end - # - # Options: - # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base. - # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, the purpose of this is to access fields on joined tables in your conditions, order, or group clauses. - # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). - # The records will be returned read-only since they will have attributes that do not correspond to the table's columns. - # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). - # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. - # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not - # include the joined columns. - # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ... - # - # Examples: - # Person.calculate(:count, :all) # The same as Person.count - # Person.average(:age) # SELECT AVG(age) FROM people... - # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for everyone with a last name other than 'Drake' - # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors - # Person.sum("2 * age") - def calculate(operation, column_name, options = {}) - construct_calculation_arel(options).calculate(operation, column_name, options.slice(:distinct)) - rescue ThrowResult - 0 - end - - private - def validate_calculation_options(options = {}) - options.assert_valid_keys(CALCULATIONS_OPTIONS) - end - - def construct_calculation_arel(options = {}) - validate_calculation_options(options) - options = options.except(:distinct) - - merge_with_includes = current_scoped_methods ? current_scoped_methods.includes_values : [] - includes = (merge_with_includes + Array.wrap(options[:include])).uniq - - if includes.any? - merge_with_joins = current_scoped_methods ? current_scoped_methods.joins_values : [] - joins = (merge_with_joins + Array.wrap(options[:joins])).uniq - join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, includes, construct_join(joins)) - construct_finder_arel_with_included_associations(options, join_dependency) - else - scoped.apply_finder_options(options) - end - end - - end - end -end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 9fcdabdb44..9044ca418b 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -81,10 +81,10 @@ module ActiveRecord relation = self.class.unscoped affected_rows = relation.where( - relation[self.class.primary_key].eq(quoted_id).and( - relation[self.class.locking_column].eq(quote_value(previous_value)) + relation.table[self.class.primary_key].eq(quoted_id).and( + relation.table[self.class.locking_column].eq(quote_value(previous_value)) ) - ).update(arel_attributes_values(false, false, attribute_names)) + ).arel.update(arel_attributes_values(false, false, attribute_names)) unless affected_rows == 1 diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 92030e5bfd..ff6c041ef4 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -148,18 +148,6 @@ module ActiveRecord relation end - def find(*args) - options = args.extract_options! - relation = options.present? ? apply_finder_options(options) : self - - case args.first - when :first, :last, :all - relation.send(args.first) - else - options.present? ? relation.find(*args) : super - end - end - def first(*args) if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash)) to_a.first(*args) @@ -176,13 +164,8 @@ module ActiveRecord end end - def count(*args) - options = args.extract_options! - options.present? ? apply_finder_options(options).count(*args) : super - end - def ==(other) - to_a == other.to_a + other.respond_to?(:to_ary) ? to_a == other.to_a : false end private diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index e37e692a97..1a96cdad17 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -5,9 +5,10 @@ module ActiveRecord MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from] - include FinderMethods, CalculationMethods, SpawnMethods, QueryMethods + include FinderMethods, Calculations, SpawnMethods, QueryMethods delegate :length, :collect, :map, :each, :all?, :include?, :to => :to_a + delegate :insert, :to => :arel attr_reader :table, :klass @@ -31,7 +32,7 @@ module ActiveRecord end def respond_to?(method, include_private = false) - return true if arel.respond_to?(method, include_private) || Array.method_defined?(method) + return true if arel.respond_to?(method, include_private) || Array.method_defined?(method) || @klass.respond_to?(method, include_private) if match = DynamicFinderMatch.match(method) return true if @klass.send(:all_attributes_exists?, match.attribute_names) @@ -45,12 +46,10 @@ module ActiveRecord def to_a return @records if loaded? - eager_loading = @eager_load_values.any? || (@includes_values.any? && references_eager_loaded_tables?) - - @records = eager_loading ? find_with_associations : @klass.find_by_sql(arel.to_sql) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql) preload = @preload_values - preload += @includes_values unless eager_loading + preload += @includes_values unless eager_loading? preload.each {|associations| @klass.send(:preload_associations, @records, associations) } # @readonly_value is true only if set explicity. @implicit_readonly is true if there are JOINS and no explicit SELECT. @@ -61,8 +60,6 @@ module ActiveRecord @records end - alias all to_a - def size loaded? ? @records.length : count end @@ -83,19 +80,177 @@ module ActiveRecord if block_given? to_a.many? { |*block_args| yield(*block_args) } else - arel.send(:taken).present? ? to_a.many? : size > 1 + @limit_value.present? ? to_a.many? : size > 1 end end - def destroy_all - to_a.each {|object| object.destroy} - reset + # Updates all records with details given if they match a set of conditions supplied, limits and order can + # also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the + # database. It does not instantiate the involved models and it does not trigger Active Record callbacks + # or validations. + # + # ==== Parameters + # + # * +updates+ - A string, array, or hash representing the SET part of an SQL statement. + # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. See conditions in the intro. + # * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage. + # + # ==== Examples + # + # # Update all customers with the given attributes + # Customer.update_all :wants_email => true + # + # # Update all books with 'Rails' in their title + # Book.update_all "author = 'David'", "title LIKE '%Rails%'" + # + # # Update all avatars migrated more than a week ago + # Avatar.update_all ['migrated_at = ?', Time.now.utc], ['migrated_at > ?', 1.week.ago] + # + # # Update all books that match our conditions, but limit it to 5 ordered by date + # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5 + def update_all(updates, conditions = nil, options = {}) + if conditions || options.present? + where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates) + else + # Apply limit and order only if they're both present + if @limit_value.present? == @order_values.present? + arel.update(@klass.send(:sanitize_sql_for_assignment, updates)) + else + except(:limit, :order).update_all(updates) + end + end end - def delete_all - arel.delete.tap { reset } + # Updates an object (or multiple objects) and saves it to the database, if validations pass. + # The resulting object is returned whether the object was saved successfully to the database or not. + # + # ==== Parameters + # + # * +id+ - This should be the id or an array of ids to be updated. + # * +attributes+ - This should be a hash of attributes to be set on the object, or an array of hashes. + # + # ==== Examples + # + # # Updating one record: + # Person.update(15, :user_name => 'Samuel', :group => 'expert') + # + # # Updating multiple records: + # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } + # Person.update(people.keys, people.values) + def update(id, attributes) + if id.is_a?(Array) + idx = -1 + id.collect { |one_id| idx += 1; update(one_id, attributes[idx]) } + else + object = find(id) + object.update_attributes(attributes) + object + end + end + + # Destroys the records matching +conditions+ by instantiating each + # record and calling its +destroy+ method. Each object's callbacks are + # executed (including <tt>:dependent</tt> association options and + # +before_destroy+/+after_destroy+ Observer methods). Returns the + # collection of objects that were destroyed; each will be frozen, to + # reflect that no changes should be made (since they can't be + # persisted). + # + # Note: Instantiation, callback execution, and deletion of each + # record can be time consuming when you're removing many records at + # once. It generates at least one SQL +DELETE+ query per record (or + # possibly more, to enforce your callbacks). If you want to delete many + # rows quickly, without concern for their associations or callbacks, use + # +delete_all+ instead. + # + # ==== Parameters + # + # * +conditions+ - A string, array, or hash that specifies which records + # to destroy. If omitted, all records are destroyed. See the + # Conditions section in the introduction to ActiveRecord::Base for + # more information. + # + # ==== Examples + # + # Person.destroy_all("last_login < '2004-04-04'") + # Person.destroy_all(:status => "inactive") + def destroy_all(conditions = nil) + if conditions + where(conditions).destroy_all + else + to_a.each {|object| object.destroy} + reset + end end + # Destroy an object (or multiple objects) that has the given id, the object is instantiated first, + # therefore all callbacks and filters are fired off before the object is deleted. This method is + # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run. + # + # This essentially finds the object (or multiple objects) with the given id, creates a new object + # from the attributes, and then calls destroy on it. + # + # ==== Parameters + # + # * +id+ - Can be either an Integer or an Array of Integers. + # + # ==== Examples + # + # # Destroy a single object + # Todo.destroy(1) + # + # # Destroy multiple objects + # todos = [1,2,3] + # Todo.destroy(todos) + def destroy(id) + if id.is_a?(Array) + id.map { |one_id| destroy(one_id) } + else + find(id).destroy + end + end + + # Deletes the records matching +conditions+ without instantiating the records first, and hence not + # calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that + # goes straight to the database, much more efficient than +destroy_all+. Be careful with relations + # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns + # the number of rows affected. + # + # ==== Parameters + # + # * +conditions+ - Conditions are specified the same way as with +find+ method. + # + # ==== Example + # + # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')") + # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else']) + # + # Both calls delete the affected posts all at once with a single DELETE statement. If you need to destroy dependent + # associations or call your <tt>before_*</tt> or +after_destroy+ callbacks, use the +destroy_all+ method instead. + def delete_all(conditions = nil) + conditions ? where(conditions).delete_all : arel.delete.tap { reset } + end + + # Deletes the row with a primary key matching the +id+ argument, using a + # SQL +DELETE+ statement, and returns the number of rows deleted. Active + # Record objects are not instantiated, so the object's callbacks are not + # executed, including any <tt>:dependent</tt> association options or + # Observer methods. + # + # You can delete multiple rows at once by passing an Array of <tt>id</tt>s. + # + # Note: Although it is often much faster than the alternative, + # <tt>#destroy</tt>, skipping callbacks might bypass business logic in + # your application that ensures referential integrity or performs other + # essential jobs. + # + # ==== Examples + # + # # Delete a single row + # Todo.delete(1) + # + # # Delete multiple rows + # Todo.delete([2,3,4]) def delete(id_or_array) where(@klass.primary_key => id_or_array).delete_all end @@ -112,6 +267,7 @@ module ActiveRecord def reset @first = @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil + @should_eager_load = @join_dependency = nil @records = [] self end @@ -126,20 +282,29 @@ module ActiveRecord def scope_for_create @scope_for_create ||= begin - @create_with_value || wheres.inject({}) do |hash, where| - hash[where.operand1.name] = where.operand2.value if where.is_a?(Arel::Predicates::Equality) + @create_with_value || @where_values.inject({}) do |hash, where| + if where.is_a?(Arel::Predicates::Equality) + hash[where.operand1.name] = where.operand2.respond_to?(:value) ? where.operand2.value : where.operand2 + end + hash end end end + def eager_loading? + @should_eager_load ||= (@eager_load_values.any? || (@includes_values.any? && references_eager_loaded_tables?)) + end + protected def method_missing(method, *args, &block) - if arel.respond_to?(method) - arel.send(method, *args, &block) - elsif Array.method_defined?(method) + if Array.method_defined?(method) to_a.send(method, *args, &block) + elsif @klass.respond_to?(method) + @klass.send(:with_scope, self) { @klass.send(method, *args, &block) } + elsif arel.respond_to?(method) + arel.send(method, *args, &block) elsif match = DynamicFinderMatch.match(method) attributes = match.attribute_names super unless @klass.send(:all_attributes_exists?, attributes) @@ -160,10 +325,6 @@ module ActiveRecord @klass.send(:with_scope, :create => scope_for_create, :find => {}) { yield } end - def where_clause(join_string = " AND ") - arel.send(:where_clauses).join(join_string) - end - def references_eager_loaded_tables? joined_tables = (tables_in_string(arel.joins(arel)) + [table.name, table.table_alias]).compact.uniq (tables_in_string(to_sql) - joined_tables).any? diff --git a/activerecord/lib/active_record/relation/calculation_methods.rb b/activerecord/lib/active_record/relation/calculation_methods.rb deleted file mode 100644 index 91de89e607..0000000000 --- a/activerecord/lib/active_record/relation/calculation_methods.rb +++ /dev/null @@ -1,172 +0,0 @@ -module ActiveRecord - module CalculationMethods - - def count(*args) - calculate(:count, *construct_count_options_from_args(*args)) - end - - def average(column_name) - calculate(:average, column_name) - end - - def minimum(column_name) - calculate(:minimum, column_name) - end - - def maximum(column_name) - calculate(:maximum, column_name) - end - - def sum(column_name) - calculate(:sum, column_name) - end - - def calculate(operation, column_name, options = {}) - operation = operation.to_s.downcase - - if operation == "count" - joins = arel.joins(arel) - if joins.present? && joins =~ /LEFT OUTER/i - distinct = true - column_name = @klass.primary_key if column_name == :all - end - - distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i - distinct ||= options[:distinct] - else - distinct = nil - end - - distinct = options[:distinct] || distinct - column_name = :all if column_name.blank? && operation == "count" - - if @group_values.any? - return execute_grouped_calculation(operation, column_name) - else - return execute_simple_calculation(operation, column_name, distinct) - end - rescue ThrowResult - 0 - end - - private - - def execute_simple_calculation(operation, column_name, distinct) #:nodoc: - column = if @klass.column_names.include?(column_name.to_s) - Arel::Attribute.new(@klass.unscoped, column_name) - else - Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s) - end - - relation = select(operation == 'count' ? column.count(distinct) : column.send(operation)) - type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation) - end - - def execute_grouped_calculation(operation, column_name) #:nodoc: - group_attr = @group_values.first - association = @klass.reflect_on_association(group_attr.to_sym) - associated = association && association.macro == :belongs_to # only count belongs_to associations - group_field = associated ? association.primary_key_name : group_attr - group_alias = column_alias_for(group_field) - group_column = column_for(group_field) - - group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field - - aggregate_alias = column_alias_for(operation, column_name) - - select_statement = if operation == 'count' && column_name == :all - "COUNT(*) AS count_all" - else - Arel::Attribute.new(@klass.unscoped, column_name).send(operation).as(aggregate_alias).to_sql - end - - select_statement << ", #{group_field} AS #{group_alias}" - - relation = select(select_statement).group(group) - - calculated_data = @klass.connection.select_all(relation.to_sql) - - if association - key_ids = calculated_data.collect { |row| row[group_alias] } - key_records = association.klass.base_class.find(key_ids) - key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) } - end - - calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row| - key = type_cast_calculated_value(row[group_alias], group_column) - key = key_records[key] if associated - value = row[aggregate_alias] - all[key] = type_cast_calculated_value(value, column_for(column_name), operation) - all - end - end - - def construct_count_options_from_args(*args) - options = {} - column_name = :all - - # Handles count(), count(:column), count(:distinct => true), count(:column, :distinct => true) - case args.size - when 0 - select = get_projection_name_from_chained_relations - column_name = select if select !~ /(,|\*)/ - when 1 - if args[0].is_a?(Hash) - select = get_projection_name_from_chained_relations - column_name = select if select !~ /(,|\*)/ - options = args[0] - else - column_name = args[0] - end - when 2 - column_name, options = args - else - raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}" - end - - [column_name || :all, options] - end - - # Converts the given keys to the value that the database adapter returns as - # a usable column name: - # - # column_alias_for("users.id") # => "users_id" - # column_alias_for("sum(id)") # => "sum_id" - # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id" - # column_alias_for("count(*)") # => "count_all" - # column_alias_for("count", "id") # => "count_id" - def column_alias_for(*keys) - table_name = keys.join(' ') - table_name.downcase! - table_name.gsub!(/\*/, 'all') - table_name.gsub!(/\W+/, ' ') - table_name.strip! - table_name.gsub!(/ +/, '_') - - @klass.connection.table_alias_for(table_name) - end - - def column_for(field) - field_name = field.to_s.split('.').last - @klass.columns.detect { |c| c.name.to_s == field_name } - end - - def type_cast_calculated_value(value, column, operation = nil) - case operation - when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || '0', column) - when 'average' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d - else type_cast_using_column(value, column) - end - end - - def type_cast_using_column(value, column) - column ? column.type_cast(value) : value - end - - def get_projection_name_from_chained_relations - @select_values.join(", ") if @select_values.present? - end - - end -end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb new file mode 100644 index 0000000000..e77424a64b --- /dev/null +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -0,0 +1,259 @@ +module ActiveRecord + module Calculations + # Count operates using three different approaches. + # + # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model. + # * Count using column: By passing a column name to count, it will return a count of all the rows for the model with supplied column present + # * Count using options will find the row count matched by the options used. + # + # The third approach, count using options, accepts an option hash as the only parameter. The options are: + # + # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base. + # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed) + # or named associations in the same form used for the <tt>:include</tt> option, which will perform an INNER JOIN on the associated table(s). + # If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns. + # Pass <tt>:readonly => false</tt> to override. + # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer + # to already defined associations. When using named associations, count returns the number of DISTINCT items for the model you're counting. + # See eager loading under Associations. + # * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). + # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. + # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join but not + # include the joined columns. + # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ... + # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name + # of a database view). + # + # Examples for counting all: + # Person.count # returns the total count of all people + # + # Examples for counting by column: + # Person.count(:age) # returns the total count of all people whose age is present in database + # + # Examples for count with options: + # Person.count(:conditions => "age > 26") + # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN. + # Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins. + # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id) + # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*') + # + # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. Use Person.count instead. + def count(column_name = nil, options = {}) + column_name, options = nil, column_name if column_name.is_a?(Hash) + calculate(:count, column_name, options) + end + + # Calculates the average value on a given column. The value is returned as + # a float, or +nil+ if there's no row. See +calculate+ for examples with + # options. + # + # Person.average('age') # => 35.8 + def average(column_name, options = {}) + calculate(:average, column_name, options) + end + + # Calculates the minimum value on a given column. The value is returned + # with the same data type of the column, or +nil+ if there's no row. See + # +calculate+ for examples with options. + # + # Person.minimum('age') # => 7 + def minimum(column_name, options = {}) + calculate(:minimum, column_name, options) + end + + # Calculates the maximum value on a given column. The value is returned + # with the same data type of the column, or +nil+ if there's no row. See + # +calculate+ for examples with options. + # + # Person.maximum('age') # => 93 + def maximum(column_name, options = {}) + calculate(:maximum, column_name, options) + end + + # Calculates the sum of values on a given column. The value is returned + # with the same data type of the column, 0 if there's no row. See + # +calculate+ for examples with options. + # + # Person.sum('age') # => 4562 + def sum(column_name, options = {}) + calculate(:sum, column_name, options) + end + + # This calculates aggregate values in the given column. Methods for count, sum, average, minimum, and maximum have been added as shortcuts. + # Options such as <tt>:conditions</tt>, <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query. + # + # There are two basic forms of output: + # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float for AVG, and the given column's type for everything else. + # * Grouped values: This returns an ordered hash of the values and groups them by the <tt>:group</tt> option. It takes either a column name, or the name + # of a belongs_to association. + # + # values = Person.maximum(:age, :group => 'last_name') + # puts values["Drake"] + # => 43 + # + # drake = Family.find_by_last_name('Drake') + # values = Person.maximum(:age, :group => :family) # Person belongs_to :family + # puts values[drake] + # => 43 + # + # values.each do |family, max_age| + # ... + # end + # + # Options: + # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base. + # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, the purpose of this is to access fields on joined tables in your conditions, order, or group clauses. + # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). + # The records will be returned read-only since they will have attributes that do not correspond to the table's columns. + # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations). + # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause. + # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not + # include the joined columns. + # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ... + # + # Examples: + # Person.calculate(:count, :all) # The same as Person.count + # Person.average(:age) # SELECT AVG(age) FROM people... + # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for everyone with a last name other than 'Drake' + # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors + # Person.sum("2 * age") + def calculate(operation, column_name, options = {}) + if options.except(:distinct).present? + apply_finder_options(options.except(:distinct)).calculate(operation, column_name, :distinct => options[:distinct]) + else + if eager_loading? || includes_values.present? + construct_relation_for_association_calculations.calculate(operation, column_name, options) + else + perform_calculation(operation, column_name, options) + end + end + rescue ThrowResult + 0 + end + + private + + def perform_calculation(operation, column_name, options = {}) + operation = operation.to_s.downcase + + if operation == "count" + column_name ||= (select_for_count || :all) + + joins = arel.joins(arel) + if joins.present? && joins =~ /LEFT OUTER/i + distinct = true + column_name = @klass.primary_key if column_name == :all + end + + distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i + distinct ||= options[:distinct] + else + distinct = nil + end + + distinct = options[:distinct] || distinct + column_name = :all if column_name.blank? && operation == "count" + + if @group_values.any? + return execute_grouped_calculation(operation, column_name) + else + return execute_simple_calculation(operation, column_name, distinct) + end + end + + def execute_simple_calculation(operation, column_name, distinct) #:nodoc: + column = if @klass.column_names.include?(column_name.to_s) + Arel::Attribute.new(@klass.unscoped, column_name) + else + Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s) + end + + # Postgresql doesn't like ORDER BY when there are no GROUP BY + relation = except(:order).select(operation == 'count' ? column.count(distinct) : column.send(operation)) + type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation) + end + + def execute_grouped_calculation(operation, column_name) #:nodoc: + group_attr = @group_values.first + association = @klass.reflect_on_association(group_attr.to_sym) + associated = association && association.macro == :belongs_to # only count belongs_to associations + group_field = associated ? association.primary_key_name : group_attr + group_alias = column_alias_for(group_field) + group_column = column_for(group_field) + + group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field + + aggregate_alias = column_alias_for(operation, column_name) + + select_statement = if operation == 'count' && column_name == :all + "COUNT(*) AS count_all" + else + Arel::Attribute.new(@klass.unscoped, column_name).send(operation).as(aggregate_alias).to_sql + end + + select_statement << ", #{group_field} AS #{group_alias}" + + relation = select(select_statement).group(group) + + calculated_data = @klass.connection.select_all(relation.to_sql) + + if association + key_ids = calculated_data.collect { |row| row[group_alias] } + key_records = association.klass.base_class.find(key_ids) + key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) } + end + + calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row| + key = type_cast_calculated_value(row[group_alias], group_column) + key = key_records[key] if associated + value = row[aggregate_alias] + all[key] = type_cast_calculated_value(value, column_for(column_name), operation) + all + end + end + + # Converts the given keys to the value that the database adapter returns as + # a usable column name: + # + # column_alias_for("users.id") # => "users_id" + # column_alias_for("sum(id)") # => "sum_id" + # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id" + # column_alias_for("count(*)") # => "count_all" + # column_alias_for("count", "id") # => "count_id" + def column_alias_for(*keys) + table_name = keys.join(' ') + table_name.downcase! + table_name.gsub!(/\*/, 'all') + table_name.gsub!(/\W+/, ' ') + table_name.strip! + table_name.gsub!(/ +/, '_') + + @klass.connection.table_alias_for(table_name) + end + + def column_for(field) + field_name = field.to_s.split('.').last + @klass.columns.detect { |c| c.name.to_s == field_name } + end + + def type_cast_calculated_value(value, column, operation = nil) + case operation + when 'count' then value.to_i + when 'sum' then type_cast_using_column(value || '0', column) + when 'average' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d + else type_cast_using_column(value, column) + end + end + + def type_cast_using_column(value, column) + column ? column.type_cast(value) : value + end + + def select_for_count + if @select_values.present? + select = @select_values.join(", ") + select if select !~ /(,|\*)/ + end + end + end +end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 980c5796f3..d6d3d66642 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,44 +1,157 @@ module ActiveRecord module FinderMethods - - def find(*ids, &block) + # Find operates with four different retrieval approaches: + # + # * Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). + # If no record can be found for all of the listed ids, then RecordNotFound will be raised. + # * Find first - This will return the first record matched by the options used. These options can either be specific + # conditions or merely an order. If no record can be matched, +nil+ is returned. Use + # <tt>Model.find(:first, *args)</tt> or its shortcut <tt>Model.first(*args)</tt>. + # * Find last - This will return the last record matched by the options used. These options can either be specific + # conditions or merely an order. If no record can be matched, +nil+ is returned. Use + # <tt>Model.find(:last, *args)</tt> or its shortcut <tt>Model.last(*args)</tt>. + # * Find all - This will return all the records matched by the options used. + # If no records are found, an empty array is returned. Use + # <tt>Model.find(:all, *args)</tt> or its shortcut <tt>Model.all(*args)</tt>. + # + # All approaches accept an options hash as their last parameter. + # + # ==== Parameters + # + # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>[ "user_name = ?", username ]</tt>, or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro. + # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name". + # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. + # * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause. + # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned. + # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip rows 0 through 4. + # * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed), + # named associations in the same form used for the <tt>:include</tt> option, which will perform an <tt>INNER JOIN</tt> on the associated table(s), + # or an array containing a mixture of both strings and named associations. + # If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns. + # Pass <tt>:readonly => false</tt> to override. + # * <tt>:include</tt> - Names associations that should be loaded alongside. The symbols named refer + # to already defined associations. See eager loading under Associations. + # * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you, for example, want to do a join but not + # include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name"). + # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name + # of a database view). + # * <tt>:readonly</tt> - Mark the returned records read-only so they cannot be saved or updated. + # * <tt>:lock</tt> - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE". + # <tt>:lock => true</tt> gives connection's default exclusive lock, usually "FOR UPDATE". + # + # ==== Examples + # + # # find by id + # Person.find(1) # returns the object for ID = 1 + # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6) + # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17) + # Person.find([1]) # returns an array for the object with ID = 1 + # Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC") + # + # Note that returned records may not be in the same order as the ids you + # provide since database rows are unordered. Give an explicit <tt>:order</tt> + # to ensure the results are sorted. + # + # ==== Examples + # + # # find first + # Person.find(:first) # returns the first object fetched by SELECT * FROM people + # Person.find(:first, :conditions => [ "user_name = ?", user_name]) + # Person.find(:first, :conditions => [ "user_name = :u", { :u => user_name }]) + # Person.find(:first, :order => "created_on DESC", :offset => 5) + # + # # find last + # Person.find(:last) # returns the last object fetched by SELECT * FROM people + # Person.find(:last, :conditions => [ "user_name = ?", user_name]) + # Person.find(:last, :order => "created_on DESC", :offset => 5) + # + # # find all + # Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people + # Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50) + # Person.find(:all, :conditions => { :friends => ["Bob", "Steve", "Fred"] } + # Person.find(:all, :offset => 10, :limit => 10) + # Person.find(:all, :include => [ :account, :friends ]) + # Person.find(:all, :group => "category") + # + # Example for find with a lock: Imagine two concurrent transactions: + # each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting + # in two saves of <tt>person.visits = 3</tt>. By locking the row, the second + # transaction has to wait until the first is finished; we get the + # expected <tt>person.visits == 4</tt>. + # + # Person.transaction do + # person = Person.find(1, :lock => true) + # person.visits += 1 + # person.save! + # end + def find(*args, &block) return to_a.find(&block) if block_given? - expects_array = ids.first.kind_of?(Array) - return ids.first if expects_array && ids.first.empty? - - ids = ids.flatten.compact.uniq + options = args.extract_options! - case ids.size - when 0 - raise RecordNotFound, "Couldn't find #{@klass.name} without an ID" - when 1 - result = find_one(ids.first) - expects_array ? [ result ] : result + if options.present? + apply_finder_options(options).find(*args) else - find_some(ids) + case args.first + when :first, :last, :all + send(args.first) + else + find_with_ids(*args) + end end end - def exists?(id = nil) - relation = select(primary_key).limit(1) - relation = relation.where(primary_key.eq(id)) if id - relation.first ? true : false + # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the + # same arguments to this method as you can to <tt>find(:first)</tt>. + def first(*args) + args.any? ? apply_finder_options(args.first).first : find_first end - def first - if loaded? - @records.first - else - @first ||= limit(1).to_a[0] - end + # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the + # same arguments to this method as you can to <tt>find(:last)</tt>. + def last(*args) + args.any? ? apply_finder_options(args.first).last : find_last end - def last - if loaded? - @records.last + # A convenience wrapper for <tt>find(:all, *args)</tt>. You can pass in all the + # same arguments to this method as you can to <tt>find(:all)</tt>. + def all(*args) + args.any? ? apply_finder_options(args.first).to_a : to_a + end + + # Returns true if a record exists in the table that matches the +id+ or + # conditions given, or false otherwise. The argument can take five forms: + # + # * Integer - Finds the record with this primary key. + # * String - Finds the record with a primary key corresponding to this + # string (such as <tt>'5'</tt>). + # * Array - Finds the record that matches these +find+-style conditions + # (such as <tt>['color = ?', 'red']</tt>). + # * Hash - Finds the record that matches these +find+-style conditions + # (such as <tt>{:color => 'red'}</tt>). + # * No args - Returns false if the table is empty, true otherwise. + # + # For more information about specifying conditions as a Hash or Array, + # see the Conditions section in the introduction to ActiveRecord::Base. + # + # Note: You can't pass in a condition as a string (like <tt>name = + # 'Jamie'</tt>), since it would be sanitized and then queried against + # the primary key column, like <tt>id = 'name = \'Jamie\''</tt>. + # + # ==== Examples + # Person.exists?(5) + # Person.exists?('5') + # Person.exists?(:name => "David") + # Person.exists?(['name LIKE ?', "%#{query}%"]) + # Person.exists? + def exists?(id = nil) + case id + when Array, Hash + where(id).exists? else - @last ||= reverse_order.limit(1).to_a[0] + relation = select(primary_key).limit(1) + relation = relation.where(primary_key.eq(id)) if id + relation.first ? true : false end end @@ -53,9 +166,20 @@ module ActiveRecord [] end + def construct_relation_for_association_calculations + including = (@eager_load_values + @includes_values).uniq + join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.joins(arel)) + + relation = except(:includes, :eager_load, :preload) + apply_join_dependency(relation, join_dependency) + end + def construct_relation_for_association_find(join_dependency) relation = except(:includes, :eager_load, :preload, :select).select(@klass.send(:column_aliases, join_dependency)) + apply_join_dependency(relation, join_dependency) + end + def apply_join_dependency(relation, join_dependency) for association in join_dependency.join_associations relation = association.join_relation(relation) end @@ -113,11 +237,30 @@ module ActiveRecord record end + def find_with_ids(*ids, &block) + return to_a.find(&block) if block_given? + + expects_array = ids.first.kind_of?(Array) + return ids.first if expects_array && ids.first.empty? + + ids = ids.flatten.compact.uniq + + case ids.size + when 0 + raise RecordNotFound, "Couldn't find #{@klass.name} without an ID" + when 1 + result = find_one(ids.first) + expects_array ? [ result ] : result + else + find_some(ids) + end + end + def find_one(id) record = where(primary_key.eq(id)).first unless record - conditions = where_clause(', ') + conditions = arel.send(:where_clauses).join(', ') conditions = " [WHERE #{conditions}]" if conditions.present? raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}" end @@ -129,21 +272,21 @@ module ActiveRecord result = where(primary_key.in(ids)).all expected_size = - if arel.taken && ids.size > arel.taken - arel.taken + if @limit_value && ids.size > @limit_value + @limit_value else ids.size end # 11 ids with limit 3, offset 9 should give 2 results. - if arel.skipped && (ids.size - arel.skipped < expected_size) - expected_size = ids.size - arel.skipped + if @offset_value && (ids.size - @offset_value < expected_size) + expected_size = ids.size - @offset_value end if result.size == expected_size result else - conditions = where_clause(', ') + conditions = arel.send(:where_clauses).join(', ') conditions = " [WHERE #{conditions}]" if conditions.present? error = "Couldn't find all #{@klass.name.pluralize} with IDs " @@ -152,5 +295,21 @@ module ActiveRecord end end + def find_first + if loaded? + @records.first + else + @first ||= limit(1).to_a[0] + end + end + + def find_last + if loaded? + @records.last + else + @last ||= reverse_order.limit(1).to_a[0] + end + end + end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index a3ac58bc81..8954f2d12b 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -8,11 +8,10 @@ module ActiveRecord class_eval <<-CEVAL def #{query_method}(*args) - spawn.tap do |new_relation| - new_relation.#{query_method}_values ||= [] - value = Array.wrap(args.flatten).reject {|x| x.blank? } - new_relation.#{query_method}_values += value if value.present? - end + new_relation = spawn + value = Array.wrap(args.flatten).reject {|x| x.blank? } + new_relation.#{query_method}_values += value if value.present? + new_relation end CEVAL end @@ -20,11 +19,10 @@ module ActiveRecord [:where, :having].each do |query_method| class_eval <<-CEVAL def #{query_method}(*args) - spawn.tap do |new_relation| - new_relation.#{query_method}_values ||= [] - value = build_where(*args) - new_relation.#{query_method}_values += [*value] if value.present? - end + new_relation = spawn + value = build_where(*args) + new_relation.#{query_method}_values += [*value] if value.present? + new_relation end CEVAL end @@ -34,9 +32,9 @@ module ActiveRecord class_eval <<-CEVAL def #{query_method}(value = true) - spawn.tap do |new_relation| - new_relation.#{query_method}_value = value - end + new_relation = spawn + new_relation.#{query_method}_value = value + new_relation end CEVAL end @@ -77,7 +75,7 @@ module ActiveRecord # Build association joins first joins.each do |join| - association_joins << join if [Hash, Array, Symbol].include?(join.class) && !@klass.send(:array_of_strings?, join) + association_joins << join if [Hash, Array, Symbol].include?(join.class) && !array_of_strings?(join) end if association_joins.any? @@ -110,7 +108,7 @@ module ActiveRecord when Relation::JoinOperation arel = arel.join(join.relation, join.join_class).on(*join.on) when Hash, Array, Symbol - if @klass.send(:array_of_strings?, join) + if array_of_strings?(join) join_string = join.join(' ') arel = arel.join(join_string) end @@ -119,8 +117,16 @@ module ActiveRecord end end - @where_values.uniq.each do |w| - arel = w.is_a?(String) ? arel.where(w) : arel.where(*w) + @where_values.uniq.each do |where| + next if where.blank? + + case where + when Arel::SqlLiteral + arel = arel.where(where) + else + sql = where.is_a?(String) ? where : where.to_sql + arel = arel.where(Arel::SqlLiteral.new("(#{sql})")) + end end @having_values.uniq.each do |h| @@ -135,21 +141,23 @@ module ActiveRecord end @order_values.uniq.each do |o| - arel = arel.order(o) if o.present? + arel = arel.order(Arel::SqlLiteral.new(o.to_s)) if o.present? end selects = @select_values.uniq + quoted_table_name = @klass.quoted_table_name + if selects.present? selects.each do |s| @implicit_readonly = false arel = arel.project(s) if s.present? end - elsif joins.present? - arel = arel.project(@klass.quoted_table_name + '.*') + else + arel = arel.project(quoted_table_name + '.*') end - arel = arel.from(@from_value) if @from_value.present? + arel = @from_value.present? ? arel.from(@from_value) : arel.from(quoted_table_name) case @lock_value when TrueClass @@ -167,8 +175,7 @@ module ActiveRecord builder = PredicateBuilder.new(table.engine) conditions = if [String, Array].include?(args.first.class) - merged = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first) - Arel::SqlLiteral.new(merged) if merged + @klass.send(:sanitize_sql, args.size > 1 ? args : args.first) elsif args.first.is_a?(Hash) attributes = @klass.send(:expand_hash_conditions_for_aggregates, args.first) builder.build_from_hash(attributes, table) @@ -193,5 +200,9 @@ module ActiveRecord }.join(',') end + def array_of_strings?(o) + o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)} + end + end end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index d5b13c6100..cccf413e67 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -1,17 +1,7 @@ module ActiveRecord module SpawnMethods - def spawn(arel_table = self.table) - relation = self.class.new(@klass, arel_table) - - (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).each do |query_method| - relation.send(:"#{query_method}_values=", send(:"#{query_method}_values")) - end - - Relation::SINGLE_VALUE_METHODS.each do |query_method| - relation.send(:"#{query_method}_value=", send(:"#{query_method}_value")) - end - - relation + def spawn + clone.reset end def merge(r) @@ -98,19 +88,12 @@ module ActiveRecord options.assert_valid_keys(VALID_FIND_OPTIONS) - relation = relation.joins(options[:joins]). - where(options[:conditions]). - select(options[:select]). - group(options[:group]). - having(options[:having]). - order(options[:order]). - limit(options[:limit]). - offset(options[:offset]). - from(options[:from]). - includes(options[:include]) - - relation = relation.lock(options[:lock]) if options[:lock].present? - relation = relation.readonly(options[:readonly]) if options.has_key?(:readonly) + [:joins, :select, :group, :having, :order, :limit, :offset, :from, :lock, :readonly].each do |finder| + relation = relation.send(finder, options[finder]) if options.has_key?(finder) + end + + relation = relation.where(options[:conditions]) if options.has_key?(:conditions) + relation = relation.includes(options[:include]) if options.has_key?(:include) relation end diff --git a/activerecord/lib/active_record/types.rb b/activerecord/lib/active_record/types.rb deleted file mode 100644 index 74f569352b..0000000000 --- a/activerecord/lib/active_record/types.rb +++ /dev/null @@ -1,38 +0,0 @@ -module ActiveRecord - module Types - extend ActiveSupport::Concern - - module ClassMethods - - def attribute_types - attribute_types = {} - columns.each do |column| - options = {} - options[:time_zone_aware] = time_zone_aware?(column.name) - options[:serialize] = serialized_attributes[column.name] - - attribute_types[column.name] = to_type(column, options) - end - attribute_types - end - - private - - def to_type(column, options = {}) - type_class = if options[:time_zone_aware] - Type::TimeWithZone - elsif options[:serialize] - Type::Serialize - elsif [ :integer, :float, :decimal ].include?(column.type) - Type::Number - else - Type::Object - end - - type_class.new(column, options) - end - - end - - end -end diff --git a/activerecord/lib/active_record/types/number.rb b/activerecord/lib/active_record/types/number.rb deleted file mode 100644 index cfbe877575..0000000000 --- a/activerecord/lib/active_record/types/number.rb +++ /dev/null @@ -1,30 +0,0 @@ -module ActiveRecord - module Type - class Number < Object - - def boolean(value) - value = cast(value) - !(value.nil? || value.zero?) - end - - def precast(value) - convert_number_column_value(value) - end - - private - - def convert_number_column_value(value) - if value == false - 0 - elsif value == true - 1 - elsif value.is_a?(String) && value.blank? - nil - else - value - end - end - - end - end -end
\ No newline at end of file diff --git a/activerecord/lib/active_record/types/object.rb b/activerecord/lib/active_record/types/object.rb deleted file mode 100644 index ec3f861abd..0000000000 --- a/activerecord/lib/active_record/types/object.rb +++ /dev/null @@ -1,37 +0,0 @@ -module ActiveRecord - module Type - module Casting - - def cast(value) - typecaster.type_cast(value) - end - - def precast(value) - value - end - - def boolean(value) - cast(value).present? - end - - # Attributes::Typecasting stores appendable? types (e.g. serialized Arrays) when typecasting reads. - def appendable? - false - end - - end - - class Object - include Casting - - attr_reader :name, :options - attr_reader :typecaster - - def initialize(typecaster = nil, options = {}) - @typecaster, @options = typecaster, options - end - - end - - end -end
\ No newline at end of file diff --git a/activerecord/lib/active_record/types/serialize.rb b/activerecord/lib/active_record/types/serialize.rb deleted file mode 100644 index 7b6af1981f..0000000000 --- a/activerecord/lib/active_record/types/serialize.rb +++ /dev/null @@ -1,33 +0,0 @@ -module ActiveRecord - module Type - class Serialize < Object - - def cast(value) - unserialize(value) - end - - def appendable? - true - end - - protected - - def unserialize(value) - unserialized_object = object_from_yaml(value) - - if unserialized_object.is_a?(@options[:serialize]) || unserialized_object.nil? - unserialized_object - else - raise SerializationTypeMismatch, - "#{name} was supposed to be a #{@options[:serialize]}, but was a #{unserialized_object.class.to_s}" - end - end - - def object_from_yaml(string) - return string unless string.is_a?(String) && string =~ /^---/ - YAML::load(string) rescue string - end - - end - end -end
\ No newline at end of file diff --git a/activerecord/lib/active_record/types/time_with_zone.rb b/activerecord/lib/active_record/types/time_with_zone.rb deleted file mode 100644 index 3a8b9292f9..0000000000 --- a/activerecord/lib/active_record/types/time_with_zone.rb +++ /dev/null @@ -1,20 +0,0 @@ -module ActiveRecord - module Type - class TimeWithZone < Object - - def cast(time) - time = super(time) - time.acts_like?(:time) ? time.in_time_zone : time - end - - def precast(time) - unless time.acts_like?(:time) - time = time.is_a?(String) ? ::Time.zone.parse(time) : time.to_time rescue time - end - time = time.in_time_zone rescue nil if time - super(time) - end - - end - end -end diff --git a/activerecord/lib/active_record/types/unknown.rb b/activerecord/lib/active_record/types/unknown.rb deleted file mode 100644 index f832c7b304..0000000000 --- a/activerecord/lib/active_record/types/unknown.rb +++ /dev/null @@ -1,37 +0,0 @@ -module ActiveRecord - module Type - # Useful for handling attributes not mapped to types. Performs some boolean typecasting, - # but otherwise leaves the value untouched. - class Unknown - - def cast(value) - value - end - - def precast(value) - value - end - - # Attempts typecasting to handle numeric, false and blank values. - def boolean(value) - empty = (numeric?(value) && value.to_i.zero?) || false?(value) || value.blank? - !empty - end - - def appendable? - false - end - - protected - - def false?(value) - ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) - end - - def numeric?(value) - Numeric === value || value !~ /[^0-9]/ - end - - end - end -end
\ No newline at end of file |