diff options
Diffstat (limited to 'activerecord')
35 files changed, 318 insertions, 414 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index aa84dacf07..a661a44f1f 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,11 @@ ## Rails 4.0.0 (unreleased) ## +* Added default order to `first` to assure consistent results among + diferent database engines. Introduced `take` as a replacement to + the old behavior of `first`. + + *Marcelo Silveira* + * Added an :index option to automatically create indexes for references and belongs_to statements in migrations. @@ -580,19 +586,6 @@ a URI that specifies the connection configuration. For example: ActiveRecord::Base.establish_connection 'postgres://localhost/foo' -* Active Record's dynamic finder will now raise the error if you passing in less number of arguments than what you call in method signature. - - So if you were doing this and expecting the second argument to be nil: - - User.find_by_username_and_group("sikachu") - - You'll now get `ArgumentError: wrong number of arguments (1 for 2).` You'll then have to do this: - - User.find_by_username_and_group("sikachu", nil) - - *Prem Sichanugrist* - - ## Rails 3.1.0 (August 30, 2011) ## * Add a proxy_association method to association proxies, which can be called by association diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 4090293b56..7feb0b75a0 100755..100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,4 +1,3 @@ -#!/usr/bin/env rake require 'rake/testtask' require 'rake/packagetask' require 'rubygems/package_task' diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index c4b10a8dae..ed26b4899f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -63,8 +63,6 @@ module ActiveRecord autoload :CounterCache autoload :ConnectionHandling autoload :DynamicMatchers - autoload :DynamicFinderMatch - autoload :DynamicScopeMatch autoload :Explain autoload :Inheritance autoload :Integration diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index c39284539c..a4db627535 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -86,6 +86,12 @@ module ActiveRecord # customer.address_street = "Hyancintvej" # customer.address_city = "Copenhagen" # customer.address # => Address.new("Hyancintvej", "Copenhagen") + # + # customer.address_street = "Vesterbrogade" + # customer.address # => Address.new("Hyancintvej", "Copenhagen") + # customer.clear_aggregation_cache + # customer.address # => Address.new("Vesterbrogade", "Copenhagen") + # # customer.address = Address.new("May Street", "Chicago") # customer.address_street # => "May Street" # customer.address_city # => "Chicago" @@ -101,8 +107,8 @@ module ActiveRecord # ActiveRecord::Base classes are entity objects. # # It's also important to treat the value objects as immutable. Don't allow the Money object to have - # its amount changed after creation. Create a new Money object with the new value instead. This - # is exemplified by the Money#exchange_to method that returns a new value object instead of changing + # its amount changed after creation. Create a new Money object with the new value instead. The + # Money#exchange_to method is an example of this. It returns a new value object instead of changing # its own values. Active Record won't persist value objects that have been changed through means # other than the writer method. # @@ -119,7 +125,7 @@ module ActiveRecord # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows # a custom constructor to be specified. # - # When a new value is assigned to the value object the default assumption is that the new value + # When a new value is assigned to the value object, the default assumption is that the new value # is an instance of the value class. Specifying a custom converter allows the new value to be automatically # converted to an instance of value class if necessary. # diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b901f06ca4..c30e8e08b8 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1129,7 +1129,7 @@ module ActiveRecord # it would skip the first 4 rows. # [:select] # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if - # you, for example, want to do a join but not include the joined columns. Do not forget + # you want to do a join but not include the joined columns, for example. Do not forget # to include the primary and foreign keys, otherwise it will raise an error. # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). @@ -1264,8 +1264,8 @@ module ActiveRecord # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, - # you want to do a join but not include the joined columns. Do not forget to include the + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if + # you want to do a join but not include the joined columns, for example. Do not forget to include the # primary and foreign keys, otherwise it will raise an error. # [:through] # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt>, @@ -1355,7 +1355,7 @@ module ActiveRecord # SQL fragment, such as <tt>authorized = 1</tt>. # [:select] # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed - # if, for example, you want to do a join but not include the joined columns. Do not + # if you want to do a join but not include the joined columns, for example. Do not # forget to include the primary and foreign keys, otherwise it will raise an error. # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name @@ -1382,7 +1382,7 @@ module ActiveRecord # and +decrement_counter+. The counter cache is incremented when an object of this # class is created and decremented when it's destroyed. This requires that a column # named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class) - # is used on the associate class (such as a Post class) - that is the migration for + # is used on the associate class (such as a Post class) - that is the migration for # <tt>#{table_name}_count</tt> is created on the associate class (such that Post.comments_count will # return the count cached, see note below). You can also specify a custom counter # cache column by providing a column name instead of a +true+/+false+ value to this @@ -1432,7 +1432,7 @@ module ActiveRecord # Specifies a many-to-many relationship with another class. This associates two classes via an # intermediate join table. Unless the join table is explicitly specified as an option, it is # guessed using the lexical order of the class names. So a join between Developer and Project - # will give the default join table name of "developers_projects" because "D" outranks "P". + # will give the default join table name of "developers_projects" because "D" precedes "P" alphabetically. # Note that this precedence is calculated using the <tt><</tt> operator for String. This # means that if the strings are of different lengths, and the strings are equal when compared # up to the shortest length, then the longer string is considered of higher @@ -1576,8 +1576,8 @@ module ActiveRecord # An integer determining the offset from where the rows should be fetched. So at 5, # it would skip the first 4 rows. # [:select] - # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, - # you want to do a join but not include the joined columns. Do not forget to include the primary + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if + # you want to do a join but exclude the joined columns, for example. Do not forget to include the primary # and foreign keys, otherwise it will raise an error. # [:readonly] # If true, all the associated objects are readonly through the association. @@ -1596,7 +1596,7 @@ module ActiveRecord # has_and_belongs_to_many :categories, :join_table => "prods_cats" # has_and_belongs_to_many :categories, :readonly => true # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql => - # "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}" + # proc { |record| "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}" } def has_and_belongs_to_many(name, options = {}, &extension) Builder::HasAndBelongsToMany.build(self, name, options, &extension) end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index ad029d1101..261a829281 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -79,9 +79,9 @@ module ActiveRecord end def method_missing(method, *args, &block) - match = DynamicFinderMatch.match(method) - if match && match.instantiator? - send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r| + match = DynamicMatchers::Method.match(self, method) + if match && match.is_a?(DynamicMatchers::Instantiator) + scoped.send(method, *args) do |r| proxy_association.send :set_owner_attributes, r proxy_association.send :add_to_target, r yield(r) if block_given? @@ -101,7 +101,7 @@ module ActiveRecord end else - scoped.readonly(nil).send(method, *args, &block) + scoped.readonly(nil).public_send(method, *args, &block) end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index fc3906d395..189985b671 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -304,21 +304,22 @@ module ActiveRecord #:nodoc: # (or a bad spelling of an existing one). # * AssociationTypeMismatch - The object assigned to the association wasn't of the type # specified in the association definition. - # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter. + # * AttributeAssignmentError - An error occurred while doing a mass assignment through the + # <tt>attributes=</tt> method. + # You can inspect the +attribute+ property of the exception object to determine which attribute + # triggered the error. # * ConnectionNotEstablished - No connection has been established. Use <tt>establish_connection</tt> # before querying. - # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist - # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal - # nothing was found, please check its documentation for further details. - # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message. # * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of # AttributeAssignmentError # objects that should be inspected to determine which attributes triggered the errors. - # * AttributeAssignmentError - An error occurred while doing a mass assignment through the - # <tt>attributes=</tt> method. - # You can inspect the +attribute+ property of the exception object to determine which attribute - # triggered the error. + # * RecordInvalid - raised by save! and create! when the record is invalid. + # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist + # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal + # nothing was found, please check its documentation for further details. + # * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter. + # * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message. # # *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level). # So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 3546873550..f0b6ae2b7d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -408,7 +408,7 @@ module ActiveRecord # t.remove(:qualification) # t.remove(:qualification, :experience) def remove(*column_names) - @base.remove_column(@table_name, column_names) + @base.remove_column(@table_name, *column_names) end # Removes the given index from the table. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 30a4f9aa35..e7a4f061fd 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -618,8 +618,6 @@ module ActiveRecord end def columns_for_remove(table_name, *column_names) - column_names = column_names.flatten - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank? column_names.map {|column_name| quote_column_name(column_name) } end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index b7e1513422..9af8e46120 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -95,7 +95,7 @@ module ActiveRecord case type when :string, :text then value - when :integer then value.to_i rescue value ? 1 : 0 + when :integer then value.to_i when :float then value.to_f when :decimal then klass.value_to_decimal(value) when :datetime, :timestamp then klass.string_to_time(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 273c165084..4d5459939b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -511,8 +511,13 @@ module ActiveRecord else super end when Float - return super unless value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" + if value.infinite? && column.type == :datetime + "'#{value.to_s.downcase}'" + elsif value.infinite? || value.nan? + "'#{value.to_s}'" + else + super + end when Numeric return super unless column.sql_type == 'money' # Not truly string input, so doesn't require (or allow) escape string syntax. @@ -1233,7 +1238,10 @@ module ActiveRecord # Construct a clean list of column names from the ORDER BY clause, removing # any ASC/DESC modifiers - order_columns = orders.collect { |s| s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') } + order_columns = orders.collect do |s| + s = s.to_sql unless s.is_a?(String) + s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') + end order_columns.delete_if { |c| c.blank? } order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 83f75e3505..44e407a561 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -446,7 +446,7 @@ module ActiveRecord def remove_column(table_name, *column_names) #:nodoc: raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? - column_names.flatten.each do |column_name| + column_names.each do |column_name| alter_table(table_name) do |definition| definition.columns.delete(definition[column_name]) end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index eb8f4ad669..9a76c9f617 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,5 +1,6 @@ require 'active_support/concern' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/object/duplicable' require 'thread' module ActiveRecord @@ -165,7 +166,9 @@ module ActiveRecord # # Instantiates a single new object bypassing mass-assignment security # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) def initialize(attributes = nil, options = {}) - @attributes = self.class.initialize_attributes(self.class.column_defaults.dup) + # TODO: use deep_dup after fixing it to also dup values + defaults = Hash[self.class.column_defaults.map { |k, v| [k, v.duplicable? ? v.dup : v] }] + @attributes = self.class.initialize_attributes(defaults) @columns_hash = self.class.column_types.dup init_internals diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb deleted file mode 100644 index 0473d6aafc..0000000000 --- a/activerecord/lib/active_record/dynamic_finder_match.rb +++ /dev/null @@ -1,108 +0,0 @@ -module ActiveRecord - - # = Active Record Dynamic Finder Match - # - # Refer to ActiveRecord::Base documentation for Dynamic attribute-based finders for detailed info - # - class DynamicFinderMatch - def self.match(method) - method = method.to_s - klass = klasses.find do |_klass| - _klass.matches?(method) - end - klass.new(method) if klass - end - - def self.matches?(method) - method =~ self::METHOD_PATTERN - end - - def self.klasses - [FindBy, FindByBang, FindOrInitializeCreateBy, FindOrCreateByBang] - end - - def initialize(method) - @finder = :first - @instantiator = nil - match_data = method.match(self.class::METHOD_PATTERN) - @attribute_names = match_data[-1].split("_and_") - initialize_from_match_data(match_data) - end - - attr_reader :finder, :attribute_names, :instantiator - - def finder? - @finder && !@instantiator - end - - def creator? - @finder == :first && @instantiator == :create - end - - def instantiator? - @instantiator - end - - def bang? - false - end - - def valid_arguments?(arguments) - arguments.size >= @attribute_names.size - end - - def save_record? - @instantiator == :create - end - - def save_method - bang? ? :save! : :save - end - - private - - def initialize_from_match_data(match_data) - end - end - - class FindBy < DynamicFinderMatch - METHOD_PATTERN = /^find_(all_|last_)?by_([_a-zA-Z]\w*)$/ - - def initialize_from_match_data(match_data) - @finder = :last if match_data[1] == 'last_' - @finder = :all if match_data[1] == 'all_' - end - end - - class FindByBang < DynamicFinderMatch - METHOD_PATTERN = /^find_by_([_a-zA-Z]\w*)\!$/ - - def bang? - true - end - end - - class FindOrInitializeCreateBy < DynamicFinderMatch - METHOD_PATTERN = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/ - - def initialize_from_match_data(match_data) - @instantiator = match_data[1] == 'initialize' ? :new : :create - end - - def valid_arguments?(arguments) - arguments.size == 1 && arguments.first.is_a?(Hash) || super - end - end - - class FindOrCreateByBang < DynamicFinderMatch - METHOD_PATTERN = /^find_or_create_by_([_a-zA-Z]\w*)\!$/ - - def initialize_from_match_data(match_data) - @instantiator = :create - end - - def bang? - true - end - end -end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index e35b1c91a0..e278e62ce7 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,83 +1,130 @@ module ActiveRecord - module DynamicMatchers - def respond_to?(method_id, include_private = false) - match = find_dynamic_match(method_id) - valid_match = match && all_attributes_exists?(match.attribute_names) + module DynamicMatchers #:nodoc: + # This code in this file seems to have a lot of indirection, but the indirection + # is there to provide extension points for the active_record_deprecated_finders + # gem. When we stop supporting active_record_deprecated_finders (from Rails 5), + # then we can remove the indirection. - valid_match || super + def respond_to?(name, include_private = false) + match = Method.match(self, name) + match && match.valid? || super end private - # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and - # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders - # section at the top of this file for more detailed information. - # - # It's even possible to use all the additional parameters to +find+. For example, the - # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>. - # - # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it - # is first invoked, so that future attempts to use it do not run through method_missing. - def method_missing(method_id, *arguments, &block) - if match = find_dynamic_match(method_id) - attribute_names = match.attribute_names - super unless all_attributes_exists?(attribute_names) - - unless match.valid_arguments?(arguments) - method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'" - backtrace = [method_trace] + caller - raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace - end + def method_missing(name, *arguments, &block) + match = Method.match(self, name) - if match.respond_to?(:scope?) && match.scope? - define_scope_method(method_id, attribute_names) - send(method_id, *arguments) - elsif match.finder? - options = arguments.extract_options! - relation = options.any? ? scoped(options) : scoped - relation.send :find_by_attributes, match, attribute_names, *arguments, &block - elsif match.instantiator? - scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block - end + if match && match.valid? + match.define + send(name, *arguments, &block) else super end end - def define_scope_method(method_id, attribute_names) #:nodoc - self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) - conditions = Hash[[:#{attribute_names.join(',:')}].zip(args)] # conditions = Hash[[:user_name, :password].zip(args)] - where(conditions) # where(conditions) - end # end - METHOD - end + class Method + @matchers = [] - def find_dynamic_match(method_id) #:nodoc: - DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id) - end + class << self + attr_reader :matchers - # Similar in purpose to +expand_hash_conditions_for_aggregates+. - def expand_attribute_names_for_aggregates(attribute_names) - attribute_names.map do |attribute_name| - if aggregation = reflect_on_aggregation(attribute_name.to_sym) - aggregate_mapping(aggregation).map do |field_attr, _| - field_attr.to_sym - end - else - attribute_name.to_sym + def match(model, name) + klass = matchers.find { |k| name =~ k.pattern } + klass.new(model, name) if klass + end + + def pattern + /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/ + end + + def prefix + raise NotImplementedError + end + + def suffix + '' end - end.flatten + end + + attr_reader :model, :name, :attribute_names + + def initialize(model, name) + @model = model + @name = name.to_s + @attribute_names = @name.match(self.class.pattern)[1].split('_and_') + end + + def valid? + attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) } + end + + def define + model.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def self.#{name}(#{signature}) + #{body} + end + CODE + end + + def body + raise NotImplementedError + end end - def all_attributes_exists?(attribute_names) - (expand_attribute_names_for_aggregates(attribute_names) - - column_methods_hash.keys).empty? + module Finder + # Extended in active_record_deprecated_finders + def body + result + end + + # Extended in active_record_deprecated_finders + def result + "#{finder}(#{attributes_hash})" + end + + # Extended in active_record_deprecated_finders + def signature + attribute_names.join(', ') + end + + def attributes_hash + "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}" + end + + def finder + raise NotImplementedError + end end - def aggregate_mapping(reflection) - mapping = reflection.options[:mapping] || [reflection.name, reflection.name] - mapping.first.is_a?(Array) ? mapping : [mapping] + class FindBy < Method + Method.matchers << self + include Finder + + def self.prefix + "find_by" + end + + def finder + "find_by" + end + end + + class FindByBang < Method + Method.matchers << self + include Finder + + def self.prefix + "find_by" + end + + def self.suffix + "!" + end + + def finder + "find_by!" + end end end end diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb deleted file mode 100644 index 6c043d29c4..0000000000 --- a/activerecord/lib/active_record/dynamic_scope_match.rb +++ /dev/null @@ -1,30 +0,0 @@ -module ActiveRecord - - # = Active Record Dynamic Scope Match - # - # Provides dynamic attribute-based scopes such as <tt>scoped_by_price(4.99)</tt> - # if, for example, the <tt>Product</tt> has an attribute with that name. You can - # chain more <tt>scoped_by_* </tt> methods after the other. It acts like a named - # scope except that it's dynamic. - class DynamicScopeMatch - METHOD_PATTERN = /^scoped_by_([_a-zA-Z]\w*)$/ - - def self.match(method) - if method.to_s =~ METHOD_PATTERN - new(true, $1 && $1.split('_and_')) - end - end - - def initialize(scope, attribute_names) - @scope = scope - @attribute_names = attribute_names - end - - attr_reader :scope, :attribute_names - alias :scope? :scope - - def valid_arguments?(arguments) - arguments.size >= @attribute_names.size - end - end -end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 01cacf6153..313fdb3487 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -70,7 +70,7 @@ module ActiveRecord # the threshold is set to 0. # # As the name of the method suggests this only applies to automatic - # EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run. + # EXPLAINs, manual calls to <tt>ActiveRecord::Relation#explain</tt> run. def silence_auto_explain current = Thread.current original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 9796b0a321..a01e2f74ff 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -823,7 +823,7 @@ module ActiveRecord end def setup_fixtures - return unless !ActiveRecord::Base.configurations.blank? + return if ActiveRecord::Base.configurations.blank? if pre_loaded_fixtures && !use_transactional_fixtures raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 29b8b2fb73..4d8283bcff 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -3,7 +3,7 @@ require 'active_support/deprecation' module ActiveRecord module Querying - delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped + delegate :find, :take, :take!, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped delegate :find_by, :find_by!, :to => :scoped delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index d4f4d593c6..c380b5c029 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -153,6 +153,10 @@ module ActiveRecord # Holds all the meta-data about an aggregation as it was specified in the # Active Record class. class AggregateReflection < MacroReflection #:nodoc: + def mapping + mapping = options[:mapping] || [name, name] + mapping.first.is_a?(Array) ? mapping : [mapping] + end end # Holds all the meta-data about an association as it was specified in the diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 333d31d8a3..779e052e3c 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -390,6 +390,8 @@ module ActiveRecord # 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) + raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value + if conditions where(conditions).delete_all else diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index f388b75c05..3ce9995031 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -144,7 +144,7 @@ module ActiveRecord # Examples: # # Person.ids # SELECT people.id FROM people - # Person.joins(:companies).ids # SELECT people.id FROM PEOPLE INNER JOIN companies ON companies.person_id = people.id + # Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id def ids pluck primary_key end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 1ceb1949a4..a78e3d08e4 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -60,6 +60,28 @@ module ActiveRecord where(*args).first! end + # Gives a record (or N records if a parameter is supplied) without any implied + # order. The order will depend on the database implementation. + # If an order is supplied it will be respected. + # + # Examples: + # + # Person.take # returns an object fetched by SELECT * FROM people + # Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5 + # Person.where(["name LIKE '%?'", name]).take + def take(limit = nil) + limit ? limit(limit).to_a : find_take + end + + # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. Note that <tt>take!</tt> accepts no arguments. + def take! + take or raise RecordNotFound + end + + # Find the first record (or first N records if a parameter is supplied). + # If no order is defined it will order by primary key. + # # Examples: # # Person.first # returns the first object fetched by SELECT * FROM people @@ -67,7 +89,15 @@ module ActiveRecord # Person.where(["user_name = :u", { :u => user_name }]).first # Person.order("created_on DESC").offset(5).first def first(limit = nil) - limit ? limit(limit).to_a : find_first + if limit + if order_values.empty? && primary_key + order(arel_table[primary_key].asc).limit(limit).to_a + else + limit(limit).to_a + end + else + find_first + end end # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -76,6 +106,9 @@ module ActiveRecord first or raise RecordNotFound end + # Find the last record (or last N records if a parameter is supplied). + # If no order is defined it will order by primary key. + # # Examples: # # Person.last # returns the last object fetched by SELECT * FROM people @@ -83,8 +116,8 @@ module ActiveRecord # Person.order("created_on DESC").offset(5).last def last(limit = nil) if limit - if order_values.empty? - order("#{primary_key} DESC").limit(limit).reverse + if order_values.empty? && primary_key + order(arel_table[primary_key].desc).limit(limit).reverse else to_a.last(limit) end @@ -210,47 +243,6 @@ module ActiveRecord ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) end - def find_by_attributes(match, attributes, *args) - conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}] - result = where(conditions).send(match.finder) - - if match.bang? && result.blank? - raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}" - else - if block_given? && result - yield(result) - else - result - end - end - end - - def find_or_instantiator_by_attributes(match, attributes, *args) - options = args.size > 1 && args.last(2).all?{ |a| a.is_a?(Hash) } ? args.extract_options! : {} - protected_attributes_for_create, unprotected_attributes_for_create = {}, {} - args.each_with_index do |arg, i| - if arg.is_a?(Hash) - protected_attributes_for_create = args[i].with_indifferent_access - else - unprotected_attributes_for_create[attributes[i]] = args[i] - end - end - - conditions = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes).symbolize_keys - - record = where(conditions).first - - unless record - record = @klass.new(protected_attributes_for_create, options) do |r| - r.assign_attributes(unprotected_attributes_for_create, :without_protection => true) - end - yield(record) if block_given? - record.send(match.save_method) if match.save_record? - end - - record - end - def find_with_ids(*ids) return to_a.find { |*block_args| yield(*block_args) } if block_given? @@ -315,11 +307,24 @@ module ActiveRecord end end + def find_take + if loaded? + @records.take(1).first + else + @take ||= limit(1).to_a.first + end + end + def find_first if loaded? @records.first else - @first ||= limit(1).to_a[0] + @first ||= + if order_values.empty? && primary_key + order(arel_table[primary_key].asc).limit(1).to_a.first + else + limit(1).to_a.first + end end end @@ -331,7 +336,7 @@ module ActiveRecord if offset_value || limit_value to_a.last else - reverse_order.limit(1).to_a[0] + reverse_order.limit(1).to_a.first end end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 81b13fe529..5530be3219 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -58,7 +58,7 @@ module ActiveRecord expanded_attrs = {} attrs.each do |attr, value| if aggregation = reflect_on_aggregation(attr.to_sym) - mapping = aggregate_mapping(aggregation) + mapping = aggregation.mapping mapping.each do |field_attr, aggregate_attr| if mapping.size == 1 && !value.respond_to?(aggregate_attr) expanded_attrs[field_attr] = value diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 172055f15c..f8a605b67c 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -19,6 +19,18 @@ module ActiveRecord assert_equal 'f', @conn.type_cast(false, nil) assert_equal 'f', @conn.type_cast(false, c) end + + def test_quote_float_nan + nan = 0.0/0 + c = Column.new(nil, 1, 'float') + assert_equal "'NaN'", @conn.quote(nan, c) + end + + def test_quote_float_infinity + infinity = 1.0/0 + c = Column.new(nil, 1, 'float') + assert_equal "'Infinity'", @conn.quote(infinity, c) + end end end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index a98e5b115c..f74fe42dc2 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1703,4 +1703,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase ensure ActiveRecord::Base.dependent_restrict_raises = option_before end + + def test_collection_association_with_private_kernel_method + firm = companies(:first_firm) + assert_equal [accounts(:signals37)], firm.accounts.open + end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 35e0045e4c..c1b0cb8886 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1909,7 +1909,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_attribute_names - assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id"], + assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"], Company.attribute_names end @@ -2001,6 +2001,12 @@ class BasicsTest < ActiveRecord::TestCase assert_nil hash['firm_name'] end + def test_default_values_are_deeply_dupped + company = Company.new + company.description << "foo" + assert_equal "", Company.new.description + end + ["find_by", "find_by!"].each do |meth| test "#{meth} delegates to scoped" do record = stub diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb index ccc57cb876..4fcf8a33a4 100644 --- a/activerecord/test/cases/column_test.rb +++ b/activerecord/test/cases/column_test.rb @@ -24,6 +24,30 @@ module ActiveRecord assert !column.type_cast('off') assert !column.type_cast('OFF') end + + def test_type_cast_integer + column = Column.new("field", nil, "integer") + assert_equal 1, column.type_cast(1) + assert_equal 1, column.type_cast('1') + assert_equal 1, column.type_cast('1ignore') + assert_equal 0, column.type_cast('bad1') + assert_equal 0, column.type_cast('bad') + assert_equal 1, column.type_cast(1.7) + assert_nil column.type_cast(nil) + end + + def test_type_cast_non_integer_to_integer + column = Column.new("field", nil, "integer") + assert_raises(NoMethodError) do + column.type_cast([]) + end + assert_raises(NoMethodError) do + column.type_cast(true) + end + assert_raises(NoMethodError) do + column.type_cast(false) + end + end end end end diff --git a/activerecord/test/cases/dynamic_finder_match_test.rb b/activerecord/test/cases/dynamic_finder_match_test.rb deleted file mode 100644 index db619faa83..0000000000 --- a/activerecord/test/cases/dynamic_finder_match_test.rb +++ /dev/null @@ -1,106 +0,0 @@ -require "cases/helper" - -module ActiveRecord - class DynamicFinderMatchTest < ActiveRecord::TestCase - def test_find_or_create_by - match = DynamicFinderMatch.match("find_or_create_by_age_and_sex_and_location") - assert_not_nil match - assert !match.finder? - assert match.instantiator? - assert_equal :first, match.finder - assert_equal :create, match.instantiator - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_or_initialize_by - match = DynamicFinderMatch.match("find_or_initialize_by_age_and_sex_and_location") - assert_not_nil match - assert !match.finder? - assert match.instantiator? - assert_equal :first, match.finder - assert_equal :new, match.instantiator - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_no_match - assert_nil DynamicFinderMatch.match("not_a_finder") - end - - def find_by_bang - match = DynamicFinderMatch.match("find_by_age_and_sex_and_location!") - assert_not_nil match - assert match.finder? - assert match.bang? - assert_equal :first, match.finder - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_by - match = DynamicFinderMatch.match("find_by_age_and_sex_and_location") - assert_not_nil match - assert match.finder? - assert_equal :first, match.finder - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_by_with_symbol - m = DynamicFinderMatch.match(:find_by_foo) - assert_equal :first, m.finder - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_all_by_with_symbol - m = DynamicFinderMatch.match(:find_all_by_foo) - assert_equal :all, m.finder - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_all_by - match = DynamicFinderMatch.match("find_all_by_age_and_sex_and_location") - assert_not_nil match - assert match.finder? - assert_equal :all, match.finder - assert_equal %w(age sex location), match.attribute_names - end - - def test_find_last_by - m = DynamicFinderMatch.match(:find_last_by_foo) - assert_equal :last, m.finder - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_by! - m = DynamicFinderMatch.match(:find_by_foo!) - assert_equal :first, m.finder - assert m.bang?, 'should be banging' - assert_equal %w{ foo }, m.attribute_names - end - - def test_find_or_create - m = DynamicFinderMatch.match(:find_or_create_by_foo) - assert_equal :first, m.finder - assert_equal %w{ foo }, m.attribute_names - assert_equal :create, m.instantiator - end - - def test_find_or_create! - m = DynamicFinderMatch.match(:find_or_create_by_foo!) - assert_equal :first, m.finder - assert m.bang?, 'should be banging' - assert_equal %w{ foo }, m.attribute_names - assert_equal :create, m.instantiator - end - - def test_find_or_initialize - m = DynamicFinderMatch.match(:find_or_initialize_by_foo) - assert_equal :first, m.finder - assert_equal %w{ foo }, m.attribute_names - assert_equal :new, m.instantiator - end - - def test_garbage - assert !DynamicFinderMatch.match(:fooo), 'should be false' - assert !DynamicFinderMatch.match(:find_by), 'should be false' - end - end -end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 29469c42ed..54801bd101 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -143,6 +143,26 @@ class FinderTest < ActiveRecord::TestCase assert_equal [Account], accounts.collect(&:class).uniq end + def test_take + assert_equal topics(:first), Topic.take + end + + def test_take_failing + assert_nil Topic.where("title = 'This title does not exist'").take + end + + def test_take_bang_present + assert_nothing_raised do + assert_equal topics(:second), Topic.where("title = 'The Second Topic of the day'").take! + end + end + + def test_take_bang_missing + assert_raises ActiveRecord::RecordNotFound do + Topic.where("title = 'This title does not exist'").take! + end + end + def test_first assert_equal topics(:second).title, Topic.where("title = 'The Second Topic of the day'").first.title end @@ -163,6 +183,12 @@ class FinderTest < ActiveRecord::TestCase end end + def test_first_have_primary_key_order_by_default + expected = topics(:first) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.first + end + def test_model_class_responds_to_first_bang assert Topic.first! Topic.delete_all @@ -191,7 +217,8 @@ class FinderTest < ActiveRecord::TestCase end end - def test_first_and_last_with_integer_should_use_sql_limit + def test_take_and_first_and_last_with_integer_should_use_sql_limit + assert_sql(/LIMIT 3|ROWNUM <= 3/) { Topic.take(3).entries } assert_sql(/LIMIT 2|ROWNUM <= 2/) { Topic.first(2).entries } assert_sql(/LIMIT 5|ROWNUM <= 5/) { Topic.last(5).entries } end @@ -212,7 +239,8 @@ class FinderTest < ActiveRecord::TestCase assert_no_match(/LIMIT/, query.first) end - def test_first_and_last_with_integer_should_return_an_array + def test_take_and_first_and_last_with_integer_should_return_an_array + assert_kind_of Array, Topic.take(5) assert_kind_of Array, Topic.first(5) assert_kind_of Array, Topic.last(5) end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index e71b84a3a5..f788690b0d 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -588,14 +588,14 @@ class ChangeTableMigrationsTest < ActiveRecord::TestCase def test_remove_drops_single_column with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, [:bar]) + @connection.expects(:remove_column).with(:delete_me, :bar) t.remove :bar end end def test_remove_drops_multiple_columns with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, [:bar, :baz]) + @connection.expects(:remove_column).with(:delete_me, :bar, :baz) t.remove :bar, :baz end end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index f8557259fb..2e87c5be2f 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -442,13 +442,12 @@ end class DynamicScopeMatchTest < ActiveRecord::TestCase def test_scoped_by_no_match - assert_nil ActiveRecord::DynamicScopeMatch.match("not_scoped_at_all") + assert_nil ActiveRecord::DynamicMatchers::Method.match(nil, "not_scoped_at_all") end def test_scoped_by - match = ActiveRecord::DynamicScopeMatch.match("scoped_by_age_and_sex_and_location") + match = ActiveRecord::DynamicMatchers::Method.match(nil, "scoped_by_age_and_sex_and_location") assert_not_nil match - assert match.scope? assert_equal %w(age sex location), match.attribute_names end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 4b8b57bac2..3ef357e297 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -688,6 +688,10 @@ class RelationTest < ActiveRecord::TestCase assert davids.loaded? end + def test_delete_all_limit_error + assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all } + end + def test_select_argument_error assert_raises(ArgumentError) { Developer.select } end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index fbdfaa2c29..7b993d5a2c 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -198,6 +198,11 @@ class Account < ActiveRecord::Base @destroyed_account_ids ||= Hash.new { |h,k| h[k] = [] } end + # Test private kernel method through collection proxy using has_many. + def self.open + where('firm_name = ?', '37signals') + end + before_destroy do |account| if account.firm Account.destroyed_account_ids[account.firm.id] << account.id diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 377fde5c96..7082d5dc86 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -173,6 +173,7 @@ ActiveRecord::Schema.define do t.integer :client_of t.integer :rating, :default => 1 t.integer :account_id + t.string :description, :null => false, :default => "" end add_index :companies, [:firm_id, :type, :rating, :ruby_type], :name => "company_index" |