Sync 'rails/rails/master'

This commit is contained in:
Yehuda Katz
2009-01-11 13:02:34 -08:00
18 changed files with 776 additions and 455 deletions

View File

@@ -1,65 +0,0 @@
require 'abstract_unit'
unless defined? ApplicationController
class ApplicationController < ActionController::Base
end
end
class UploadTestController < ActionController::Base
def update
SessionUploadTest.last_request_type = ActionController::Base.param_parsers[request.content_type]
render :text => "got here"
end
def read
render :text => "File: #{params[:uploaded_data].read}"
end
end
class SessionUploadTest < ActionController::IntegrationTest
FILES_DIR = File.dirname(__FILE__) + '/../fixtures/multipart'
class << self
attr_accessor :last_request_type
end
def test_upload_and_read_file
with_test_routing do
post '/read', :uploaded_data => fixture_file_upload(FILES_DIR + "/hello.txt", "text/plain")
assert_equal "File: Hello", response.body
end
end
# The lint wrapper is used in integration tests
# instead of a normal StringIO class
InputWrapper = Rack::Lint::InputWrapper
def test_post_with_upload_with_unrewindable_input
InputWrapper.any_instance.expects(:rewind).raises(Errno::ESPIPE)
with_test_routing do
post '/read', :uploaded_data => fixture_file_upload(FILES_DIR + "/hello.txt", "text/plain")
assert_equal "File: Hello", response.body
end
end
def test_post_with_upload_with_params_parsing
with_test_routing do
params = { :uploaded_data => fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg") }
post '/update', params, :location => 'blah'
assert_equal(:multipart_form, SessionUploadTest.last_request_type)
end
end
private
def with_test_routing
with_routing do |set|
set.draw do |map|
map.update 'update', :controller => "upload_test", :action => "update", :method => :post
map.read 'read', :controller => "upload_test", :action => "read", :method => :post
end
yield
end
end
end

View File

@@ -0,0 +1,167 @@
require 'abstract_unit'
class MultipartParamsParsingTest < ActionController::IntegrationTest
class TestController < ActionController::Base
class << self
attr_accessor :last_request_parameters, :last_request_type
end
def parse
self.class.last_request_type = ActionController::Base.param_parsers[request.content_type]
self.class.last_request_parameters = request.request_parameters
head :ok
end
def read
render :text => "File: #{params[:uploaded_data].read}"
end
end
FIXTURE_PATH = File.dirname(__FILE__) + '/../../fixtures/multipart'
def teardown
TestController.last_request_parameters = nil
TestController.last_request_type = nil
end
test "parses single parameter" do
assert_equal({ 'foo' => 'bar' }, parse_multipart('single_parameter'))
end
test "parses bracketed parameters" do
assert_equal({ 'foo' => { 'baz' => 'bar'}}, parse_multipart('bracketed_param'))
end
test "parses text file" do
params = parse_multipart('text_file')
assert_equal %w(file foo), params.keys.sort
assert_equal 'bar', params['foo']
file = params['file']
assert_kind_of StringIO, file
assert_equal 'file.txt', file.original_filename
assert_equal "text/plain", file.content_type
assert_equal 'contents', file.read
end
test "parses boundary problem file" do
params = parse_multipart('boundary_problem_file')
assert_equal %w(file foo), params.keys.sort
file = params['file']
foo = params['foo']
assert_kind_of Tempfile, file
assert_equal 'file.txt', file.original_filename
assert_equal "text/plain", file.content_type
assert_equal 'bar', foo
end
test "parses large text file" do
params = parse_multipart('large_text_file')
assert_equal %w(file foo), params.keys.sort
assert_equal 'bar', params['foo']
file = params['file']
assert_kind_of Tempfile, file
assert_equal 'file.txt', file.original_filename
assert_equal "text/plain", file.content_type
assert ('a' * 20480) == file.read
end
test "parses binary file" do
params = parse_multipart('binary_file')
assert_equal %w(file flowers foo), params.keys.sort
assert_equal 'bar', params['foo']
file = params['file']
assert_kind_of StringIO, file
assert_equal 'file.csv', file.original_filename
assert_nil file.content_type
assert_equal 'contents', file.read
file = params['flowers']
assert_kind_of StringIO, file
assert_equal 'flowers.jpg', file.original_filename
assert_equal "image/jpeg", file.content_type
assert_equal 19512, file.size
end
test "parses mixed files" do
params = parse_multipart('mixed_files')
assert_equal %w(files foo), params.keys.sort
assert_equal 'bar', params['foo']
# Ruby CGI doesn't handle multipart/mixed for us.
files = params['files']
assert_kind_of String, files
files.force_encoding('ASCII-8BIT') if files.respond_to?(:force_encoding)
assert_equal 19756, files.size
end
test "uploads and parses parameters" do
with_test_routing do
params = { :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/mona_lisa.jpg", "image/jpg") }
post '/parse', params, :location => 'blah'
assert_equal(:multipart_form, TestController.last_request_type)
end
end
test "uploads and reads file" do
with_test_routing do
post '/read', :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain")
assert_equal "File: Hello", response.body
end
end
# The lint wrapper is used in integration tests
# instead of a normal StringIO class
InputWrapper = Rack::Lint::InputWrapper
test "parses unwindable stream" do
InputWrapper.any_instance.expects(:rewind).raises(Errno::ESPIPE)
params = parse_multipart('large_text_file')
assert_equal %w(file foo), params.keys.sort
assert_equal 'bar', params['foo']
end
test "uploads and reads file with unwindable input" do
InputWrapper.any_instance.expects(:rewind).raises(Errno::ESPIPE)
with_test_routing do
post '/read', :uploaded_data => fixture_file_upload(FIXTURE_PATH + "/hello.txt", "text/plain")
assert_equal "File: Hello", response.body
end
end
private
def fixture(name)
File.open(File.join(FIXTURE_PATH, name), 'rb') do |file|
{ "rack.input" => file.read,
"CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
"CONTENT_LENGTH" => file.stat.size.to_s }
end
end
def parse_multipart(name)
with_test_routing do
headers = fixture(name)
post "/parse", headers.delete("rack.input"), headers
assert_response :ok
TestController.last_request_parameters
end
end
def with_test_routing
with_routing do |set|
set.draw do |map|
map.connect ':action', :controller => "multipart_params_parsing_test/test"
end
yield
end
end
end

View File

@@ -0,0 +1,171 @@
require 'abstract_unit'
class UrlEncodedParamsParsingTest < ActionController::IntegrationTest
class TestController < ActionController::Base
class << self
attr_accessor :last_request_parameters, :last_request_type
end
def parse
self.class.last_request_parameters = request.request_parameters
head :ok
end
end
def teardown
TestController.last_request_parameters = nil
end
test "parses unbalanced query string with array" do
assert_parses(
{'location' => ["1", "2"], 'age_group' => ["2"]},
"location[]=1&location[]=2&age_group[]=2"
)
end
test "parses nested hash" do
query = [
"note[viewers][viewer][][type]=User",
"note[viewers][viewer][][id]=1",
"note[viewers][viewer][][type]=Group",
"note[viewers][viewer][][id]=2"
].join("&")
expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } }
assert_parses(expected, query)
end
test "parses more complex nesting" do
query = [
"customers[boston][first][name]=David",
"customers[boston][first][url]=http://David",
"customers[boston][second][name]=Allan",
"customers[boston][second][url]=http://Allan",
"something_else=blah",
"something_nil=",
"something_empty=",
"products[first]=Apple Computer",
"products[second]=Pc",
"=Save"
].join("&")
expected = {
"customers" => {
"boston" => {
"first" => {
"name" => "David",
"url" => "http://David"
},
"second" => {
"name" => "Allan",
"url" => "http://Allan"
}
}
},
"something_else" => "blah",
"something_empty" => "",
"something_nil" => "",
"products" => {
"first" => "Apple Computer",
"second" => "Pc"
}
}
assert_parses expected, query
end
test "parses params with array" do
query = "selected[]=1&selected[]=2&selected[]=3"
expected = { "selected" => [ "1", "2", "3" ] }
assert_parses expected, query
end
test "parses params with non alphanumeric name" do
query = "a/b[c]=d"
expected = { "a/b" => { "c" => "d" }}
assert_parses expected, query
end
test "parses params with single brackets in the middle" do
query = "a/b[c]d=e"
expected = { "a/b" => {} }
assert_parses expected, query
end
test "parses params with separated brackets" do
query = "a/b@[c]d[e]=f"
expected = { "a/b@" => { }}
assert_parses expected, query
end
test "parses params with separated brackets and array" do
query = "a/b@[c]d[e][]=f"
expected = { "a/b@" => { }}
assert_parses expected, query
end
test "parses params with unmatched brackets and array" do
query = "a/b@[c][d[e][]=f"
expected = { "a/b@" => { "c" => { }}}
assert_parses expected, query
end
test "parses params with nil key" do
query = "=&test2=value1"
expected = { "test2" => "value1" }
assert_parses expected, query
end
test "parses params with array prefix and hashes" do
query = "a[][b][c]=d"
expected = {"a" => [{"b" => {"c" => "d"}}]}
assert_parses expected, query
end
test "parses params with complex nesting" do
query = "a[][b][c][][d][]=e"
expected = {"a" => [{"b" => {"c" => [{"d" => ["e"]}]}}]}
assert_parses expected, query
end
test "parses params with file path" do
query = [
"customers[boston][first][name]=David",
"something_else=blah",
"logo=#{File.expand_path(__FILE__)}"
].join("&")
expected = {
"customers" => {
"boston" => {
"first" => {
"name" => "David"
}
}
},
"something_else" => "blah",
"logo" => File.expand_path(__FILE__),
}
assert_parses expected, query
end
private
def with_test_routing
with_routing do |set|
set.draw do |map|
map.connect ':action', :controller => "url_encoded_params_parsing_test/test"
end
yield
end
end
def assert_parses(expected, actual)
with_test_routing do
post "/parse", actual
assert_response :ok
assert_equal(expected, TestController.last_request_parameters)
end
end
end

View File

@@ -405,308 +405,3 @@ class RequestTest < ActiveSupport::TestCase
@request.request_method(true)
end
end
class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase
def test_unbalanced_query_string_with_array
assert_equal(
{'location' => ["1", "2"], 'age_group' => ["2"]},
ActionController::RequestParser.parse_request_parameters({'location[]' => ["1", "2"], 'age_group[]' => ["2"]})
)
end
def test_request_hash_parsing
query = {
"note[viewers][viewer][][type]" => ["User", "Group"],
"note[viewers][viewer][][id]" => ["1", "2"]
}
expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } }
assert_equal(expected, ActionController::RequestParser.parse_request_parameters(query))
end
def test_parse_params
input = {
"customers[boston][first][name]" => [ "David" ],
"customers[boston][first][url]" => [ "http://David" ],
"customers[boston][second][name]" => [ "Allan" ],
"customers[boston][second][url]" => [ "http://Allan" ],
"something_else" => [ "blah" ],
"something_nil" => [ nil ],
"something_empty" => [ "" ],
"products[first]" => [ "Apple Computer" ],
"products[second]" => [ "Pc" ],
"" => [ 'Save' ]
}
expected_output = {
"customers" => {
"boston" => {
"first" => {
"name" => "David",
"url" => "http://David"
},
"second" => {
"name" => "Allan",
"url" => "http://Allan"
}
}
},
"something_else" => "blah",
"something_empty" => "",
"something_nil" => "",
"products" => {
"first" => "Apple Computer",
"second" => "Pc"
}
}
assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input)
end
UploadedStringIO = ActionController::UploadedStringIO
class MockUpload < UploadedStringIO
def initialize(content_type, original_path, *args)
self.content_type = content_type
self.original_path = original_path
super *args
end
end
def test_parse_params_from_multipart_upload
file = MockUpload.new('img/jpeg', 'foo.jpg')
ie_file = MockUpload.new('img/jpeg', 'c:\\Documents and Settings\\foo\\Desktop\\bar.jpg')
non_file_text_part = MockUpload.new('text/plain', '', 'abc')
input = {
"something" => [ UploadedStringIO.new("") ],
"array_of_stringios" => [[ UploadedStringIO.new("One"), UploadedStringIO.new("Two") ]],
"mixed_types_array" => [[ UploadedStringIO.new("Three"), "NotStringIO" ]],
"mixed_types_as_checkboxes[strings][nested]" => [[ file, "String", UploadedStringIO.new("StringIO")]],
"ie_mixed_types_as_checkboxes[strings][nested]" => [[ ie_file, "String", UploadedStringIO.new("StringIO")]],
"products[string]" => [ UploadedStringIO.new("Apple Computer") ],
"products[file]" => [ file ],
"ie_products[string]" => [ UploadedStringIO.new("Microsoft") ],
"ie_products[file]" => [ ie_file ],
"text_part" => [non_file_text_part]
}
expected_output = {
"something" => "",
"array_of_stringios" => ["One", "Two"],
"mixed_types_array" => [ "Three", "NotStringIO" ],
"mixed_types_as_checkboxes" => {
"strings" => {
"nested" => [ file, "String", "StringIO" ]
},
},
"ie_mixed_types_as_checkboxes" => {
"strings" => {
"nested" => [ ie_file, "String", "StringIO" ]
},
},
"products" => {
"string" => "Apple Computer",
"file" => file
},
"ie_products" => {
"string" => "Microsoft",
"file" => ie_file
},
"text_part" => "abc"
}
params = ActionController::RequestParser.parse_request_parameters(input)
assert_equal expected_output, params
# Lone filenames are preserved.
assert_equal 'foo.jpg', params['mixed_types_as_checkboxes']['strings']['nested'].first.original_filename
assert_equal 'foo.jpg', params['products']['file'].original_filename
# But full Windows paths are reduced to their basename.
assert_equal 'bar.jpg', params['ie_mixed_types_as_checkboxes']['strings']['nested'].first.original_filename
assert_equal 'bar.jpg', params['ie_products']['file'].original_filename
end
def test_parse_params_with_file
input = {
"customers[boston][first][name]" => [ "David" ],
"something_else" => [ "blah" ],
"logo" => [ File.new(File.dirname(__FILE__) + "/rack_test.rb").path ]
}
expected_output = {
"customers" => {
"boston" => {
"first" => {
"name" => "David"
}
}
},
"something_else" => "blah",
"logo" => File.new(File.dirname(__FILE__) + "/rack_test.rb").path,
}
assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_array
input = { "selected[]" => [ "1", "2", "3" ] }
expected_output = { "selected" => [ "1", "2", "3" ] }
assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_non_alphanumeric_name
input = { "a/b[c]" => %w(d) }
expected = { "a/b" => { "c" => "d" }}
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_single_brackets_in_middle
input = { "a/b[c]d" => %w(e) }
expected = { "a/b" => {} }
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_separated_brackets
input = { "a/b@[c]d[e]" => %w(f) }
expected = { "a/b@" => { }}
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_separated_brackets_and_array
input = { "a/b@[c]d[e][]" => %w(f) }
expected = { "a/b@" => { }}
assert_equal expected , ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_unmatched_brackets_and_array
input = { "a/b@[c][d[e][]" => %w(f) }
expected = { "a/b@" => { "c" => { }}}
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_nil_key
input = { nil => nil, "test2" => %w(value1) }
expected = { "test2" => "value1" }
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_array_prefix_and_hashes
input = { "a[][b][c]" => %w(d) }
expected = {"a" => [{"b" => {"c" => "d"}}]}
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
def test_parse_params_with_complex_nesting
input = { "a[][b][c][][d][]" => %w(e) }
expected = {"a" => [{"b" => {"c" => [{"d" => ["e"]}]}}]}
assert_equal expected, ActionController::RequestParser.parse_request_parameters(input)
end
end
class MultipartRequestParameterParsingTest < ActiveSupport::TestCase
FIXTURE_PATH = File.dirname(__FILE__) + '/../fixtures/multipart'
def test_single_parameter
params = parse_multipart('single_parameter')
assert_equal({ 'foo' => 'bar' }, params)
end
def test_bracketed_param
assert_equal({ 'foo' => { 'baz' => 'bar'}}, parse_multipart('bracketed_param'))
end
def test_text_file
params = parse_multipart('text_file')
assert_equal %w(file foo), params.keys.sort
assert_equal 'bar', params['foo']
file = params['file']
assert_kind_of StringIO, file
assert_equal 'file.txt', file.original_filename
assert_equal "text/plain", file.content_type
assert_equal 'contents', file.read
end
def test_boundary_problem_file
params = parse_multipart('boundary_problem_file')
assert_equal %w(file foo), params.keys.sort
file = params['file']
foo = params['foo']
assert_kind_of Tempfile, file
assert_equal 'file.txt', file.original_filename
assert_equal "text/plain", file.content_type
assert_equal 'bar', foo
end
def test_large_text_file
params = parse_multipart('large_text_file')
assert_equal %w(file foo), params.keys.sort
assert_equal 'bar', params['foo']
file = params['file']
assert_kind_of Tempfile, file
assert_equal 'file.txt', file.original_filename
assert_equal "text/plain", file.content_type
assert ('a' * 20480) == file.read
end
uses_mocha "test_no_rewind_stream" do
def test_no_rewind_stream
# Ensures that parse_multipart_form_parameters works with streams that cannot be rewound
file = File.open(File.join(FIXTURE_PATH, 'large_text_file'), 'rb')
file.expects(:rewind).raises(Errno::ESPIPE)
params = ActionController::RequestParser.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {})
assert_not_equal 0, file.pos # file was not rewound after reading
end
end
def test_binary_file
params = parse_multipart('binary_file')
assert_equal %w(file flowers foo), params.keys.sort
assert_equal 'bar', params['foo']
file = params['file']
assert_kind_of StringIO, file
assert_equal 'file.csv', file.original_filename
assert_nil file.content_type
assert_equal 'contents', file.read
file = params['flowers']
assert_kind_of StringIO, file
assert_equal 'flowers.jpg', file.original_filename
assert_equal "image/jpeg", file.content_type
assert_equal 19512, file.size
#assert_equal File.read(File.dirname(__FILE__) + '/../../../activerecord/test/fixtures/flowers.jpg'), file.read
end
def test_mixed_files
params = parse_multipart('mixed_files')
assert_equal %w(files foo), params.keys.sort
assert_equal 'bar', params['foo']
# Ruby CGI doesn't handle multipart/mixed for us.
files = params['files']
assert_kind_of String, files
files.force_encoding('ASCII-8BIT') if files.respond_to?(:force_encoding)
assert_equal 19756, files.size
end
private
def parse_multipart(name)
File.open(File.join(FIXTURE_PATH, name), 'rb') do |file|
params = ActionController::RequestParser.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {})
assert_equal 0, file.pos # file was rewound after reading
params
end
end
end

View File

@@ -1,5 +1,7 @@
*2.3.0/3.0*
* Support nested transactions using database savepoints. #383 [Jonathan Viney, Hongli Lai]
* Added dynamic scopes ala dynamic finders #1648 [Yaroslav Markin]
* Fixed that ActiveRecord::Base#new_record? should return false (not nil) for existing records #1219 [Yaroslav Markin]

View File

@@ -53,36 +53,120 @@ module ActiveRecord
def delete(sql, name = nil)
delete_sql(sql, name)
end
# Checks whether there is currently no transaction active. This is done
# by querying the database driver, and does not use the transaction
# house-keeping information recorded by #increment_open_transactions and
# friends.
#
# Returns true if there is no transaction active, false if there is a
# transaction active, and nil if this information is unknown.
#
# Not all adapters supports transaction state introspection. Currently,
# only the PostgreSQL adapter supports this.
def outside_transaction?
nil
end
# Runs the given block in a database transaction, and returns the result
# of the block.
#
# == Nested transactions support
#
# Most databases don't support true nested transactions. At the time of
# writing, the only database that supports true nested transactions that
# we're aware of, is MS-SQL.
#
# In order to get around this problem, #transaction will emulate the effect
# of nested transactions, by using savepoints:
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
# Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
#
# It is safe to call this method if a database transaction is already open,
# i.e. if #transaction is called within another #transaction block. In case
# of a nested call, #transaction will behave as follows:
#
# - The block will be run without doing anything. All database statements
# that happen within the block are effectively appended to the already
# open database transaction.
# - However, if +requires_new+ is set, the block will be wrapped in a
# database savepoint acting as a sub-transaction.
#
# === Caveats
#
# MySQL doesn't support DDL transactions. If you perform a DDL operation,
# then any created savepoints will be automatically released. For example,
# if you've created a savepoint, then you execute a CREATE TABLE statement,
# then the savepoint that was created will be automatically released.
#
# This means that, on MySQL, you shouldn't execute DDL operations inside
# a #transaction call that you know might create a savepoint. Otherwise,
# #transaction will raise exceptions when it tries to release the
# already-automatically-released savepoints:
#
# Model.connection.transaction do # BEGIN
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...)
# # active_record_1 now automatically released
# end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error!
# end
def transaction(options = {})
options.assert_valid_keys :requires_new, :joinable
last_transaction_joinable, @transaction_joinable =
@transaction_joinable, options[:joinable] || true
requires_new = options[:requires_new] || !last_transaction_joinable
# Wrap a block in a transaction. Returns result of block.
def transaction(start_db_transaction = true)
transaction_open = false
begin
if block_given?
if start_db_transaction
begin_db_transaction
if requires_new || open_transactions == 0
if open_transactions == 0
begin_db_transaction
elsif requires_new
create_savepoint
end
increment_open_transactions
transaction_open = true
end
yield
end
rescue Exception => database_transaction_rollback
if transaction_open
if transaction_open && !outside_transaction?
transaction_open = false
rollback_db_transaction
decrement_open_transactions
if open_transactions == 0
rollback_db_transaction
else
rollback_to_savepoint
end
end
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
end
ensure
if transaction_open
@transaction_joinable = last_transaction_joinable
if outside_transaction?
@open_transactions = 0
elsif transaction_open
decrement_open_transactions
begin
commit_db_transaction
if open_transactions == 0
commit_db_transaction
else
release_savepoint
end
rescue Exception => database_transaction_rollback
rollback_db_transaction
if open_transactions == 0
rollback_db_transaction
else
rollback_to_savepoint
end
raise
end
end
end
# Begins the transaction (and turns off auto-committing).
def begin_db_transaction() end

View File

@@ -66,6 +66,12 @@ module ActiveRecord
def supports_ddl_transactions?
false
end
# Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite
# does not.
def supports_savepoints?
false
end
# Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value
@@ -160,6 +166,23 @@ module ActiveRecord
@open_transactions -= 1
end
def transaction_joinable=(joinable)
@transaction_joinable = joinable
end
def create_savepoint
end
def rollback_to_savepoint
end
def release_savepoint
end
def current_savepoint_name
"active_record_#{open_transactions}"
end
def log_info(sql, name, ms)
if @logger && @logger.debug?
name = '%s (%.1fms)' % [name || 'SQL', ms]

View File

@@ -210,6 +210,10 @@ module ActiveRecord
def supports_migrations? #:nodoc:
true
end
def supports_savepoints? #:nodoc:
true
end
def native_database_types #:nodoc:
NATIVE_DATABASE_TYPES
@@ -349,6 +353,17 @@ module ActiveRecord
# Transactions aren't supported
end
def create_savepoint
execute("SAVEPOINT #{current_savepoint_name}")
end
def rollback_to_savepoint
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
end
def release_savepoint
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end
def add_limit_offset!(sql, options) #:nodoc:
if limit = options[:limit]

View File

@@ -272,6 +272,10 @@ module ActiveRecord
def supports_ddl_transactions?
true
end
def supports_savepoints?
true
end
# Returns the configured supported identifier length supported by PostgreSQL,
# or report the default of 63 on PostgreSQL 7.x.
@@ -528,45 +532,28 @@ module ActiveRecord
def rollback_db_transaction
execute "ROLLBACK"
end
# ruby-pg defines Ruby constants for transaction status,
# ruby-postgres does not.
PQTRANS_IDLE = defined?(PGconn::PQTRANS_IDLE) ? PGconn::PQTRANS_IDLE : 0
# Check whether a transaction is active.
def transaction_active?
@connection.transaction_status != PQTRANS_IDLE
end
# Wrap a block in a transaction. Returns result of block.
def transaction(start_db_transaction = true)
transaction_open = false
begin
if block_given?
if start_db_transaction
begin_db_transaction
transaction_open = true
end
yield
end
rescue Exception => database_transaction_rollback
if transaction_open && transaction_active?
transaction_open = false
rollback_db_transaction
end
raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
end
ensure
if transaction_open && transaction_active?
begin
commit_db_transaction
rescue Exception => database_transaction_rollback
rollback_db_transaction
raise
end
if PGconn.public_method_defined?(:transaction_status)
# ruby-pg defines Ruby constants for transaction status,
# ruby-postgres does not.
PQTRANS_IDLE = defined?(PGconn::PQTRANS_IDLE) ? PGconn::PQTRANS_IDLE : 0
def outside_transaction?
@connection.transaction_status == PQTRANS_IDLE
end
end
def create_savepoint
execute("SAVEPOINT #{current_savepoint_name}")
end
def rollback_to_savepoint
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
end
def release_savepoint
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end
# SCHEMA STATEMENTS ========================================

View File

@@ -516,7 +516,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
all_loaded_fixtures.update(fixtures_map)
connection.transaction(connection.open_transactions.zero?) do
connection.transaction(:requires_new => true) do
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
fixtures.each { |fixture| fixture.insert_fixtures }
@@ -937,6 +937,7 @@ module ActiveRecord
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
ActiveRecord::Base.connection.increment_open_transactions
ActiveRecord::Base.connection.transaction_joinable = false
ActiveRecord::Base.connection.begin_db_transaction
# Load fixtures for every test.
else

View File

@@ -120,16 +120,66 @@ module ActiveRecord
# end
#
# One should restart the entire transaction if a StatementError occurred.
#
# == Nested transactions
#
# #transaction calls can be nested. By default, this makes all database
# statements in the nested transaction block become part of the parent
# transaction. For example:
#
# User.transaction do
# User.create(:username => 'Kotori')
# User.transaction do
# User.create(:username => 'Nemu')
# raise ActiveRecord::Rollback
# end
# end
#
# User.find(:all) # => empty
#
# It is also possible to requires a sub-transaction by passing
# <tt>:requires_new => true</tt>. If anything goes wrong, the
# database rolls back to the beginning of the sub-transaction
# without rolling back the parent transaction. For example:
#
# User.transaction do
# User.create(:username => 'Kotori')
# User.transaction(:requires_new => true) do
# User.create(:username => 'Nemu')
# raise ActiveRecord::Rollback
# end
# end
#
# User.find(:all) # => Returns only Kotori
#
# Most databases don't support true nested transactions. At the time of
# writing, the only database that we're aware of that supports true nested
# transactions, is MS-SQL. Because of this, Active Record emulates nested
# transactions by using savepoints. See
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
# for more information about savepoints.
#
# === Caveats
#
# If you're on MySQL, then do not use DDL operations in nested transactions
# blocks that are emulated with savepoints. That is, do not execute statements
# like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
# releases all savepoints upon executing a DDL operation. When #transaction
# is finished and tries to release the savepoint it created earlier, a
# database error will occur because the savepoint has already been
# automatically released. The following example demonstrates the problem:
#
# Model.connection.transaction do # BEGIN
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...) # active_record_1 now automatically released
# end # RELEASE savepoint active_record_1
# # ^^^^ BOOM! database error!
# end
module ClassMethods
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
def transaction(&block)
connection.increment_open_transactions
begin
connection.transaction(connection.open_transactions == 1, &block)
ensure
connection.decrement_open_transactions
end
def transaction(options = {}, &block)
# See the ConnectionAdapters::DatabaseStatements#transaction API docs.
connection.transaction(options, &block)
end
end

View File

@@ -18,11 +18,43 @@ class DefaultTest < ActiveRecord::TestCase
end
end
if current_adapter?(:MysqlAdapter)
if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
def test_default_integers
default = Default.new
assert_instance_of Fixnum, default.positive_integer
assert_equal 1, default.positive_integer
assert_instance_of Fixnum, default.negative_integer
assert_equal -1, default.negative_integer
assert_instance_of BigDecimal, default.decimal_number
assert_equal BigDecimal.new("2.78"), default.decimal_number
end
end
if current_adapter?(:PostgreSQLAdapter)
def test_multiline_default_text
# older postgres versions represent the default with escapes ("\\012" for a newline)
assert ( "--- []\n\n" == Default.columns_hash['multiline_default'].default ||
"--- []\\012\\012" == Default.columns_hash['multiline_default'].default)
end
end
end
#MySQL 5 and higher is quirky with not null text/blob columns.
#With MySQL Text/blob columns cannot have defaults. If the column is not null MySQL will report that the column has a null default
#but it behaves as though the column had a default of ''
if current_adapter?(:MysqlAdapter)
class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase
# ActiveRecord::Base#create! (and #save and other related methods) will
# open a new transaction. When in transactional fixtures mode, this will
# cause ActiveRecord to create a new savepoint. However, since MySQL doesn't
# support DDL transactions, creating a table will result in any created
# savepoints to be automatically released. This in turn causes the savepoint
# release code in AbstractAdapter#transaction to fail.
#
# We don't want that to happen, so we disable transactional fixtures here.
self.use_transactional_fixtures = false
# MySQL 5 and higher is quirky with not null text/blob columns.
# With MySQL Text/blob columns cannot have defaults. If the column is not
# null MySQL will report that the column has a null default
# but it behaves as though the column had a default of ''
def test_mysql_text_not_null_defaults
klass = Class.new(ActiveRecord::Base)
klass.table_name = 'test_mysql_text_not_null_defaults'
@@ -48,8 +80,7 @@ class DefaultTest < ActiveRecord::TestCase
ensure
klass.connection.drop_table(klass.table_name) rescue nil
end
# MySQL uses an implicit default 0 rather than NULL unless in strict mode.
# We use an implicit NULL so schema.rb is compatible with other databases.
def test_mysql_integer_not_null_defaults
@@ -77,24 +108,4 @@ class DefaultTest < ActiveRecord::TestCase
klass.connection.drop_table(klass.table_name) rescue nil
end
end
if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
def test_default_integers
default = Default.new
assert_instance_of Fixnum, default.positive_integer
assert_equal 1, default.positive_integer
assert_instance_of Fixnum, default.negative_integer
assert_equal -1, default.negative_integer
assert_instance_of BigDecimal, default.decimal_number
assert_equal BigDecimal.new("2.78"), default.decimal_number
end
end
if current_adapter?(:PostgreSQLAdapter)
def test_multiline_default_text
# older postgres versions represent the default with escapes ("\\012" for a newline)
assert ( "--- []\n\n" == Default.columns_hash['multiline_default'].default ||
"--- []\\012\\012" == Default.columns_hash['multiline_default'].default)
end
end
end

View File

@@ -34,7 +34,7 @@ rescue LoadError
end
ActiveRecord::Base.connection.class.class_eval do
IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/]
IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/]
def execute_with_query_record(sql, name = nil, &block)
$queries_executed ||= []

View File

@@ -213,11 +213,104 @@ class TransactionTest < ActiveRecord::TestCase
assert Topic.find(2).approved?, "Second should still be approved"
end
def test_invalid_keys_for_transaction
assert_raises ArgumentError do
Topic.transaction :nested => true do
end
end
end
def test_force_savepoint_in_nested_transaction
Topic.transaction do
@first.approved = true
@second.approved = false
@first.save!
@second.save!
begin
Topic.transaction :requires_new => true do
@first.happy = false
@first.save!
raise
end
rescue
end
end
assert @first.reload.approved?
assert !@second.reload.approved?
end if Topic.connection.supports_savepoints?
def test_no_savepoint_in_nested_transaction_without_force
Topic.transaction do
@first.approved = true
@second.approved = false
@first.save!
@second.save!
begin
Topic.transaction do
@first.approved = false
@first.save!
raise
end
rescue
end
end
assert !@first.reload.approved?
assert !@second.reload.approved?
end if Topic.connection.supports_savepoints?
def test_many_savepoints
Topic.transaction do
@first.content = "One"
@first.save!
begin
Topic.transaction :requires_new => true do
@first.content = "Two"
@first.save!
begin
Topic.transaction :requires_new => true do
@first.content = "Three"
@first.save!
begin
Topic.transaction :requires_new => true do
@first.content = "Four"
@first.save!
raise
end
rescue
end
@three = @first.reload.content
raise
end
rescue
end
@two = @first.reload.content
raise
end
rescue
end
@one = @first.reload.content
end
assert_equal "One", @one
assert_equal "Two", @two
assert_equal "Three", @three
end if Topic.connection.supports_savepoints?
uses_mocha 'mocking connection.commit_db_transaction' do
def test_rollback_when_commit_raises
Topic.connection.expects(:begin_db_transaction)
Topic.connection.expects(:transaction_active?).returns(true) if current_adapter?(:PostgreSQLAdapter)
Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
Topic.connection.expects(:outside_transaction?).returns(false)
Topic.connection.expects(:rollback_db_transaction)
assert_raise RuntimeError do
@@ -227,6 +320,39 @@ class TransactionTest < ActiveRecord::TestCase
end
end
end
if current_adapter?(:PostgreSQLAdapter) && PGconn.public_method_defined?(:transaction_status)
def test_outside_transaction_works
Topic.logger.info("-------------")
assert Topic.connection.outside_transaction?
Topic.connection.begin_db_transaction
assert !Topic.connection.outside_transaction?
Topic.connection.rollback_db_transaction
assert Topic.connection.outside_transaction?
end
uses_mocha 'mocking connection.rollback_db_transaction' do
def test_rollback_wont_be_executed_if_no_transaction_active
assert_raise RuntimeError do
Topic.transaction do
Topic.connection.rollback_db_transaction
Topic.connection.expects(:rollback_db_transaction).never
raise "Rails doesn't scale!"
end
end
end
end
def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active
Topic.transaction do
Topic.transaction do
Topic.connection.rollback_db_transaction
end
assert_equal 0, Topic.connection.open_transactions
end
assert_equal 0, Topic.connection.open_transactions
end
end
def test_sqlite_add_column_in_transaction_raises_statement_invalid
return true unless current_adapter?(:SQLite3Adapter, :SQLiteAdapter)
@@ -282,6 +408,45 @@ class TransactionTest < ActiveRecord::TestCase
end
end
class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
self.use_transactional_fixtures = true
fixtures :topics
def test_automatic_savepoint_in_outer_transaction
@first = Topic.find(1)
begin
Topic.transaction do
@first.approved = true
@first.save!
raise
end
rescue
assert !@first.reload.approved?
end
end
def test_no_automatic_savepoint_for_inner_transaction
@first = Topic.find(1)
Topic.transaction do
@first.approved = true
@first.save!
begin
Topic.transaction do
@first.approved = false
@first.save!
raise
end
rescue
end
end
assert !@first.reload.approved?
end
end if Topic.connection.supports_savepoints?
if current_adapter?(:PostgreSQLAdapter)
class ConcurrentTransactionTest < TransactionTest
use_concurrent_connections

View File

@@ -1,5 +1,7 @@
*2.3.0 [Edge]*
* TimeWithZone#xmlschema accepts optional fraction_digits argument [#1725 state:resolved] [Nicholas Dainty]
* Object#tap shim for Ruby < 1.8.7. Similar to Object#returning, tap yields self then returns self. [Jeremy Kemper]
array.select { ... }.tap(&:inspect).map { ... }

View File

@@ -99,8 +99,12 @@ module ActiveSupport
"#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}"
end
def xmlschema
"#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{formatted_offset(true, 'Z')}"
def xmlschema(fraction_digits = 0)
fraction = if fraction_digits > 0
".%i" % time.usec.to_s[0, fraction_digits]
end
"#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}"
end
alias_method :iso8601, :xmlschema

View File

@@ -105,6 +105,15 @@ class TimeWithZoneTest < Test::Unit::TestCase
end
end
def test_xmlschema_with_fractional_seconds
silence_warnings do # silence warnings raised by tzinfo gem
@twz += 0.123456 # advance the time by a fraction of a second
assert_equal "1999-12-31T19:00:00.123-05:00", @twz.xmlschema(3)
assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(6)
assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(12)
end
end
def test_to_yaml
silence_warnings do # silence warnings raised by tzinfo gem
assert_equal "--- 1999-12-31 19:00:00 -05:00\n", @twz.to_yaml