aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md19
-rw-r--r--[-rwxr-xr-x]activerecord/Rakefile1
-rw-r--r--activerecord/lib/active_record.rb2
-rw-r--r--activerecord/lib/active_record/associations.rb2
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb2
-rw-r--r--activerecord/lib/active_record/dynamic_finder_match.rb108
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb247
-rw-r--r--activerecord/lib/active_record/dynamic_scope_match.rb30
-rw-r--r--activerecord/lib/active_record/querying.rb2
-rw-r--r--activerecord/lib/active_record/reflection.rb4
-rw-r--r--activerecord/lib/active_record/relation.rb2
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb97
-rw-r--r--activerecord/lib/active_record/sanitization.rb2
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb5
-rw-r--r--activerecord/test/cases/column_test.rb24
-rw-r--r--activerecord/test/cases/dynamic_finder_match_test.rb106
-rw-r--r--activerecord/test/cases/finder_test.rb32
-rw-r--r--activerecord/test/cases/migration_test.rb4
-rw-r--r--activerecord/test/cases/named_scope_test.rb5
-rw-r--r--activerecord/test/cases/relations_test.rb4
-rw-r--r--activerecord/test/models/company.rb5
26 files changed, 347 insertions, 375 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/associations.rb b/activerecord/lib/active_record/associations.rb
index c7a24de11d..c30e8e08b8 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -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/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..68cf495025 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1233,7 +1233,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/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..8726442d0b 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -1,10 +1,8 @@
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)
-
- valid_match || super
+ def respond_to?(name, include_private = false)
+ match = Method.match(self, name)
+ match && match.valid? || super
end
private
@@ -18,66 +16,217 @@ module ActiveRecord
#
# 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
+ class Method
+ def self.match(model, name)
+ klass = klasses.find { |k| name =~ k.pattern }
+ klass.new(model, name) if klass
+ end
+
+ def self.klasses
+ [
+ FindBy, FindAllBy, FindLastBy, FindByBang, ScopedBy,
+ FindOrInitializeBy, FindOrCreateBy, FindOrCreateByBang
+ ]
+ end
+
+ def self.pattern
+ /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/
+ end
+
+ def self.prefix
+ raise NotImplementedError
+ end
+
+ def self.suffix
+ ''
+ 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 find_dynamic_match(method_id) #:nodoc:
- DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id)
+ class Finder < Method
+ def body
+ <<-CODE
+ result = #{result}
+ result && block_given? ? yield(result) : result
+ CODE
+ end
+
+ def result
+ "scoped.apply_finder_options(options).#{finder}(#{attributes_hash})"
+ end
+
+ def signature
+ attribute_names.join(', ') + ", options = {}"
+ end
+
+ def attributes_hash
+ "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}"
+ end
+
+ def finder
+ raise NotImplementedError
+ end
end
- # 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
+ class FindBy < Finder
+ def self.prefix
+ "find_by"
+ end
+
+ def finder
+ "find_by"
+ end
+ end
+
+ class FindByBang < Finder
+ def self.prefix
+ "find_by"
+ end
+
+ def self.suffix
+ "!"
+ end
+
+ def finder
+ "find_by!"
+ end
+ end
+
+ class FindAllBy < Finder
+ def self.prefix
+ "find_all_by"
+ end
+
+ def finder
+ "where"
+ end
+
+ def result
+ "#{super}.to_a"
+ end
+ end
+
+ class FindLastBy < Finder
+ def self.prefix
+ "find_last_by"
+ end
+
+ def finder
+ "where"
+ end
+
+ def result
+ "#{super}.last"
+ end
+ end
+
+ class ScopedBy < Finder
+ def self.prefix
+ "scoped_by"
+ end
+
+ def body
+ "where(#{attributes_hash})"
+ end
+ end
+
+ class Instantiator < Method
+ # This is nasty, but it doesn't matter because it will be deprecated.
+ def self.dispatch(klass, attribute_names, instantiator, args, block)
+ if args.length == 1 && args.first.is_a?(Hash)
+ attributes = args.first.stringify_keys
+ conditions = attributes.slice(*attribute_names)
+ rest = [attributes.except(*attribute_names)]
else
- attribute_name.to_sym
+ raise ArgumentError, "too few arguments" unless args.length >= attribute_names.length
+
+ conditions = Hash[attribute_names.map.with_index { |n, i| [n, args[i]] }]
+ rest = args.drop(attribute_names.length)
end
- end.flatten
+
+ klass.where(conditions).first ||
+ klass.create_with(conditions).send(instantiator, *rest, &block)
+ end
+
+ def signature
+ "*args, &block"
+ end
+
+ def body
+ "#{self.class}.dispatch(self, #{attribute_names.inspect}, #{instantiator.inspect}, args, block)"
+ end
+
+ def instantiator
+ raise NotImplementedError
+ end
end
- def all_attributes_exists?(attribute_names)
- (expand_attribute_names_for_aggregates(attribute_names) -
- column_methods_hash.keys).empty?
+ class FindOrInitializeBy < Instantiator
+ def self.prefix
+ "find_or_initialize_by"
+ end
+
+ def instantiator
+ "new"
+ end
end
- def aggregate_mapping(reflection)
- mapping = reflection.options[:mapping] || [reflection.name, reflection.name]
- mapping.first.is_a?(Array) ? mapping : [mapping]
+ class FindOrCreateBy < Instantiator
+ def self.prefix
+ "find_or_create_by"
+ end
+
+ def instantiator
+ "create"
+ end
+ end
+
+ class FindOrCreateByBang < Instantiator
+ def self.prefix
+ "find_or_create_by"
+ end
+
+ def self.suffix
+ "!"
+ end
+
+ def instantiator
+ "create!"
+ 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/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/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/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/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..393e578219 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::ScopedBy.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::ScopedBy.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