aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorXavier Noria <fxn@hashref.com>2011-03-05 12:06:43 +0100
committerXavier Noria <fxn@hashref.com>2011-03-05 12:06:43 +0100
commit9092052cb010ab9155cff196ef139cec41695324 (patch)
tree6b8f39ddddef07105820ce825260ebc4e265b12a
parent34f5628a072f7afa677d25c9559076d5c21721ce (diff)
parent20768176292cbcb883ab152b4aa9ed8c664771cd (diff)
downloadrails-9092052cb010ab9155cff196ef139cec41695324.tar.gz
rails-9092052cb010ab9155cff196ef139cec41695324.tar.bz2
rails-9092052cb010ab9155cff196ef139cec41695324.zip
merges rails
-rw-r--r--.gitignore1
-rw-r--r--actionmailer/test/asset_host_test.rb4
-rw-r--r--actionmailer/test/base_test.rb6
-rw-r--r--actionpack/CHANGELOG4
-rw-r--r--actionpack/lib/action_controller/caching/actions.rb8
-rw-r--r--actionpack/lib/action_controller/metal.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/stack.rb58
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb8
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb7
-rw-r--r--actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb2
-rw-r--r--actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb2
-rw-r--r--actionpack/lib/action_view/helpers/form_helper.rb24
-rw-r--r--actionpack/test/controller/caching_test.rb5
-rw-r--r--actionpack/test/dispatch/middleware_stack/middleware_test.rb77
-rw-r--r--actionpack/test/template/asset_tag_helper_test.rb24
-rw-r--r--actionpack/test/template/date_helper_test.rb17
-rw-r--r--actionpack/test/template/form_helper_test.rb135
-rw-r--r--activemodel/CHANGELOG2
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb44
-rw-r--r--activemodel/lib/active_model/errors.rb4
-rw-r--r--activemodel/lib/active_model/naming.rb15
-rw-r--r--activemodel/lib/active_model/translation.rb2
-rw-r--r--activemodel/test/cases/attribute_methods_test.rb10
-rw-r--r--activemodel/test/cases/translation_test.rb7
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb19
-rw-r--r--activemodel/test/models/person.rb7
-rw-r--r--activerecord/CHANGELOG12
-rw-r--r--activerecord/lib/active_record.rb1
-rw-r--r--activerecord/lib/active_record/association_preload.rb433
-rw-r--r--activerecord/lib/active_record/associations.rb4
-rw-r--r--activerecord/lib/active_record/associations/association.rb4
-rw-r--r--activerecord/lib/active_record/associations/class_methods/join_dependency.rb233
-rw-r--r--activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb281
-rw-r--r--activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb26
-rw-r--r--activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb80
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb231
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb279
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb24
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb78
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb177
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb126
-rw-r--r--activerecord/lib/active_record/associations/preloader/belongs_to.rb17
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb24
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb60
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many.rb17
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb15
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one.rb23
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one_through.rb9
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb21
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb66
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb11
-rw-r--r--activerecord/lib/active_record/base.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb10
-rw-r--r--activerecord/lib/active_record/reflection.rb10
-rw-r--r--activerecord/lib/active_record/relation.rb6
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb14
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb4
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb6
-rw-r--r--activerecord/lib/active_record/test_case.rb1
-rw-r--r--activerecord/test/cases/associations/eager_test.rb45
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb2
-rw-r--r--activerecord/test/cases/base_test.rb1
-rw-r--r--activerecord/test/cases/calculations_test.rb30
-rw-r--r--activerecord/test/cases/inheritance_test.rb2
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb5
-rw-r--r--activerecord/test/models/category.rb3
-rw-r--r--activesupport/CHANGELOG9
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb51
-rw-r--r--activesupport/lib/active_support/configurable.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date/calculations.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/date/zones.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/calculations.rb13
-rw-r--r--activesupport/lib/active_support/core_ext/hash/reverse_merge.rb19
-rw-r--r--activesupport/lib/active_support/core_ext/object/blank.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/time/calculations.rb17
-rw-r--r--activesupport/lib/active_support/dependencies.rb73
-rw-r--r--activesupport/lib/active_support/ordered_options.rb2
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb2
-rw-r--r--activesupport/test/class_cache_test.rb108
-rw-r--r--activesupport/test/configurable_test.rb8
-rw-r--r--activesupport/test/core_ext/date_ext_test.rb32
-rw-r--r--activesupport/test/core_ext/date_time_ext_test.rb8
-rw-r--r--activesupport/test/core_ext/duration_test.rb10
-rw-r--r--activesupport/test/core_ext/numeric_ext_test.rb10
-rw-r--r--activesupport/test/core_ext/time_ext_test.rb16
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb15
-rw-r--r--activesupport/test/dependencies_test.rb8
-rw-r--r--activesupport/test/inflector_test.rb8
-rw-r--r--activesupport/test/test_case_test.rb4
-rwxr-xr-xci/ci_build.rb48
-rw-r--r--railties/guides/source/active_support_core_extensions.textile10
-rw-r--r--railties/guides/source/contributing_to_ruby_on_rails.textile15
-rw-r--r--railties/lib/rails/commands/server.rb4
-rw-r--r--railties/lib/rails/engine.rb5
-rw-r--r--railties/lib/rails/generators/actions.rb7
-rw-r--r--railties/lib/rails/generators/base.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/index.html16
-rw-r--r--railties/test/generators/actions_test.rb25
98 files changed, 2114 insertions, 1334 deletions
diff --git a/.gitignore b/.gitignore
index 35402a22f7..8daa1e4dcd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ activerecord/doc
actionpack/doc
actionmailer/doc
activesupport/doc
+activesupport/test/tmp
activemodel/test/fixtures/fixture_database.sqlite3
actionpack/test/tmp
activesupport/test/fixtures/isolation_test
diff --git a/actionmailer/test/asset_host_test.rb b/actionmailer/test/asset_host_test.rb
index 069860ff06..b24eca5fbb 100644
--- a/actionmailer/test/asset_host_test.rb
+++ b/actionmailer/test/asset_host_test.rb
@@ -29,7 +29,7 @@ class AssetHostTest < Test::Unit::TestCase
assert_equal %Q{<img alt="Somelogo" src="http://www.example.com/images/somelogo.png" />}, mail.body.to_s.strip
end
- def test_asset_host_as_one_arguement_proc
+ def test_asset_host_as_one_argument_proc
AssetHostMailer.config.asset_host = Proc.new { |source|
if source.starts_with?('/images')
"http://images.example.com"
@@ -41,7 +41,7 @@ class AssetHostTest < Test::Unit::TestCase
assert_equal %Q{<img alt="Somelogo" src="http://images.example.com/images/somelogo.png" />}, mail.body.to_s.strip
end
- def test_asset_host_as_two_arguement_proc
+ def test_asset_host_as_two_argument_proc
ActionController::Base.config.asset_host = Proc.new {|source,request|
if request && request.ssl?
"https://www.example.com"
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index 7ed9d4a5c0..1b793d255e 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -32,21 +32,21 @@ class BaseTest < ActiveSupport::TestCase
end
test "mail() with bcc, cc, content_type, charset, mime_version, reply_to and date" do
- @time = Time.now.beginning_of_day.to_datetime
+ time = Time.now.beginning_of_day.to_datetime
email = BaseMailer.welcome(:bcc => 'bcc@test.lindsaar.net',
:cc => 'cc@test.lindsaar.net',
:content_type => 'multipart/mixed',
:charset => 'iso-8559-1',
:mime_version => '2.0',
:reply_to => 'reply-to@test.lindsaar.net',
- :date => @time)
+ :date => time)
assert_equal(['bcc@test.lindsaar.net'], email.bcc)
assert_equal(['cc@test.lindsaar.net'], email.cc)
assert_equal('multipart/mixed; charset=iso-8559-1', email.content_type)
assert_equal('iso-8559-1', email.charset)
assert_equal('2.0', email.mime_version)
assert_equal(['reply-to@test.lindsaar.net'], email.reply_to)
- assert_equal(@time, email.date)
+ assert_equal(time, email.date)
end
test "mail() renders the template using the method being processed" do
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG
index ee9d30e1fb..fc3410ba6e 100644
--- a/actionpack/CHANGELOG
+++ b/actionpack/CHANGELOG
@@ -1,5 +1,9 @@
*Rails 3.1.0 (unreleased)*
+* ActionDispatch::MiddlewareStack now uses composition over inheritance. It is
+no longer an array which means there may be methods missing that were not
+tested.
+
* Add an :authenticity_token option to form_tag for custom handling or to omit the token (pass :authenticity_token => false). [Jakub Kuźma, Igor Wiedler]
* HTML5 button_tag helper. [Rizwan Reza]
diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb
index baff6d4be9..2c8a6e4d4d 100644
--- a/actionpack/lib/action_controller/caching/actions.rb
+++ b/actionpack/lib/action_controller/caching/actions.rb
@@ -103,12 +103,14 @@ module ActionController #:nodoc:
end
def _save_fragment(name, options)
- return unless caching_allowed?
-
content = response_body
content = content.join if content.is_a?(Array)
- write_fragment(name, content, options)
+ if caching_allowed?
+ write_fragment(name, content, options)
+ else
+ content
+ end
end
protected
diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb
index b2c8053584..e5db31061b 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -36,7 +36,7 @@ module ActionController
action = action.to_s
raise "MiddlewareStack#build requires an app" unless app
- reverse.inject(app) do |a, middleware|
+ middlewares.reverse.inject(app) do |a, middleware|
middleware.valid?(action) ?
middleware.build(a) : a
end
diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb
index e3cd779756..a4308f528c 100644
--- a/actionpack/lib/action_dispatch/middleware/stack.rb
+++ b/actionpack/lib/action_dispatch/middleware/stack.rb
@@ -2,17 +2,26 @@ require "active_support/inflector/methods"
require "active_support/dependencies"
module ActionDispatch
- class MiddlewareStack < Array
+ class MiddlewareStack
class Middleware
- attr_reader :args, :block
+ attr_reader :args, :block, :name, :classcache
def initialize(klass_or_name, *args, &block)
- @ref = ActiveSupport::Dependencies::Reference.new(klass_or_name)
+ @klass = nil
+
+ if klass_or_name.respond_to?(:name)
+ @klass = klass_or_name
+ @name = @klass.name
+ else
+ @name = klass_or_name.to_s
+ end
+
+ @classcache = ActiveSupport::Dependencies::Reference
@args, @block = args, block
end
def klass
- @ref.get
+ @klass || classcache[@name]
end
def ==(middleware)
@@ -22,7 +31,7 @@ module ActionDispatch
when Class
klass == middleware
else
- normalize(@ref.name) == normalize(middleware)
+ normalize(@name) == normalize(middleware)
end
end
@@ -41,18 +50,39 @@ module ActionDispatch
end
end
- # Use this instead of super to work around a warning.
- alias :array_initialize :initialize
+ include Enumerable
+
+ attr_accessor :middlewares
def initialize(*args)
- array_initialize(*args)
+ @middlewares = []
yield(self) if block_given?
end
+ def each
+ @middlewares.each { |x| yield x }
+ end
+
+ def size
+ middlewares.size
+ end
+
+ def last
+ middlewares.last
+ end
+
+ def [](i)
+ middlewares[i]
+ end
+
+ def initialize_copy(other)
+ self.middlewares = other.middlewares.dup
+ end
+
def insert(index, *args, &block)
index = assert_index(index, :before)
middleware = self.class::Middleware.new(*args, &block)
- super(index, middleware)
+ middlewares.insert(index, middleware)
end
alias_method :insert_before, :insert
@@ -67,21 +97,25 @@ module ActionDispatch
delete(target)
end
+ def delete(target)
+ middlewares.delete target
+ end
+
def use(*args, &block)
middleware = self.class::Middleware.new(*args, &block)
- push(middleware)
+ middlewares.push(middleware)
end
def build(app = nil, &block)
app ||= block
raise "MiddlewareStack#build requires an app" unless app
- reverse.inject(app) { |a, e| e.build(a) }
+ middlewares.reverse.inject(app) { |a, e| e.build(a) }
end
protected
def assert_index(index, where)
- i = index.is_a?(Integer) ? index : self.index(index)
+ i = index.is_a?(Integer) ? index : middlewares.index(index)
raise "No such middleware to insert #{where}: #{index.inspect}" unless i
i
end
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index 913b899e20..c57f694c4d 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -3,10 +3,10 @@ require 'rack/utils'
module ActionDispatch
class FileHandler
def initialize(at, root)
- @at, @root = at.chomp('/'), root.chomp('/')
- @compiled_at = (Regexp.compile(/^#{Regexp.escape(at)}/) unless @at.blank?)
- @compiled_root = Regexp.compile(/^#{Regexp.escape(root)}/)
- @file_server = ::Rack::File.new(@root)
+ @at, @root = at.chomp('/'), root.chomp('/')
+ @compiled_at = @at.blank? ? nil : /^#{Regexp.escape(at)}/
+ @compiled_root = /^#{Regexp.escape(root)}/
+ @file_server = ::Rack::File.new(@root)
end
def match?(path)
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 4b4e9da173..fc86d52a3a 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -50,12 +50,13 @@ module ActionDispatch
private
def controller_reference(controller_param)
+ controller_name = "#{controller_param.camelize}Controller"
+
unless controller = @controllers[controller_param]
- controller_name = "#{controller_param.camelize}Controller"
controller = @controllers[controller_param] =
- ActiveSupport::Dependencies.ref(controller_name)
+ ActiveSupport::Dependencies.reference(controller_name)
end
- controller.get
+ controller.get(controller_name)
end
def dispatch(controller, action, env)
diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb
index b9126af944..82bbfcc7d2 100644
--- a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb
+++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb
@@ -69,7 +69,7 @@ module ActionView
def register_javascript_expansion(expansions)
js_expansions = JavascriptIncludeTag.expansions
expansions.each do |key, values|
- js_expansions[key] = (js_expansions[key] || []) | Array(values) if values
+ js_expansions[key] = (js_expansions[key] || []) | Array(values)
end
end
end
diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb
index f3e041de95..a48c87b49a 100644
--- a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb
+++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb
@@ -46,7 +46,7 @@ module ActionView
def register_stylesheet_expansion(expansions)
style_expansions = StylesheetIncludeTag.expansions
expansions.each do |key, values|
- style_expansions[key] = (style_expansions[key] || []) | Array(values) if values
+ style_expansions[key] = (style_expansions[key] || []) | Array(values)
end
end
end
diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb
index befaa3e8d9..669ccd2a2d 100644
--- a/actionpack/lib/action_view/helpers/form_helper.rb
+++ b/actionpack/lib/action_view/helpers/form_helper.rb
@@ -262,6 +262,24 @@ module ActionView
# ...
# </form>
#
+ # === Removing hidden model id's
+ #
+ # The form_for method automatically includes the model id as a hidden field in the form.
+ # This is used to maintain the correlation between the form data and it's associated model.
+ # Some ORM systems do not use id's on nested models so in this case you want to be able
+ # to disable the hidden id.
+ #
+ # In the following example the Post model has many Comments stored within it in a NoSQL database,
+ # thus there is no primary key for comments.
+ #
+ # Example:
+ #
+ # <%= form(@post) do |f| %>
+ # <% f.fields_for(:comments, :include_id => false) do |cf| %>
+ # ...
+ # <% end %>
+ # <% end %>
+ #
# === Customized form builders
#
# You can also build forms using a customized FormBuilder class. Subclass
@@ -332,7 +350,7 @@ module ActionView
options[:html][:remote] = options.delete(:remote)
options[:html][:authenticity_token] = options.delete(:authenticity_token)
-
+
builder = options[:parent_builder] = instantiate_builder(object_name, object, options, &proc)
fields_for = fields_for(object_name, object, options, &proc)
default_options = builder.multipart? ? { :multipart => true } : {}
@@ -1326,7 +1344,9 @@ module ActionView
def fields_for_nested_model(name, object, options, block)
object = convert_to_model(object)
- options[:hidden_field_id] = object.persisted?
+ parent_include_id = self.options.fetch(:include_id, true)
+ include_id = options.fetch(:include_id, parent_include_id)
+ options[:hidden_field_id] = object.persisted? && include_id
@template.fields_for(name, object, options, &block)
end
diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb
index cc393d3ef4..01f3e8f2b6 100644
--- a/actionpack/test/controller/caching_test.rb
+++ b/actionpack/test/controller/caching_test.rb
@@ -559,6 +559,11 @@ class ActionCacheTest < ActionController::TestCase
assert_response 404
end
+ def test_four_oh_four_renders_content
+ get :four_oh_four
+ assert_equal "404'd!", @response.body
+ end
+
def test_simple_runtime_error_returns_500_for_multiple_requests
get :simple_runtime_error
assert_response 500
diff --git a/actionpack/test/dispatch/middleware_stack/middleware_test.rb b/actionpack/test/dispatch/middleware_stack/middleware_test.rb
new file mode 100644
index 0000000000..9607f026db
--- /dev/null
+++ b/actionpack/test/dispatch/middleware_stack/middleware_test.rb
@@ -0,0 +1,77 @@
+require 'abstract_unit'
+require 'action_dispatch/middleware/stack'
+
+module ActionDispatch
+ class MiddlewareStack
+ class MiddlewareTest < ActiveSupport::TestCase
+ class Omg; end
+
+ {
+ 'concrete' => Omg,
+ 'anonymous' => Class.new
+ }.each do |name, klass|
+
+ define_method("test_#{name}_klass") do
+ mw = Middleware.new klass
+ assert_equal klass, mw.klass
+ end
+
+ define_method("test_#{name}_==") do
+ mw1 = Middleware.new klass
+ mw2 = Middleware.new klass
+ assert_equal mw1, mw2
+ end
+
+ end
+
+ def test_string_class
+ mw = Middleware.new Omg.name
+ assert_equal Omg, mw.klass
+ end
+
+ def test_double_equal_works_with_classes
+ k = Class.new
+ mw = Middleware.new k
+ assert_operator mw, :==, k
+
+ result = mw != Class.new
+ assert result, 'middleware should not equal other anon class'
+ end
+
+ def test_double_equal_works_with_strings
+ mw = Middleware.new Omg
+ assert_operator mw, :==, Omg.name
+ end
+
+ def test_double_equal_normalizes_strings
+ mw = Middleware.new Omg
+ assert_operator mw, :==, "::#{Omg.name}"
+ end
+
+ def test_middleware_loads_classnames_from_cache
+ mw = Class.new(Middleware) {
+ attr_accessor :classcache
+ }.new(Omg.name)
+
+ fake_cache = { mw.name => Omg }
+ mw.classcache = fake_cache
+
+ assert_equal Omg, mw.klass
+
+ fake_cache[mw.name] = Middleware
+ assert_equal Middleware, mw.klass
+ end
+
+ def test_middleware_always_returns_class
+ mw = Class.new(Middleware) {
+ attr_accessor :classcache
+ }.new(Omg)
+
+ fake_cache = { mw.name => Middleware }
+ mw.classcache = fake_cache
+
+ assert_equal Omg, mw.klass
+ end
+ end
+ end
+end
diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb
index a1a6b5f1d0..1bf748af14 100644
--- a/actionpack/test/template/asset_tag_helper_test.rb
+++ b/actionpack/test/template/asset_tag_helper_test.rb
@@ -303,8 +303,17 @@ class AssetTagHelperTest < ActionView::TestCase
end
def test_custom_javascript_expansions_with_undefined_symbol
+ assert_raise(ArgumentError) { javascript_include_tag('first', :unknown, 'last') }
+ end
+
+ def test_custom_javascript_expansions_with_nil_value
ActionView::Helpers::AssetTagHelper::register_javascript_expansion :monkey => nil
- assert_raise(ArgumentError) { javascript_include_tag('first', :monkey, 'last') }
+ assert_dom_equal %(<script src="/javascripts/first.js" type="text/javascript"></script>\n<script src="/javascripts/last.js" type="text/javascript"></script>), javascript_include_tag('first', :monkey, 'last')
+ end
+
+ def test_custom_javascript_expansions_with_empty_array_value
+ ActionView::Helpers::AssetTagHelper::register_javascript_expansion :monkey => []
+ assert_dom_equal %(<script src="/javascripts/first.js" type="text/javascript"></script>\n<script src="/javascripts/last.js" type="text/javascript"></script>), javascript_include_tag('first', :monkey, 'last')
end
def test_custom_javascript_and_stylesheet_expansion_with_same_name
@@ -379,9 +388,18 @@ class AssetTagHelperTest < ActionView::TestCase
assert_dom_equal %(<link href="/stylesheets/london.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/wellington.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/amsterdam.css" media="screen" rel="stylesheet" type="text/css" />), stylesheet_link_tag('london', :cities)
end
- def test_custom_stylesheet_expansions_with_undefined_symbol
+ def test_custom_stylesheet_expansions_with_unknown_symbol
+ assert_raise(ArgumentError) { stylesheet_link_tag('first', :unknown, 'last') }
+ end
+
+ def test_custom_stylesheet_expansions_with_nil_value
ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :monkey => nil
- assert_raise(ArgumentError) { stylesheet_link_tag('first', :monkey, 'last') }
+ assert_dom_equal %(<link href="/stylesheets/first.css" rel="stylesheet" type="text/css" media="screen" />\n<link href="/stylesheets/last.css" rel="stylesheet" type="text/css" media="screen" />), stylesheet_link_tag('first', :monkey, 'last')
+ end
+
+ def test_custom_stylesheet_expansions_with_empty_array_value
+ ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :monkey => []
+ assert_dom_equal %(<link href="/stylesheets/first.css" rel="stylesheet" type="text/css" media="screen" />\n<link href="/stylesheets/last.css" rel="stylesheet" type="text/css" media="screen" />), stylesheet_link_tag('first', :monkey, 'last')
end
def test_registering_stylesheet_expansions_merges_with_existing_expansions
diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb
index 3334f4ffb0..aca2fef170 100644
--- a/actionpack/test/template/date_helper_test.rb
+++ b/actionpack/test/template/date_helper_test.rb
@@ -1882,10 +1882,17 @@ class DateHelperTest < ActionView::TestCase
end
def test_datetime_select_defaults_to_time_zone_now_when_config_time_zone_is_set
- time = stub(:year => 2004, :month => 6, :day => 15, :hour => 16, :min => 35, :sec => 0)
- time_zone = mock()
- time_zone.expects(:now).returns time
- Time.zone_default = time_zone
+ # The love zone is UTC+0
+ mytz = Class.new(ActiveSupport::TimeZone) {
+ attr_accessor :now
+ }.create('tenderlove', 0)
+
+ now = Time.mktime(2004, 6, 15, 16, 35, 0)
+ mytz.now = now
+ Time.zone = mytz
+
+ assert_equal mytz, Time.zone
+
@post = Post.new
expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
@@ -1912,7 +1919,7 @@ class DateHelperTest < ActionView::TestCase
assert_dom_equal expected, datetime_select("post", "updated_at")
ensure
- Time.zone_default = nil
+ Time.zone = nil
end
def test_datetime_select_with_html_options_within_fields_for
diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb
index 31da26de7f..359b078466 100644
--- a/actionpack/test/template/form_helper_test.rb
+++ b/actionpack/test/template/form_helper_test.rb
@@ -1103,6 +1103,61 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id
+ @post.author = Author.new(321)
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author, :include_id => false) { |af|
+ af.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do
+ '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_inherited
+ @post.author = Author.new(321)
+
+ form_for(@post, :include_id => false) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author) { |af|
+ af.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do
+ '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_override
+ @post.author = Author.new(321)
+
+ form_for(@post, :include_id => false) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author, :include_id => true) { |af|
+ af.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do
+ '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' +
+ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
def test_nested_fields_for_with_existing_records_on_a_nested_attributes_one_to_one_association_with_explicit_hidden_field_placement
@post.author = Author.new(321)
@@ -1146,6 +1201,86 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+ @post.author = Author.new(321)
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author) { |af|
+ concat af.text_field(:name)
+ }
+ @post.comments.each do |comment|
+ concat f.fields_for(:comments, comment, :include_id => false) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do
+ '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' +
+ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_inherited
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+ @post.author = Author.new(321)
+
+ form_for(@post, :include_id => false) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author) { |af|
+ concat af.text_field(:name)
+ }
+ @post.comments.each do |comment|
+ concat f.fields_for(:comments, comment) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do
+ '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_override
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+ @post.author = Author.new(321)
+
+ form_for(@post, :include_id => false) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author, :include_id => true) { |af|
+ concat af.text_field(:name)
+ }
+ @post.comments.each do |comment|
+ concat f.fields_for(:comments, comment) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do
+ '<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' +
+ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_using_erb_and_inline_block
@post.comments = Array.new(2) { |id| Comment.new(id + 1) }
diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG
index 9dd5e03685..3082c7186a 100644
--- a/activemodel/CHANGELOG
+++ b/activemodel/CHANGELOG
@@ -2,6 +2,8 @@
* Added ActiveModel::SecurePassword to encapsulate dead-simple password usage with BCrypt encryption and salting [DHH]
+* ActiveModel::AttributeMethods allows attributes to be defined on demand [Alexander Uvarov]
+
*Rails 3.0.2 (unreleased)*
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 8f3782eb48..2a99450a3d 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -260,30 +260,30 @@ module ActiveModel
# end
# end
def define_attribute_methods(attr_names)
- return if attribute_methods_generated?
- attr_names.each do |attr_name|
- attribute_method_matchers.each do |matcher|
- unless instance_method_already_implemented?(matcher.method_name(attr_name))
- generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}"
+ attr_names.each { |attr_name| define_attribute_method(attr_name) }
+ end
+
+ def define_attribute_method(attr_name)
+ attribute_method_matchers.each do |matcher|
+ unless instance_method_already_implemented?(matcher.method_name(attr_name))
+ generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}"
- if respond_to?(generate_method)
- send(generate_method, attr_name)
- else
- method_name = matcher.method_name(attr_name)
+ if respond_to?(generate_method)
+ send(generate_method, attr_name)
+ else
+ method_name = matcher.method_name(attr_name)
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- if method_defined?('#{method_name}')
- undef :'#{method_name}'
- end
- define_method('#{method_name}') do |*args|
- send('#{matcher.method_missing_target}', '#{attr_name}', *args)
- end
- STR
- end
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ if method_defined?('#{method_name}')
+ undef :'#{method_name}'
+ end
+ define_method('#{method_name}') do |*args|
+ send('#{matcher.method_missing_target}', '#{attr_name}', *args)
+ end
+ STR
end
end
end
- @attribute_methods_generated = true
end
# Removes all the previously dynamically defined methods from the class
@@ -291,7 +291,6 @@ module ActiveModel
generated_attribute_methods.module_eval do
instance_methods.each { |m| undef_method(m) }
end
- @attribute_methods_generated = nil
end
# Returns true if the attribute methods defined have been generated.
@@ -303,11 +302,6 @@ module ActiveModel
end
end
- # Returns true if the attribute methods defined have been generated.
- def attribute_methods_generated?
- @attribute_methods_generated ||= nil
- end
-
protected
def instance_method_already_implemented?(method_name)
method_defined?(method_name)
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 5e3cf510b0..c2f0228785 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -295,8 +295,8 @@ module ActiveModel
type = options.delete(:message) if options[:message].is_a?(Symbol)
defaults = @base.class.lookup_ancestors.map do |klass|
- [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
- :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
+ [ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.attributes.#{attribute}.#{type}",
+ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.#{type}" ]
end
defaults << options.delete(:message)
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 3f430f94a6..eb9b847509 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -4,7 +4,7 @@ require 'active_support/core_ext/module/introspection'
module ActiveModel
class Name < String
- attr_reader :singular, :plural, :element, :collection, :partial_path, :route_key, :param_key, :i18n_key
+ attr_reader :singular, :plural, :element, :collection, :partial_path, :route_key, :param_key
alias_method :cache_key, :collection
def initialize(klass, namespace = nil)
@@ -20,7 +20,6 @@ module ActiveModel
@partial_path = "#{@collection}/#{@element}".freeze
@param_key = (namespace ? _singularize(@unnamespaced) : @singular).freeze
@route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural).freeze
- @i18n_key = _singularize(self, '.').to_sym
end
# Transform the model name into a more humane format, using I18n. By default,
@@ -34,7 +33,7 @@ module ActiveModel
@klass.respond_to?(:i18n_scope)
defaults = @klass.lookup_ancestors.map do |klass|
- klass.model_name.i18n_key
+ klass.model_name.underscore.to_sym
end
defaults << options[:default] if options[:default]
@@ -45,10 +44,9 @@ module ActiveModel
end
private
-
- def _singularize(string, replacement='_')
- ActiveSupport::Inflector.underscore(string).tr('/', replacement)
- end
+ def _singularize(str)
+ ActiveSupport::Inflector.underscore(str).tr('/', '_')
+ end
end
# == Active Model Naming
@@ -64,9 +62,6 @@ module ActiveModel
# BookCover.model_name # => "BookCover"
# BookCover.model_name.human # => "Book cover"
#
- # BookCover.model_name.i18n_key # => "book_cover"
- # BookModule::BookCover.model_name.i18n_key # => "book_module.book_cover"
- #
# Providing the functionality that ActiveModel::Naming provides in your object
# is required to pass the Active Model Lint test. So either extending the provided
# method below, or rolling your own is required.
diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb
index 920a133159..dbb76244e4 100644
--- a/activemodel/lib/active_model/translation.rb
+++ b/activemodel/lib/active_model/translation.rb
@@ -44,7 +44,7 @@ module ActiveModel
# Specify +options+ with additional translating options.
def human_attribute_name(attribute, options = {})
defaults = lookup_ancestors.map do |klass|
- :"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}"
+ :"#{self.i18n_scope}.attributes.#{klass.model_name.underscore}.#{attribute}"
end
defaults << :"attributes.#{attribute}"
diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb
index 422aa25668..b001adb35a 100644
--- a/activemodel/test/cases/attribute_methods_test.rb
+++ b/activemodel/test/cases/attribute_methods_test.rb
@@ -42,10 +42,16 @@ class AttributeMethodsTest < ActiveModel::TestCase
ModelWithAttributes2.send(:attribute_method_matchers)
end
+ test '#define_attribute_method generates attribute method' do
+ ModelWithAttributes.define_attribute_method(:foo)
+
+ assert_respond_to ModelWithAttributes.new, :foo
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ end
+
test '#define_attribute_methods generates attribute methods' do
ModelWithAttributes.define_attribute_methods([:foo])
- assert ModelWithAttributes.attribute_methods_generated?
assert_respond_to ModelWithAttributes.new, :foo
assert_equal "value of foo", ModelWithAttributes.new.foo
end
@@ -53,7 +59,6 @@ class AttributeMethodsTest < ActiveModel::TestCase
test '#define_attribute_methods generates attribute methods with spaces in their names' do
ModelWithAttributesWithSpaces.define_attribute_methods([:'foo bar'])
- assert ModelWithAttributesWithSpaces.attribute_methods_generated?
assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar'
assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar')
end
@@ -69,7 +74,6 @@ class AttributeMethodsTest < ActiveModel::TestCase
ModelWithAttributes.define_attribute_methods([:foo])
ModelWithAttributes.undefine_attribute_methods
- assert !ModelWithAttributes.attribute_methods_generated?
assert !ModelWithAttributes.new.respond_to?(:foo)
assert_raises(NoMethodError) { ModelWithAttributes.new.foo }
end
diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb
index c299d6eb5e..1b1d972d5c 100644
--- a/activemodel/test/cases/translation_test.rb
+++ b/activemodel/test/cases/translation_test.rb
@@ -49,6 +49,13 @@ class ActiveModelI18nTests < ActiveModel::TestCase
assert_equal 'person name attribute', Child.human_attribute_name('name')
end
+ def test_translated_model_attributes_with_attribute_matching_namespaced_model_name
+ I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:gender => 'person gender'}, :"person/gender" => {:attribute => 'person gender attribute'}}}
+
+ assert_equal 'person gender', Person.human_attribute_name('gender')
+ assert_equal 'person gender attribute', Person::Gender.human_attribute_name('attribute')
+ end
+
def test_translated_model_names
I18n.backend.store_translations 'en', :activemodel => {:models => {:person => 'person model'} }
assert_equal 'person model', Person.model_name.human
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
index 5cb7bff4e7..e9f0e430fe 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -55,14 +55,6 @@ class I18nValidationTest < ActiveModel::TestCase
assert_equal ["Person's name not found"], @person.errors.full_messages
end
- def test_errors_full_messages_translates_human_attribute_name_for_model_in_module_attributes
- I18n.backend.store_translations('en', :activemodel => {:attributes => {:person_module => {:person => {:name => "Person in Module's name"}}}})
- person = PersonModule::Person.new
- person.errors.add(:name, 'not found')
- PersonModule::Person.expects(:human_attribute_name).with(:name, :default => 'Name').returns("Person in Module's name")
- assert_equal ["Person in Module's name not found"], person.errors.full_messages
- end
-
def test_errors_full_messages_uses_format
I18n.backend.store_translations('en', :errors => {:format => "Field %{attribute} %{message}"})
@person.errors.add('name', 'empty')
@@ -371,15 +363,4 @@ class I18nValidationTest < ActiveModel::TestCase
assert_equal ["I am a custom error"], @person.errors[:title]
end
- def test_model_with_module_i18n_scope
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person_module => {:person => {:blank => 'generic blank'}}}}}
- PersonModule::Person.validates_presence_of :title
- person = PersonModule::Person.new
- person.valid?
- assert_equal ['generic blank'], person.errors[:title]
-
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person_module => {:person => {:attributes => {:title => {:blank => 'title cannot be blank'}}}}}}}
- person.valid?
- assert_equal ['title cannot be blank'], person.errors[:title]
- end
end
diff --git a/activemodel/test/models/person.rb b/activemodel/test/models/person.rb
index eb84f7a27d..e896e90f98 100644
--- a/activemodel/test/models/person.rb
+++ b/activemodel/test/models/person.rb
@@ -9,10 +9,9 @@ class Person
end
end
-class Child < Person
+class Person::Gender
+ extend ActiveModel::Translation
end
-module PersonModule
- class Person < ::Person
- end
+class Child < Person
end
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 1f343f690c..8b4e9a34cb 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,17 @@
*Rails 3.1.0 (unreleased)*
+* The configuration for the current database connection is now accessible via
+ ActiveRecord::Base.connection_config. [fxn]
+
+* limits and offsets are removed from COUNT queries unless both are supplied.
+ For example:
+
+ People.limit(1).count # => 'SELECT COUNT(*) FROM people'
+ People.offset(1).count # => 'SELECT COUNT(*) FROM people'
+ People.limit(1).offset(1).count # => 'SELECT COUNT(*) FROM people LIMIT 1 OFFSET 1'
+
+ [lighthouse #6262]
+
* ActiveRecord::Associations::AssociationProxy has been split. There is now an Association class
(and subclasses) which are responsible for operating on associations, and then a separate,
thin wrapper called CollectionProxy, which proxies collection associations.
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 0777f85869..59cf42a377 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -43,7 +43,6 @@ module ActiveRecord
autoload :ConnectionNotEstablished, 'active_record/errors'
autoload :Aggregations
- autoload :AssociationPreload
autoload :Associations
autoload :AttributeMethods
autoload :AutosaveAssociation
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
deleted file mode 100644
index a34a73cf5d..0000000000
--- a/activerecord/lib/active_record/association_preload.rb
+++ /dev/null
@@ -1,433 +0,0 @@
-require 'active_support/core_ext/array/wrap'
-require 'active_support/core_ext/enumerable'
-
-module ActiveRecord
- # See ActiveRecord::AssociationPreload::ClassMethods for documentation.
- module AssociationPreload #:nodoc:
- extend ActiveSupport::Concern
-
- # Implements the details of eager loading of Active Record associations.
- # Application developers should not use this module directly.
- #
- # <tt>ActiveRecord::Base</tt> is extended with this module. The source code in
- # <tt>ActiveRecord::Base</tt> references methods defined in this module.
- #
- # Note that 'eager loading' and 'preloading' are actually the same thing.
- # However, there are two different eager loading strategies.
- #
- # The first one is by using table joins. This was only strategy available
- # prior to Rails 2.1. Suppose that you have an Author model with columns
- # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
- # this strategy, Active Record would try to retrieve all data for an author
- # and all of its books via a single query:
- #
- # SELECT * FROM authors
- # LEFT OUTER JOIN books ON authors.id = books.id
- # WHERE authors.name = 'Ken Akamatsu'
- #
- # However, this could result in many rows that contain redundant data. After
- # having received the first row, we already have enough data to instantiate
- # the Author object. In all subsequent rows, only the data for the joined
- # 'books' table is useful; the joined 'authors' data is just redundant, and
- # processing this redundant data takes memory and CPU time. The problem
- # quickly becomes worse and worse as the level of eager loading increases
- # (i.e. if Active Record is to eager load the associations' associations as
- # well).
- #
- # The second strategy is to use multiple database queries, one for each
- # level of association. Since Rails 2.1, this is the default strategy. In
- # situations where a table join is necessary (e.g. when the +:conditions+
- # option references an association's column), it will fallback to the table
- # join strategy.
- #
- # See also ActiveRecord::Associations::ClassMethods, which explains eager
- # loading in a more high-level (application developer-friendly) manner.
- module ClassMethods
- protected
-
- # Eager loads the named associations for the given Active Record record(s).
- #
- # In this description, 'association name' shall refer to the name passed
- # to an association creation method. For example, a model that specifies
- # <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
- # names +:author+ and +:buyers+.
- #
- # == Parameters
- # +records+ is an array of ActiveRecord::Base. This array needs not be flat,
- # i.e. +records+ itself may also contain arrays of records. In any case,
- # +preload_associations+ will preload the all associations records by
- # flattening +records+.
- #
- # +associations+ specifies one or more associations that you want to
- # preload. It may be:
- # - a Symbol or a String which specifies a single association name. For
- # example, specifying +:books+ allows this method to preload all books
- # for an Author.
- # - an Array which specifies multiple association names. This array
- # is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
- # allows this method to preload an author's avatar as well as all of his
- # books.
- # - a Hash which specifies multiple association names, as well as
- # association names for the to-be-preloaded association objects. For
- # example, specifying <tt>{ :author => :avatar }</tt> will preload a
- # book's author, as well as that author's avatar.
- #
- # +:associations+ has the same format as the +:include+ option for
- # <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
- #
- # :books
- # [ :books, :author ]
- # { :author => :avatar }
- # [ :books, { :author => :avatar } ]
- #
- # +preload_options+ contains options that will be passed to ActiveRecord::Base#find
- # (which is called under the hood for preloading records). But it is passed
- # only one level deep in the +associations+ argument, i.e. it's not passed
- # to the child associations when +associations+ is a Hash.
- def preload_associations(records, associations, preload_options={})
- records = Array.wrap(records).compact.uniq
- return if records.empty?
- case associations
- when Array then associations.each {|association| preload_associations(records, association, preload_options)}
- when Symbol, String then preload_one_association(records, associations.to_sym, preload_options)
- when Hash then
- associations.each do |parent, child|
- 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.sum { |record| Array.wrap(record.send(reflection.name)) }
- unless parents.empty?
- parents.first.class.preload_associations(parents, child)
- end
- end
- end
- end
-
- private
-
- # Preloads a specific named association for the given records. This is
- # called by +preload_associations+ as its base case.
- def preload_one_association(records, association, preload_options={})
- class_to_reflection = {}
- # Not all records have the same class, so group then preload
- # group on the reflection itself so that if various subclass share the same association then
- # we do not split them 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.
- send("preload_#{reflection.macro}_association", _records, reflection, preload_options)
- end
- end
-
- def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record)
- parent_records.each do |parent_record|
- association = parent_record.association(reflection_name)
- association.loaded!
- association.target.concat(Array.wrap(associated_record))
- association.set_inverse_instance(associated_record)
- end
- end
-
- def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
- parent_records.each do |parent_record|
- parent_record.association(reflection_name).target = associated_record
- end
- end
-
- def set_association_collection_records(id_to_parent_map, reflection_name, associated_records, key)
- associated_records.each do |associated_record|
- parent_records = id_to_parent_map[associated_record[key].to_s]
- add_preloaded_records_to_collection(parent_records, reflection_name, associated_record)
- end
- end
-
- def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
- seen_keys = {}
- associated_records.each do |associated_record|
- seen_key = associated_record[key].to_s
-
- #this is a has_one or belongs_to: there should only be one record.
- #Unfortunately we can't (in portable way) ask the database for
- #'all records where foo_id in (x,y,z), but please
- # only one row per distinct foo_id' so this where we enforce that
- next if seen_keys.key? seen_key
-
- seen_keys[seen_key] = true
- mapped_records = id_to_record_map[seen_key]
- mapped_records.each do |mapped_record|
- association_proxy = mapped_record.association(reflection_name)
- association_proxy.target = associated_record
- association_proxy.send(:set_inverse_instance, associated_record)
- end
- end
-
- id_to_record_map.each do |id, records|
- next if seen_keys.include?(id)
- add_preloaded_record_to_collection(records, reflection_name, nil)
- end
- end
-
- # Given a collection of Active Record objects, constructs a Hash which maps
- # the objects' IDs to the relevant objects. Returns a 2-tuple
- # <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash,
- # and +ids+ is an Array of record IDs.
- def construct_id_map(records, primary_key=nil)
- records.group_by do |record|
- primary_key ||= record.class.primary_key
- record[primary_key].to_s
- end
- end
-
- def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
-
- left = reflection.klass.arel_table
-
- id_to_record_map = construct_id_map(records)
-
- records.each { |record| record.association(reflection.name).loaded! }
- options = reflection.options
-
- right = Arel::Table.new(options[:join_table]).alias('t0')
-
- join_condition = left[reflection.klass.primary_key].eq(
- right[reflection.association_foreign_key])
-
- join = left.create_join(right, left.create_on(join_condition))
- select = [
- # FIXME: options[:select] is always nil in the tests. Do we really
- # need it?
- options[:select] || left[Arel.star],
- right[reflection.foreign_key].as(
- Arel.sql('the_parent_record_id'))
- ]
-
- associated_records_proxy = reflection.klass.unscoped.
- includes(options[:include]).
- order(options[:order])
-
- associated_records_proxy.joins_values = [join]
- associated_records_proxy.select_values = select
-
- custom_conditions = append_conditions(reflection, preload_options)
-
- klass = associated_records_proxy.klass
-
- associated_records(id_to_record_map.keys) { |some_ids|
- method = in_or_equal(some_ids)
- conditions = right.create_and(
- [right[reflection.foreign_key].send(*method)] +
- custom_conditions)
-
- relation = associated_records_proxy.where(conditions)
- klass.connection.select_all(relation.arel.to_sql, 'SQL', relation.bind_values)
- }.map! { |row|
- parent_records = id_to_record_map[row['the_parent_record_id'].to_s]
- associated_record = klass.instantiate row
- add_preloaded_records_to_collection(
- parent_records, reflection.name, associated_record)
- associated_record
- }
- end
-
- def preload_has_one_association(records, reflection, preload_options={})
- return if records.first.association(reflection.name).loaded?
- id_to_record_map = construct_id_map(records, reflection.options[:primary_key])
- options = reflection.options
-
- add_preloaded_record_to_collection(records, reflection.name, nil)
-
- if options[:through]
- through_records = preload_through_records(records, reflection, options[:through])
-
- unless through_records.empty?
- through_reflection = reflections[options[:through]]
- through_primary_key = through_reflection.foreign_key
- source = reflection.source_reflection.name
- through_records.first.class.preload_associations(through_records, source)
- if through_reflection.macro == :belongs_to
- id_to_record_map = construct_id_map(records, through_primary_key)
- through_primary_key = through_reflection.klass.primary_key
- end
-
- through_records.each do |through_record|
- add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
- reflection.name, through_record.send(source))
- end
- end
- else
- set_association_single_records(id_to_record_map, reflection.name, find_associated_records(id_to_record_map.keys, reflection, preload_options), reflection.foreign_key)
- end
- end
-
- def preload_has_many_association(records, reflection, preload_options={})
- return if records.first.send(reflection.name).loaded?
- options = reflection.options
-
- foreign_key = reflection.through_reflection_foreign_key
- id_to_record_map = construct_id_map(records, foreign_key || reflection.options[:primary_key])
- records.each { |record| record.association(reflection.name).loaded! }
-
- if options[:through]
- through_records = preload_through_records(records, reflection, options[:through])
- unless through_records.empty?
- source = reflection.source_reflection.name
- through_records.first.class.preload_associations(through_records, source, options)
- through_records.each do |through_record|
- through_record_id = through_record[reflection.through_reflection_primary_key].to_s
- add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source))
- end
- records.each { |record| record.send(reflection.name).target.uniq! } if options[:uniq]
- end
-
- else
- set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(id_to_record_map.keys, reflection, preload_options),
- reflection.foreign_key)
- end
- end
-
- def preload_through_records(records, reflection, through_association)
- if reflection.options[:source_type]
- interface = reflection.source_reflection.foreign_type
- preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
-
- records.compact!
- records.first.class.preload_associations(records, through_association, preload_options)
-
- # Dont cache the association - we would only be caching a subset
- records.map { |record|
- proxy = record.association(through_association)
-
- if proxy.respond_to?(:target)
- Array.wrap(proxy.target).tap { proxy.reset }
- else # this is a has_one :through reflection
- [proxy].compact
- end
- }.flatten(1)
- else
- options = {}
- options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions]
- options[:order] = reflection.options[:order]
- options[:conditions] = reflection.options[:conditions]
- records.first.class.preload_associations(records, through_association, options)
-
- records.map { |record|
- Array.wrap(record.send(through_association))
- }.flatten(1)
- end
- end
-
- def preload_belongs_to_association(records, reflection, preload_options={})
- return if records.first.association(reflection.name).loaded?
- options = reflection.options
-
- klasses_and_ids = {}
-
- if options[:polymorphic]
- # Construct a mapping from klass to a list of ids to load and a mapping of those ids back
- # to their parent_records
- records.each do |record|
- if klass = record.send(reflection.foreign_type)
- klass_id = record.send(reflection.foreign_key)
- if klass_id
- id_map = klasses_and_ids[klass.constantize] ||= {}
- (id_map[klass_id.to_s] ||= []) << record
- end
- end
- end
- else
- id_map = records.group_by do |record|
- key = record.send(reflection.foreign_key)
- key && key.to_s
- end
- klasses_and_ids[reflection.klass] = id_map unless id_map.empty?
- end
-
- klasses_and_ids.each do |klass, _id_map|
- primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s
- keys = _id_map.keys.compact
-
- unless keys.empty?
- table = klass.arel_table
- method = in_or_equal(keys)
- conditions = table[primary_key].send(*method)
-
- custom_conditions = append_conditions(reflection, preload_options)
- conditions = custom_conditions.inject(conditions) do |ast, cond|
- ast.and cond
- end
-
- associated_records = klass.unscoped.where(conditions).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a
- else
- associated_records = []
- end
-
- set_association_single_records(_id_map, reflection.name, associated_records, primary_key)
- end
- end
-
- def find_associated_records(ids, reflection, preload_options)
- options = reflection.options
- table = reflection.klass.arel_table
-
- conditions = []
-
- key = reflection.foreign_key
-
- if interface = reflection.options[:as]
- key = "#{interface}_id"
- conditions << table["#{interface}_type"].eq(base_class.sti_name)
- end
-
- conditions.concat append_conditions(reflection, preload_options)
-
- find_options = {
- :select => preload_options[:select] || options[:select] || table[Arel.star],
- :include => preload_options[:include] || options[:include],
- :joins => options[:joins],
- :group => preload_options[:group] || options[:group],
- :order => preload_options[:order] || options[:order]
- }
-
- associated_records(ids) do |some_ids|
- method = in_or_equal(some_ids)
- where = table.create_and(conditions + [table[key].send(*method)])
-
- reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => where)).to_a
- end
- end
-
- def process_conditions(conditions, klass = self)
- if conditions.respond_to?(:to_proc)
- conditions = instance_eval(&conditions)
- end
-
- klass.send(:sanitize_sql, conditions)
- end
-
- def append_conditions(reflection, preload_options)
- [
- ('(' + process_conditions(reflection.options[:conditions], reflection.klass) + ')' if reflection.options[:conditions]),
- ('(' + process_conditions(preload_options[:conditions]) + ')' if preload_options[:conditions]),
- ].compact.map { |x| Arel.sql x }
- end
-
- def in_or_equal(ids)
- ids.length == 1 ? ['eq', ids.first] : ['in', ids]
- end
-
- # Some databases impose a limit on the number of ids in a list (in Oracle its 1000)
- # Make several smaller queries if necessary or make one query if the adapter supports it
- def associated_records(ids)
- in_clause_length = connection.in_clause_length || ids.size
- records = []
- ids.each_slice(in_clause_length) do |some_ids|
- records.concat yield(some_ids)
- end
- records
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index e0b4db498d..e91cbd7f33 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -5,7 +5,6 @@ require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/conversions'
require 'active_support/core_ext/module/remove_method'
require 'active_support/core_ext/class/attribute'
-require 'active_record/associations/class_methods/join_dependency'
module ActiveRecord
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
@@ -143,6 +142,9 @@ module ActiveRecord
autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
end
+ autoload :Preloader, 'active_record/associations/preloader'
+ autoload :JoinDependency, 'active_record/associations/join_dependency'
+
# Clears out the association cache.
def clear_association_cache #:nodoc:
@association_cache.clear if persisted?
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 81904afe3a..25b4b9d90d 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -191,8 +191,8 @@ module ActiveRecord
else
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
- if options[:as]
- attributes["#{options[:as]}_type"] = owner.class.base_class.name
+ if reflection.options[:as]
+ attributes[reflection.type] = owner.class.base_class.name
end
end
attributes
diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb
deleted file mode 100644
index b711ff35ca..0000000000
--- a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb
+++ /dev/null
@@ -1,233 +0,0 @@
-require 'active_record/associations/class_methods/join_dependency/join_part'
-require 'active_record/associations/class_methods/join_dependency/join_base'
-require 'active_record/associations/class_methods/join_dependency/join_association'
-
-module ActiveRecord
- module Associations
- module ClassMethods
- class JoinDependency # :nodoc:
- attr_reader :join_parts, :reflections, :table_aliases, :active_record
-
- def initialize(base, associations, joins)
- @active_record = base
- @table_joins = joins
- @join_parts = [JoinBase.new(base)]
- @associations = {}
- @reflections = []
- @table_aliases = Hash.new do |h,name|
- h[name] = count_aliases_from_table_joins(name.downcase)
- end
- @table_aliases[base.table_name] = 1
- build(associations)
- end
-
- def graft(*associations)
- associations.each do |association|
- join_associations.detect {|a| association == a} ||
- build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
- end
- self
- end
-
- def join_associations
- join_parts.last(join_parts.length - 1)
- end
-
- def join_base
- join_parts.first
- end
-
- def columns
- join_parts.collect { |join_part|
- table = join_part.aliased_table
- join_part.column_names_with_alias.collect{ |column_name, aliased_name|
- table[column_name].as Arel.sql(aliased_name)
- }
- }.flatten
- end
-
- def count_aliases_from_table_joins(name)
- return 0 if Arel::Table === @table_joins
-
- # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
- quoted_name = active_record.connection.quote_table_name(name).downcase
-
- @table_joins.map { |join|
- # Table names + table aliases
- join.left.downcase.scan(
- /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
- ).size
- }.sum
- end
-
- def instantiate(rows)
- primary_key = join_base.aliased_primary_key
- parents = {}
-
- records = rows.map { |model|
- primary_id = model[primary_key]
- parent = parents[primary_id] ||= join_base.instantiate(model)
- construct(parent, @associations, join_associations, model)
- parent
- }.uniq
-
- remove_duplicate_results!(active_record, records, @associations)
- records
- end
-
- def remove_duplicate_results!(base, records, associations)
- case associations
- when Symbol, String
- reflection = base.reflections[associations]
- remove_uniq_by_reflection(reflection, records)
- when Array
- associations.each do |association|
- remove_duplicate_results!(base, records, association)
- end
- when Hash
- associations.keys.each do |name|
- reflection = base.reflections[name]
- remove_uniq_by_reflection(reflection, records)
-
- parent_records = []
- records.each do |record|
- if descendant = record.send(reflection.name)
- if reflection.collection?
- parent_records.concat descendant.target.uniq
- else
- parent_records << descendant
- end
- end
- end
-
- remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
- end
- end
- end
-
- protected
-
- def cache_joined_association(association)
- associations = []
- parent = association.parent
- while parent != join_base
- associations.unshift(parent.reflection.name)
- parent = parent.parent
- end
- ref = @associations
- associations.each do |key|
- ref = ref[key]
- end
- ref[association.reflection.name] ||= {}
- end
-
- def build(associations, parent = nil, join_type = Arel::InnerJoin)
- parent ||= join_parts.last
- case associations
- when Symbol, String
- reflection = parent.reflections[associations.to_s.intern] or
- raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
- unless join_association = find_join_association(reflection, parent)
- @reflections << reflection
- join_association = build_join_association(reflection, parent)
- join_association.join_type = join_type
- @join_parts << join_association
- cache_joined_association(join_association)
- end
- join_association
- when Array
- associations.each do |association|
- build(association, parent, join_type)
- end
- when Hash
- associations.keys.sort_by { |a| a.to_s }.each do |name|
- join_association = build(name, parent, join_type)
- build(associations[name], join_association, join_type)
- end
- else
- raise ConfigurationError, associations.inspect
- end
- end
-
- def find_join_association(name_or_reflection, parent)
- if String === name_or_reflection
- name_or_reflection = name_or_reflection.to_sym
- end
-
- join_associations.detect { |j|
- j.reflection == name_or_reflection && j.parent == parent
- }
- end
-
- def remove_uniq_by_reflection(reflection, records)
- if reflection && reflection.collection?
- records.each { |record| record.send(reflection.name).target.uniq! }
- end
- end
-
- def build_join_association(reflection, parent)
- JoinAssociation.new(reflection, self, parent)
- end
-
- def construct(parent, associations, join_parts, row)
- case associations
- when Symbol, String
- name = associations.to_s
-
- join_part = join_parts.detect { |j|
- j.reflection.name.to_s == name &&
- j.parent_table_name == parent.class.table_name }
-
- raise(ConfigurationError, "No such association") unless join_part
-
- join_parts.delete(join_part)
- construct_association(parent, join_part, row)
- when Array
- associations.each do |association|
- construct(parent, association, join_parts, row)
- end
- when Hash
- associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
- association = construct(parent, association_name, join_parts, row)
- construct(association, assoc, join_parts, row) if association
- end
- else
- raise ConfigurationError, associations.inspect
- end
- end
-
- def construct_association(record, join_part, row)
- return if record.id.to_s != join_part.parent.record_id(row).to_s
-
- macro = join_part.reflection.macro
- if macro == :has_one
- return if record.association_cache.key?(join_part.reflection.name)
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
- set_target_and_inverse(join_part, association, record)
- else
- return if row[join_part.aliased_primary_key].nil?
- association = join_part.instantiate(row)
- case macro
- when :has_many, :has_and_belongs_to_many
- other = record.association(join_part.reflection.name)
- other.loaded!
- other.target.push(association)
- other.set_inverse_instance(association)
- when :belongs_to
- set_target_and_inverse(join_part, association, record)
- else
- raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
- end
- end
- association
- end
-
- def set_target_and_inverse(join_part, association, record)
- other = record.association(join_part.reflection.name)
- other.target = association
- other.set_inverse_instance(association)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb
deleted file mode 100644
index aaa475109e..0000000000
--- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb
+++ /dev/null
@@ -1,281 +0,0 @@
-module ActiveRecord
- module Associations
- module ClassMethods
- class JoinDependency # :nodoc:
- class JoinAssociation < JoinPart # :nodoc:
- # The reflection of the association represented
- attr_reader :reflection
-
- # The JoinDependency object which this JoinAssociation exists within. This is mainly
- # relevant for generating aliases which do not conflict with other joins which are
- # part of the query.
- attr_reader :join_dependency
-
- # A JoinBase instance representing the active record we are joining onto.
- # (So in Author.has_many :posts, the Author would be that base record.)
- attr_reader :parent
-
- # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
- attr_accessor :join_type
-
- # These implement abstract methods from the superclass
- attr_reader :aliased_prefix, :aliased_table_name
-
- delegate :options, :through_reflection, :source_reflection, :to => :reflection
- delegate :table, :table_name, :to => :parent, :prefix => :parent
-
- def initialize(reflection, join_dependency, parent = nil)
- reflection.check_validity!
-
- if reflection.options[:polymorphic]
- raise EagerLoadPolymorphicError.new(reflection)
- end
-
- super(reflection.klass)
-
- @reflection = reflection
- @join_dependency = join_dependency
- @parent = parent
- @join_type = Arel::InnerJoin
- @aliased_prefix = "t#{ join_dependency.join_parts.size }"
-
- # This must be done eagerly upon initialisation because the alias which is produced
- # depends on the state of the join dependency, but we want it to work the same way
- # every time.
- allocate_aliases
- @table = Arel::Table.new(
- table_name, :as => aliased_table_name, :engine => arel_engine
- )
- end
-
- def ==(other)
- other.class == self.class &&
- other.reflection == reflection &&
- other.parent == parent
- end
-
- def find_parent_in(other_join_dependency)
- other_join_dependency.join_parts.detect do |join_part|
- parent == join_part
- end
- end
-
- def join_to(relation)
- send("join_#{reflection.macro}_to", relation)
- end
-
- def join_relation(joining_relation)
- self.join_type = Arel::OuterJoin
- joining_relation.joins(self)
- end
-
- attr_reader :table
- # More semantic name given we are talking about associations
- alias_method :target_table, :table
-
- protected
-
- def aliased_table_name_for(name, suffix = nil)
- aliases = @join_dependency.table_aliases
-
- if aliases[name] != 0 # We need an alias
- connection = active_record.connection
-
- name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
- aliases[name] += 1
- name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1
- else
- aliases[name] += 1
- end
-
- name
- end
-
- def pluralize(table_name)
- ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
- end
-
- private
-
- def allocate_aliases
- @aliased_table_name = aliased_table_name_for(table_name)
-
- if reflection.macro == :has_and_belongs_to_many
- @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
- elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
- @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
- end
- end
-
- def process_conditions(conditions, table_name)
- if conditions.respond_to?(:to_proc)
- conditions = instance_eval(&conditions)
- end
-
- Arel.sql(sanitize_sql(conditions, table_name))
- end
-
- def sanitize_sql(condition, table_name)
- active_record.send(:sanitize_sql, condition, table_name)
- end
-
- def join_target_table(relation, condition)
- conditions = [condition]
-
- # If the target table is an STI model then we must be sure to only include records of
- # its type and its sub-types.
- unless active_record.descends_from_active_record?
- sti_column = target_table[active_record.inheritance_column]
- subclasses = active_record.descendants
- sti_condition = sti_column.eq(active_record.sti_name)
-
- conditions << subclasses.inject(sti_condition) { |attr,subclass|
- attr.or(sti_column.eq(subclass.sti_name))
- }
- end
-
- # If the reflection has conditions, add them
- if options[:conditions]
- conditions << process_conditions(options[:conditions], aliased_table_name)
- end
-
- ands = relation.create_and(conditions)
-
- join = relation.create_join(
- target_table,
- relation.create_on(ands),
- join_type)
-
- relation.from join
- end
-
- def join_has_and_belongs_to_many_to(relation)
- join_table = Arel::Table.new(
- options[:join_table]
- ).alias(@aliased_join_table_name)
-
- fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
- klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key
-
- relation = relation.join(join_table, join_type)
- relation = relation.on(
- join_table[fk].
- eq(parent_table[reflection.active_record.primary_key])
- )
-
- join_target_table(
- relation,
- target_table[reflection.klass.primary_key].
- eq(join_table[klass_fk])
- )
- end
-
- def join_has_many_to(relation)
- if reflection.options[:through]
- join_has_many_through_to(relation)
- elsif reflection.options[:as]
- join_has_many_polymorphic_to(relation)
- else
- foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
- primary_key = options[:primary_key] || parent.primary_key
-
- join_target_table(
- relation,
- target_table[foreign_key].
- eq(parent_table[primary_key])
- )
- end
- end
- alias :join_has_one_to :join_has_many_to
-
- def join_has_many_through_to(relation)
- join_table = Arel::Table.new(
- through_reflection.klass.table_name
- ).alias @aliased_join_table_name
-
- jt_conditions = []
- first_key = second_key = nil
-
- if through_reflection.macro == :belongs_to
- jt_primary_key = through_reflection.foreign_key
- jt_foreign_key = through_reflection.association_primary_key
- else
- jt_primary_key = through_reflection.active_record_primary_key
- jt_foreign_key = through_reflection.foreign_key
-
- if through_reflection.options[:as] # has_many :through against a polymorphic join
- jt_conditions <<
- join_table["#{through_reflection.options[:as]}_type"].
- eq(parent.active_record.base_class.name)
- end
- end
-
- case source_reflection.macro
- when :has_many
- second_key = options[:foreign_key] || primary_key
-
- if source_reflection.options[:as]
- first_key = "#{source_reflection.options[:as]}_id"
- else
- first_key = through_reflection.klass.base_class.to_s.foreign_key
- end
-
- unless through_reflection.klass.descends_from_active_record?
- jt_conditions <<
- join_table[through_reflection.active_record.inheritance_column].
- eq(through_reflection.klass.sti_name)
- end
- when :belongs_to
- first_key = primary_key
-
- if reflection.options[:source_type]
- second_key = source_reflection.association_foreign_key
-
- jt_conditions <<
- join_table[reflection.source_reflection.foreign_type].
- eq(reflection.options[:source_type])
- else
- second_key = source_reflection.foreign_key
- end
- end
-
- jt_conditions <<
- parent_table[jt_primary_key].
- eq(join_table[jt_foreign_key])
-
- if through_reflection.options[:conditions]
- jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name)
- end
-
- relation = relation.join(join_table, join_type).on(*jt_conditions)
-
- join_target_table(
- relation,
- target_table[first_key].eq(join_table[second_key])
- )
- end
-
- def join_has_many_polymorphic_to(relation)
- join_target_table(
- relation,
- target_table["#{reflection.options[:as]}_id"].
- eq(parent_table[parent.primary_key]).and(
- target_table["#{reflection.options[:as]}_type"].
- eq(parent.active_record.base_class.name))
- )
- end
-
- def join_belongs_to_to(relation)
- foreign_key = options[:foreign_key] || reflection.foreign_key
- primary_key = options[:primary_key] || reflection.klass.primary_key
-
- join_target_table(
- relation,
- target_table[primary_key].eq(parent_table[foreign_key])
- )
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb
deleted file mode 100644
index 67567f06df..0000000000
--- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_base.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module ActiveRecord
- module Associations
- module ClassMethods
- class JoinDependency # :nodoc:
- class JoinBase < JoinPart # :nodoc:
- def ==(other)
- other.class == self.class &&
- other.active_record == active_record
- end
-
- def aliased_prefix
- "t0"
- end
-
- def table
- Arel::Table.new(table_name, arel_engine)
- end
-
- def aliased_table_name
- active_record.table_name
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb
deleted file mode 100644
index cd16ae5a8b..0000000000
--- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_part.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-module ActiveRecord
- module Associations
- module ClassMethods
- class JoinDependency # :nodoc:
- # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited
- # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
- # everything else is being joined onto. A JoinAssociation represents an association which
- # is joining to the base. A JoinAssociation may result in more than one actual join
- # operations (for example a has_and_belongs_to_many JoinAssociation would result in
- # two; one for the join table and one for the target table).
- class JoinPart # :nodoc:
- # The Active Record class which this join part is associated 'about'; for a JoinBase
- # this is the actual base model, for a JoinAssociation this is the target model of the
- # association.
- attr_reader :active_record
-
- delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record
-
- def initialize(active_record)
- @active_record = active_record
- @cached_record = {}
- @column_names_with_alias = nil
- end
-
- def aliased_table
- Arel::Nodes::TableAlias.new aliased_table_name, table
- end
-
- def ==(other)
- raise NotImplementedError
- end
-
- # An Arel::Table for the active_record
- def table
- raise NotImplementedError
- end
-
- # The prefix to be used when aliasing columns in the active_record's table
- def aliased_prefix
- raise NotImplementedError
- end
-
- # The alias for the active_record's table
- def aliased_table_name
- raise NotImplementedError
- end
-
- # The alias for the primary key of the active_record's table
- def aliased_primary_key
- "#{aliased_prefix}_r0"
- end
-
- # An array of [column_name, alias] pairs for the table
- def column_names_with_alias
- unless @column_names_with_alias
- @column_names_with_alias = []
-
- ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
- @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
- end
- end
- @column_names_with_alias
- end
-
- def extract_record(row)
- Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}]
- end
-
- def record_id(row)
- row[aliased_primary_key]
- end
-
- def instantiate(row)
- @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row))
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
new file mode 100644
index 0000000000..c7c3cf521c
--- /dev/null
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -0,0 +1,231 @@
+module ActiveRecord
+ module Associations
+ class JoinDependency # :nodoc:
+ autoload :JoinPart, 'active_record/associations/join_dependency/join_part'
+ autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
+ autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
+
+ attr_reader :join_parts, :reflections, :table_aliases, :active_record
+
+ def initialize(base, associations, joins)
+ @active_record = base
+ @table_joins = joins
+ @join_parts = [JoinBase.new(base)]
+ @associations = {}
+ @reflections = []
+ @table_aliases = Hash.new do |h,name|
+ h[name] = count_aliases_from_table_joins(name.downcase)
+ end
+ @table_aliases[base.table_name] = 1
+ build(associations)
+ end
+
+ def graft(*associations)
+ associations.each do |association|
+ join_associations.detect {|a| association == a} ||
+ build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
+ end
+ self
+ end
+
+ def join_associations
+ join_parts.last(join_parts.length - 1)
+ end
+
+ def join_base
+ join_parts.first
+ end
+
+ def columns
+ join_parts.collect { |join_part|
+ table = join_part.aliased_table
+ join_part.column_names_with_alias.collect{ |column_name, aliased_name|
+ table[column_name].as Arel.sql(aliased_name)
+ }
+ }.flatten
+ end
+
+ def count_aliases_from_table_joins(name)
+ return 0 if Arel::Table === @table_joins
+
+ # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
+ quoted_name = active_record.connection.quote_table_name(name).downcase
+
+ @table_joins.map { |join|
+ # Table names + table aliases
+ join.left.downcase.scan(
+ /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
+ ).size
+ }.sum
+ end
+
+ def instantiate(rows)
+ primary_key = join_base.aliased_primary_key
+ parents = {}
+
+ records = rows.map { |model|
+ primary_id = model[primary_key]
+ parent = parents[primary_id] ||= join_base.instantiate(model)
+ construct(parent, @associations, join_associations, model)
+ parent
+ }.uniq
+
+ remove_duplicate_results!(active_record, records, @associations)
+ records
+ end
+
+ def remove_duplicate_results!(base, records, associations)
+ case associations
+ when Symbol, String
+ reflection = base.reflections[associations]
+ remove_uniq_by_reflection(reflection, records)
+ when Array
+ associations.each do |association|
+ remove_duplicate_results!(base, records, association)
+ end
+ when Hash
+ associations.keys.each do |name|
+ reflection = base.reflections[name]
+ remove_uniq_by_reflection(reflection, records)
+
+ parent_records = []
+ records.each do |record|
+ if descendant = record.send(reflection.name)
+ if reflection.collection?
+ parent_records.concat descendant.target.uniq
+ else
+ parent_records << descendant
+ end
+ end
+ end
+
+ remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
+ end
+ end
+ end
+
+ protected
+
+ def cache_joined_association(association)
+ associations = []
+ parent = association.parent
+ while parent != join_base
+ associations.unshift(parent.reflection.name)
+ parent = parent.parent
+ end
+ ref = @associations
+ associations.each do |key|
+ ref = ref[key]
+ end
+ ref[association.reflection.name] ||= {}
+ end
+
+ def build(associations, parent = nil, join_type = Arel::InnerJoin)
+ parent ||= join_parts.last
+ case associations
+ when Symbol, String
+ reflection = parent.reflections[associations.to_s.intern] or
+ raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
+ unless join_association = find_join_association(reflection, parent)
+ @reflections << reflection
+ join_association = build_join_association(reflection, parent)
+ join_association.join_type = join_type
+ @join_parts << join_association
+ cache_joined_association(join_association)
+ end
+ join_association
+ when Array
+ associations.each do |association|
+ build(association, parent, join_type)
+ end
+ when Hash
+ associations.keys.sort_by { |a| a.to_s }.each do |name|
+ join_association = build(name, parent, join_type)
+ build(associations[name], join_association, join_type)
+ end
+ else
+ raise ConfigurationError, associations.inspect
+ end
+ end
+
+ def find_join_association(name_or_reflection, parent)
+ if String === name_or_reflection
+ name_or_reflection = name_or_reflection.to_sym
+ end
+
+ join_associations.detect { |j|
+ j.reflection == name_or_reflection && j.parent == parent
+ }
+ end
+
+ def remove_uniq_by_reflection(reflection, records)
+ if reflection && reflection.collection?
+ records.each { |record| record.send(reflection.name).target.uniq! }
+ end
+ end
+
+ def build_join_association(reflection, parent)
+ JoinAssociation.new(reflection, self, parent)
+ end
+
+ def construct(parent, associations, join_parts, row)
+ case associations
+ when Symbol, String
+ name = associations.to_s
+
+ join_part = join_parts.detect { |j|
+ j.reflection.name.to_s == name &&
+ j.parent_table_name == parent.class.table_name }
+
+ raise(ConfigurationError, "No such association") unless join_part
+
+ join_parts.delete(join_part)
+ construct_association(parent, join_part, row)
+ when Array
+ associations.each do |association|
+ construct(parent, association, join_parts, row)
+ end
+ when Hash
+ associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
+ association = construct(parent, association_name, join_parts, row)
+ construct(association, assoc, join_parts, row) if association
+ end
+ else
+ raise ConfigurationError, associations.inspect
+ end
+ end
+
+ def construct_association(record, join_part, row)
+ return if record.id.to_s != join_part.parent.record_id(row).to_s
+
+ macro = join_part.reflection.macro
+ if macro == :has_one
+ return if record.association_cache.key?(join_part.reflection.name)
+ association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
+ set_target_and_inverse(join_part, association, record)
+ else
+ return if row[join_part.aliased_primary_key].nil?
+ association = join_part.instantiate(row)
+ case macro
+ when :has_many, :has_and_belongs_to_many
+ other = record.association(join_part.reflection.name)
+ other.loaded!
+ other.target.push(association)
+ other.set_inverse_instance(association)
+ when :belongs_to
+ set_target_and_inverse(join_part, association, record)
+ else
+ raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
+ end
+ end
+ association
+ end
+
+ def set_target_and_inverse(join_part, association, record)
+ other = record.association(join_part.reflection.name)
+ other.target = association
+ other.set_inverse_instance(association)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
new file mode 100644
index 0000000000..ebe39c35fe
--- /dev/null
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -0,0 +1,279 @@
+module ActiveRecord
+ module Associations
+ class JoinDependency # :nodoc:
+ class JoinAssociation < JoinPart # :nodoc:
+ # The reflection of the association represented
+ attr_reader :reflection
+
+ # The JoinDependency object which this JoinAssociation exists within. This is mainly
+ # relevant for generating aliases which do not conflict with other joins which are
+ # part of the query.
+ attr_reader :join_dependency
+
+ # A JoinBase instance representing the active record we are joining onto.
+ # (So in Author.has_many :posts, the Author would be that base record.)
+ attr_reader :parent
+
+ # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
+ attr_accessor :join_type
+
+ # These implement abstract methods from the superclass
+ attr_reader :aliased_prefix, :aliased_table_name
+
+ delegate :options, :through_reflection, :source_reflection, :to => :reflection
+ delegate :table, :table_name, :to => :parent, :prefix => :parent
+
+ def initialize(reflection, join_dependency, parent = nil)
+ reflection.check_validity!
+
+ if reflection.options[:polymorphic]
+ raise EagerLoadPolymorphicError.new(reflection)
+ end
+
+ super(reflection.klass)
+
+ @reflection = reflection
+ @join_dependency = join_dependency
+ @parent = parent
+ @join_type = Arel::InnerJoin
+ @aliased_prefix = "t#{ join_dependency.join_parts.size }"
+
+ # This must be done eagerly upon initialisation because the alias which is produced
+ # depends on the state of the join dependency, but we want it to work the same way
+ # every time.
+ allocate_aliases
+ @table = Arel::Table.new(
+ table_name, :as => aliased_table_name, :engine => arel_engine
+ )
+ end
+
+ def ==(other)
+ other.class == self.class &&
+ other.reflection == reflection &&
+ other.parent == parent
+ end
+
+ def find_parent_in(other_join_dependency)
+ other_join_dependency.join_parts.detect do |join_part|
+ parent == join_part
+ end
+ end
+
+ def join_to(relation)
+ send("join_#{reflection.macro}_to", relation)
+ end
+
+ def join_relation(joining_relation)
+ self.join_type = Arel::OuterJoin
+ joining_relation.joins(self)
+ end
+
+ attr_reader :table
+ # More semantic name given we are talking about associations
+ alias_method :target_table, :table
+
+ protected
+
+ def aliased_table_name_for(name, suffix = nil)
+ aliases = @join_dependency.table_aliases
+
+ if aliases[name] != 0 # We need an alias
+ connection = active_record.connection
+
+ name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
+ aliases[name] += 1
+ name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1
+ else
+ aliases[name] += 1
+ end
+
+ name
+ end
+
+ def pluralize(table_name)
+ ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
+ end
+
+ private
+
+ def allocate_aliases
+ @aliased_table_name = aliased_table_name_for(table_name)
+
+ if reflection.macro == :has_and_belongs_to_many
+ @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
+ elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
+ @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
+ end
+ end
+
+ def process_conditions(conditions, table_name)
+ if conditions.respond_to?(:to_proc)
+ conditions = instance_eval(&conditions)
+ end
+
+ Arel.sql(sanitize_sql(conditions, table_name))
+ end
+
+ def sanitize_sql(condition, table_name)
+ active_record.send(:sanitize_sql, condition, table_name)
+ end
+
+ def join_target_table(relation, condition)
+ conditions = [condition]
+
+ # If the target table is an STI model then we must be sure to only include records of
+ # its type and its sub-types.
+ unless active_record.descends_from_active_record?
+ sti_column = target_table[active_record.inheritance_column]
+ subclasses = active_record.descendants
+ sti_condition = sti_column.eq(active_record.sti_name)
+
+ conditions << subclasses.inject(sti_condition) { |attr,subclass|
+ attr.or(sti_column.eq(subclass.sti_name))
+ }
+ end
+
+ # If the reflection has conditions, add them
+ if options[:conditions]
+ conditions << process_conditions(options[:conditions], aliased_table_name)
+ end
+
+ ands = relation.create_and(conditions)
+
+ join = relation.create_join(
+ target_table,
+ relation.create_on(ands),
+ join_type)
+
+ relation.from join
+ end
+
+ def join_has_and_belongs_to_many_to(relation)
+ join_table = Arel::Table.new(
+ options[:join_table]
+ ).alias(@aliased_join_table_name)
+
+ fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
+ klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key
+
+ relation = relation.join(join_table, join_type)
+ relation = relation.on(
+ join_table[fk].
+ eq(parent_table[reflection.active_record.primary_key])
+ )
+
+ join_target_table(
+ relation,
+ target_table[reflection.klass.primary_key].
+ eq(join_table[klass_fk])
+ )
+ end
+
+ def join_has_many_to(relation)
+ if reflection.options[:through]
+ join_has_many_through_to(relation)
+ elsif reflection.options[:as]
+ join_has_many_polymorphic_to(relation)
+ else
+ foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
+ primary_key = options[:primary_key] || parent.primary_key
+
+ join_target_table(
+ relation,
+ target_table[foreign_key].
+ eq(parent_table[primary_key])
+ )
+ end
+ end
+ alias :join_has_one_to :join_has_many_to
+
+ def join_has_many_through_to(relation)
+ join_table = Arel::Table.new(
+ through_reflection.klass.table_name
+ ).alias @aliased_join_table_name
+
+ jt_conditions = []
+ first_key = second_key = nil
+
+ if through_reflection.macro == :belongs_to
+ jt_primary_key = through_reflection.foreign_key
+ jt_foreign_key = through_reflection.association_primary_key
+ else
+ jt_primary_key = through_reflection.active_record_primary_key
+ jt_foreign_key = through_reflection.foreign_key
+
+ if through_reflection.options[:as] # has_many :through against a polymorphic join
+ jt_conditions <<
+ join_table["#{through_reflection.options[:as]}_type"].
+ eq(parent.active_record.base_class.name)
+ end
+ end
+
+ case source_reflection.macro
+ when :has_many
+ second_key = options[:foreign_key] || primary_key
+
+ if source_reflection.options[:as]
+ first_key = "#{source_reflection.options[:as]}_id"
+ else
+ first_key = through_reflection.klass.base_class.to_s.foreign_key
+ end
+
+ unless through_reflection.klass.descends_from_active_record?
+ jt_conditions <<
+ join_table[through_reflection.active_record.inheritance_column].
+ eq(through_reflection.klass.sti_name)
+ end
+ when :belongs_to
+ first_key = primary_key
+
+ if reflection.options[:source_type]
+ second_key = source_reflection.association_foreign_key
+
+ jt_conditions <<
+ join_table[reflection.source_reflection.foreign_type].
+ eq(reflection.options[:source_type])
+ else
+ second_key = source_reflection.foreign_key
+ end
+ end
+
+ jt_conditions <<
+ parent_table[jt_primary_key].
+ eq(join_table[jt_foreign_key])
+
+ if through_reflection.options[:conditions]
+ jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name)
+ end
+
+ relation = relation.join(join_table, join_type).on(*jt_conditions)
+
+ join_target_table(
+ relation,
+ target_table[first_key].eq(join_table[second_key])
+ )
+ end
+
+ def join_has_many_polymorphic_to(relation)
+ join_target_table(
+ relation,
+ target_table["#{reflection.options[:as]}_id"].
+ eq(parent_table[parent.primary_key]).and(
+ target_table["#{reflection.options[:as]}_type"].
+ eq(parent.active_record.base_class.name))
+ )
+ end
+
+ def join_belongs_to_to(relation)
+ foreign_key = options[:foreign_key] || reflection.foreign_key
+ primary_key = options[:primary_key] || reflection.klass.primary_key
+
+ join_target_table(
+ relation,
+ target_table[primary_key].eq(parent_table[foreign_key])
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
new file mode 100644
index 0000000000..3920e84976
--- /dev/null
+++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
@@ -0,0 +1,24 @@
+module ActiveRecord
+ module Associations
+ class JoinDependency # :nodoc:
+ class JoinBase < JoinPart # :nodoc:
+ def ==(other)
+ other.class == self.class &&
+ other.active_record == active_record
+ end
+
+ def aliased_prefix
+ "t0"
+ end
+
+ def table
+ Arel::Table.new(table_name, arel_engine)
+ end
+
+ def aliased_table_name
+ active_record.table_name
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
new file mode 100644
index 0000000000..3279e56e7d
--- /dev/null
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -0,0 +1,78 @@
+module ActiveRecord
+ module Associations
+ class JoinDependency # :nodoc:
+ # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited
+ # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
+ # everything else is being joined onto. A JoinAssociation represents an association which
+ # is joining to the base. A JoinAssociation may result in more than one actual join
+ # operations (for example a has_and_belongs_to_many JoinAssociation would result in
+ # two; one for the join table and one for the target table).
+ class JoinPart # :nodoc:
+ # The Active Record class which this join part is associated 'about'; for a JoinBase
+ # this is the actual base model, for a JoinAssociation this is the target model of the
+ # association.
+ attr_reader :active_record
+
+ delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record
+
+ def initialize(active_record)
+ @active_record = active_record
+ @cached_record = {}
+ @column_names_with_alias = nil
+ end
+
+ def aliased_table
+ Arel::Nodes::TableAlias.new aliased_table_name, table
+ end
+
+ def ==(other)
+ raise NotImplementedError
+ end
+
+ # An Arel::Table for the active_record
+ def table
+ raise NotImplementedError
+ end
+
+ # The prefix to be used when aliasing columns in the active_record's table
+ def aliased_prefix
+ raise NotImplementedError
+ end
+
+ # The alias for the active_record's table
+ def aliased_table_name
+ raise NotImplementedError
+ end
+
+ # The alias for the primary key of the active_record's table
+ def aliased_primary_key
+ "#{aliased_prefix}_r0"
+ end
+
+ # An array of [column_name, alias] pairs for the table
+ def column_names_with_alias
+ unless @column_names_with_alias
+ @column_names_with_alias = []
+
+ ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
+ @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
+ end
+ end
+ @column_names_with_alias
+ end
+
+ def extract_record(row)
+ Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}]
+ end
+
+ def record_id(row)
+ row[aliased_primary_key]
+ end
+
+ def instantiate(row)
+ @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
new file mode 100644
index 0000000000..fafed94ff2
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -0,0 +1,177 @@
+module ActiveRecord
+ module Associations
+ # Implements the details of eager loading of Active Record associations.
+ #
+ # Note that 'eager loading' and 'preloading' are actually the same thing.
+ # However, there are two different eager loading strategies.
+ #
+ # The first one is by using table joins. This was only strategy available
+ # prior to Rails 2.1. Suppose that you have an Author model with columns
+ # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
+ # this strategy, Active Record would try to retrieve all data for an author
+ # and all of its books via a single query:
+ #
+ # SELECT * FROM authors
+ # LEFT OUTER JOIN books ON authors.id = books.id
+ # WHERE authors.name = 'Ken Akamatsu'
+ #
+ # However, this could result in many rows that contain redundant data. After
+ # having received the first row, we already have enough data to instantiate
+ # the Author object. In all subsequent rows, only the data for the joined
+ # 'books' table is useful; the joined 'authors' data is just redundant, and
+ # processing this redundant data takes memory and CPU time. The problem
+ # quickly becomes worse and worse as the level of eager loading increases
+ # (i.e. if Active Record is to eager load the associations' associations as
+ # well).
+ #
+ # The second strategy is to use multiple database queries, one for each
+ # level of association. Since Rails 2.1, this is the default strategy. In
+ # situations where a table join is necessary (e.g. when the +:conditions+
+ # option references an association's column), it will fallback to the table
+ # join strategy.
+ class Preloader #:nodoc:
+ autoload :Association, 'active_record/associations/preloader/association'
+ autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
+ autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
+ autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
+
+ autoload :HasMany, 'active_record/associations/preloader/has_many'
+ autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
+ autoload :HasOne, 'active_record/associations/preloader/has_one'
+ autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
+ autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
+ autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
+
+ attr_reader :records, :associations, :options, :model
+
+ # Eager loads the named associations for the given Active Record record(s).
+ #
+ # In this description, 'association name' shall refer to the name passed
+ # to an association creation method. For example, a model that specifies
+ # <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
+ # names +:author+ and +:buyers+.
+ #
+ # == Parameters
+ # +records+ is an array of ActiveRecord::Base. This array needs not be flat,
+ # i.e. +records+ itself may also contain arrays of records. In any case,
+ # +preload_associations+ will preload the all associations records by
+ # flattening +records+.
+ #
+ # +associations+ specifies one or more associations that you want to
+ # preload. It may be:
+ # - a Symbol or a String which specifies a single association name. For
+ # example, specifying +:books+ allows this method to preload all books
+ # for an Author.
+ # - an Array which specifies multiple association names. This array
+ # is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
+ # allows this method to preload an author's avatar as well as all of his
+ # books.
+ # - a Hash which specifies multiple association names, as well as
+ # association names for the to-be-preloaded association objects. For
+ # example, specifying <tt>{ :author => :avatar }</tt> will preload a
+ # book's author, as well as that author's avatar.
+ #
+ # +:associations+ has the same format as the +:include+ option for
+ # <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
+ #
+ # :books
+ # [ :books, :author ]
+ # { :author => :avatar }
+ # [ :books, { :author => :avatar } ]
+ #
+ # +options+ contains options that will be passed to ActiveRecord::Base#find
+ # (which is called under the hood for preloading records). But it is passed
+ # only one level deep in the +associations+ argument, i.e. it's not passed
+ # to the child associations when +associations+ is a Hash.
+ def initialize(records, associations, options = {})
+ @records = Array.wrap(records).compact.uniq
+ @associations = Array.wrap(associations)
+ @options = options
+ end
+
+ def run
+ unless records.empty?
+ associations.each { |association| preload(association) }
+ end
+ end
+
+ private
+
+ def preload(association)
+ case association
+ when Hash
+ preload_hash(association)
+ when String, Symbol
+ preload_one(association.to_sym)
+ else
+ raise ArgumentError, "#{association.inspect} was not recognised for preload"
+ end
+ end
+
+ def preload_hash(association)
+ association.each do |parent, child|
+ Preloader.new(records, parent, options).run
+ Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
+ end
+ end
+
+ # Not all records have the same class, so group then preload group on the reflection
+ # itself so that if various subclass share the same association then we do not split
+ # them unnecessarily
+ #
+ # Additionally, polymorphic belongs_to associations can have multiple associated
+ # classes, depending on the polymorphic_type field. So we group by the classes as
+ # well.
+ def preload_one(association)
+ grouped_records(association).each do |reflection, klasses|
+ klasses.each do |klass, records|
+ preloader_for(reflection).new(klass, records, reflection, options).run
+ end
+ end
+ end
+
+ def grouped_records(association)
+ Hash[
+ records_by_reflection(association).map do |reflection, records|
+ [reflection, records.group_by { |record| association_klass(reflection, record) }]
+ end
+ ]
+ end
+
+ def records_by_reflection(association)
+ records.group_by do |record|
+ reflection = record.class.reflections[association]
+
+ unless reflection
+ raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
+ "perhaps you misspelled it?"
+ end
+
+ reflection
+ end
+ end
+
+ def association_klass(reflection, record)
+ if reflection.macro == :belongs_to && reflection.options[:polymorphic]
+ klass = record.send(reflection.foreign_type)
+ klass && klass.constantize
+ else
+ reflection.klass
+ end
+ end
+
+ def preloader_for(reflection)
+ case reflection.macro
+ when :has_many
+ reflection.options[:through] ? HasManyThrough : HasMany
+ when :has_one
+ reflection.options[:through] ? HasOneThrough : HasOne
+ when :has_and_belongs_to_many
+ HasAndBelongsToMany
+ when :belongs_to
+ BelongsTo
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
new file mode 100644
index 0000000000..7256dd5288
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -0,0 +1,126 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class Association #:nodoc:
+ attr_reader :owners, :reflection, :preload_options, :model, :klass
+
+ def initialize(klass, owners, reflection, preload_options)
+ @klass = klass
+ @owners = owners
+ @reflection = reflection
+ @preload_options = preload_options || {}
+ @model = owners.first && owners.first.class
+ @scoped = nil
+ @owners_by_key = nil
+ end
+
+ def run
+ unless owners.first.association(reflection.name).loaded?
+ preload
+ end
+ end
+
+ def preload
+ raise NotImplementedError
+ end
+
+ def scoped
+ @scoped ||= build_scope
+ end
+
+ def records_for(ids)
+ scoped.where(association_key.in(ids))
+ end
+
+ def table
+ klass.arel_table
+ end
+
+ # The name of the key on the associated records
+ def association_key_name
+ raise NotImplementedError
+ end
+
+ # This is overridden by HABTM as the condition should be on the foreign_key column in
+ # the join table
+ def association_key
+ table[association_key_name]
+ end
+
+ # The name of the key on the model which declares the association
+ def owner_key_name
+ raise NotImplementedError
+ end
+
+ # We're converting to a string here because postgres will return the aliased association
+ # key in a habtm as a string (for whatever reason)
+ def owners_by_key
+ @owners_by_key ||= owners.group_by do |owner|
+ key = owner[owner_key_name]
+ key && key.to_s
+ end
+ end
+
+ def options
+ reflection.options
+ end
+
+ private
+
+ def associated_records_by_owner
+ owner_keys = owners.map { |owner| owner[owner_key_name] }.compact.uniq
+
+ if klass.nil? || owner_keys.empty?
+ records = []
+ else
+ # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
+ # Make several smaller queries if necessary or make one query if the adapter supports it
+ sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size)
+ records = sliced.map { |slice| records_for(slice) }.flatten
+ end
+
+ # Each record may have multiple owners, and vice-versa
+ records_by_owner = Hash[owners.map { |owner| [owner, []] }]
+ records.each do |record|
+ owner_key = record[association_key_name].to_s
+
+ owners_by_key[owner_key].each do |owner|
+ records_by_owner[owner] << record
+ end
+ end
+ records_by_owner
+ end
+
+ def build_scope
+ scope = klass.scoped
+
+ scope = scope.where(process_conditions(options[:conditions]))
+ scope = scope.where(process_conditions(preload_options[:conditions]))
+
+ scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star])
+ scope = scope.includes(preload_options[:include] || options[:include])
+
+ if options[:as]
+ scope = scope.where(
+ klass.table_name => {
+ reflection.type => model.base_class.sti_name
+ }
+ )
+ end
+
+ scope
+ end
+
+ def process_conditions(conditions)
+ if conditions.respond_to?(:to_proc)
+ conditions = klass.send(:instance_eval, &conditions)
+ end
+
+ if conditions
+ klass.send(:sanitize_sql, conditions)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb
new file mode 100644
index 0000000000..5091d4717a
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/belongs_to.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class BelongsTo < SingularAssociation #:nodoc:
+
+ def association_key_name
+ reflection.options[:primary_key] || klass && klass.primary_key
+ end
+
+ def owner_key_name
+ reflection.foreign_key
+ end
+
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
new file mode 100644
index 0000000000..c248aeaaf6
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb
@@ -0,0 +1,24 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class CollectionAssociation < Association #:nodoc:
+
+ private
+
+ def build_scope
+ super.order(preload_options[:order] || options[:order])
+ end
+
+ def preload
+ associated_records_by_owner.each do |owner, records|
+ association = owner.association(reflection.name)
+ association.loaded!
+ association.target.concat(records)
+ records.each { |record| association.set_inverse_instance(record) }
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
new file mode 100644
index 0000000000..24be279449
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
@@ -0,0 +1,60 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class HasAndBelongsToMany < CollectionAssociation #:nodoc:
+ attr_reader :join_table
+
+ def initialize(klass, records, reflection, preload_options)
+ super
+ @join_table = Arel::Table.new(options[:join_table]).alias('t0')
+ end
+
+ # Unlike the other associations, we want to get a raw array of rows so that we can
+ # access the aliased column on the join table
+ def records_for(ids)
+ scope = super
+ klass.connection.select_all(scope.arel.to_sql, 'SQL', scope.bind_values)
+ end
+
+ def owner_key_name
+ reflection.active_record_primary_key
+ end
+
+ def association_key_name
+ 'ar_association_key_name'
+ end
+
+ def association_key
+ join_table[reflection.foreign_key]
+ end
+
+ private
+
+ # Once we have used the join table column (in super), we manually instantiate the
+ # actual records, ensuring that we don't create more than one instances of the same
+ # record
+ def associated_records_by_owner
+ records = {}
+ super.each do |owner_key, rows|
+ rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) }
+ end
+ end
+
+ def build_scope
+ super.joins(join).select(join_select)
+ end
+
+ def join_select
+ association_key.as(Arel.sql(association_key_name))
+ end
+
+ def join
+ condition = table[reflection.association_primary_key].eq(
+ join_table[reflection.association_foreign_key])
+
+ table.create_join(join_table, table.create_on(condition))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many.rb b/activerecord/lib/active_record/associations/preloader/has_many.rb
new file mode 100644
index 0000000000..3ea91a8c11
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/has_many.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class HasMany < CollectionAssociation #:nodoc:
+
+ def association_key_name
+ reflection.foreign_key
+ end
+
+ def owner_key_name
+ reflection.active_record_primary_key
+ end
+
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
new file mode 100644
index 0000000000..c6e9ede356
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class HasManyThrough < CollectionAssociation #:nodoc:
+ include ThroughAssociation
+
+ def associated_records_by_owner
+ super.each do |owner, records|
+ records.uniq! if options[:uniq]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb
new file mode 100644
index 0000000000..848448bb48
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/has_one.rb
@@ -0,0 +1,23 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class HasOne < SingularAssociation #:nodoc:
+
+ def association_key_name
+ reflection.foreign_key
+ end
+
+ def owner_key_name
+ reflection.active_record_primary_key
+ end
+
+ private
+
+ def build_scope
+ super.order(preload_options[:order] || options[:order])
+ end
+
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/has_one_through.rb b/activerecord/lib/active_record/associations/preloader/has_one_through.rb
new file mode 100644
index 0000000000..f063f85574
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/has_one_through.rb
@@ -0,0 +1,9 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class HasOneThrough < SingularAssociation #:nodoc:
+ include ThroughAssociation
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb
new file mode 100644
index 0000000000..44e804d785
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ class SingularAssociation < Association #:nodoc:
+
+ private
+
+ def preload
+ associated_records_by_owner.each do |owner, associated_records|
+ record = associated_records.first
+
+ association = owner.association(reflection.name)
+ association.target = record
+ association.set_inverse_instance(record)
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
new file mode 100644
index 0000000000..d630fc4c63
--- /dev/null
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -0,0 +1,66 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ module ThroughAssociation #:nodoc:
+
+ def through_reflection
+ reflection.through_reflection
+ end
+
+ def source_reflection
+ reflection.source_reflection
+ end
+
+ def associated_records_by_owner
+ through_records = through_records_by_owner
+
+ ActiveRecord::Associations::Preloader.new(
+ through_records.values.flatten,
+ source_reflection.name, options
+ ).run
+
+ through_records.each do |owner, owner_through_records|
+ owner_through_records.map! { |r| r.send(source_reflection.name) }.flatten!
+ end
+ end
+
+ private
+
+ def through_records_by_owner
+ ActiveRecord::Associations::Preloader.new(
+ owners, through_reflection.name,
+ through_options
+ ).run
+
+ Hash[owners.map do |owner|
+ through_records = Array.wrap(owner.send(through_reflection.name))
+
+ # Dont cache the association - we would only be caching a subset
+ if reflection.options[:source_type] && through_reflection.collection?
+ owner.association(through_reflection.name).reset
+ end
+
+ [owner, through_records]
+ end]
+ end
+
+ def through_options
+ through_options = {}
+
+ if options[:source_type]
+ through_options[:conditions] = { reflection.foreign_type => options[:source_type] }
+ else
+ if options[:conditions]
+ through_options[:include] = options[:include] || options[:source]
+ through_options[:conditions] = options[:conditions]
+ end
+
+ through_options[:order] = options[:order]
+ end
+
+ through_options
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 284ae2bebc..5833c65893 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -10,7 +10,18 @@ module ActiveRecord
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods
+ return if attribute_methods_generated?
super(column_names)
+ @attribute_methods_generated = true
+ end
+
+ def attribute_methods_generated?
+ @attribute_methods_generated ||= false
+ end
+
+ def undefine_attribute_methods(*args)
+ super
+ @attribute_methods_generated = false
end
# Checks whether the method is defined in the model or any of its subclasses
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index e530d2ff87..4db08c774b 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1950,7 +1950,7 @@ MSG
include AttributeMethods::Dirty
include ActiveModel::MassAssignmentSecurity
include Callbacks, ActiveModel::Observing, Timestamp
- include Associations, AssociationPreload, NamedScope
+ include Associations, NamedScope
include IdentityMap
include ActiveModel::SecurePassword
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
index 3716937689..d88720c8bf 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
@@ -89,6 +89,16 @@ module ActiveRecord
retrieve_connection
end
+ # Returns the configuration of the associated connection as a hash:
+ #
+ # ActiveRecord::Base.connection_config
+ # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"}
+ #
+ # Please use only for reading.
+ def connection_config
+ connection_pool.spec.config
+ end
+
def connection_pool
connection_handler.retrieve_connection_pool(self)
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 49659b3aa5..5de08953f9 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -210,6 +210,10 @@ module ActiveRecord
@foreign_type ||= options[:foreign_type] || "#{name}_type"
end
+ def type
+ @type ||= "#{options[:as]}_type"
+ end
+
def primary_key_column
@primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key }
end
@@ -359,6 +363,8 @@ module ActiveRecord
# Holds all the meta-data about a :through association as it was specified
# in the Active Record class.
class ThroughReflection < AssociationReflection #:nodoc:
+ delegate :association_primary_key, :foreign_type, :to => :source_reflection
+
# Gets the source of the through reflection. It checks both a singularized
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
#
@@ -402,10 +408,6 @@ module ActiveRecord
through_reflection.options
end
- def association_primary_key
- source_reflection.association_primary_key
- end
-
def check_validity!
if through_reflection.nil?
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 3c7533ea48..f939bedc81 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -86,7 +86,9 @@ module ActiveRecord
preload = @preload_values
preload += @includes_values unless eager_loading?
- preload.each {|associations| @klass.send(:preload_associations, @records, associations) }
+ preload.each do |associations|
+ ActiveRecord::Associations::Preloader.new(@records, associations).run
+ end
# @readonly_value is true only if set explicitly. @implicit_readonly is true if there
# are JOINS and no explicit SELECT.
@@ -190,7 +192,7 @@ module ActiveRecord
end
stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)))
- stmt.take limit
+ stmt.take limit if limit
stmt.order(*order)
stmt.key = table[primary_key]
@klass.connection.update stmt.to_sql
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index abc4c54109..c1842b1a96 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -204,7 +204,19 @@ module ActiveRecord
relation.select_values = [select_value]
- type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation)
+ query_builder = relation.arel
+
+ if operation == "count"
+ limit = relation.limit_value
+ offset = relation.offset_value
+
+ unless limit && offset
+ query_builder.limit = nil
+ query_builder.offset = nil
+ end
+ end
+
+ type_cast_calculated_value(@klass.connection.select_value(query_builder.to_sql), column_for(column_name), operation)
end
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 7f32e5538e..426000fde1 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -187,7 +187,7 @@ module ActiveRecord
def find_with_associations
including = (@eager_load_values + @includes_values).uniq
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, [])
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, [])
relation = construct_relation_for_association_find(join_dependency)
rows = connection.select_all(relation.to_sql, 'SQL', relation.bind_values)
join_dependency.instantiate(rows)
@@ -197,7 +197,7 @@ module ActiveRecord
def construct_relation_for_association_calculations
including = (@eager_load_values + @includes_values).uniq
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.froms.first)
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first)
relation = except(:includes, :eager_load, :preload)
apply_join_dependency(relation, join_dependency)
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index f76681e880..cd1d7108b3 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -236,7 +236,7 @@ module ActiveRecord
'string_join'
when Hash, Symbol, Array
'association_join'
- when ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation
+ when ActiveRecord::Associations::JoinDependency::JoinAssociation
'stashed_join'
when Arel::Nodes::Join
'join_node'
@@ -254,7 +254,7 @@ module ActiveRecord
join_list = custom_join_ast(manager, string_joins)
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(
@klass,
association_joins,
join_list
@@ -284,7 +284,7 @@ module ActiveRecord
@implicit_readonly = false
arel.project(*selects)
else
- arel.project(Arel.sql(@klass.quoted_table_name + '.*'))
+ arel.project(@klass.arel_table[Arel.star])
end
end
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
index 4e711c4884..29efbbcb8c 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -26,6 +26,7 @@ module ActiveRecord
def assert_sql(*patterns_to_match)
$queries_executed = []
yield
+ $queries_executed
ensure
failed_patterns = []
patterns_to_match.each do |pattern|
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index ca71cd8ed3..48406a9b98 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -120,30 +120,29 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_load_associated_records_in_one_query_when_adapter_has_no_limit
Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
- Post.expects(:i_was_called).with([1,2,3,4,5,6,7]).returns([1])
- associated_records = Post.send(:associated_records, [1,2,3,4,5,6,7]) do |some_ids|
- Post.i_was_called(some_ids)
+
+ post = posts(:welcome)
+ assert_queries(2) do
+ Post.includes(:comments).where(:id => post.id).to_a
end
- assert_equal [1], associated_records
end
def test_load_associated_records_in_several_queries_when_many_ids_passed
- Post.connection.expects(:in_clause_length).at_least_once.returns(5)
- Post.expects(:i_was_called).with([1,2,3,4,5]).returns([1])
- Post.expects(:i_was_called).with([6,7]).returns([6])
- associated_records = Post.send(:associated_records, [1,2,3,4,5,6,7]) do |some_ids|
- Post.i_was_called(some_ids)
+ Post.connection.expects(:in_clause_length).at_least_once.returns(1)
+
+ post1, post2 = posts(:welcome), posts(:thinking)
+ assert_queries(3) do
+ Post.includes(:comments).where(:id => [post1.id, post2.id]).to_a
end
- assert_equal [1,6], associated_records
end
def test_load_associated_records_in_one_query_when_a_few_ids_passed
- Post.connection.expects(:in_clause_length).at_least_once.returns(5)
- Post.expects(:i_was_called).with([1,2,3]).returns([1])
- associated_records = Post.send(:associated_records, [1,2,3]) do |some_ids|
- Post.i_was_called(some_ids)
+ Post.connection.expects(:in_clause_length).at_least_once.returns(3)
+
+ post = posts(:welcome)
+ assert_queries(2) do
+ Post.includes(:comments).where(:id => post.id).to_a
end
- assert_equal [1], associated_records
end
def test_including_duplicate_objects_from_belongs_to
@@ -526,6 +525,22 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert posts[1].categories.include?(categories(:general))
end
+ # This is only really relevant when the identity map is off. Since the preloader for habtm
+ # gets raw row hashes from the database and then instantiates them, this test ensures that
+ # it only instantiates one actual object per record from the database.
+ def test_has_and_belongs_to_many_should_not_instantiate_same_records_multiple_times
+ welcome = posts(:welcome)
+ categories = Category.includes(:posts)
+
+ general = categories.find { |c| c == categories(:general) }
+ technology = categories.find { |c| c == categories(:technology) }
+
+ post1 = general.posts.to_a.find { |p| p == posts(:welcome) }
+ post2 = technology.posts.to_a.find { |p| p == posts(:welcome) }
+
+ assert_equal post1.object_id, post2.object_id
+ end
+
def test_eager_with_has_many_and_limit_and_conditions_on_the_eagers
posts = authors(:david).posts.find(:all,
:include => :comments,
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 6d7f905dc5..19303fef9f 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -214,7 +214,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_with_piggyback
- assert_equal "2", categories(:sti_test).authors.first.post_id.to_s
+ assert_equal "2", categories(:sti_test).authors_with_select.first.post_id.to_s
end
def test_include_has_many_through
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index ed59b2105d..d03fc68a11 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -134,6 +134,7 @@ class BasicsTest < ActiveRecord::TestCase
fakepool = Class.new(Struct.new(:spec)) {
def with_connection; yield self; end
def connection_pool; self; end
+ def table_exists?(name); false; end
def quote_table_name(*args); raise "lol quote_table_name"; end
}
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 3121f1615d..caf07a7357 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -109,6 +109,36 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal [2, 6], c.keys.compact
end
+ def test_limit_with_offset_is_kept
+ return if current_adapter?(:OracleAdapter)
+
+ queries = assert_sql { Account.limit(1).offset(1).count }
+ assert_equal 1, queries.length
+ assert_match(/LIMIT/, queries.first)
+ assert_match(/OFFSET/, queries.first)
+ end
+
+ def test_offset_without_limit_removes_offset
+ queries = assert_sql { Account.offset(1).count }
+ assert_equal 1, queries.length
+ assert_no_match(/LIMIT/, queries.first)
+ assert_no_match(/OFFSET/, queries.first)
+ end
+
+ def test_limit_without_offset_removes_limit
+ queries = assert_sql { Account.limit(1).count }
+ assert_equal 1, queries.length
+ assert_no_match(/LIMIT/, queries.first)
+ assert_no_match(/OFFSET/, queries.first)
+ end
+
+ def test_no_limit_no_offset
+ queries = assert_sql { Account.count }
+ assert_equal 1, queries.length
+ assert_no_match(/LIMIT/, queries.first)
+ assert_no_match(/OFFSET/, queries.first)
+ end
+
def test_should_group_by_summed_field_having_condition
c = Account.sum(:credit_limit, :group => :firm_id,
:having => 'sum(credit_limit) > 50')
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index 680d4ca5dd..b5d8314541 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -208,7 +208,7 @@ class InheritanceTest < ActiveRecord::TestCase
def test_eager_load_belongs_to_primary_key_quoting
con = Account.connection
- assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1/) do
+ assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)/) do
Account.find(1, :include => :firm)
end
end
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index 6269437b14..379cf5b44e 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -94,6 +94,11 @@ class PooledConnectionsTest < ActiveRecord::TestCase
ActiveRecord::Base.connection_handler = old_handler
end
+ def test_connection_config
+ ActiveRecord::Base.establish_connection(@connection)
+ assert_equal @connection, ActiveRecord::Base.connection_config
+ end
+
def test_with_connection_nesting_safety
ActiveRecord::Base.establish_connection(@connection.merge({:pool => 1, :wait_timeout => 0.1}))
diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb
index 06908ea85e..8f37433ec6 100644
--- a/activerecord/test/models/category.rb
+++ b/activerecord/test/models/category.rb
@@ -22,7 +22,8 @@ class Category < ActiveRecord::Base
end
has_many :categorizations
- has_many :authors, :through => :categorizations, :select => 'authors.*, categorizations.post_id'
+ has_many :authors, :through => :categorizations
+ has_many :authors_with_select, :through => :categorizations, :source => :author, :select => 'authors.*, categorizations.post_id'
scope :general, :conditions => { :name => 'General' }
end
diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG
index 1b8bcf649c..373236ce9a 100644
--- a/activesupport/CHANGELOG
+++ b/activesupport/CHANGELOG
@@ -1,5 +1,14 @@
*Rails 3.1.0 (unreleased)*
+* LocalCache strategy is now a real middleware class, not an anonymous class
+posing for pictures.
+
+* ActiveSupport::Dependencies::ClassCache class has been introduced for
+holding references to reloadable classes.
+
+* ActiveSupport::Dependencies::Reference has been refactored to take direct
+advantage of the new ClassCache.
+
* Backports Range#cover? as an alias for Range#include? in Ruby 1.8 [Diego Carrion, fxn]
* Added weeks_ago and prev_week to Date/DateTime/Time. [Rob Zolkos, fxn]
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb
index 4dce35f1c9..0649a058aa 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -50,34 +50,39 @@ module ActiveSupport
end
end
- # Middleware class can be inserted as a Rack handler to be local cache for the
- # duration of request.
- def middleware
- @middleware ||= begin
- klass = Class.new
- klass.class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- class << self
- def name
- "ActiveSupport::Cache::Strategy::LocalCache"
- end
- alias :to_s :name
- end
+ #--
+ # This class wraps up local storage for middlewares. Only the middleware method should
+ # construct them.
+ class Middleware # :nodoc:
+ attr_reader :name, :thread_local_key
- def initialize(app)
- @app = app
- end
+ def initialize(name, thread_local_key)
+ @name = name
+ @thread_local_key = thread_local_key
+ @app = nil
+ end
- def call(env)
- Thread.current[:#{thread_local_key}] = LocalStore.new
- @app.call(env)
- ensure
- Thread.current[:#{thread_local_key}] = nil
- end
- EOS
- klass
+ def new(app)
+ @app = app
+ self
+ end
+
+ def call(env)
+ Thread.current[thread_local_key] = LocalStore.new
+ @app.call(env)
+ ensure
+ Thread.current[thread_local_key] = nil
end
end
+ # Middleware class can be inserted as a Rack handler to be local cache for the
+ # duration of request.
+ def middleware
+ @middleware ||= Middleware.new(
+ "ActiveSupport::Cache::Strategy::LocalCache",
+ thread_local_key)
+ end
+
def clear(options = nil) # :nodoc:
local_cache.clear(options) if local_cache
super
diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb
index 644db0b205..be19189c04 100644
--- a/activesupport/lib/active_support/configurable.rb
+++ b/activesupport/lib/active_support/configurable.rb
@@ -26,7 +26,7 @@ module ActiveSupport
module ClassMethods
def config
- @_config ||= if superclass.respond_to?(:config)
+ @_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
superclass.config.inheritable_copy
else
# create a new "anonymous" class that will host the compiled reader methods
diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb
index f34185f22c..724e076407 100644
--- a/activesupport/lib/active_support/core_ext/date/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date/calculations.rb
@@ -36,9 +36,9 @@ class Date
::Date.current.tomorrow
end
- # Returns Time.zone.today when config.time_zone is set, otherwise just returns Date.today.
+ # Returns Time.zone.today when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns Date.today.
def current
- ::Time.zone_default ? ::Time.zone.today : ::Date.today
+ ::Time.zone ? ::Time.zone.today : ::Date.today
end
end
diff --git a/activesupport/lib/active_support/core_ext/date/zones.rb b/activesupport/lib/active_support/core_ext/date/zones.rb
index 3a83af6be2..a70b47b7bc 100644
--- a/activesupport/lib/active_support/core_ext/date/zones.rb
+++ b/activesupport/lib/active_support/core_ext/date/zones.rb
@@ -2,10 +2,10 @@ require 'date'
require 'active_support/core_ext/time/zones'
class Date
- # Converts Date to a TimeWithZone in the current zone if Time.zone_default is set,
- # otherwise converts Date to a Time via Date#to_time
+ # Converts Date to a TimeWithZone in the current zone if Time.zone or Time.zone_default
+ # is set, otherwise converts Date to a Time via Date#to_time
def to_time_in_current_zone
- if ::Time.zone_default
+ if ::Time.zone
::Time.zone.local(year, month, day)
else
to_time
diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
index 1dc3933e12..8d924ad420 100644
--- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
@@ -1,6 +1,4 @@
require 'rational' unless RUBY_VERSION >= '1.9.2'
-require 'active_support/core_ext/object/acts_like'
-require 'active_support/core_ext/time/zones'
class DateTime
class << self
@@ -9,8 +7,9 @@ class DateTime
::Time.local(2007).utc_offset.to_r / 86400
end
+ # Returns <tt>Time.zone.now.to_datetime</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise returns <tt>Time.now.to_datetime</tt>.
def current
- ::Time.zone_default ? ::Time.zone.now.to_datetime : ::Time.now.to_datetime
+ ::Time.zone ? ::Time.zone.now.to_datetime : ::Time.now.to_datetime
end
end
@@ -104,11 +103,7 @@ class DateTime
end
# Layers additional behavior on DateTime#<=> so that Time and ActiveSupport::TimeWithZone instances can be compared with a DateTime
- def compare_with_coercion(other)
- other = other.comparable_time if other.respond_to?(:comparable_time)
- other = other.to_datetime unless other.acts_like?(:date)
- compare_without_coercion(other)
+ def <=>(other)
+ super other.to_datetime
end
- alias_method :compare_without_coercion, :<=>
- alias_method :<=>, :compare_with_coercion
end
diff --git a/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb b/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb
index 87a7bebd7b..63b4ba49e9 100644
--- a/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb
+++ b/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb
@@ -1,11 +1,9 @@
class Hash
- # Allows for reverse merging two hashes where the keys in the calling hash take precedence over those
- # in the <tt>other_hash</tt>. This is particularly useful for initializing an option hash with default values:
+ # Merges the caller into +other_hash+. For example,
#
- # def setup(options = {})
- # options.reverse_merge! :size => 25, :velocity => 10
- # end
+ # options = options.reverse_merge(:size => 25, :velocity => 10)
#
+<<<<<<< HEAD
# The default <tt>:size</tt> and <tt>:velocity</tt> are only set if the +options+ hash passed in doesn't already
# have the respective key.
#
@@ -14,12 +12,19 @@ class Hash
# def setup(options = {})
# options = { :size => 25, :velocity => 10 }.merge(options)
# end
+=======
+ # is equivalent to
+ #
+ # options = {:size => 25, :velocity => 10}.merge(options)
+ #
+ # This is particularly useful for initializing an options hash
+ # with default values.
+>>>>>>> 20768176292cbcb883ab152b4aa9ed8c664771cd
def reverse_merge(other_hash)
other_hash.merge(self)
end
- # Performs the opposite of <tt>merge</tt>, with the keys and values from the first hash taking precedence over the second.
- # Modifies the receiver in place.
+ # Destructive +reverse_merge+.
def reverse_merge!(other_hash)
# right wins if there is no left
merge!( other_hash ){|key,left,right| left }
diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb
index f70853073a..7b5832b51a 100644
--- a/activesupport/lib/active_support/core_ext/object/blank.rb
+++ b/activesupport/lib/active_support/core_ext/object/blank.rb
@@ -13,7 +13,11 @@ class Object
respond_to?(:empty?) ? empty? : !self
end
+<<<<<<< HEAD
# An object is present if it's not #blank?.
+=======
+ # An object is present if it's not <tt>blank?</tt>.
+>>>>>>> 20768176292cbcb883ab152b4aa9ed8c664771cd
def present?
!blank?
end
diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb
index fa052fa86b..7e134db118 100644
--- a/activesupport/lib/active_support/core_ext/time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/time/calculations.rb
@@ -1,7 +1,4 @@
require 'active_support/duration'
-require 'active_support/core_ext/date/acts_like'
-require 'active_support/core_ext/date/calculations'
-require 'active_support/core_ext/date_time/conversions'
class Time
COMMON_YEAR_DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
@@ -41,9 +38,9 @@ class Time
time_with_datetime_fallback(:local, *args)
end
- # Returns <tt>Time.zone.now</tt> when <tt>config.time_zone</tt> is set, otherwise just returns <tt>Time.now</tt>.
+ # Returns <tt>Time.zone.now</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns <tt>Time.now</tt>.
def current
- ::Time.zone_default ? ::Time.zone.now : ::Time.now
+ ::Time.zone ? ::Time.zone.now : ::Time.now
end
end
@@ -283,14 +280,8 @@ class Time
# Layers additional behavior on Time#<=> so that DateTime and ActiveSupport::TimeWithZone instances
# can be chronologically compared with a Time
def compare_with_coercion(other)
- # if other is an ActiveSupport::TimeWithZone, coerce a Time instance from it so we can do <=> comparison
- other = other.comparable_time if other.respond_to?(:comparable_time)
- if other.acts_like?(:date)
- # other is a Date/DateTime, so coerce self #to_datetime and hand off to DateTime#<=>
- to_datetime.compare_without_coercion(other)
- else
- compare_without_coercion(other)
- end
+ # we're avoiding Time#to_datetime cause it's expensive
+ other.is_a?(Time) ? compare_without_coercion(other.to_time) : to_datetime <=> other
end
alias_method :compare_without_coercion, :<=>
alias_method :<=>, :compare_with_coercion
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index daa98162d0..dc10f78104 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -5,6 +5,7 @@ require 'active_support/core_ext/module/aliasing'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/module/anonymous'
+require 'active_support/core_ext/module/deprecation'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/load_error'
require 'active_support/core_ext/name_error'
@@ -47,9 +48,6 @@ module ActiveSupport #:nodoc:
mattr_accessor :autoloaded_constants
self.autoloaded_constants = []
- mattr_accessor :references
- self.references = {}
-
# An array of constant names that need to be unloaded on every request. Used
# to allow arbitrary constants to be marked for unloading.
mattr_accessor :explicitly_unloadable_constants
@@ -524,31 +522,76 @@ module ActiveSupport #:nodoc:
explicitly_unloadable_constants.each { |const| remove_constant const }
end
- class Reference
- @@constants = Hash.new { |h, k| h[k] = Inflector.constantize(k) }
+ class ClassCache
+ def initialize
+ @store = Hash.new { |h, k| h[k] = Inflector.constantize(k) }
+ end
+
+ def empty?
+ @store.empty?
+ end
+
+ def key?(key)
+ @store.key?(key)
+ end
+
+ def []=(key, value)
+ return unless key.respond_to?(:name)
+
+ raise(ArgumentError, 'anonymous classes cannot be cached') if key.name.blank?
+
+ @store[key.name] = value
+ end
+
+ def [](key)
+ key = key.name if key.respond_to?(:name)
+
+ @store[key]
+ end
+ alias :get :[]
- attr_reader :name
+ class Getter # :nodoc:
+ def initialize(name)
+ @name = name
+ end
- def initialize(name)
- @name = name.to_s
- @@constants[@name] = name if name.respond_to?(:name)
+ def get
+ Reference.get @name
+ end
+ deprecate :get
end
- def get
- @@constants[@name]
+ def new(name)
+ self[name] = name
+ Getter.new(name)
end
+ deprecate :new
- def self.clear!
- @@constants.clear
+ def store(name)
+ self[name] = name
+ self
+ end
+
+ def clear!
+ @store.clear
end
end
+ Reference = ClassCache.new
+
def ref(name)
- references[name] ||= Reference.new(name)
+ Reference.new(name)
+ end
+ deprecate :ref
+
+ # Store a reference to a class +klass+.
+ def reference(klass)
+ Reference.store klass
end
+ # Get the reference for class named +name+.
def constantize(name)
- ref(name).get
+ Reference.get(name)
end
# Determine if the given constant has been automatically loaded.
diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb
index f4c27ac935..8d8e6ebc58 100644
--- a/activesupport/lib/active_support/ordered_options.rb
+++ b/activesupport/lib/active_support/ordered_options.rb
@@ -31,7 +31,7 @@ module ActiveSupport #:nodoc:
def method_missing(name, *args)
if name.to_s =~ /(.*)=$/
- self[$1.to_sym] = args.first
+ self[$1] = args.first
else
self[name]
end
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index 3da216ac78..c66aa78ce8 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -281,7 +281,7 @@ module ActiveSupport
# A TimeWithZone acts like a Time, so just return +self+.
def to_time
- self
+ utc
end
def to_datetime
diff --git a/activesupport/test/class_cache_test.rb b/activesupport/test/class_cache_test.rb
new file mode 100644
index 0000000000..8445af8d25
--- /dev/null
+++ b/activesupport/test/class_cache_test.rb
@@ -0,0 +1,108 @@
+require 'abstract_unit'
+require 'active_support/dependencies'
+
+module ActiveSupport
+ module Dependencies
+ class ClassCacheTest < ActiveSupport::TestCase
+ def setup
+ @cache = ClassCache.new
+ end
+
+ def test_empty?
+ assert @cache.empty?
+ @cache[ClassCacheTest] = ClassCacheTest
+ assert !@cache.empty?
+ end
+
+ def test_clear!
+ assert @cache.empty?
+ @cache[ClassCacheTest] = ClassCacheTest
+ assert !@cache.empty?
+ @cache.clear!
+ assert @cache.empty?
+ end
+
+ def test_set_key
+ @cache[ClassCacheTest] = ClassCacheTest
+ assert @cache.key?(ClassCacheTest.name)
+ end
+
+ def test_set_rejects_strings
+ @cache[ClassCacheTest.name] = ClassCacheTest
+ assert @cache.empty?
+ end
+
+ def test_get_with_class
+ @cache[ClassCacheTest] = ClassCacheTest
+ assert_equal ClassCacheTest, @cache[ClassCacheTest]
+ end
+
+ def test_get_with_name
+ @cache[ClassCacheTest] = ClassCacheTest
+ assert_equal ClassCacheTest, @cache[ClassCacheTest.name]
+ end
+
+ def test_get_constantizes
+ assert @cache.empty?
+ assert_equal ClassCacheTest, @cache[ClassCacheTest.name]
+ end
+
+ def test_get_is_an_alias
+ assert_equal @cache[ClassCacheTest], @cache.get(ClassCacheTest.name)
+ end
+
+ def test_new
+ assert_deprecated do
+ @cache.new ClassCacheTest
+ end
+ assert @cache.key?(ClassCacheTest.name)
+ end
+
+ def test_new_rejects_strings
+ assert_deprecated do
+ @cache.new ClassCacheTest.name
+ end
+ assert !@cache.key?(ClassCacheTest.name)
+ end
+
+ def test_new_rejects_strings
+ @cache.store ClassCacheTest.name
+ assert !@cache.key?(ClassCacheTest.name)
+ end
+
+ def test_store_returns_self
+ x = @cache.store ClassCacheTest
+ assert_equal @cache, x
+ end
+
+ def test_new_returns_proxy
+ v = nil
+ assert_deprecated do
+ v = @cache.new ClassCacheTest.name
+ end
+
+ assert_deprecated do
+ assert_equal ClassCacheTest, v.get
+ end
+ end
+
+ def test_anonymous_class_fail
+ assert_raises(ArgumentError) do
+ assert_deprecated do
+ @cache.new Class.new
+ end
+ end
+
+ assert_raises(ArgumentError) do
+ x = Class.new
+ @cache[x] = x
+ end
+
+ assert_raises(ArgumentError) do
+ x = Class.new
+ @cache.store x
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb
index 9c773c1944..2b28e61815 100644
--- a/activesupport/test/configurable_test.rb
+++ b/activesupport/test/configurable_test.rb
@@ -21,6 +21,12 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase
assert_equal({ :foo => :bar }, Parent.config)
end
+ test "adds a configuration hash to a module as well" do
+ mixin = Module.new { include ActiveSupport::Configurable }
+ mixin.config.foo = :bar
+ assert_equal({ :foo => :bar }, mixin.config)
+ end
+
test "configuration hash is inheritable" do
assert_equal :bar, Child.config.foo
assert_equal :bar, Parent.config.foo
@@ -57,4 +63,4 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase
assert_respond_to child.config, :bar
assert_respond_to child.new.config, :bar
end
-end \ No newline at end of file
+end
diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb
index b4d7633e5f..03b84ae2e5 100644
--- a/activesupport/test/core_ext/date_ext_test.rb
+++ b/activesupport/test/core_ext/date_ext_test.rb
@@ -259,7 +259,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Date.current - 1, Date.yesterday
end
- def test_yesterday_constructor_when_zone_default_is_not_set
+ def test_yesterday_constructor_when_zone_is_not_set
with_env_tz 'UTC' do
with_tz_default do
Time.stubs(:now).returns Time.local(2000, 1, 1)
@@ -268,7 +268,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
end
- def test_yesterday_constructor_when_zone_default_is_set
+ def test_yesterday_constructor_when_zone_is_set
with_env_tz 'UTC' do
with_tz_default ActiveSupport::TimeZone['Eastern Time (US & Canada)'] do # UTC -5
Time.stubs(:now).returns Time.local(2000, 1, 1)
@@ -281,7 +281,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Date.current + 1, Date.tomorrow
end
- def test_tomorrow_constructor_when_zone_default_is_not_set
+ def test_tomorrow_constructor_when_zone_is_not_set
with_env_tz 'UTC' do
with_tz_default do
Time.stubs(:now).returns Time.local(1999, 12, 31)
@@ -290,7 +290,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
end
- def test_tomorrow_constructor_when_zone_default_is_set
+ def test_tomorrow_constructor_when_zone_is_set
with_env_tz 'UTC' do
with_tz_default ActiveSupport::TimeZone['Europe/Paris'] do # UTC +1
Time.stubs(:now).returns Time.local(1999, 12, 31, 23)
@@ -303,7 +303,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005,2,21,0,0,45), Date.new(2005,2,21).since(45)
end
- def test_since_when_zone_default_is_set
+ def test_since_when_zone_is_set
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'UTC' do
with_tz_default zone do
@@ -317,7 +317,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005,2,20,23,59,15), Date.new(2005,2,21).ago(45)
end
- def test_ago_when_zone_default_is_set
+ def test_ago_when_zone_is_set
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'UTC' do
with_tz_default zone do
@@ -331,7 +331,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005,2,21,0,0,0), Date.new(2005,2,21).beginning_of_day
end
- def test_beginning_of_day_when_zone_default_is_set
+ def test_beginning_of_day_when_zone_is_set
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'UTC' do
with_tz_default zone do
@@ -345,7 +345,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005,2,21,23,59,59,999999.999), Date.new(2005,2,21).end_of_day
end
- def test_end_of_day_when_zone_default_is_set
+ def test_end_of_day_when_zone_is_set
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'UTC' do
with_tz_default zone do
@@ -367,7 +367,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
end
- def test_xmlschema_when_zone_default_is_set
+ def test_xmlschema_when_zone_is_set
with_env_tz 'UTC' do
with_tz_default ActiveSupport::TimeZone['Eastern Time (US & Canada)'] do # UTC -5
assert_match(/^1980-02-28T00:00:00-05:?00$/, Date.new(1980, 2, 28).xmlschema)
@@ -407,7 +407,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal true, Date.new(2000,1,2).future?
end
- def test_current_returns_date_today_when_zone_default_not_set
+ def test_current_returns_date_today_when_zone_not_set
with_env_tz 'US/Central' do
Time.stubs(:now).returns Time.local(1999, 12, 31, 23)
assert_equal Date.new(1999, 12, 31), Date.today
@@ -415,15 +415,15 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
end
- def test_current_returns_time_zone_today_when_zone_default_set
- Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ def test_current_returns_time_zone_today_when_zone_is_set
+ Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'US/Central' do
Time.stubs(:now).returns Time.local(1999, 12, 31, 23)
assert_equal Date.new(1999, 12, 31), Date.today
assert_equal Date.new(2000, 1, 1), Date.current
end
ensure
- Time.zone_default = nil
+ Time.zone = nil
end
def test_date_advance_should_not_change_passed_options_hash
@@ -441,11 +441,11 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
def with_tz_default(tz = nil)
- old_tz = Time.zone_default
- Time.zone_default = tz
+ old_tz = Time.zone
+ Time.zone = tz
yield
ensure
- Time.zone_default = old_tz
+ Time.zone = old_tz
end
end
diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb
index 8edb95b63a..456736cbad 100644
--- a/activesupport/test/core_ext/date_time_ext_test.rb
+++ b/activesupport/test/core_ext/date_time_ext_test.rb
@@ -282,21 +282,21 @@ class DateTimeExtCalculationsTest < Test::Unit::TestCase
assert_equal true, DateTime.civil(2005,2,10,20,30,46).future?
end
- def test_current_returns_date_today_when_zone_default_not_set
+ def test_current_returns_date_today_when_zone_is_not_set
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(1999, 12, 31, 23, 59, 59)
assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current
end
end
- def test_current_returns_time_zone_today_when_zone_default_set
- Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ def test_current_returns_time_zone_today_when_zone_is_set
+ Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(1999, 12, 31, 23, 59, 59)
assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current
end
ensure
- Time.zone_default = nil
+ Time.zone = nil
end
def test_current_without_time_zone
diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb
index 6a01eeed6b..c0b529d9f8 100644
--- a/activesupport/test/core_ext/duration_test.rb
+++ b/activesupport/test/core_ext/duration_test.rb
@@ -89,8 +89,8 @@ class DurationTest < ActiveSupport::TestCase
assert_in_delta((7 * 24 * 1.7).hours.ago(t), 1.7.weeks.ago(t), 1)
end
- def test_since_and_ago_anchored_to_time_now_when_time_zone_default_not_set
- Time.zone_default = nil
+ def test_since_and_ago_anchored_to_time_now_when_time_zone_is_not_set
+ Time.zone = nil
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
@@ -102,8 +102,8 @@ class DurationTest < ActiveSupport::TestCase
end
end
- def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_default_set
- Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_is_set
+ Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
@@ -116,7 +116,7 @@ class DurationTest < ActiveSupport::TestCase
assert_equal 'Eastern Time (US & Canada)', 5.seconds.ago.time_zone.name
end
ensure
- Time.zone_default = nil
+ Time.zone = nil
end
def test_adding_hours_across_dst_boundary
diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb
index 6ef4e37b26..3a2452b4b0 100644
--- a/activesupport/test/core_ext/numeric_ext_test.rb
+++ b/activesupport/test/core_ext/numeric_ext_test.rb
@@ -89,8 +89,8 @@ class NumericExtTimeAndDateTimeTest < Test::Unit::TestCase
assert_equal DateTime.civil(2005,2,28,15,15,10), DateTime.civil(2004,2,29,15,15,10) + 1.year
end
- def test_since_and_ago_anchored_to_time_now_when_time_zone_default_not_set
- Time.zone_default = nil
+ def test_since_and_ago_anchored_to_time_now_when_time_zone_is_not_set
+ Time.zone = nil
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
@@ -102,8 +102,8 @@ class NumericExtTimeAndDateTimeTest < Test::Unit::TestCase
end
end
- def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_default_set
- Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_is_set
+ Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
@@ -116,7 +116,7 @@ class NumericExtTimeAndDateTimeTest < Test::Unit::TestCase
assert_equal 'Eastern Time (US & Canada)', 5.ago.time_zone.name
end
ensure
- Time.zone_default = nil
+ Time.zone = nil
end
protected
diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb
index 891a6badac..53d497013a 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -533,9 +533,19 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
Time::DATE_FORMATS.delete(:custom)
end
- def test_conversion_methods_are_publicized
- assert Time.public_instance_methods.include?(:to_date) || Time.public_instance_methods.include?('to_date')
- assert Time.public_instance_methods.include?(:to_datetime) || Time.public_instance_methods.include?('to_datetime')
+ def test_to_date
+ assert_equal Date.new(2005, 2, 21), Time.local(2005, 2, 21, 17, 44, 30).to_date
+ end
+
+ def test_to_datetime
+ assert_equal Time.utc(2005, 2, 21, 17, 44, 30).to_datetime, DateTime.civil(2005, 2, 21, 17, 44, 30, 0, 0)
+ with_env_tz 'US/Eastern' do
+ assert_equal Time.local(2005, 2, 21, 17, 44, 30).to_datetime, DateTime.civil(2005, 2, 21, 17, 44, 30, Rational(Time.local(2005, 2, 21, 17, 44, 30).utc_offset, 86400), 0)
+ end
+ with_env_tz 'NZ' do
+ assert_equal Time.local(2005, 2, 21, 17, 44, 30).to_datetime, DateTime.civil(2005, 2, 21, 17, 44, 30, Rational(Time.local(2005, 2, 21, 17, 44, 30).utc_offset, 86400), 0)
+ end
+ assert_equal ::Date::ITALY, Time.utc(2005, 2, 21, 17, 44, 30).to_datetime.start # use Ruby's default start value
end
def test_to_time
diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb
index 5c226c2d09..bafa335a09 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -768,10 +768,10 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase
end
def test_localtime
- Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
assert_equal @dt.in_time_zone.localtime, @dt.in_time_zone.utc.to_time.getlocal
ensure
- Time.zone_default = nil
+ Time.zone = nil
end
def test_use_zone
@@ -801,7 +801,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase
assert_equal nil, Time.zone
end
- def test_time_zone_getter_and_setter_with_zone_default
+ def test_time_zone_getter_and_setter_with_zone_default_set
Time.zone_default = ActiveSupport::TimeZone['Alaska']
assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
Time.zone = ActiveSupport::TimeZone['Hawaii']
@@ -809,6 +809,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase
Time.zone = nil
assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
ensure
+ Time.zone = nil
Time.zone_default = nil
end
@@ -849,7 +850,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase
assert_nil Time.zone
end
- def test_current_returns_time_now_when_zone_default_not_set
+ def test_current_returns_time_now_when_zone_not_set
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
assert_equal false, Time.current.is_a?(ActiveSupport::TimeWithZone)
@@ -857,8 +858,8 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase
end
end
- def test_current_returns_time_zone_now_when_zone_default_set
- Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ def test_current_returns_time_zone_now_when_zone_set
+ Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
assert_equal true, Time.current.is_a?(ActiveSupport::TimeWithZone)
@@ -866,7 +867,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase
assert_equal Time.utc(2000), Time.current.time
end
ensure
- Time.zone_default = nil
+ Time.zone = nil
end
protected
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index bc7f597f1d..ef017d7436 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -477,15 +477,15 @@ class DependenciesTest < Test::Unit::TestCase
def test_references_should_work
with_loading 'dependencies' do
- c = ActiveSupport::Dependencies.ref("ServiceOne")
+ c = ActiveSupport::Dependencies.reference("ServiceOne")
service_one_first = ServiceOne
- assert_equal service_one_first, c.get
+ assert_equal service_one_first, c.get("ServiceOne")
ActiveSupport::Dependencies.clear
assert ! defined?(ServiceOne)
service_one_second = ServiceOne
- assert_not_equal service_one_first, c.get
- assert_equal service_one_second, c.get
+ assert_not_equal service_one_first, c.get("ServiceOne")
+ assert_equal service_one_second, c.get("ServiceOne")
end
end
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index f55116dfab..1670d9ee7d 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -51,21 +51,21 @@ class InflectorTest < Test::Unit::TestCase
end
SingularToPlural.each do |singular, plural|
- define_method "test_pluralize_#{singular}" do
+ define_method "test_pluralize_singular_#{singular}" do
assert_equal(plural, ActiveSupport::Inflector.pluralize(singular))
assert_equal(plural.capitalize, ActiveSupport::Inflector.pluralize(singular.capitalize))
end
end
SingularToPlural.each do |singular, plural|
- define_method "test_singularize_#{plural}" do
+ define_method "test_singularize_plural_#{plural}" do
assert_equal(singular, ActiveSupport::Inflector.singularize(plural))
assert_equal(singular.capitalize, ActiveSupport::Inflector.singularize(plural.capitalize))
end
end
-
+
SingularToPlural.each do |singular, plural|
- define_method "test_pluralize_#{plural}" do
+ define_method "test_pluralize_plural_#{plural}" do
assert_equal(plural, ActiveSupport::Inflector.pluralize(plural))
assert_equal(plural.capitalize, ActiveSupport::Inflector.pluralize(plural.capitalize))
end
diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb
index 7e65c63062..756d21b3e4 100644
--- a/activesupport/test/test_case_test.rb
+++ b/activesupport/test/test_case_test.rb
@@ -12,6 +12,10 @@ module ActiveSupport
def puke(klass, name, e)
@puked << [klass, name, e]
end
+
+ def options
+ nil
+ end
end
if defined?(MiniTest::Assertions) && TestCase < MiniTest::Assertions
diff --git a/ci/ci_build.rb b/ci/ci_build.rb
index 964e2d4eb8..c3af1f0177 100755
--- a/ci/ci_build.rb
+++ b/ci/ci_build.rb
@@ -82,32 +82,72 @@ end
rm_f "#{root_dir}/activerecord/debug.log"
cd "#{root_dir}/activerecord" do
puts
- puts "[CruiseControl] Building Active Record with MySQL"
+ puts "[CruiseControl] Building Active Record with MySQL IM enabled"
puts
+ ENV['IM'] = 'true'
build_results[:activerecord_mysql] = rake 'mysql:rebuild_databases', 'mysql:test'
build_results[:activerecord_mysql_isolated] = rake 'mysql:rebuild_databases', 'mysql:isolated_test'
end
cd "#{root_dir}/activerecord" do
puts
- puts "[CruiseControl] Building Active Record with MySQL2"
+ puts "[CruiseControl] Building Active Record with MySQL IM disabled"
puts
+ ENV['IM'] = 'false'
+ build_results[:activerecord_mysql] = rake 'mysql:rebuild_databases', 'mysql:test'
+ build_results[:activerecord_mysql_isolated] = rake 'mysql:rebuild_databases', 'mysql:isolated_test'
+end
+
+cd "#{root_dir}/activerecord" do
+ puts
+ puts "[CruiseControl] Building Active Record with MySQL2 IM enabled"
+ puts
+ ENV['IM'] = 'true'
+ build_results[:activerecord_mysql2] = rake 'mysql:rebuild_databases', 'mysql2:test'
+ build_results[:activerecord_mysql2_isolated] = rake 'mysql:rebuild_databases', 'mysql2:isolated_test'
+end
+
+cd "#{root_dir}/activerecord" do
+ puts
+ puts "[CruiseControl] Building Active Record with MySQL2 IM disabled"
+ puts
+ ENV['IM'] = 'false'
build_results[:activerecord_mysql2] = rake 'mysql:rebuild_databases', 'mysql2:test'
build_results[:activerecord_mysql2_isolated] = rake 'mysql:rebuild_databases', 'mysql2:isolated_test'
end
cd "#{root_dir}/activerecord" do
puts
- puts "[CruiseControl] Building Active Record with PostgreSQL"
+ puts "[CruiseControl] Building Active Record with PostgreSQL IM enabled"
puts
+ ENV['IM'] = 'true'
build_results[:activerecord_postgresql8] = rake 'postgresql:rebuild_databases', 'postgresql:test'
build_results[:activerecord_postgresql8_isolated] = rake 'postgresql:rebuild_databases', 'postgresql:isolated_test'
end
cd "#{root_dir}/activerecord" do
puts
- puts "[CruiseControl] Building Active Record with SQLite 3"
+ puts "[CruiseControl] Building Active Record with PostgreSQL IM disabled"
+ puts
+ ENV['IM'] = 'false'
+ build_results[:activerecord_postgresql8] = rake 'postgresql:rebuild_databases', 'postgresql:test'
+ build_results[:activerecord_postgresql8_isolated] = rake 'postgresql:rebuild_databases', 'postgresql:isolated_test'
+end
+
+cd "#{root_dir}/activerecord" do
+ puts
+ puts "[CruiseControl] Building Active Record with SQLite 3 IM enabled"
+ puts
+ ENV['IM'] = 'true'
+ build_results[:activerecord_sqlite3] = rake 'sqlite3:test'
+ build_results[:activerecord_sqlite3_isolated] = rake 'sqlite3:isolated_test'
+end
+
+cd "#{root_dir}/activerecord" do
+ puts
+ puts "[CruiseControl] Building Active Record with SQLite 3 IM disabled"
puts
+ ENV['IM'] = 'false'
build_results[:activerecord_sqlite3] = rake 'sqlite3:test'
build_results[:activerecord_sqlite3_isolated] = rake 'sqlite3:isolated_test'
end
diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile
index 7becefee09..1df36137b4 100644
--- a/railties/guides/source/active_support_core_extensions.textile
+++ b/railties/guides/source/active_support_core_extensions.textile
@@ -2793,6 +2793,8 @@ h5. +Date.current+
Active Support defines +Date.current+ to be today in the current time zone. That's like +Date.today+, except that it honors the user time zone, if defined. It also defines +Date.yesterday+ and +Date.tomorrow+, and the instance predicates +past?+, +today?+, and +future?+, all of them relative to +Date.current+.
+When making Date comparisons using methods which honor the user time zone, make sure to use +Date.current+ and not +Date.today+. There are cases where the user time zone might be in the future compared to the system time zone, which +Date.today+ uses by default. This means +Date.today+ may equal +Date.yesterday+.
+
h5. Named dates
h6. +prev_year+, +next_year+
@@ -3107,7 +3109,7 @@ h5. Named Datetimes
h6. +DateTime.current+
-Active Support defines +DateTime.current+ to be like +Time.now.to_datetime+, except that it honors the user time zone, if defined. It also defines instance predicates +past?+, and +future?+ relative to +DateTime.current+.
+Active Support defines +DateTime.current+ to be like +Time.now.to_datetime+, except that it honors the user time zone, if defined. It also defines +DateTime.yesterday+ and +DateTime.tomorrow+, and the instance predicates +past?+, and +future?+ relative to +DateTime.current+.
h5. Other Extensions
@@ -3284,6 +3286,12 @@ t.advance(:seconds => 1)
* If +since+ or +ago+ jump to a time that can't be expressed with +Time+ a +DateTime+ object is returned instead.
+h5. +Time.current+
+
+Active Support defines +Time.current+ to be today in the current time zone. That's like +Time.now+, except that it honors the user time zone, if defined. It also defines +Time.yesterday+ and +Time.tomorrow+, and the instance predicates +past?+, +today?+, and +future?+, all of them relative to +Time.current+.
+
+When making Time comparisons using methods which honor the user time zone, make sure to use +Time.current+ and not +Time.now+. There are cases where the user time zone might be in the future compared to the system time zone, which +Time.today+ uses by default. This means +Time.now+ may equal +Time.yesterday+.
+
h4. Time Constructors
Active Support defines +Time.current+ to be +Time.zone.now+ if there's a user time zone defined, with fallback to +Time.now+:
diff --git a/railties/guides/source/contributing_to_ruby_on_rails.textile b/railties/guides/source/contributing_to_ruby_on_rails.textile
index 9739da2666..82e7edfc84 100644
--- a/railties/guides/source/contributing_to_ruby_on_rails.textile
+++ b/railties/guides/source/contributing_to_ruby_on_rails.textile
@@ -349,7 +349,20 @@ $ git commit -a
$ git format-patch master --stdout > my_new_patch.diff
</shell>
-Sanity check the results of this operation: open the diff file in your text editor of choice and make sure that no unintended changes crept in.
+Open the diff file in your text editor of choice to sanity check the results, and make sure that no unintended changes crept in.
+
+You can also perform an extra check by applying the patch to a different dedicated branch:
+
+<shell>
+$ git checkout -b testing_branch
+$ git apply --check my_new_patch.diff
+</shell>
+
+Please make sure the patch does not introduce whitespace errors:
+
+<shell>
+$ git apply --whitespace=error-all mynew_patch.diff
+</shell>
You can check your patches by applying your patch to an different dedicated branch:
diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb
index c3927b6613..e447209242 100644
--- a/railties/lib/rails/commands/server.rb
+++ b/railties/lib/rails/commands/server.rb
@@ -42,6 +42,10 @@ module Rails
set_environment
end
+ def app
+ @app ||= super.instance
+ end
+
def opt_parser
Options.new
end
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index 4ce874d4b9..50bba22a3a 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -382,7 +382,10 @@ module Rails
# Finds engine with given path
def find(path)
- Rails::Engine::Railties.engines.find { |r| File.expand_path(r.root.to_s) == File.expand_path(path.to_s) }
+ path = path.to_s
+ Rails::Engine::Railties.engines.find { |r|
+ File.expand_path(r.root.to_s) == File.expand_path(path)
+ }
end
end
diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb
index d7a86a5c40..c323df3e95 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -264,17 +264,18 @@ module Rails
# readme "README"
#
def readme(path)
- say File.read(find_in_source_paths(path))
+ log File.read(find_in_source_paths(path))
end
protected
# Define log for backwards compatibility. If just one argument is sent,
- # invoke say, otherwise invoke say_status.
+ # invoke say, otherwise invoke say_status. Differently from say and
+ # similarly to say_status, this method respects the quiet? option given.
#
def log(*args)
if args.size == 1
- say args.first.to_s
+ say args.first.to_s unless options.quiet?
else
args << (self.behavior == :invoke ? :green : :red)
say_status *args
diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb
index 131eb6ff6f..dfecd2a6e4 100644
--- a/railties/lib/rails/generators/base.rb
+++ b/railties/lib/rails/generators/base.rb
@@ -3,7 +3,7 @@ begin
rescue LoadError
puts "Thor is not available.\nIf you ran this command from a git checkout " \
"of Rails, please make sure thor is installed,\nand run this command " \
- "as `ruby #{$0} #{ARGV.join(" ")} --dev`"
+ "as `ruby #{$0} #{(ARGV | ['--dev']).join(" ")}`"
exit
end
diff --git a/railties/lib/rails/generators/rails/app/templates/public/index.html b/railties/lib/rails/generators/rails/app/templates/public/index.html
index 75d5edd06d..13a203dd08 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/index.html
+++ b/railties/lib/rails/generators/rails/app/templates/public/index.html
@@ -52,7 +52,6 @@
clear: both;
}
-
#header, #about, #getting-started {
padding-left: 75px;
padding-right: 30px;
@@ -168,6 +167,9 @@
margin-bottom: 5px;
}
+ .filename {
+ font-style: italic;
+ }
</style>
<script type="text/javascript">
function about() {
@@ -190,10 +192,10 @@
<li>
<h3>Browse the documentation</h3>
<ul class="links">
- <li><a href="http://api.rubyonrails.org/">Rails API</a></li>
- <li><a href="http://stdlib.rubyonrails.org/">Ruby standard library</a></li>
- <li><a href="http://corelib.rubyonrails.org/">Ruby core</a></li>
<li><a href="http://guides.rubyonrails.org/">Rails Guides</a></li>
+ <li><a href="http://api.rubyonrails.org/">Rails API</a></li>
+ <li><a href="http://www.ruby-doc.org/core/">Ruby core</a></li>
+ <li><a href="http://www.ruby-doc.org/stdlib/">Ruby standard library</a></li>
</ul>
</li>
</ul>
@@ -221,13 +223,13 @@
</li>
<li>
- <h2>Set up a default route and remove or rename this file</h2>
- <p>Routes are set up in config/routes.rb.</p>
+ <h2>Set up a default route and remove <span class="filename">public/index.html</span></h2>
+ <p>Routes are set up in <span class="filename">config/routes.rb</span>.</p>
</li>
<li>
<h2>Create your database</h2>
- <p>Run <code>rake db:migrate</code> to create your database. If you're not using SQLite (the default), edit <code>config/database.yml</code> with your username and password.</p>
+ <p>Run <code>rake db:create</code> to create your database. If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p>
</li>
</ol>
</div>
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 4b29afdc8f..68d4c17623 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -191,6 +191,31 @@ class ActionsTest < Rails::Generators::TestCase
assert_match(/Welcome to Rails/, action(:readme, "README"))
end
+ def test_readme_with_quiet
+ generator(default_arguments, :quiet => true)
+ run_generator
+ Rails::Generators::AppGenerator.expects(:source_root).times(2).returns(destination_root)
+ assert_no_match(/Welcome to Rails/, action(:readme, "README"))
+ end
+
+ def test_log
+ assert_equal("YES\n", action(:log, "YES"))
+ end
+
+ def test_log_with_status
+ assert_equal(" yes YES\n", action(:log, :yes, "YES"))
+ end
+
+ def test_log_with_quiet
+ generator(default_arguments, :quiet => true)
+ assert_equal("", action(:log, "YES"))
+ end
+
+ def test_log_with_status_with_quiet
+ generator(default_arguments, :quiet => true)
+ assert_equal("", action(:log, :yes, "YES"))
+ end
+
protected
def action(*args, &block)