aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG4
-rw-r--r--activerecord/lib/active_record.rb1
-rw-r--r--activerecord/lib/active_record/association_preload.rb35
-rwxr-xr-xactiverecord/lib/active_record/associations.rb38
-rwxr-xr-xactiverecord/lib/active_record/base.rb90
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb48
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb24
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb14
-rw-r--r--activerecord/lib/active_record/dirty.rb2
-rw-r--r--activerecord/lib/active_record/dynamic_scope_match.rb25
-rw-r--r--activerecord/lib/active_record/query_cache.rb38
-rw-r--r--activerecord/lib/active_record/timestamp.rb4
-rw-r--r--activerecord/lib/active_record/validations.rb1
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb8
-rw-r--r--activerecord/test/cases/associations/eager_test.rb19
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb13
-rwxr-xr-xactiverecord/test/cases/base_test.rb15
-rw-r--r--activerecord/test/cases/method_scoping_test.rb18
-rw-r--r--activerecord/test/cases/named_scope_test.rb20
-rw-r--r--activerecord/test/fixtures/people.yml11
-rw-r--r--activerecord/test/models/company.rb1
-rw-r--r--activerecord/test/models/person.rb6
-rw-r--r--activerecord/test/schema/schema.rb6
24 files changed, 341 insertions, 112 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index d057ddfcd0..c750f486f9 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,9 @@
*2.3.0/3.0*
+* Added dynamic scopes ala dynamic finders #1648 [Yaroslav Markin]
+
+* Fixed that ActiveRecord::Base#new_record? should return false (not nil) for existing records #1219 [Yaroslav Markin]
+
* I18n the word separator for error messages. Introduces the activerecord.errors.format.separator translation key. #1294 [Akira Matsuda]
* Add :having as a key to find and the relevant associations. [Emilio Tagua]
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index c428366a04..390c005785 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -51,6 +51,7 @@ module ActiveRecord
autoload :Callbacks, 'active_record/callbacks'
autoload :Dirty, 'active_record/dirty'
autoload :DynamicFinderMatch, 'active_record/dynamic_finder_match'
+ autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match'
autoload :Migration, 'active_record/migration'
autoload :Migrator, 'active_record/migration'
autoload :NamedScope, 'active_record/named_scope'
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index 7b1b2d9ad9..9d0bf3a308 100644
--- a/activerecord/lib/active_record/association_preload.rb
+++ b/activerecord/lib/active_record/association_preload.rb
@@ -43,7 +43,7 @@ module ActiveRecord
# loading in a more high-level (application developer-friendly) manner.
module ClassMethods
protected
-
+
# Eager loads the named associations for the given ActiveRecord record(s).
#
# In this description, 'association name' shall refer to the name passed
@@ -94,8 +94,8 @@ module ActiveRecord
raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol)
preload_associations(records, parent, preload_options)
reflection = reflections[parent]
- parents = records.map {|record| record.send(reflection.name)}.flatten
- unless parents.empty? || parents.first.nil?
+ parents = records.map {|record| record.send(reflection.name)}.flatten.compact
+ unless parents.empty?
parents.first.class.preload_associations(parents, child)
end
end
@@ -113,7 +113,7 @@ module ActiveRecord
# unnecessarily
records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
-
+
# 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus,
# the following could call 'preload_belongs_to_association',
# 'preload_has_many_association', etc.
@@ -128,7 +128,7 @@ module ActiveRecord
association_proxy.target.push(*[associated_record].flatten)
end
end
-
+
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record|
parent_record.send("set_#{reflection_name}_target", associated_record)
@@ -183,18 +183,19 @@ module ActiveRecord
conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}"
conditions << append_conditions(reflection, preload_options)
- associated_records = reflection.klass.find(:all, :conditions => [conditions, ids],
- :include => 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])
-
+ associated_records = reflection.klass.with_exclusive_scope do
+ reflection.klass.find(:all, :conditions => [conditions, ids],
+ :include => 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])
+ end
set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id')
end
def preload_has_one_association(records, reflection, preload_options={})
return if records.first.send("loaded_#{reflection.name}?")
- id_to_record_map, ids = construct_id_map(records)
+ id_to_record_map, ids = construct_id_map(records)
options = reflection.options
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
if options[:through]
@@ -248,7 +249,7 @@ module ActiveRecord
reflection.primary_key_name)
end
end
-
+
def preload_through_records(records, reflection, through_association)
through_reflection = reflections[through_association]
through_primary_key = through_reflection.primary_key_name
@@ -333,11 +334,13 @@ module ActiveRecord
end
conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
conditions << append_conditions(reflection, preload_options)
- associated_records = klass.find(:all, :conditions => [conditions, ids],
+ associated_records = klass.with_exclusive_scope do
+ klass.find(:all, :conditions => [conditions, ids],
:include => options[:include],
:select => options[:select],
:joins => options[:joins],
:order => options[:order])
+ end
set_association_single_records(id_map, reflection.name, associated_records, primary_key)
end
end
@@ -355,13 +358,15 @@ module ActiveRecord
conditions << append_conditions(reflection, preload_options)
- reflection.klass.find(:all,
+ reflection.klass.with_exclusive_scope do
+ reflection.klass.find(:all,
: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])
+ end
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 07bc50c886..c154a5087c 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1216,11 +1216,11 @@ module ActiveRecord
# callbacks will be executed after the association is wiped out.
old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
class_eval <<-end_eval unless method_defined?(old_method)
- alias_method :#{old_method}, :destroy_without_callbacks
- def destroy_without_callbacks
- #{reflection.name}.clear
- #{old_method}
- end
+ alias_method :#{old_method}, :destroy_without_callbacks # alias_method :destroy_without_habtm_shim_for_posts, :destroy_without_callbacks
+ def destroy_without_callbacks # def destroy_without_callbacks
+ #{reflection.name}.clear # posts.clear
+ #{old_method} # destroy_without_habtm_shim_for_posts
+ end # end
end_eval
add_association_callbacks(reflection.name, options)
@@ -1453,7 +1453,7 @@ module ActiveRecord
dependent_conditions << sanitize_sql(reflection.options[:conditions]) if reflection.options[:conditions]
dependent_conditions << extra_conditions if extra_conditions
dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
-
+ dependent_conditions = dependent_conditions.gsub('@', '\@')
case reflection.options[:dependent]
when :destroy
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
@@ -1463,22 +1463,22 @@ module ActiveRecord
before_destroy method_name
when :delete_all
module_eval %Q{
- before_destroy do |record|
- delete_all_has_many_dependencies(record,
- "#{reflection.name}",
- #{reflection.class_name},
- "#{dependent_conditions}")
- end
+ before_destroy do |record| # before_destroy do |record|
+ delete_all_has_many_dependencies(record, # delete_all_has_many_dependencies(record,
+ "#{reflection.name}", # "posts",
+ #{reflection.class_name}, # Post,
+ %@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...)
+ end # end
}
when :nullify
module_eval %Q{
- before_destroy do |record|
- nullify_has_many_dependencies(record,
- "#{reflection.name}",
- #{reflection.class_name},
- "#{reflection.primary_key_name}",
- "#{dependent_conditions}")
- end
+ before_destroy do |record| # before_destroy do |record|
+ nullify_has_many_dependencies(record, # nullify_has_many_dependencies(record,
+ "#{reflection.name}", # "posts",
+ #{reflection.class_name}, # Post,
+ "#{reflection.primary_key_name}", # "user_id",
+ %@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...)
+ end # end
}
else
raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, or :nullify (#{reflection.options[:dependent].inspect})"
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index aa6013583d..cca012ed55 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1456,7 +1456,10 @@ module ActiveRecord #:nodoc:
def respond_to?(method_id, include_private = false)
if match = DynamicFinderMatch.match(method_id)
return true if all_attributes_exists?(match.attribute_names)
+ elsif match = DynamicScopeMatch.match(method_id)
+ return true if all_attributes_exists?(match.attribute_names)
end
+
super
end
@@ -1494,11 +1497,16 @@ module ActiveRecord #:nodoc:
end
if scoped?(:find, :order)
- scoped_order = reverse_sql_order(scope(:find, :order))
- scoped_methods.select { |s| s[:find].update(:order => scoped_order) }
+ scope = scope(:find)
+ original_scoped_order = scope[:order]
+ scope[:order] = reverse_sql_order(original_scoped_order)
end
- find_initial(options.merge({ :order => order }))
+ begin
+ find_initial(options.merge({ :order => order }))
+ ensure
+ scope[:order] = original_scoped_order if original_scoped_order
+ end
end
def reverse_sql_order(order_query)
@@ -1804,7 +1812,11 @@ module ActiveRecord #:nodoc:
# This also enables you to initialize a record if it is not found, such as find_or_initialize_by_amount(amount)
# or find_or_create_by_user_and_password(user, password).
#
- # Each dynamic finder or initializer/creator is also defined in the class after it is first invoked, so that future
+ # Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that
+ # are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password])
+ # respectively.
+ #
+ # Each dynamic finder, scope or initializer/creator 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 = DynamicFinderMatch.match(method_id)
@@ -1813,10 +1825,31 @@ module ActiveRecord #:nodoc:
if match.finder?
finder = match.finder
bang = match.bang?
+ # def self.find_by_login_and_activated(*args)
+ # options = args.extract_options!
+ # attributes = construct_attributes_from_arguments(
+ # [:login,:activated],
+ # args
+ # )
+ # finder_options = { :conditions => attributes }
+ # validate_find_options(options)
+ # set_readonly_option!(options)
+ #
+ # if options[:conditions]
+ # with_scope(:find => finder_options) do
+ # find(:first, options)
+ # end
+ # else
+ # find(:first, options.merge(finder_options))
+ # end
+ # end
self.class_eval %{
def self.#{method_id}(*args)
options = args.extract_options!
- attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
+ attributes = construct_attributes_from_arguments(
+ [:#{attribute_names.join(',:')}],
+ args
+ )
finder_options = { :conditions => attributes }
validate_find_options(options)
set_readonly_option!(options)
@@ -1834,6 +1867,31 @@ module ActiveRecord #:nodoc:
send(method_id, *arguments)
elsif match.instantiator?
instantiator = match.instantiator
+ # def self.find_or_create_by_user_id(*args)
+ # guard_protected_attributes = false
+ #
+ # if args[0].is_a?(Hash)
+ # guard_protected_attributes = true
+ # attributes = args[0].with_indifferent_access
+ # find_attributes = attributes.slice(*[:user_id])
+ # else
+ # find_attributes = attributes = construct_attributes_from_arguments([:user_id], args)
+ # end
+ #
+ # options = { :conditions => find_attributes }
+ # set_readonly_option!(options)
+ #
+ # record = find(:first, options)
+ #
+ # if record.nil?
+ # record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
+ # yield(record) if block_given?
+ # record.save
+ # record
+ # else
+ # record
+ # end
+ # end
self.class_eval %{
def self.#{method_id}(*args)
guard_protected_attributes = false
@@ -1863,6 +1921,22 @@ module ActiveRecord #:nodoc:
}, __FILE__, __LINE__
send(method_id, *arguments, &block)
end
+ elsif match = DynamicScopeMatch.match(method_id)
+ attribute_names = match.attribute_names
+ super unless all_attributes_exists?(attribute_names)
+ if match.scope?
+ self.class_eval %{
+ def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
+ options = args.extract_options! # options = args.extract_options!
+ attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments(
+ [:#{attribute_names.join(',:')}], args # [:user_name, :password], args
+ ) # )
+ #
+ scoped(:conditions => attributes) # scoped(:conditions => attributes)
+ end # end
+ }, __FILE__, __LINE__
+ send(method_id, *arguments)
+ end
else
super
end
@@ -2401,9 +2475,9 @@ module ActiveRecord #:nodoc:
write_attribute(self.class.primary_key, value)
end
- # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet.
+ # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet; otherwise, returns false.
def new_record?
- defined?(@new_record) && @new_record
+ @new_record || false
end
# :call-seq:
@@ -3010,7 +3084,7 @@ module ActiveRecord #:nodoc:
end
Base.class_eval do
- extend QueryCache
+ extend QueryCache::ClassMethods
include Validations
include Locking::Optimistic, Locking::Pessimistic
include AttributeMethods
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 950bd72101..00c71090f3 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -14,12 +14,12 @@ module ActiveRecord
def dirties_query_cache(base, *method_names)
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__
- def #{method_name}_with_query_dirty(*args)
- clear_query_cache if @query_cache_enabled
- #{method_name}_without_query_dirty(*args)
- end
-
- alias_method_chain :#{method_name}, :query_dirty
+ def #{method_name}_with_query_dirty(*args) # def update_with_query_dirty(*args)
+ clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
+ #{method_name}_without_query_dirty(*args) # update_without_query_dirty(*args)
+ end # end
+ #
+ alias_method_chain :#{method_name}, :query_dirty # alias_method_chain :update, :query_dirty
end_code
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 fe9cbcf024..273f823e7f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -476,12 +476,12 @@ module ActiveRecord
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
class_eval <<-EOV
- def #{column_type}(*args)
- options = args.extract_options!
- column_names = args
-
- column_names.each { |name| column(name, '#{column_type}', options) }
- end
+ def #{column_type}(*args) # def string(*args)
+ options = args.extract_options! # options = args.extract_options!
+ column_names = args # column_names = args
+ #
+ column_names.each { |name| column(name, '#{column_type}', options) } # column_names.each { |name| column(name, 'string', options) }
+ end # end
EOV
end
@@ -676,24 +676,24 @@ module ActiveRecord
# t.string(:goat, :sheep)
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
class_eval <<-EOV
- def #{column_type}(*args)
- options = args.extract_options!
- column_names = args
-
- column_names.each do |name|
- column = ColumnDefinition.new(@base, name, '#{column_type}')
- if options[:limit]
- column.limit = options[:limit]
- elsif native['#{column_type}'.to_sym].is_a?(Hash)
- column.limit = native['#{column_type}'.to_sym][:limit]
- end
- column.precision = options[:precision]
- column.scale = options[:scale]
- column.default = options[:default]
- column.null = options[:null]
- @base.add_column(@table_name, name, column.sql_type, options)
- end
- end
+ def #{column_type}(*args) # def string(*args)
+ options = args.extract_options! # options = args.extract_options!
+ column_names = args # column_names = args
+ #
+ column_names.each do |name| # column_names.each do |name|
+ column = ColumnDefinition.new(@base, name, '#{column_type}') # column = ColumnDefinition.new(@base, name, 'string')
+ if options[:limit] # if options[:limit]
+ column.limit = options[:limit] # column.limit = options[:limit]
+ elsif native['#{column_type}'.to_sym].is_a?(Hash) # elsif native['string'.to_sym].is_a?(Hash)
+ column.limit = native['#{column_type}'.to_sym][:limit] # column.limit = native['string'.to_sym][:limit]
+ end # end
+ column.precision = options[:precision] # column.precision = options[:precision]
+ column.scale = options[:scale] # column.scale = options[:scale]
+ column.default = options[:default] # column.default = options[:default]
+ column.null = options[:null] # column.null = options[:null]
+ @base.add_column(@table_name, name, column.sql_type, options) # @base.add_column(@table_name, name, column.sql_type, options)
+ end # end
+ end # end
EOV
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 46d4b6c89c..60729c63db 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -13,23 +13,25 @@ module MysqlCompat #:nodoc:
# C driver >= 2.7 returns null values in each_hash
if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700)
target.class_eval <<-'end_eval'
- def all_hashes
- rows = []
- each_hash { |row| rows << row }
- rows
- end
+ def all_hashes # def all_hashes
+ rows = [] # rows = []
+ each_hash { |row| rows << row } # each_hash { |row| rows << row }
+ rows # rows
+ end # end
end_eval
# adapters before 2.7 don't have a version constant
# and don't return null values in each_hash
else
target.class_eval <<-'end_eval'
- def all_hashes
- rows = []
- all_fields = fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
- each_hash { |row| rows << all_fields.dup.update(row) }
- rows
- end
+ def all_hashes # def all_hashes
+ rows = [] # rows = []
+ all_fields = fetch_fields.inject({}) { |fields, f| # all_fields = fetch_fields.inject({}) { |fields, f|
+ fields[f.name] = nil; fields # fields[f.name] = nil; fields
+ } # }
+ each_hash { |row| rows << all_fields.dup.update(row) } # each_hash { |row| rows << all_fields.dup.update(row) }
+ rows # rows
+ end # end
end_eval
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 60ec01b95e..6685cb8663 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -950,13 +950,13 @@ module ActiveRecord
# should know about this but can't detect it there, so deal with it here.
money_precision = (postgresql_version >= 80300) ? 19 : 10
PostgreSQLColumn.module_eval(<<-end_eval)
- def extract_precision(sql_type)
- if sql_type =~ /^money$/
- #{money_precision}
- else
- super
- end
- end
+ def extract_precision(sql_type) # def extract_precision(sql_type)
+ if sql_type =~ /^money$/ # if sql_type =~ /^money$/
+ #{money_precision} # 19
+ else # else
+ super # super
+ end # end
+ end # end
end_eval
configure_connection
diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb
index a1760875ba..4c899f58e5 100644
--- a/activerecord/lib/active_record/dirty.rb
+++ b/activerecord/lib/active_record/dirty.rb
@@ -174,7 +174,7 @@ module ActiveRecord
alias_attribute_without_dirty(new_name, old_name)
DIRTY_SUFFIXES.each do |suffix|
module_eval <<-STR, __FILE__, __LINE__+1
- def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end
+ def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end
STR
end
end
diff --git a/activerecord/lib/active_record/dynamic_scope_match.rb b/activerecord/lib/active_record/dynamic_scope_match.rb
new file mode 100644
index 0000000000..f796ba669a
--- /dev/null
+++ b/activerecord/lib/active_record/dynamic_scope_match.rb
@@ -0,0 +1,25 @@
+module ActiveRecord
+ class DynamicScopeMatch
+ def self.match(method)
+ ds_match = self.new(method)
+ ds_match.scope ? ds_match : nil
+ end
+
+ def initialize(method)
+ @scope = true
+ case method.to_s
+ when /^scoped_by_([_a-zA-Z]\w*)$/
+ names = $1
+ else
+ @scope = nil
+ end
+ @attribute_names = names && names.split('_and_')
+ end
+
+ attr_reader :scope, :attribute_names
+
+ def scope?
+ !@scope.nil?
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index a8af89fcb9..eb92bc2545 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -1,20 +1,32 @@
module ActiveRecord
- module QueryCache
- # Enable the query cache within the block if Active Record is configured.
- def cache(&block)
- if ActiveRecord::Base.configurations.blank?
- yield
- else
- connection.cache(&block)
+ class QueryCache
+ module ClassMethods
+ # Enable the query cache within the block if Active Record is configured.
+ def cache(&block)
+ if ActiveRecord::Base.configurations.blank?
+ yield
+ else
+ connection.cache(&block)
+ end
end
+
+ # Disable the query cache within the block if Active Record is configured.
+ def uncached(&block)
+ if ActiveRecord::Base.configurations.blank?
+ yield
+ else
+ connection.uncached(&block)
+ end
+ end
+ end
+
+ def initialize(app)
+ @app = app
end
- # Disable the query cache within the block if Active Record is configured.
- def uncached(&block)
- if ActiveRecord::Base.configurations.blank?
- yield
- else
- connection.uncached(&block)
+ def call(env)
+ ActiveRecord::Base.cache do
+ @app.call(env)
end
end
end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index a9e0efa6fe..8dbe80a01a 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -23,8 +23,8 @@ module ActiveRecord
write_attribute('created_at', t) if respond_to?(:created_at) && created_at.nil?
write_attribute('created_on', t) if respond_to?(:created_on) && created_on.nil?
- write_attribute('updated_at', t) if respond_to?(:updated_at)
- write_attribute('updated_on', t) if respond_to?(:updated_on)
+ write_attribute('updated_at', t) if respond_to?(:updated_at) && updated_at.nil?
+ write_attribute('updated_on', t) if respond_to?(:updated_on) && updated_on.nil?
end
create_without_timestamps
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 617b3f440f..6a9690ba85 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -203,7 +203,6 @@ module ActiveRecord
if attr == "base"
full_messages << message
else
- #key = :"activerecord.att.#{@base.class.name.underscore.to_sym}.#{attr}"
attr_name = @base.class.human_attribute_name(attr)
full_messages << attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + message
end
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index 8c9ae8a031..45e74ea024 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -104,6 +104,14 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
authors.first.posts.first.special_comments.first.post.very_special_comment
end
end
+
+ def test_eager_association_loading_where_first_level_returns_nil
+ authors = Author.find(:all, :include => {:post_about_thinking => :comments}, :order => 'authors.id DESC')
+ assert_equal [authors(:mary), authors(:david)], authors
+ assert_no_queries do
+ authors[1].post_about_thinking.comments.first
+ end
+ end
end
require 'models/vertex'
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 42063d18a3..afbd9fddf9 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -729,12 +729,12 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal authors(:david), assert_no_queries { posts[0].author}
posts = assert_queries(2) do
- Post.find(:all, :include => :author, :joins => {:taggings => :tag}, :conditions => "tags.name = 'General'")
+ Post.find(:all, :include => :author, :joins => {:taggings => :tag}, :conditions => "tags.name = 'General'", :order => 'posts.id')
end
assert_equal posts(:welcome, :thinking), posts
posts = assert_queries(2) do
- Post.find(:all, :include => :author, :joins => {:taggings => {:tag => :taggings}}, :conditions => "taggings_tags.super_tag_id=2")
+ Post.find(:all, :include => :author, :joins => {:taggings => {:tag => :taggings}}, :conditions => "taggings_tags.super_tag_id=2", :order => 'posts.id')
end
assert_equal posts(:welcome, :thinking), posts
@@ -771,4 +771,19 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal author_addresses(:david_address), authors[0].author_address
end
+ def test_preload_belongs_to_uses_exclusive_scope
+ people = Person.males.find(:all, :include => :primary_contact)
+ assert_not_equal people.length, 0
+ people.each do |person|
+ assert_no_queries {assert_not_nil person.primary_contact}
+ assert_equal Person.find(person.id).primary_contact, person.primary_contact
+ end
+ end
+
+ def test_preload_has_many_uses_exclusive_scope
+ people = Person.males.find :all, :include => :agents
+ people.each do |person|
+ assert_equal Person.find(person.id).agents, person.agents
+ 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 816ceb6855..20b9acda44 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -665,6 +665,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Client.find_all_by_client_of(firm.id).size
end
+ def test_dependent_association_respects_optional_hash_conditions_on_delete
+ firm = companies(:odegy)
+ Client.create(:client_of => firm.id, :name => "BigShot Inc.")
+ Client.create(:client_of => firm.id, :name => "SmallTime Inc.")
+ # only one of two clients is included in the association due to the :conditions key
+ assert_equal 2, Client.find_all_by_client_of(firm.id).size
+ assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size
+ firm.destroy
+ # only the correctly associated client should have been deleted
+ assert_equal 1, Client.find_all_by_client_of(firm.id).size
+ end
+
+
def test_creation_respects_hash_condition
ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 5f54931d00..0f03dae829 100755
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -16,6 +16,7 @@ require 'models/post'
require 'models/comment'
require 'models/minimalistic'
require 'models/warehouse_thing'
+require 'models/parrot'
require 'rexml/document'
class Category < ActiveRecord::Base; end
@@ -1197,6 +1198,11 @@ class BasicsTest < ActiveRecord::TestCase
assert b_true.value?
end
+ def test_new_record_returns_boolean
+ assert_equal Topic.new.new_record?, true
+ assert_equal Topic.find(1).new_record?, false
+ end
+
def test_clone
topic = Topic.find(1)
cloned_topic = nil
@@ -2071,6 +2077,15 @@ class BasicsTest < ActiveRecord::TestCase
ActiveRecord::Base.logger = original_logger
end
+ def test_create_with_custom_timestamps
+ custom_datetime = 1.hour.ago.beginning_of_day
+
+ %w(created_at created_on updated_at updated_on).each do |attribute|
+ parrot = LiveParrot.create(:name => "colombian", attribute => custom_datetime)
+ assert_equal custom_datetime, parrot[attribute]
+ end
+ end
+
private
def with_kcode(kcode)
if RUBY_VERSION < '1.9'
diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb
index 6372b4f6aa..80a06116ad 100644
--- a/activerecord/test/cases/method_scoping_test.rb
+++ b/activerecord/test/cases/method_scoping_test.rb
@@ -27,6 +27,24 @@ class MethodScopingTest < ActiveRecord::TestCase
end
end
+ def test_scoped_find_last
+ highest_salary = Developer.find(:first, :order => "salary DESC")
+
+ Developer.with_scope(:find => { :order => "salary" }) do
+ assert_equal highest_salary, Developer.last
+ end
+ end
+
+ def test_scoped_find_last_preserves_scope
+ lowest_salary = Developer.find(:first, :order => "salary ASC")
+ highest_salary = Developer.find(:first, :order => "salary DESC")
+
+ Developer.with_scope(:find => { :order => "salary" }) do
+ assert_equal highest_salary, Developer.last
+ assert_equal lowest_salary, Developer.first
+ end
+ end
+
def test_scoped_find_combines_conditions
Developer.with_scope(:find => { :conditions => "salary = 9000" }) do
assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => "name = 'Jamis'")
diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb
index 64e899780c..b152f95a15 100644
--- a/activerecord/test/cases/named_scope_test.rb
+++ b/activerecord/test/cases/named_scope_test.rb
@@ -278,3 +278,23 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size
end
end
+
+class DynamicScopeMatchTest < ActiveRecord::TestCase
+ def test_scoped_by_no_match
+ assert_nil ActiveRecord::DynamicScopeMatch.match("not_scoped_at_all")
+ end
+
+ def test_scoped_by
+ match = ActiveRecord::DynamicScopeMatch.match("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
+
+class DynamicScopeTest < ActiveRecord::TestCase
+ def test_dynamic_scope
+ assert_equal Post.scoped_by_author_id(1).find(1), Post.find(1)
+ assert_equal Post.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, Post.find(:first, :conditions => { :author_id => 1, :title => "Welcome to the weblog"})
+ end
+end
diff --git a/activerecord/test/fixtures/people.yml b/activerecord/test/fixtures/people.yml
index d5a69e561d..3babb1fe59 100644
--- a/activerecord/test/fixtures/people.yml
+++ b/activerecord/test/fixtures/people.yml
@@ -1,6 +1,15 @@
michael:
id: 1
first_name: Michael
+ primary_contact_id: 2
+ gender: M
david:
id: 2
- first_name: David \ No newline at end of file
+ first_name: David
+ primary_contact_id: 3
+ gender: M
+susan:
+ id: 3
+ first_name: Susan
+ primary_contact_id: 2
+ gender: F \ No newline at end of file
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 0e3fafa37c..3b27a9e272 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -80,6 +80,7 @@ class ExclusivelyDependentFirm < Company
has_one :account, :foreign_key => "firm_id", :dependent => :delete
has_many :dependent_sanitized_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => "name = 'BigShot Inc.'"
has_many :dependent_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => ["name = ?", 'BigShot Inc.']
+ has_many :dependent_hash_conditional_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all, :conditions => {:name => 'BigShot Inc.'}
end
class Client < Company
diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb
index 430d0b38f7..ec2f684a6e 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -7,4 +7,10 @@ class Person < ActiveRecord::Base
has_many :jobs, :through => :references
has_one :favourite_reference, :class_name => 'Reference', :conditions => ['favourite=?', true]
has_many :posts_with_comments_sorted_by_comment_id, :through => :readers, :source => :post, :include => :comments, :order => 'comments.id'
+
+ belongs_to :primary_contact, :class_name => 'Person'
+ has_many :agents, :class_name => 'Person', :foreign_key => 'primary_contact_id'
+
+ named_scope :males, :conditions => { :gender => 'M' }
+ named_scope :females, :conditions => { :gender => 'F' }
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index fbacc692b4..8199cb8fc7 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -298,8 +298,10 @@ ActiveRecord::Schema.define do
end
create_table :people, :force => true do |t|
- t.string :first_name, :null => false
- t.integer :lock_version, :null => false, :default => 0
+ t.string :first_name, :null => false
+ t.references :primary_contact
+ t.string :gender, :limit => 1
+ t.integer :lock_version, :null => false, :default => 0
end
create_table :pets, :primary_key => :pet_id ,:force => true do |t|