aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/CHANGELOG.md16
-rw-r--r--actionpack/lib/action_controller.rb1
-rw-r--r--actionpack/lib/action_controller/metal/live.rb138
-rw-r--r--actionpack/lib/action_controller/test_case.rb12
-rw-r--r--actionpack/lib/action_dispatch.rb3
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb30
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb28
-rw-r--r--actionpack/test/controller/live_stream_test.rb120
-rw-r--r--actionpack/test/dispatch/live_response_test.rb66
-rw-r--r--activerecord/CHANGELOG.md15
-rw-r--r--activerecord/lib/active_record/persistence.rb5
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb4
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb4
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb4
-rw-r--r--activerecord/test/cases/persistence_test.rb55
-rw-r--r--activerecord/test/cases/relation_scoping_test.rb8
-rw-r--r--activerecord/test/cases/relations_test.rb10
-rw-r--r--activesupport/lib/active_support/concurrency/latch.rb27
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute.rb9
-rw-r--r--guides/source/active_record_querying.textile9
-rw-r--r--guides/source/upgrading_ruby_on_rails.textile2
22 files changed, 475 insertions, 93 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index ec85f67e58..d5326e3d0b 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,5 +1,21 @@
## Rails 4.0.0 (unreleased) ##
+* Added ActionController::Live. Mix it in to your controller and you can
+ stream data to the client live. For example:
+
+ class FooController < ActionController::Base
+ include ActionController::Live
+
+ def index
+ 100.times {
+ # Client will see this as it's written
+ response.stream.write "hello world\n"
+ sleep 1
+ }
+ response.stream.close
+ end
+ end
+
* Remove ActionDispatch::Head middleware in favor of Rack::Head. *Santiago Pastorino*
* Deprecate `:confirm` in favor of `:data => { :confirm => "Text" }` option for `button_to`, `button_tag`, `image_submit_tag`, `link_to` and `submit_tag` helpers.
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index 7c10fcbb8a..1d06c83338 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -1,5 +1,6 @@
require 'abstract_controller'
require 'action_dispatch'
+require 'action_controller/metal/live'
module ActionController
extend ActiveSupport::Autoload
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
new file mode 100644
index 0000000000..e6ada0c106
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -0,0 +1,138 @@
+require 'action_dispatch/http/response'
+require 'delegate'
+
+module ActionController
+ # Mix this module in to your controller, and all actions in that controller
+ # will be able to stream data to the client as it's written.
+ #
+ # class MyController < ActionController::Base
+ # include ActionController::Live
+ #
+ # def stream
+ # response.headers['Content-Type'] = 'text/event-stream'
+ # 100.times {
+ # response.stream.write "hello world\n"
+ # sleep 1
+ # }
+ # response.stream.close
+ # end
+ # end
+ #
+ # There are a few caveats with this use. You *cannot* write headers after the
+ # response has been committed (Response#committed? will return truthy).
+ # Calling +write+ or +close+ on the response stream will cause the response
+ # object to be committed. Make sure all headers are set before calling write
+ # or close on your stream.
+ #
+ # You *must* call close on your stream when you're finished, otherwise the
+ # socket may be left open forever.
+ #
+ # The final caveat is that your actions are executed in a separate thread than
+ # the main thread. Make sure your actions are thread safe, and this shouldn't
+ # be a problem (don't share state across threads, etc).
+ module Live
+ class Buffer < ActionDispatch::Response::Buffer #:nodoc:
+ def initialize(response)
+ super(response, Queue.new)
+ end
+
+ def write(string)
+ unless @response.committed?
+ @response.headers["Cache-Control"] = "no-cache"
+ @response.headers.delete("Content-Length")
+ end
+
+ super
+ end
+
+ def each
+ while str = @buf.pop
+ yield str
+ end
+ end
+
+ def close
+ super
+ @buf.push nil
+ end
+ end
+
+ class Response < ActionDispatch::Response #:nodoc: all
+ class Header < DelegateClass(Hash)
+ def initialize(response, header)
+ @response = response
+ super(header)
+ end
+
+ def []=(k,v)
+ if @response.committed?
+ raise ActionDispatch::IllegalStateError, 'header already sent'
+ end
+
+ super
+ end
+
+ def to_hash
+ __getobj__.dup
+ end
+ end
+
+ def initialize(status = 200, header = {}, body = [])
+ header = Header.new self, header
+ super(status, header, body)
+ end
+
+ def commit!
+ headers.freeze
+ super
+ end
+
+ private
+
+ def build_buffer(response, body)
+ buf = Live::Buffer.new response
+ body.each { |part| buf.write part }
+ buf
+ end
+ end
+
+ def process(name)
+ t1 = Thread.current
+ locals = t1.keys.map { |key| [key, t1[key]] }
+
+ # This processes the action in a child thread. It lets us return the
+ # response code and headers back up the rack stack, and still process
+ # the body in parallel with sending data to the client
+ Thread.new {
+ t2 = Thread.current
+ t2.abort_on_exception = true
+
+ # Since we're processing the view in a different thread, copy the
+ # thread locals from the main thread to the child thread. :'(
+ locals.each { |k,v| t2[k] = v }
+
+ begin
+ super(name)
+ ensure
+ @_response.commit!
+ end
+ }
+
+ @_response.await_commit
+ end
+
+ def response_body=(body)
+ super
+ response.stream.close if response
+ end
+
+ def set_response!(request)
+ if request.env["HTTP_VERSION"] == "HTTP/1.0"
+ super
+ else
+ @_response = Live::Response.new
+ @_response.request = request
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index c4c825ba6b..ca1ecc43a1 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -517,8 +517,8 @@ module ActionController
end
def setup_controller_request_and_response
- @request = TestRequest.new
- @response = TestResponse.new
+ @request = build_request
+ @response = build_response
@response.request = @request
@controller = nil unless defined? @controller
@@ -539,6 +539,14 @@ module ActionController
end
end
+ def build_request
+ TestRequest.new
+ end
+
+ def build_response
+ TestResponse.new
+ end
+
included do
include ActionController::TemplateAssertions
include ActionDispatch::Assertions
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 1e4ac70f3d..c259b865cc 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -36,6 +36,9 @@ end
module ActionDispatch
extend ActiveSupport::Autoload
+ class IllegalStateError < StandardError
+ end
+
autoload_under 'http' do
autoload :Request
autoload :Response
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index 0b895e7860..647a9e3c19 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -86,21 +86,29 @@ module ActionDispatch
CACHE_CONTROL = "Cache-Control".freeze
SPESHUL_KEYS = %w[extras no-cache max-age public must-revalidate]
+ def cache_control_segments
+ if cache_control = self[CACHE_CONTROL]
+ cache_control.delete(' ').split(',')
+ else
+ []
+ end
+ end
+
def cache_control_headers
cache_control = {}
- if cc = self[CACHE_CONTROL]
- cc.delete(' ').split(',').each do |segment|
- directive, argument = segment.split('=', 2)
- case directive
- when *SPESHUL_KEYS
- key = directive.tr('-', '_')
- cache_control[key.to_sym] = argument || true
- else
- cache_control[:extras] ||= []
- cache_control[:extras] << segment
- end
+
+ cache_control_segments.each do |segment|
+ directive, argument = segment.split('=', 2)
+
+ if SPESHUL_KEYS.include? directive
+ key = directive.tr('-', '_')
+ cache_control[key.to_sym] = argument || true
+ else
+ cache_control[:extras] ||= []
+ cache_control[:extras] << segment
end
end
+
cache_control
end
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index fa48594c93..17e74656af 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -216,17 +216,7 @@ module ActionDispatch # :nodoc:
end
def to_a
- assign_default_content_type_and_charset!
- handle_conditional_get!
-
- @header[SET_COOKIE] = @header[SET_COOKIE].join("\n") if @header[SET_COOKIE].respond_to?(:join)
-
- if [204, 304].include?(@status)
- @header.delete CONTENT_TYPE
- [@status, @header, []]
- else
- [@status, @header, self]
- end
+ rack_response @status, @header.to_hash
end
alias prepare! to_a
alias to_ary to_a # For implicit splat on 1.9.2
@@ -258,7 +248,7 @@ module ActionDispatch # :nodoc:
body.respond_to?(:each) ? body : [body]
end
- def assign_default_content_type_and_charset!
+ def assign_default_content_type_and_charset!(headers)
return if headers[CONTENT_TYPE].present?
@content_type ||= Mime::HTML
@@ -269,5 +259,19 @@ module ActionDispatch # :nodoc:
headers[CONTENT_TYPE] = type
end
+
+ def rack_response(status, header)
+ assign_default_content_type_and_charset!(header)
+ handle_conditional_get!
+
+ header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join)
+
+ if [204, 304].include?(@status)
+ header.delete CONTENT_TYPE
+ [status, header, []]
+ else
+ [status, header, self]
+ end
+ end
end
end
diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb
new file mode 100644
index 0000000000..5ce1acccf1
--- /dev/null
+++ b/actionpack/test/controller/live_stream_test.rb
@@ -0,0 +1,120 @@
+require 'abstract_unit'
+require 'active_support/concurrency/latch'
+
+module ActionController
+ class LiveStreamTest < ActionController::TestCase
+ class TestController < ActionController::Base
+ include ActionController::Live
+
+ attr_accessor :latch, :tc
+
+ def self.controller_path
+ 'test'
+ end
+
+ def render_text
+ render :text => 'zomg'
+ end
+
+ def default_header
+ response.stream.write "<html><body>hi</body></html>"
+ response.stream.close
+ end
+
+ def basic_stream
+ response.headers['Content-Type'] = 'text/event-stream'
+ %w{ hello world }.each do |word|
+ response.stream.write word
+ end
+ response.stream.close
+ end
+
+ def blocking_stream
+ response.headers['Content-Type'] = 'text/event-stream'
+ %w{ hello world }.each do |word|
+ response.stream.write word
+ latch.await
+ end
+ response.stream.close
+ end
+
+ def thread_locals
+ tc.assert_equal 'aaron', Thread.current[:setting]
+ tc.refute_equal Thread.current.object_id, Thread.current[:originating_thread]
+
+ response.headers['Content-Type'] = 'text/event-stream'
+ %w{ hello world }.each do |word|
+ response.stream.write word
+ end
+ response.stream.close
+ end
+ end
+
+ tests TestController
+
+ class TestResponse < Live::Response
+ def recycle!
+ initialize
+ end
+ end
+
+ def build_response
+ TestResponse.new
+ end
+
+ def test_set_response!
+ @controller.set_response!(@request)
+ assert_kind_of(Live::Response, @controller.response)
+ assert_equal @request, @controller.response.request
+ end
+
+ def test_write_to_stream
+ @controller = TestController.new
+ get :basic_stream
+ assert_equal "helloworld", @response.body
+ assert_equal 'text/event-stream', @response.headers['Content-Type']
+ end
+
+ def test_async_stream
+ @controller.latch = ActiveSupport::Concurrency::Latch.new
+ parts = ['hello', 'world']
+
+ @controller.request = @request
+ @controller.response = @response
+
+ t = Thread.new(@response) { |resp|
+ resp.stream.each do |part|
+ assert_equal parts.shift, part
+ ol = @controller.latch
+ @controller.latch = ActiveSupport::Concurrency::Latch.new
+ ol.release
+ end
+ }
+
+ @controller.process :blocking_stream
+
+ assert t.join
+ end
+
+ def test_thread_locals_get_copied
+ @controller.tc = self
+ Thread.current[:originating_thread] = Thread.current.object_id
+ Thread.current[:setting] = 'aaron'
+
+ get :thread_locals
+ end
+
+ def test_live_stream_default_header
+ @controller.request = @request
+ @controller.response = @response
+ @controller.process :default_header
+ _, headers, _ = @response.prepare!
+ assert headers['Content-Type']
+ end
+
+ def test_render_text
+ get :render_text
+ assert @response.stream.closed?, 'stream should be closed'
+ end
+ end
+end
diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb
new file mode 100644
index 0000000000..87a6b1383d
--- /dev/null
+++ b/actionpack/test/dispatch/live_response_test.rb
@@ -0,0 +1,66 @@
+require 'abstract_unit'
+require 'active_support/concurrency/latch'
+
+module ActionController
+ module Live
+ class ResponseTest < ActiveSupport::TestCase
+ def setup
+ @response = Live::Response.new
+ end
+
+ def test_parallel
+ latch = ActiveSupport::Concurrency::Latch.new
+
+ t = Thread.new {
+ @response.stream.write 'foo'
+ latch.await
+ @response.stream.close
+ }
+
+ @response.each do |part|
+ assert_equal 'foo', part
+ latch.release
+ end
+ assert t.join
+ end
+
+ def test_setting_body_populates_buffer
+ @response.body = 'omg'
+ @response.close
+ assert_equal ['omg'], @response.body_parts
+ end
+
+ def test_cache_control_is_set
+ @response.stream.write 'omg'
+ assert_equal 'no-cache', @response.headers['Cache-Control']
+ end
+
+ def test_content_length_is_removed
+ @response.headers['Content-Length'] = "1234"
+ @response.stream.write 'omg'
+ assert_nil @response.headers['Content-Length']
+ end
+
+ def test_headers_cannot_be_written_after_write
+ @response.stream.write 'omg'
+
+ assert @response.headers.frozen?
+ e = assert_raises(ActionDispatch::IllegalStateError) do
+ @response.headers['Content-Length'] = "zomg"
+ end
+
+ assert_equal 'header already sent', e.message
+ end
+
+ def test_headers_cannot_be_written_after_close
+ @response.stream.close
+
+ assert @response.headers.frozen?
+ e = assert_raises(ActionDispatch::IllegalStateError) do
+ @response.headers['Content-Length'] = "zomg"
+ end
+ assert_equal 'header already sent', e.message
+ end
+ end
+ end
+end
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index deb0dc0c33..00bc45e4cc 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,5 +1,14 @@
## Rails 4.0.0 (unreleased) ##
+* AR::Relation#order: make new order prepend old one.
+
+ User.order("name asc").order("created_at desc")
+ # SELECT * FROM users ORDER BY created_at desc, name asc
+
+ This also affects order defined in `default_scope` or any kind of associations.
+
+ *Bogdan Gusiev*
+
* `Model.all` now returns an `ActiveRecord::Relation`, rather than an
array of records. Use `Model.to_a` or `Relation#to_a` if you really
want an array.
@@ -19,15 +28,11 @@
*Jon Leighton*
-* Deprecate `update_column` method in favor of `update_columns`.
-
- *Rafael Mendonça França*
-
* Added an `update_columns` method. This new method updates the given attributes on an object,
without calling save, hence skipping validations and callbacks.
Example:
- User.first.update_columns({:name => "sebastian", :age => 25}) # => true
+ User.first.update_columns(name: "sebastian", age: 25) # => true
*Sebastian Martinez + Rafael Mendonça França*
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 569ef4bcda..32b62d12e1 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -204,11 +204,6 @@ module ActiveRecord
# Raises an +ActiveRecordError+ when called on new objects, or when the +name+
# attribute is marked as readonly.
def update_column(name, value)
- msg = "update_column is deprecated and will be removed in 4.1. Please use update_columns. " \
- "E.g. update_columns(foo: 'bar')"
-
- ActiveSupport::Deprecation.warn(msg)
-
update_columns(name => value)
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index c01aed2d8e..97c3db683f 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -311,7 +311,7 @@ module ActiveRecord
@records.first
else
@first ||=
- if order_values.empty? && primary_key
+ if with_default_scope.order_values.empty? && primary_key
order(arel_table[primary_key].asc).limit(1).to_a.first
else
limit(1).to_a.first
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 94db2846f3..967b82aeb9 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -214,7 +214,7 @@ module ActiveRecord
references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
references!(references) if references.any?
- self.order_values += args
+ self.order_values = args + self.order_values
self
end
@@ -226,7 +226,7 @@ module ActiveRecord
#
# User.order('email DESC').reorder('id ASC').order('name ASC')
#
- # generates a query with 'ORDER BY id ASC, name ASC'.
+ # generates a query with 'ORDER BY name ASC, id ASC'.
def reorder(*args)
args.blank? ? self : spawn.reorder!(*args)
end
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index a9e18dd8fe..e48ac84592 100644
--- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -513,9 +513,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal high_id_jamis, projects(:active_record).developers.find_by_name('Jamis')
end
- def test_find_should_append_to_association_order
+ def test_find_should_prepend_to_association_order
ordered_developers = projects(:active_record).developers.order('projects.id')
- assert_equal ['developers.name desc, developers.id desc', 'projects.id'], ordered_developers.order_values
+ assert_equal ['projects.id', 'developers.name desc, developers.id desc'], ordered_developers.order_values
end
def test_dynamic_find_all_should_respect_readonly_access
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 6122104335..6b675d3d54 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -231,9 +231,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, companies(:first_firm).limited_clients.limit(nil).to_a.size
end
- def test_find_should_append_to_association_order
+ def test_find_should_prepend_to_association_order
ordered_clients = companies(:first_firm).clients_sorted_desc.order('companies.id')
- assert_equal ['id DESC', 'companies.id'], ordered_clients.order_values
+ assert_equal ['companies.id', 'id DESC'], ordered_clients.order_values
end
def test_dynamic_find_should_respect_association_order
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index fa65d957b8..72b8219782 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -377,21 +377,22 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_column
topic = Topic.find(1)
- assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
- topic.update_column("approved", true)
- end
+ topic.update_column("approved", true)
assert topic.approved?
topic.reload
assert topic.approved?
+
+ topic.update_column(:approved, false)
+ assert !topic.approved?
+ topic.reload
+ assert !topic.approved?
end
def test_update_column_should_not_use_setter_method
dev = Developer.find(1)
dev.instance_eval { def salary=(value); write_attribute(:salary, value * 2); end }
- assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
- dev.update_column(:salary, 80000)
- end
+ dev.update_column(:salary, 80000)
assert_equal 80000, dev.salary
dev.reload
@@ -400,29 +401,19 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_column_should_raise_exception_if_new_record
topic = Topic.new
- assert_raises(ActiveRecord::ActiveRecordError) do
- assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
- topic.update_column("approved", false)
- end
- end
+ assert_raises(ActiveRecord::ActiveRecordError) { topic.update_column("approved", false) }
end
def test_update_column_should_not_leave_the_object_dirty
topic = Topic.find(1)
- assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
- topic.update_column("content", "Have a nice day")
- end
+ topic.update_column("content", "Have a nice day")
topic.reload
- assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
- topic.update_column(:content, "You too")
- end
+ topic.update_column(:content, "You too")
assert_equal [], topic.changed
topic.reload
- assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
- topic.update_column("content", "Have a nice day")
- end
+ topic.update_column("content", "Have a nice day")
assert_equal [], topic.changed
end
@@ -430,20 +421,14 @@ class PersistencesTest < ActiveRecord::TestCase
minivan = Minivan.find('m1')
new_name = 'sebavan'
- assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
- minivan.update_column(:name, new_name)
- end
+ minivan.update_column(:name, new_name)
assert_equal new_name, minivan.name
end
def test_update_column_for_readonly_attribute
minivan = Minivan.find('m1')
prev_color = minivan.color
- assert_raises(ActiveRecord::ActiveRecordError) do
- assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
- minivan.update_column(:color, 'black')
- end
- end
+ assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_column(:color, 'black') }
assert_equal prev_color, minivan.color
end
@@ -451,14 +436,10 @@ class PersistencesTest < ActiveRecord::TestCase
developer = Developer.find(1)
prev_month = Time.now.prev_month
- assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
- developer.update_column(:updated_at, prev_month)
- end
+ developer.update_column(:updated_at, prev_month)
assert_equal prev_month, developer.updated_at
- assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
- developer.update_column(:salary, 80001)
- end
+ developer.update_column(:salary, 80001)
assert_equal prev_month, developer.updated_at
developer.reload
@@ -467,11 +448,9 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_column_with_one_changed_and_one_updated
t = Topic.order('id').limit(1).first
- author_name = t.author_name
+ title, author_name = t.title, t.author_name
t.author_name = 'John'
- assert_deprecated "update_column is deprecated and will be removed in 4.1. Please use update_columns. E.g. update_columns(foo: 'bar')" do
- t.update_column(:title, 'super_title')
- end
+ t.update_column(:title, 'super_title')
assert_equal 'John', t.author_name
assert_equal 'super_title', t.title
assert t.changed?, "topic should have changed"
diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb
index ba77bdcc8b..ef9905da2e 100644
--- a/activerecord/test/cases/relation_scoping_test.rb
+++ b/activerecord/test/cases/relation_scoping_test.rb
@@ -385,7 +385,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_scope_overwrites_default
- expected = Developer.all.merge!(:order => 'salary DESC, name DESC').to_a.collect { |dev| dev.name }
+ expected = Developer.all.merge!(:order => ' name DESC, salary DESC').to_a.collect { |dev| dev.name }
received = DeveloperOrderedBySalary.by_name.to_a.collect { |dev| dev.name }
assert_equal expected, received
end
@@ -397,13 +397,13 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_order_after_reorder_combines_orders
- expected = Developer.order('name DESC, id DESC').collect { |dev| [dev.name, dev.id] }
+ expected = Developer.order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] }
received = Developer.order('name ASC').reorder('name DESC').order('id DESC').collect { |dev| [dev.name, dev.id] }
assert_equal expected, received
end
- def test_order_in_default_scope_should_prevail
- expected = Developer.all.merge!(:order => 'salary desc').to_a.collect { |dev| dev.salary }
+ def test_order_in_default_scope_should_not_prevail
+ expected = Developer.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary }
received = DeveloperOrderedBySalary.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary }
assert_equal expected, received
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 9c64cb35e4..4aa1977cf9 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -165,7 +165,7 @@ class RelationTest < ActiveRecord::TestCase
end
def test_finding_with_order_concatenated
- topics = Topic.order('author_name').order('title')
+ topics = Topic.order('title').order('author_name')
assert_equal 4, topics.to_a.size
assert_equal topics(:fourth).title, topics.first.title
end
@@ -1068,20 +1068,20 @@ class RelationTest < ActiveRecord::TestCase
end
def test_default_scope_order_with_scope_order
- assert_equal 'zyke', CoolCar.order_using_new_style.limit(1).first.name
- assert_equal 'zyke', FastCar.order_using_new_style.limit(1).first.name
+ assert_equal 'honda', CoolCar.order_using_new_style.limit(1).first.name
+ assert_equal 'honda', FastCar.order_using_new_style.limit(1).first.name
end
def test_order_using_scoping
car1 = CoolCar.order('id DESC').scoping do
CoolCar.all.merge!(:order => 'id asc').first
end
- assert_equal 'zyke', car1.name
+ assert_equal 'honda', car1.name
car2 = FastCar.order('id DESC').scoping do
FastCar.all.merge!(:order => 'id asc').first
end
- assert_equal 'zyke', car2.name
+ assert_equal 'honda', car2.name
end
def test_unscoped_block_style
diff --git a/activesupport/lib/active_support/concurrency/latch.rb b/activesupport/lib/active_support/concurrency/latch.rb
new file mode 100644
index 0000000000..1507de433e
--- /dev/null
+++ b/activesupport/lib/active_support/concurrency/latch.rb
@@ -0,0 +1,27 @@
+require 'thread'
+require 'monitor'
+
+module ActiveSupport
+ module Concurrency
+ class Latch
+ def initialize(count = 1)
+ @count = count
+ @lock = Monitor.new
+ @cv = @lock.new_cond
+ end
+
+ def release
+ @lock.synchronize do
+ @count -= 1 if @count > 0
+ @cv.broadcast if @count.zero?
+ end
+ end
+
+ def await
+ @lock.synchronize do
+ @cv.wait_while { @count > 0 }
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb
index da0a12136c..7b6f8ab0a1 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute.rb
@@ -79,12 +79,14 @@ class Class
def self.#{name}=(val)
singleton_class.class_eval do
- redefine_method(:#{name}) { val }
+ remove_possible_method(:#{name})
+ define_method(:#{name}) { val }
end
if singleton_class?
class_eval do
- redefine_method(:#{name}) do
+ remove_possible_method(:#{name})
+ def #{name}
defined?(@#{name}) ? @#{name} : singleton_class.#{name}
end
end
@@ -93,7 +95,8 @@ class Class
end
if instance_reader
- redefine_method(:#{name}) do
+ remove_possible_method :#{name}
+ def #{name}
defined?(@#{name}) ? @#{name} : self.class.#{name}
end
diff --git a/guides/source/active_record_querying.textile b/guides/source/active_record_querying.textile
index b13932e8cb..dff829a4c1 100644
--- a/guides/source/active_record_querying.textile
+++ b/guides/source/active_record_querying.textile
@@ -492,7 +492,7 @@ This code will generate SQL like this:
SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
</sql>
-h3. Ordering
+h3(#ordering). Ordering
To retrieve records from the database in a specific order, you can use the +order+ method.
@@ -518,6 +518,13 @@ Client.order("orders_count ASC, created_at DESC")
Client.order("orders_count ASC", "created_at DESC")
</ruby>
+If you want to call +order+ multiple times e.g. in different context, new order will prepend previous one
+
+<ruby>
+Client.order("orders_count ASC").order("created_at DESC")
+# SELECT * FROM clients ORDER BY created_at DESC, orders_count ASC
+</ruby>
+
h3. Selecting Specific Fields
By default, <tt>Model.find</tt> selects all the fields from the result set using +select *+.
diff --git a/guides/source/upgrading_ruby_on_rails.textile b/guides/source/upgrading_ruby_on_rails.textile
index 4bf4751127..5024bc4c37 100644
--- a/guides/source/upgrading_ruby_on_rails.textile
+++ b/guides/source/upgrading_ruby_on_rails.textile
@@ -42,6 +42,8 @@ h4(#active_record4_0). Active Record
The <tt>delete</tt> method in collection associations can now receive <tt>Fixnum</tt> or <tt>String</tt> arguments as record ids, besides records, pretty much like the <tt>destroy</tt> method does. Previously it raised <tt>ActiveRecord::AssociationTypeMismatch</tt> for such arguments. From Rails 4.0 on <tt>delete</tt> automatically tries to find the records matching the given ids before deleting them.
+Rails 4.0 has changed how orders get stacked in +ActiveRecord::Relation+. In previous versions of rails new order was applied after previous defined order. But this is no long true. Check "ActiveRecord Query guide":active_record_querying.html#ordering for more information.
+
h4(#active_model4_0). Active Model
Rails 4.0 has changed how errors attach with the <tt>ActiveModel::Validations::ConfirmationValidator</tt>. Now when confirmation validations fail the error will be attached to <tt>:#{attribute}_confirmation</tt> instead of <tt>attribute</tt>.