Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Merge branch 'master' of github.com:rails/rails

  • Loading branch information...
commit df40dbe6f13c6799e972b20dcc1fbf11f0a02c61 2 parents 6ebc7c8 + 5c9f27a
Yehuda Katz wycats authored

Showing 24 changed files with 667 additions and 114 deletions. Show diff stats Hide diff stats

  1. +1 1  Gemfile
  2. +0 10 actionpack/CHANGELOG
  3. +1 0  actionpack/lib/action_controller/caching/sweeping.rb
  4. +11 26 actionpack/lib/action_dispatch/routing/mapper.rb
  5. +8 6 actionpack/lib/action_view/test_case.rb
  6. +3 0  actionpack/test/abstract_unit.rb
  7. +6 0 actionpack/test/controller/filters_test.rb
  8. +22 39 actionpack/test/dispatch/routing_test.rb
  9. +4 0 actionpack/test/template/test_case_test.rb
  10. +4 0 activerecord/CHANGELOG
  11. +3 1 activerecord/lib/active_record/base.rb
  12. +1 1  activerecord/lib/active_record/callbacks.rb
  13. +56 0 activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
  14. +5 7 activerecord/lib/active_record/fixtures.rb
  15. +126 11 activerecord/lib/active_record/transactions.rb
  16. +5 3 activerecord/test/cases/active_schema_test_mysql.rb
  17. +1 0  activerecord/test/cases/active_schema_test_postgresql.rb
  18. +4 4 activerecord/test/cases/adapter_test.rb
  19. +17 0 activerecord/test/cases/base_test.rb
  20. +240 0 activerecord/test/cases/transaction_callbacks_test.rb
  21. +33 0 activerecord/test/cases/transactions_test.rb
  22. +1 1  railties/guides/source/3_0_release_notes.textile
  23. +114 3 railties/guides/source/active_support_core_extensions.textile
  24. +1 1  railties/guides/source/getting_started.textile
2  Gemfile
@@ -32,7 +32,7 @@ if mri || RUBY_ENGINE == "rbx"
32 32 gem "sqlite3-ruby", "~> 1.3.0", :require => 'sqlite3'
33 33
34 34 group :db do
35   - # gem "pg", ">= 0.9.0"
  35 + gem "pg", ">= 0.9.0"
36 36 gem "mysql", ">= 2.8.1"
37 37 end
38 38 elsif RUBY_ENGINE == "jruby"
10 actionpack/CHANGELOG
... ... @@ -1,15 +1,5 @@
1 1 *Rails 3.0.0 [beta 4] (June 8th, 2010)*
2 2
3   -* Add shallow routes back to the new router [Diego Carrion]
4   -
5   - resources :posts do
6   - shallow do
7   - resources :comments
8   - end
9   - end
10   -
11   - You can now use comment_path for /comments/1 instead of post_comment_path for /posts/1/comments/1.
12   -
13 3 * Remove middleware laziness [José Valim]
14 4
15 5 * Make session stores rely on request.cookie_jar and change set_session semantics to return the cookie value instead of a boolean. [José Valim]
1  actionpack/lib/action_controller/caching/sweeping.rb
@@ -57,6 +57,7 @@ class Sweeper < ActiveRecord::Observer #:nodoc:
57 57 def before(controller)
58 58 self.controller = controller
59 59 callback(:before) if controller.perform_caching
  60 + true # before method from sweeper should always return true
60 61 end
61 62
62 63 def after(controller)
37 actionpack/lib/action_dispatch/routing/mapper.rb
@@ -350,10 +350,6 @@ def constraints(constraints = {})
350 350 scope(:constraints => constraints) { yield }
351 351 end
352 352
353   - def shallow
354   - scope(:shallow => true) { yield }
355   - end
356   -
357 353 def defaults(defaults = {})
358 354 scope(:defaults => defaults) { yield }
359 355 end
@@ -378,21 +374,12 @@ def scope_options
378 374 @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
379 375 end
380 376
381   - def merge_shallow_scope(parent, child)
382   - parent or child
383   - end
384   -
385 377 def merge_path_scope(parent, child)
386   - parent_path = (@scope[:shallow] and child.eql?(':id')) ? parent.split('/').last : parent
387   - Mapper.normalize_path "#{parent_path}/#{child}"
  378 + Mapper.normalize_path("#{parent}/#{child}")
388 379 end
389 380
390 381 def merge_name_prefix_scope(parent, child)
391   - if @scope[:shallow]
392   - child
393   - else
394   - parent ? "#{parent}_#{child}" : child
395   - end
  382 + parent ? "#{parent}_#{child}" : child
396 383 end
397 384
398 385 def merge_module_scope(parent, child)
@@ -535,10 +522,6 @@ def nested_options
535 522 options["#{singular}_id".to_sym] = id_constraint if id_constraint?
536 523 options
537 524 end
538   -
539   - def shallow?
540   - options[:shallow]
541   - end
542 525 end
543 526
544 527 class SingletonResource < Resource #:nodoc:
@@ -620,12 +603,8 @@ def resources(*resources, &block)
620 603
621 604 resource = Resource.new(resources.pop, options)
622 605
623   - scope(:path => resource.path, :controller => resource.controller, :shallow => resource.shallow?) do
  606 + scope(:path => resource.path, :controller => resource.controller) do
624 607 with_scope_level(:resources, resource) do
625   - if @scope[:shallow] && @scope[:name_prefix]
626   - @scope[:path] = "/#{@scope[:name_prefix].pluralize}/:#{@scope[:name_prefix]}_id/#{resource.path}"
627   - end
628   -
629 608 yield if block_given?
630 609
631 610 with_scope_level(:collection) do
@@ -639,8 +618,6 @@ def resources(*resources, &block)
639 618 with_scope_level(:member) do
640 619 scope(':id') do
641 620 scope(resource.options) do
642   - @scope[:name_prefix] = nil if @scope[:shallow]
643   -
644 621 get :show if resource.actions.include?(:show)
645 622 put :update if resource.actions.include?(:update)
646 623 delete :destroy if resource.actions.include?(:destroy)
@@ -702,6 +679,14 @@ def nested
702 679 end
703 680 end
704 681
  682 + def namespace(path)
  683 + if resource_scope?
  684 + nested { super }
  685 + else
  686 + super
  687 + end
  688 + end
  689 +
705 690 def match(*args)
706 691 options = args.extract_options!
707 692
14 actionpack/lib/action_view/test_case.rb
@@ -131,12 +131,14 @@ def make_test_case_available_to_view!
131 131 end
132 132
133 133 def _view
134   - view = ActionView::Base.new(ActionController::Base.view_paths, _assigns, @controller)
135   - view.singleton_class.send :include, _helpers
136   - view.singleton_class.send :include, @controller._router.url_helpers
137   - view.singleton_class.send :delegate, :alert, :notice, :to => "request.flash"
138   - view.output_buffer = self.output_buffer
139   - view
  134 + @_view ||= begin
  135 + view = ActionView::Base.new(ActionController::Base.view_paths, _assigns, @controller)
  136 + view.singleton_class.send :include, _helpers
  137 + view.singleton_class.send :include, @controller._router.url_helpers
  138 + view.singleton_class.send :delegate, :alert, :notice, :to => "request.flash"
  139 + view.output_buffer = self.output_buffer
  140 + view
  141 + end
140 142 end
141 143
142 144 EXCLUDE_IVARS = %w{
3  actionpack/test/abstract_unit.rb
@@ -24,6 +24,9 @@
24 24 require 'action_dispatch'
25 25 require 'active_support/dependencies'
26 26 require 'active_model'
  27 +require 'active_record'
  28 +require 'action_controller/caching'
  29 +require 'action_controller/caching/sweeping'
27 30
28 31 begin
29 32 require 'ruby-debug'
6 actionpack/test/controller/filters_test.rb
@@ -445,6 +445,12 @@ def filter_three
445 445
446 446 end
447 447
  448 +
  449 + def test_before_method_of_sweeper_should_always_return_true
  450 + sweeper = ActionController::Caching::Sweeper.send(:new)
  451 + assert sweeper.before(TestController.new)
  452 + end
  453 +
448 454 def test_non_yielding_around_filters_not_returning_false_do_not_raise
449 455 controller = NonYieldingAroundFilterController.new
450 456 controller.instance_variable_set "@filter_return_value", true
61 actionpack/test/dispatch/routing_test.rb
@@ -34,33 +34,6 @@ def self.matches?(request)
34 34 end
35 35 end
36 36
37   - resources :users do
38   - shallow do
39   - resources :photos do
40   - resources :types do
41   - member do
42   - post :preview
43   - end
44   - collection do
45   - delete :erase
46   - end
47   - end
48   - end
49   - end
50   - end
51   -
52   - shallow do
53   - resources :teams do
54   - resources :players
55   - end
56   -
57   - resources :countries do
58   - resources :cities do
59   - resources :places
60   - end
61   - end
62   - end
63   -
64 37 match 'account/logout' => redirect("/logout"), :as => :logout_redirect
65 38 match 'account/login', :to => redirect("/login")
66 39
@@ -171,6 +144,16 @@ def self.matches?(request)
171 144
172 145 resources :sheep
173 146
  147 + resources :clients do
  148 + namespace :google do
  149 + resource :account do
  150 + namespace :secret do
  151 + resource :info
  152 + end
  153 + end
  154 + end
  155 + end
  156 +
174 157 match 'sprockets.js' => ::TestRoutingMapper::SprocketsApp
175 158
176 159 match 'people/:id/update', :to => 'people#update', :as => :update_person
@@ -779,18 +762,6 @@ def test_update_person_route
779 762 end
780 763 end
781 764
782   - def test_shallow_routes
783   - with_test_routes do
784   - assert_equal '/photos/4', photo_path(4)
785   - assert_equal '/types/10/edit', edit_type_path(10)
786   - assert_equal '/types/5/preview', preview_type_path(5)
787   - assert_equal '/photos/2/types', photo_types_path(2)
788   - assert_equal '/cities/1/places', url_for(:controller => :places, :action => :index, :city_id => 1, :only_path => true)
789   - assert_equal '/teams/new', url_for(:controller => :teams, :action => :new, :only_path => true)
790   - assert_equal '/photos/11/types/erase', url_for(:controller => :types, :action => :erase, :photo_id => 11, :only_path => true)
791   - end
792   - end
793   -
794 765 def test_update_project_person
795 766 with_test_routes do
796 767 get '/projects/1/people/2/update'
@@ -852,6 +823,18 @@ def test_nested_namespace
852 823 assert_equal '/account/admin/subscription', account_admin_subscription_path
853 824 end
854 825 end
  826 +
  827 + def test_namespace_nested_in_resources
  828 + with_test_routes do
  829 + get '/clients/1/google/account'
  830 + assert_equal '/clients/1/google/account', client_google_account_path(1)
  831 + assert_equal 'google/accounts#show', @response.body
  832 +
  833 + get '/clients/1/google/account/secret/info'
  834 + assert_equal '/clients/1/google/account/secret/info', client_google_account_secret_info_path(1)
  835 + assert_equal 'google/secret/infos#show', @response.body
  836 + end
  837 + end
855 838
856 839 def test_articles_with_id
857 840 with_test_routes do
4 actionpack/test/template/test_case_test.rb
@@ -37,6 +37,10 @@ class GeneralViewTest < ActionView::TestCase
37 37 include SharedTests
38 38 test_case = self
39 39
  40 + test "memoizes the _view" do
  41 + assert_same _view, _view
  42 + end
  43 +
40 44 test "works without testing a helper module" do
41 45 assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy'))
42 46 end
4 activerecord/CHANGELOG
... ... @@ -1,5 +1,7 @@
1 1 *Rails 3.0.0 [beta 4] (June 8th, 2010)*
2 2
  3 +* Fixed that ActiveRecord::Base.compute_type would swallow NoMethodError #4751 [Andrew Bloomgarden, Andrew White]
  4 +
3 5 * Add index length support for MySQL. #1852 [Emili Parreno, Pratik Naik]
4 6
5 7 Example:
@@ -12,6 +14,8 @@
12 14
13 15 * find_or_create_by_attr(value, ...) works when attr is protected. #4457 [Santiago Pastorino, Marc-André Lafortune]
14 16
  17 +* New callbacks: after_commit and after_rollback. Do expensive operations like image thumbnailing after_commit instead of after_save. #2991 [Brian Durand]
  18 +
15 19 * Serialized attributes are not converted to YAML if they are any of the formats that can be serialized to XML (like Hash, Array and Strings). [José Valim]
16 20
17 21 * Destroy uses optimistic locking. If lock_version on the record you're destroying doesn't match lock_version in the database, a StaleObjectError is raised. #1966 [Curtis Hawthorne]
4 activerecord/lib/active_record/base.rb
@@ -1219,7 +1219,9 @@ def compute_type(type_name)
1219 1219 begin
1220 1220 constant = candidate.constantize
1221 1221 return constant if candidate == constant.to_s
1222   - rescue NameError
  1222 + rescue NameError => e
  1223 + # We don't want to swallow NoMethodError < NameError errors
  1224 + raise e unless e.instance_of?(NameError)
1223 1225 rescue ArgumentError
1224 1226 end
1225 1227 end
2  activerecord/lib/active_record/callbacks.rb
@@ -31,7 +31,7 @@ module ActiveRecord
31 31 # class CreditCard < ActiveRecord::Base
32 32 # # Strip everything but digits, so the user can specify "555 234 34" or
33 33 # # "5552-3434" or both will mean "55523434"
34   - # def before_validation_on_create
  34 + # before_validation(:on => :create) do
35 35 # self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
36 36 # end
37 37 # end
56 activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -122,6 +122,8 @@ def transaction(options = {})
122 122 requires_new = options[:requires_new] || !last_transaction_joinable
123 123
124 124 transaction_open = false
  125 + @_current_transaction_records ||= []
  126 +
125 127 begin
126 128 if block_given?
127 129 if requires_new || open_transactions == 0
@@ -132,6 +134,7 @@ def transaction(options = {})
132 134 end
133 135 increment_open_transactions
134 136 transaction_open = true
  137 + @_current_transaction_records.push([])
135 138 end
136 139 yield
137 140 end
@@ -141,8 +144,10 @@ def transaction(options = {})
141 144 decrement_open_transactions
142 145 if open_transactions == 0
143 146 rollback_db_transaction
  147 + rollback_transaction_records(true)
144 148 else
145 149 rollback_to_savepoint
  150 + rollback_transaction_records(false)
146 151 end
147 152 end
148 153 raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback)
@@ -157,20 +162,35 @@ def transaction(options = {})
157 162 begin
158 163 if open_transactions == 0
159 164 commit_db_transaction
  165 + commit_transaction_records
160 166 else
161 167 release_savepoint
  168 + save_point_records = @_current_transaction_records.pop
  169 + unless save_point_records.blank?
  170 + @_current_transaction_records.push([]) if @_current_transaction_records.empty?
  171 + @_current_transaction_records.last.concat(save_point_records)
  172 + end
162 173 end
163 174 rescue Exception => database_transaction_rollback
164 175 if open_transactions == 0
165 176 rollback_db_transaction
  177 + rollback_transaction_records(true)
166 178 else
167 179 rollback_to_savepoint
  180 + rollback_transaction_records(false)
168 181 end
169 182 raise
170 183 end
171 184 end
172 185 end
173 186
  187 + # Register a record with the current transaction so that its after_commit and after_rollback callbacks
  188 + # can be called.
  189 + def add_transaction_record(record)
  190 + last_batch = @_current_transaction_records.last
  191 + last_batch << record if last_batch
  192 + end
  193 +
174 194 # Begins the transaction (and turns off auto-committing).
175 195 def begin_db_transaction() end
176 196
@@ -268,6 +288,42 @@ def sanitize_limit(limit)
268 288 limit.to_i
269 289 end
270 290 end
  291 +
  292 + # Send a rollback message to all records after they have been rolled back. If rollback
  293 + # is false, only rollback records since the last save point.
  294 + def rollback_transaction_records(rollback) #:nodoc
  295 + if rollback
  296 + records = @_current_transaction_records.flatten
  297 + @_current_transaction_records.clear
  298 + else
  299 + records = @_current_transaction_records.pop
  300 + end
  301 +
  302 + unless records.blank?
  303 + records.uniq.each do |record|
  304 + begin
  305 + record.rolledback!(rollback)
  306 + rescue Exception => e
  307 + record.logger.error(e) if record.respond_to?(:logger)
  308 + end
  309 + end
  310 + end
  311 + end
  312 +
  313 + # Send a commit message to all records after they have been committed.
  314 + def commit_transaction_records #:nodoc
  315 + records = @_current_transaction_records.flatten
  316 + @_current_transaction_records.clear
  317 + unless records.blank?
  318 + records.uniq.each do |record|
  319 + begin
  320 + record.committed!
  321 + rescue Exception => e
  322 + record.logger.error(e) if record.respond_to?(:logger)
  323 + end
  324 + end
  325 + end
  326 + end
271 327 end
272 328 end
273 329 end
12 activerecord/lib/active_record/fixtures.rb
@@ -787,16 +787,14 @@ def to_hash
787 787 end
788 788
789 789 def key_list
790   - columns = @fixture.keys.collect{ |column_name| @connection.quote_column_name(column_name) }
791   - columns.join(", ")
  790 + @fixture.keys.map { |column_name| @connection.quote_column_name(column_name) }.join(', ')
792 791 end
793 792
794 793 def value_list
795   - list = @fixture.inject([]) do |fixtures, (key, value)|
796   - col = model_class.columns_hash[key] if model_class.respond_to?(:ancestors) && model_class.ancestors.include?(ActiveRecord::Base)
797   - fixtures << @connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
798   - end
799   - list * ', '
  794 + cols = (model_class && model_class < ActiveRecord::Base) ? model_class.columns_hash : {}
  795 + @fixture.map do |key, value|
  796 + @connection.quote(value, cols[key]).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
  797 + end.join(', ')
800 798 end
801 799
802 800 def find
137 activerecord/lib/active_record/transactions.rb
@@ -8,6 +8,10 @@ module Transactions
8 8 class TransactionError < ActiveRecordError # :nodoc:
9 9 end
10 10
  11 + included do
  12 + define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name]
  13 + end
  14 +
11 15 # Transactions are protective blocks where SQL statements are only permanent
12 16 # if they can all succeed as one atomic action. The classic example is a
13 17 # transfer between two accounts where you can only have a deposit if the
@@ -72,7 +76,7 @@ class TransactionError < ActiveRecordError # :nodoc:
72 76 #
73 77 # Both +save+ and +destroy+ come wrapped in a transaction that ensures
74 78 # that whatever you do in validations or callbacks will happen under its
75   - # protected cover. So you can use validations to check for values that
  79 + # protected cover. So you can use validations to check for values that
76 80 # the transaction depends on or you can raise exceptions in the callbacks
77 81 # to rollback, including <tt>after_*</tt> callbacks.
78 82 #
@@ -158,6 +162,21 @@ class TransactionError < ActiveRecordError # :nodoc:
158 162 # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
159 163 # for more information about savepoints.
160 164 #
  165 + # === Callbacks
  166 + #
  167 + # There are two types of callbacks associated with committing and rolling back transactions:
  168 + # +after_commit+ and +after_rollback+.
  169 + #
  170 + # +after_commit+ callbacks are called on every record saved or destroyed within a
  171 + # transaction immediately after the transaction is committed. +after_rollback+ callbacks
  172 + # are called on every record saved or destroyed within a transaction immediately after the
  173 + # transaction or savepoint is rolled back.
  174 + #
  175 + # These callbacks are useful for interacting with other systems since you will be guaranteed
  176 + # that the callback is only executed when the database is in a permanent state. For example,
  177 + # +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from
  178 + # within a transaction could trigger the cache to be regenerated before the database is updated.
  179 + #
161 180 # === Caveats
162 181 #
163 182 # If you're on MySQL, then do not use DDL operations in nested transactions
@@ -182,6 +201,24 @@ def transaction(options = {}, &block)
182 201 # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
183 202 connection.transaction(options, &block)
184 203 end
  204 +
  205 + def after_commit(*args, &block)
  206 + options = args.last
  207 + if options.is_a?(Hash) && options[:on]
  208 + options[:if] = Array.wrap(options[:if])
  209 + options[:if] << "transaction_include_action?(:#{options[:on]})"
  210 + end
  211 + set_callback(:commit, :after, *args, &block)
  212 + end
  213 +
  214 + def after_rollback(*args, &block)
  215 + options = args.last
  216 + if options.is_a?(Hash) && options[:on]
  217 + options[:if] = Array.wrap(options[:if])
  218 + options[:if] << "transaction_include_action?(:#{options[:on]})"
  219 + end
  220 + set_callback(:rollback, :after, *args, &block)
  221 + end
185 222 end
186 223
187 224 # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
@@ -205,19 +242,36 @@ def save!(*) #:nodoc:
205 242
206 243 # Reset id and @new_record if the transaction rolls back.
207 244 def rollback_active_record_state!
208   - id_present = has_attribute?(self.class.primary_key)
209   - previous_id = id
210   - previous_new_record = new_record?
  245 + remember_transaction_record_state
211 246 yield
212 247 rescue Exception
213   - @new_record = previous_new_record
214   - if id_present
215   - self.id = previous_id
216   - else
217   - @attributes.delete(self.class.primary_key)
218   - @attributes_cache.delete(self.class.primary_key)
219   - end
  248 + restore_transaction_record_state
220 249 raise
  250 + ensure
  251 + clear_transaction_record_state
  252 + end
  253 +
  254 + # Call the after_commit callbacks
  255 + def committed! #:nodoc:
  256 + _run_commit_callbacks
  257 + ensure
  258 + clear_transaction_record_state
  259 + end
  260 +
  261 + # Call the after rollback callbacks. The restore_state argument indicates if the record
  262 + # state should be rolled back to the beginning or just to the last savepoint.
  263 + def rolledback!(force_restore_state = false) #:nodoc:
  264 + _run_rollback_callbacks
  265 + ensure
  266 + restore_transaction_record_state(force_restore_state)
  267 + end
  268 +
  269 + # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks
  270 + # can be called.
  271 + def add_to_transaction
  272 + if self.class.connection.add_transaction_record(self)
  273 + remember_transaction_record_state
  274 + end
221 275 end
222 276
223 277 # Executes +method+ within a transaction and captures its return value as a
@@ -229,10 +283,71 @@ def rollback_active_record_state!
229 283 def with_transaction_returning_status
230 284 status = nil
231 285 self.class.transaction do
  286 + add_to_transaction
232 287 status = yield
233 288 raise ActiveRecord::Rollback unless status
234 289 end
235 290 status
236 291 end
  292 +
  293 + protected
  294 +
  295 + # Save the new record state and id of a record so it can be restored later if a transaction fails.
  296 + def remember_transaction_record_state #:nodoc
  297 + @_start_transaction_state ||= {}
  298 + unless @_start_transaction_state.include?(:new_record)
  299 + @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
  300 + @_start_transaction_state[:new_record] = @new_record
  301 + end
  302 + unless @_start_transaction_state.include?(:destroyed)
  303 + @_start_transaction_state[:destroyed] = @destroyed
  304 + end
  305 + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
  306 + end
  307 +
  308 + # Clear the new record state and id of a record.
  309 + def clear_transaction_record_state #:nodoc
  310 + if defined?(@_start_transaction_state)
  311 + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
  312 + remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1
  313 + end
  314 + end
  315 +
  316 + # Restore the new record state and id of a record that was previously saved by a call to save_record_state.
  317 + def restore_transaction_record_state(force = false) #:nodoc
  318 + if defined?(@_start_transaction_state)
  319 + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
  320 + if @_start_transaction_state[:level] < 1
  321 + restore_state = remove_instance_variable(:@_start_transaction_state)
  322 + if restore_state
  323 + @new_record = restore_state[:new_record]
  324 + @destroyed = restore_state[:destroyed]
  325 + if restore_state[:id]
  326 + self.id = restore_state[:id]
  327 + else
  328 + @attributes.delete(self.class.primary_key)
  329 + @attributes_cache.delete(self.class.primary_key)
  330 + end
  331 + end
  332 + end
  333 + end
  334 + end
  335 +
  336 + # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
  337 + def transaction_record_state(state) #:nodoc
  338 + @_start_transaction_state[state] if defined?(@_start_transaction_state)
  339 + end
  340 +
  341 + # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
  342 + def transaction_include_action?(action) #:nodoc
  343 + case action
  344 + when :create
  345 + transaction_record_state(:new_record)
  346 + when :destroy
  347 + destroyed?
  348 + when :update
  349 + !(transaction_record_state(:new_record) || destroyed?)
  350 + end
  351 + end
237 352 end
238 353 end
8 activerecord/test/cases/active_schema_test_mysql.rb
@@ -4,6 +4,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
4 4 def setup
5 5 ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
6 6 alias_method :execute_without_stub, :execute
  7 + remove_method :execute
7 8 def execute(sql, name = nil) return sql end
8 9 end
9 10 end
@@ -66,7 +67,7 @@ def test_drop_table_with_specific_database
66 67 assert_equal "DROP TABLE `otherdb`.`people`", drop_table('otherdb.people')
67 68 end
68 69
69   - def test_add_timestamps
  70 + def test_add_timestamps
70 71 with_real_execute do
71 72 begin
72 73 ActiveRecord::Base.connection.create_table :delete_me do |t|
@@ -79,8 +80,8 @@ def test_add_timestamps
79 80 end
80 81 end
81 82 end
82   -
83   - def test_remove_timestamps
  83 +
  84 + def test_remove_timestamps
84 85 with_real_execute do
85 86 begin
86 87 ActiveRecord::Base.connection.create_table :delete_me do |t|
@@ -106,6 +107,7 @@ def with_real_execute
106 107 ensure
107 108 #before finishing, we restore the alias to the mock-up method
108 109 ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
  110 + remove_method :execute
109 111 alias_method :execute, :execute_with_stub
110 112 end
111 113 end
1  activerecord/test/cases/active_schema_test_postgresql.rb
@@ -4,6 +4,7 @@ class PostgresqlActiveSchemaTest < Test::Unit::TestCase
4 4 def setup
5 5 ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
6 6 alias_method :real_execute, :execute
  7 + remove_method :execute
7 8 def execute(sql, name = nil) sql end
8 9 end
9 10 end
8 activerecord/test/cases/adapter_test.rb
@@ -145,13 +145,13 @@ def test_foreign_key_violations_are_translated_to_specific_exception
145 145
146 146 def test_add_limit_offset_should_sanitize_sql_injection_for_limit_without_comas
147 147 sql_inject = "1 select * from schema"
148   - assert_no_match /schema/, @connection.add_limit_offset!("", :limit=>sql_inject)
149   - assert_no_match /schema/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)
  148 + assert_no_match(/schema/, @connection.add_limit_offset!("", :limit=>sql_inject))
  149 + assert_no_match(/schema/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7))
150 150 end
151 151
152 152 def test_add_limit_offset_should_sanitize_sql_injection_for_limit_with_comas
153 153 sql_inject = "1, 7 procedure help()"
154   - assert_no_match /procedure/, @connection.add_limit_offset!("", :limit=>sql_inject)
155   - assert_no_match /procedure/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)
  154 + assert_no_match(/procedure/, @connection.add_limit_offset!("", :limit=>sql_inject))
  155 + assert_no_match(/procedure/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7))
156 156 end
157 157 end
17 activerecord/test/cases/base_test.rb
@@ -2334,6 +2334,23 @@ def test_dup
2334 2334 assert !Minimalistic.new.freeze.dup.frozen?
2335 2335 end
2336 2336
  2337 + def test_compute_type_success
  2338 + assert_equal Author, ActiveRecord::Base.send(:compute_type, 'Author')
  2339 + end
  2340 +
  2341 + def test_compute_type_nonexistent_constant
  2342 + assert_raises NameError do
  2343 + ActiveRecord::Base.send :compute_type, 'NonexistentModel'
  2344 + end
  2345 + end
  2346 +
  2347 + def test_compute_type_no_method_error
  2348 + String.any_instance.stubs(:constantize).raises(NoMethodError)
  2349 + assert_raises NoMethodError do
  2350 + ActiveRecord::Base.send :compute_type, 'InvalidModel'
  2351 + end
  2352 + end
  2353 +
2337 2354 protected
2338 2355 def with_env_tz(new_tz = 'US/Eastern')
2339 2356 old_tz, ENV['TZ'] = ENV['TZ'], new_tz
240 activerecord/test/cases/transaction_callbacks_test.rb
... ... @@ -0,0 +1,240 @@
  1 +require "cases/helper"
  2 +require 'models/topic'
  3 +require 'models/reply'
  4 +
  5 +class TransactionCallbacksTest < ActiveRecord::TestCase
  6 + self.use_transactional_fixtures = false
  7 + fixtures :topics
  8 +
  9 + class TopicWithCallbacks < ActiveRecord::Base
  10 + set_table_name :topics
  11 +
  12 + after_commit{|record| record.send(:do_after_commit, nil)}
  13 + after_commit(:on => :create){|record| record.send(:do_after_commit, :create)}
  14 + after_commit(:on => :update){|record| record.send(:do_after_commit, :update)}
  15 + after_commit(:on => :destroy){|record| record.send(:do_after_commit, :destroy)}
  16 + after_rollback{|record| record.send(:do_after_rollback, nil)}
  17 + after_rollback(:on => :create){|record| record.send(:do_after_rollback, :create)}
  18 + after_rollback(:on => :update){|record| record.send(:do_after_rollback, :update)}
  19 + after_rollback(:on => :destroy){|record| record.send(:do_after_rollback, :destroy)}
  20 +
  21 + def history
  22 + @history ||= []
  23 + end
  24 +
  25 + def after_commit_block(on = nil, &block)
  26 + @after_commit ||= {}
  27 + @after_commit[on] ||= []
  28 + @after_commit[on] << block
  29 + end
  30 +
  31 + def after_rollback_block(on = nil, &block)
  32 + @after_rollback ||= {}
  33 + @after_rollback[on] ||= []
  34 + @after_rollback[on] << block
  35 + end
  36 +
  37 + def do_after_commit(on)
  38 + blocks = @after_commit[on] if defined?(@after_commit)
  39 + blocks.each{|b| b.call(self)} if blocks
  40 + end
  41 +
  42 + def do_after_rollback(on)
  43 + blocks = @after_rollback[on] if defined?(@after_rollback)
  44 + blocks.each{|b| b.call(self)} if blocks
  45 + end
  46 + end
  47 +
  48 + def setup
  49 + @first, @second = TopicWithCallbacks.find(1, 3).sort_by { |t| t.id }
  50 + end
  51 +
  52 + def test_call_after_commit_after_transaction_commits
  53 + @first.after_commit_block{|r| r.history << :after_commit}
  54 + @first.after_rollback_block{|r| r.history << :after_rollback}
  55 +
  56 + @first.save!
  57 + assert_equal [:after_commit], @first.history
  58 + end
  59 +
  60 + def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record
  61 + @first.after_commit_block(:create){|r| r.history << :commit_on_create}
  62 + @first.after_commit_block(:update){|r| r.history << :commit_on_update}
  63 + @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
  64 + @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
  65 + @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
  66 + @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
  67 +
  68 + @first.save!
  69 + assert_equal [:commit_on_update], @first.history
  70 + end
  71 +
  72 + def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_record
  73 + @first.after_commit_block(:create){|r| r.history << :commit_on_create}
  74 + @first.after_commit_block(:update){|r| r.history << :commit_on_update}
  75 + @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
  76 + @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
  77 + @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
  78 + @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
  79 +
  80 + @first.destroy
  81 + assert_equal [:commit_on_destroy], @first.history
  82 + end
  83 +
  84 + def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record
  85 + @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
  86 + @new_record.after_commit_block(:create){|r| r.history << :commit_on_create}
  87 + @new_record.after_commit_block(:update){|r| r.history << :commit_on_update}
  88 + @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
  89 + @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create}
  90 + @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update}
  91 + @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
  92 +
  93 + @new_record.save!
  94 + assert_equal [:commit_on_create], @new_record.history
  95 + end
  96 +
  97 + def test_call_after_rollback_after_transaction_rollsback
  98 + @first.after_commit_block{|r| r.history << :after_commit}
  99 + @first.after_rollback_block{|r| r.history << :after_rollback}
  100 +
  101 + Topic.transaction do
  102 + @first.save!
  103 + raise ActiveRecord::Rollback
  104 + end
  105 +
  106 + assert_equal [:after_rollback], @first.history
  107 + end
  108 +
  109 + def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record
  110 + @first.after_commit_block(:create){|r| r.history << :commit_on_create}
  111 + @first.after_commit_block(:update){|r| r.history << :commit_on_update}
  112 + @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
  113 + @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
  114 + @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
  115 + @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
  116 +
  117 + Topic.transaction do
  118 + @first.save!
  119 + raise ActiveRecord::Rollback
  120 + end
  121 +
  122 + assert_equal [:rollback_on_update], @first.history
  123 + end
  124 +
  125 + def test_only_call_after_rollback_on_destroy_after_transaction_rollsback_for_destroyed_record
  126 + @first.after_commit_block(:create){|r| r.history << :commit_on_create}
  127 + @first.after_commit_block(:update){|r| r.history << :commit_on_update}
  128 + @first.after_commit_block(:destroy){|r| r.history << :commit_on_update}
  129 + @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
  130 + @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
  131 + @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
  132 +
  133 + Topic.transaction do
  134 + @first.destroy
  135 + raise ActiveRecord::Rollback
  136 + end
  137 +
  138 + assert_equal [:rollback_on_destroy], @first.history
  139 + end
  140 +
  141 + def test_only_call_after_rollback_on_create_after_transaction_rollsback_for_new_record
  142 + @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
  143 + @new_record.after_commit_block(:create){|r| r.history << :commit_on_create}
  144 + @new_record.after_commit_block(:update){|r| r.history << :commit_on_update}
  145 + @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
  146 + @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create}
  147 + @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update}
  148 + @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
  149 +
  150 + Topic.transaction do
  151 + @new_record.save!
  152 + raise ActiveRecord::Rollback
  153 + end
  154 +
  155 + assert_equal [:rollback_on_create], @new_record.history
  156 + end
  157 +
  158 + def test_call_after_rollback_when_commit_fails
  159 + @first.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction)
  160 + begin
  161 + @first.connection.class.class_eval do
  162 + def commit_db_transaction; raise "boom!"; end
  163 + end
  164 +
  165 + @first.after_commit_block{|r| r.history << :after_commit}
  166 + @first.after_rollback_block{|r| r.history << :after_rollback}
  167 +
  168 + assert !@first.save rescue nil
  169 + assert_equal [:after_rollback], @first.history
  170 + ensure
  171 + @first.connection.class.send(:remove_method, :commit_db_transaction)
  172 + @first.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction)
  173 + end
  174 + end
  175 +
  176 + def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint
  177 + def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
  178 + def @first.commits(i=0); @commits ||= 0; @commits += i if i; end
  179 + @first.after_rollback_block{|r| r.rollbacks(1)}
  180 + @first.after_commit_block{|r| r.commits(1)}
  181 +
  182 + def @second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
  183 + def @second.commits(i=0); @commits ||= 0; @commits += i if i; end
  184 + @second.after_rollback_block{|r| r.rollbacks(1)}
  185 + @second.after_commit_block{|r| r.commits(1)}
  186 +
  187 + Topic.transaction do
  188 + @first.save!
  189 + Topic.transaction(:requires_new => true) do
  190 + @second.save!
  191 + raise ActiveRecord::Rollback
  192 + end
  193 + end
  194 +
  195 + assert_equal 1, @first.commits
  196 + assert_equal 0, @first.rollbacks
  197 + assert_equal 0, @second.commits
  198 + assert_equal 1, @second.rollbacks
  199 + end
  200 +
  201 + def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails
  202 + def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
  203 + def @first.commits(i=0); @commits ||= 0; @commits += i if i; end
  204 +
  205 + @first.after_rollback_block{|r| r.rollbacks(1)}
  206 + @first.after_commit_block{|r| r.commits(1)}
  207 +
  208 + Topic.transaction do
  209 + @first.save
  210 + Topic.transaction(:requires_new => true) do
  211 + @first.save!
  212 + raise ActiveRecord::Rollback
  213 + end
  214 + Topic.transaction(:requires_new => true) do
  215 + @first.save!
  216 + raise ActiveRecord::Rollback
  217 + end
  218 + end
  219 +
  220 + assert_equal 1, @first.commits
  221 + assert_equal 2, @first.rollbacks
  222 + end
  223 +
  224 + def test_after_transaction_callbacks_should_not_raise_errors
  225 + def @first.last_after_transaction_error=(e); @last_transaction_error = e; end
  226 + def @first.last_after_transaction_error; @last_transaction_error; end
  227 + @first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";}
  228 + @first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";}
  229 +
  230 + @first.save!
  231 + assert_equal :commit, @first.last_after_transaction_error
  232 +
  233 + Topic.transaction do
  234 + @first.save!
  235 + raise ActiveRecord::Rollback
  236 + end
  237 +
  238 + assert_equal :rollback, @first.last_after_transaction_error
  239 + end
  240 +end
33 activerecord/test/cases/transactions_test.rb
@@ -320,6 +320,33 @@ def test_rollback_when_commit_raises
320 320 end
321 321 end
322 322
  323 + def test_restore_active_record_state_for_all_records_in_a_transaction
  324 + topic_1 = Topic.new(:title => 'test_1')
  325 + topic_2 = Topic.new(:title => 'test_2')
  326 + Topic.transaction do
  327 + assert topic_1.save
  328 + assert topic_2.save
  329 + @first.save
  330 + @second.destroy
  331 + assert_equal false, topic_1.new_record?
  332 + assert_not_nil topic_1.id
  333 + assert_equal false, topic_2.new_record?
  334 + assert_not_nil topic_2.id
  335 + assert_equal false, @first.new_record?
  336 + assert_not_nil @first.id
  337 + assert_equal true, @second.destroyed?
  338 + raise ActiveRecord::Rollback
  339 + end
  340 +
  341 + assert_equal true, topic_1.new_record?
  342 + assert_nil topic_1.id
  343 + assert_equal true, topic_2.new_record?
  344 + assert_nil topic_2.id
  345 + assert_equal false, @first.new_record?
  346 + assert_not_nil @first.id
  347 + assert_equal false, @second.destroyed?
  348 + end
  349 +
323 350 if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE)
324 351 def test_outside_transaction_works
325 352 assert Topic.connection.outside_transaction?
@@ -382,6 +409,12 @@ def test_sqlite_add_column_in_transaction
382 409 end
383 410
384 411 private
  412 + def define_callback_method(callback_method)
  413 + define_method(callback_method) do
  414 + self.history << [callback_method, :method]
  415 + end
  416 + end
  417 +
385 418 def add_exception_raising_after_save_callback_to_topic
386 419 Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1
387 420 remove_method(:after_save_for_transaction)
2  railties/guides/source/3_0_release_notes.textile
Source Rendered
@@ -36,7 +36,7 @@ h4. Rails 3 requires Ruby 1.8.7+
36 36
37 37 Rails 3.0 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.0 is also compatible with Ruby 1.9.2.
38 38
39   -TIP: Note that Ruby 1.8.7 p248 and p249 has marshaling bugs that crash Rails 3.0.0. Ruby 1.9.1 outright segfaults on Rails 3.0.0, so if you want to use Rails 3 with 1.9.x, jump on 1.9.2 trunk for smooth sailing.
  39 +TIP: Note that Ruby 1.8.7 p248 and p249 has marshaling bugs that crash Rails 3.0.0. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults on Rails 3.0.0, so if you want to use Rails 3 with 1.9.x jump on 1.9.2 for smooth sailing.
40 40
41 41 h4. Rails Application object
42 42
117 railties/guides/source/active_support_core_extensions.textile
Source Rendered
@@ -2869,7 +2869,7 @@ Date.new(2010, 2, 28).advance(:days => 1).advance(:months => 1)
2869 2869 # => Thu, 01 Apr 2010
2870 2870 </ruby>
2871 2871
2872   -h5. Changing Date Components
  2872 +h5. Changing Components
2873 2873
2874 2874 The method +change+ allows you to get a new date which is the same as the receiver except for the given year, month, or day:
2875 2875
@@ -2909,11 +2909,122 @@ date.end_of_day # => Sun Jun 06 23:59:59 +0200 2010
2909 2909
2910 2910 +beginning_of_day+ is aliased to +at_beginning_of_day+, +midnight+, +at_midnight+
2911 2911
2912   -h4. Conversions
  2912 +h4(#date-conversions). Conversions
2913 2913
2914 2914 h3. Extensions to +DateTime+
2915 2915
2916   -NOTE TO SELF: Since +DateTime+ is a subclass of +Date+, you get inherited methods that return +DateTime+ objects.
  2916 +NOTE: All the following methods are defined in +active_support/core_ext/date_time/calculations.rb+.
  2917 +
  2918 +WARNING: +DateTime+ is not aware of DST rules and so some of these methods have edge cases when a DST change is going on. For example +seconds_since_midnight+ might not return the real amount in such a day.
  2919 +
  2920 +h4(#calculations-datetime). Calculations
  2921 +
  2922 +The class +DateTime+ is a subclass of +Date+ so by loading +active_support/core_ext/date/calculations.rb+ you inherit these methods and their aliases, except that they will always return datetimes:
  2923 +
  2924 +<ruby>
  2925 +yesterday
  2926 +tomorrow
  2927 +beginning_of_week
  2928 +end_on_week
  2929 +next_week
  2930 +months_ago
  2931 +months_since
  2932 +beginning_of_month
  2933 +end_of_month
  2934 +prev_month
  2935 +next_month
  2936 +beginning_of_quarter
  2937 +end_of_quarter
  2938 +beginning_of_year
  2939 +end_of_year
  2940 +years_ago
  2941 +years_since
  2942 +prev_year
  2943 +next_year