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

This commit is contained in:
Xavier Noria
2011-08-31 04:27:59 -07:00
177 changed files with 2501 additions and 1826 deletions

1
.gitignore vendored
View File

@@ -21,4 +21,5 @@ railties/doc
railties/guides/output
railties/tmp
.rvmrc
.rbenv-version
RDOC_MAIN.rdoc

View File

@@ -2,6 +2,7 @@ script: 'ci/travis.rb'
rvm:
- 1.8.7
- 1.9.2
- 1.9.3
env:
- "GEM=railties"
- "GEM=ap,am,amo,ares,as"
@@ -11,4 +12,5 @@ env:
notifications:
email: false
irc:
- "irc.freenode.org#rails-contrib"
- "irc.freenode.org#rails-contrib"
bundler_args: --path vendor/bundle

View File

@@ -11,12 +11,17 @@ gem "jquery-rails"
# it being automatically loaded by sprockets
gem "uglifier", ">= 1.0.0", :require => false
gem "rake", ">= 0.8.7"
# Temp fix until rake 0.9.3 is out
if RUBY_VERSION >= "1.9.3"
gem "rake", "0.9.3.beta.1"
else
gem "rake", ">= 0.8.7"
end
gem "mocha", ">= 0.9.8"
group :doc do
gem "rdoc", "~> 3.4"
gem "horo", "= 1.0.3"
gem "sdoc", "~> 0.3"
gem "RedCloth", "~> 4.2" if RUBY_VERSION < "1.9.3"
gem "w3c_validators"
end

View File

@@ -8,20 +8,20 @@ into three layers, each with a specific responsibility.
The View layer is composed of "templates" that are responsible for providing
appropriate representations of your application's resources. Templates
can come in a variety of formats, but most view templates are HTML with embedded Ruby
can come in a variety of formats, but most view templates are \HTML with embedded Ruby
code (.erb files).
The Model layer represents your domain model (such as Account, Product, Person, Post)
and encapsulates the business logic that is specific to your application. In Rails,
database-backed model classes are derived from ActiveRecord::Base. ActiveRecord allows
database-backed model classes are derived from ActiveRecord::Base. Active Record allows
you to present the data from database rows as objects and embellish these data objects
with business logic methods. Although most Rails models are backed by a database, models
can also be ordinary Ruby classes, or Ruby classes that implement a set of interfaces as
provided by the ActiveModel module. You can read more about Active Record in its
{README}[link:blob/master/activerecord/README.rdoc].
{README}[link:/rails/rails/blob/master/activerecord/README.rdoc].
The Controller layer is responsible for handling incoming HTTP requests and providing a
suitable response. Usually this means returning HTML, but Rails controllers can also
suitable response. Usually this means returning \HTML, but Rails controllers can also
generate XML, JSON, PDFs, mobile-specific views, and more. Controllers manipulate models
and render view templates in order to generate the appropriate HTTP response.
@@ -29,7 +29,7 @@ In Rails, the Controller and View layers are handled together by Action Pack.
These two layers are bundled in a single package due to their heavy interdependence.
This is unlike the relationship between Active Record and Action Pack which are
independent. Each of these packages can be used independently outside of Rails. You
can read more about Action Pack in its {README}[link:blob/master/actionpack/README.rdoc].
can read more about Action Pack in its {README}[link:/rails/rails/blob/master/actionpack/README.rdoc].
== Getting Started
@@ -62,7 +62,7 @@ can read more about Action Pack in its {README}[link:blob/master/actionpack/READ
* The {API Documentation}[http://api.rubyonrails.org].
== Contributing
== Contributing http://travis-ci.org/rails/rails.png
We encourage you to contribute to Ruby on Rails! Please check out the {Contributing to Rails
guide}[http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html] for guidelines about how

View File

@@ -145,18 +145,25 @@ commits should be added to the release branch besides regression fixing commits.
Many of these steps are the same as for the release candidate, so if you need
more explanation on a particular step, so the RC steps.
=== Email the rails security announce list, once for each vulnerability fixed.
You can do this, or ask the security team to do it.
FIXME: I can't remember the email addresses, but we should list them here.
FIXME: Possibly we should do this the day of the RC?
Today, do this stuff in this order:
* Apply security patches to the release branch
* Update CHANGELOG with security fixes.
* Update RAILS_VERSION to remove the rc
* Release the gems
* Email announcement
* Email security lists
* Email general announcement lists
=== Emailing the rails security announce list
Email the security announce list once for each vulnerability fixed.
You can do this, or ask the security team to do it.
Email the security reports to:
* rubyonrails-security@googlegroups.com
* linux-distros@vs.openwall.org
Be sure to note the security fixes in your announcement along with CVE numbers
and links to each patch. Some people may not be able to upgrade right away,

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env rake
require 'rdoc/task'
require 'sdoc'
require 'net/http'
$:.unshift File.expand_path('..', __FILE__)
@@ -74,7 +75,10 @@ RDoc::Task.new do |rdoc|
# since no autolinking happens there and RDoc displays the backslash
# otherwise.
rdoc_main.gsub!(/^(?=\S).*?\b(?=Rails)\b/) { "#$&\\" }
rdoc_main.gsub!(%r{link:blob/master/(\w+)/README\.rdoc}, "link:files/\\1/README_rdoc.html")
rdoc_main.gsub!(%r{link:/rails/rails/blob/master/(\w+)/README\.rdoc}, "link:files/\\1/README_rdoc.html")
# Remove Travis build status image from API pages. Only GitHub README page gets this image
rdoc_main.gsub!("http://travis-ci.org/rails/rails.png", "")
File.open(RDOC_MAIN, 'w') do |f|
f.write(rdoc_main)
@@ -86,8 +90,10 @@ RDoc::Task.new do |rdoc|
rdoc.rdoc_dir = 'doc/rdoc'
rdoc.title = "Ruby on Rails Documentation"
rdoc.options << '-f' << 'horo'
rdoc.options << '-f' << 'sdoc'
rdoc.options << '-T' << 'rails'
rdoc.options << '-c' << 'utf-8'
rdoc.options << '-g' # SDoc flag, link methods to GitHub
rdoc.options << '-m' << RDOC_MAIN
rdoc.rdoc_files.include('railties/CHANGELOG')

View File

@@ -30,6 +30,10 @@
*Rails 3.1.0 (unreleased)*
* Param values are `paramified` in controller tests. [David Chelimsky]
* x_sendfile_header now defaults to nil and config/environments/production.rb doesn't set a particular value for it. This allows servers to set it through X-Sendfile-Type. [Santiago Pastorino]
* The submit form helper does not generate an id "object_name_id" anymore. [fbrusatti]
* Make sure respond_with with :js tries to render a template in all cases [José Valim]

View File

@@ -18,13 +18,13 @@ Gem::Specification.new do |s|
s.add_dependency('activesupport', version)
s.add_dependency('activemodel', version)
s.add_dependency('rack-cache', '~> 1.0.2')
s.add_dependency('rack-cache', '~> 1.0.3')
s.add_dependency('builder', '~> 3.0.0')
s.add_dependency('i18n', '~> 0.6')
s.add_dependency('rack', '~> 1.3.2')
s.add_dependency('rack-test', '~> 0.6.0')
s.add_dependency('rack-mount', '~> 0.8.1')
s.add_dependency('sprockets', '~> 2.0.0.beta.12')
s.add_dependency('rack-test', '~> 0.6.1')
s.add_dependency('rack-mount', '~> 0.8.2')
s.add_dependency('sprockets', '~> 2.0.0')
s.add_dependency('erubis', '~> 2.7.0')
s.add_development_dependency('tzinfo', '~> 0.3.29')

View File

@@ -1,3 +1,5 @@
require 'action_view/base'
module AbstractController
module ViewPaths
extend ActiveSupport::Concern
@@ -63,7 +65,7 @@ module AbstractController
# the default view path. You may also provide a custom view path
# (see ActionView::PathSet for more information)
def append_view_path(path)
self.view_paths = view_paths.dup + Array(path)
self._view_paths = view_paths + Array(path)
end
# Prepend a path to the list of view paths for this controller.
@@ -73,7 +75,7 @@ module AbstractController
# the default view path. You may also provide a custom view path
# (see ActionView::PathSet for more information)
def prepend_view_path(path)
self.view_paths = Array(path) + view_paths.dup
self._view_paths = ActionView::PathSet.new(Array(path) + view_paths)
end
# A list of all of the default view paths for this controller.
@@ -87,8 +89,7 @@ module AbstractController
# * <tt>paths</tt> - If a PathSet is provided, use that;
# otherwise, process the parameter into a PathSet.
def view_paths=(paths)
self._view_paths = ActionView::Base.process_view_paths(paths)
self._view_paths.freeze
self._view_paths = ActionView::PathSet.new(Array.wrap(paths))
end
end
end

View File

@@ -63,7 +63,7 @@ module ActionController
#
# == Sessions
#
# Sessions allows you to store objects in between requests. This is useful for objects that are not yet ready to be persisted,
# Sessions allow you to store objects in between requests. This is useful for objects that are not yet ready to be persisted,
# such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such
# as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely
# they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at.

View File

@@ -17,7 +17,7 @@ module ActionController #:nodoc:
protected
# Sends the file. This uses a server-appropriate method (such as X-Sendfile)
# via the Rack::Sendfile middleware. The header to use is set via
# config.action_dispatch.x_sendfile_header, and defaults to "X-Sendfile".
# config.action_dispatch.x_sendfile_header.
# Your server can also configure this for you by setting the X-Sendfile-Type header.
#
# Be careful to sanitize the path parameter if it is coming from a web

View File

@@ -58,8 +58,8 @@ module ActionController
def redirect_to(*args)
ActiveSupport::Notifications.instrument("redirect_to.action_controller") do |payload|
result = super
payload[:status] = self.status
payload[:location] = self.location
payload[:status] = response.status
payload[:location] = response.location
result
end
end
@@ -97,4 +97,4 @@ module ActionController
end
end
end
end
end

View File

@@ -6,31 +6,30 @@ require 'active_support/core_ext/module/anonymous'
require 'action_dispatch/http/mime_types'
module ActionController
# Wraps parameters hash into nested hash. This will allow client to submit
# POST request without having to specify a root element in it.
# Wraps the parameters hash into a nested hash. This will allow clients to submit
# POST requests without having to specify any root elements.
#
# By default this functionality won't be enabled. You can enable
# it globally by setting +ActionController::Base.wrap_parameters+:
#
# ActionController::Base.wrap_parameters = [:json]
# This functionality is enabled in +config/initializers/wrap_parameters.rb+
# and can be customized. If you are upgrading to \Rails 3.1, this file will
# need to be created for the functionality to be enabled.
#
# You could also turn it on per controller by setting the format array to
# non-empty array:
# a non-empty array:
#
# class UsersController < ApplicationController
# wrap_parameters :format => [:json, :xml]
# end
#
# If you enable +ParamsWrapper+ for +:json+ format. Instead of having to
# If you enable +ParamsWrapper+ for +:json+ format, instead of having to
# send JSON parameters like this:
#
# {"user": {"name": "Konata"}}
#
# You can now just send a parameters like this:
# You can send parameters like this:
#
# {"name": "Konata"}
#
# And it will be wrapped into a nested hash with the key name matching
# And it will be wrapped into a nested hash with the key name matching the
# controller's name. For example, if you're posting to +UsersController+,
# your new +params+ hash will look like this:
#
@@ -82,7 +81,7 @@ module ActionController
#
# ==== Examples
# wrap_parameters :format => :xml
# # enables the parmeter wrapper for XML format
# # enables the parameter wrapper for XML format
#
# wrap_parameters :person
# # wraps parameters into +params[:person]+ hash

View File

@@ -45,7 +45,7 @@ module ActionController
# integer, or a symbol representing the downcased, underscored and symbolized description.
# Note that the status code must be a 3xx HTTP code, or redirection will not occur.
#
# It is also possible to assign a flash message as part of the redirection. There are two special accessors for commonly used the flash names
# It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names
# +alert+ and +notice+ as well as a general purpose +flash+ bucket.
#
# Examples:

View File

@@ -18,7 +18,7 @@
# @url = root_path # named route from the application.
# end
# end
# =>
#
module ActionController
module UrlFor
extend ActiveSupport::Concern

View File

@@ -180,7 +180,7 @@ module ActionController
@env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
@symbolized_path_params = nil
@method = @request_method = nil
@fullpath = @ip = @remote_ip = nil
@fullpath = @ip = @remote_ip = @protocol = nil
@env['action_dispatch.request.query_parameters'] = {}
@set_cookies ||= {}
@set_cookies.update(Hash[cookie_jar.instance_variable_get("@set_cookies").map{ |k,o| [k,o[:value]] }])
@@ -401,9 +401,7 @@ module ActionController
def paramify_values(hash_or_array_or_value)
case hash_or_array_or_value
when Hash
hash_or_array_or_value.each do |key, value|
hash_or_array_or_value[key] = paramify_values(value)
end
Hash[hash_or_array_or_value.map{|key, value| [key, paramify_values(value)] }]
when Array
hash_or_array_or_value.map {|i| paramify_values(i)}
when Rack::Test::UploadedFile
@@ -416,7 +414,7 @@ module ActionController
def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET')
# Ensure that numbers and symbols passed as params are converted to
# proper params, as is the case when engaging rack.
paramify_values(parameters)
parameters = paramify_values(parameters)
# Sanity check for required instance variables so we can give an
# understandable error message.
@@ -450,7 +448,7 @@ module ActionController
@controller.params.merge!(parameters)
build_request_uri(action, parameters)
@controller.class.class_eval { include Testing }
@controller.recycle!
@controller.recycle!
@controller.process_with_new_base_test(@request, @response)
@assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {}
@request.session.delete('flash') if @request.session['flash'].blank?

View File

@@ -156,7 +156,7 @@ module HTML #:nodoc:
end
closing = ( scanner.scan(/\//) ? :close : nil )
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:-]+/)
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[^\s!>\/]+/)
name.downcase!
unless closing

View File

@@ -11,24 +11,13 @@ module ActionDispatch
raise(ArgumentError, ':tempfile is required') unless @tempfile
end
def open
@tempfile.open
end
def path
@tempfile.path
end
def read(*args)
@tempfile.read(*args)
end
def rewind
@tempfile.rewind
end
def size
@tempfile.size
# Delegate these methods to the tempfile.
[:open, :path, :rewind, :size].each do |method|
class_eval "def #{method}; @tempfile.#{method}; end"
end
private

View File

@@ -1,10 +1,9 @@
require "action_dispatch"
require "rails"
module ActionDispatch
class Railtie < Rails::Railtie
config.action_dispatch = ActiveSupport::OrderedOptions.new
config.action_dispatch.x_sendfile_header = ""
config.action_dispatch.x_sendfile_header = nil
config.action_dispatch.ip_spoofing_check = true
config.action_dispatch.show_exceptions = true
config.action_dispatch.best_standards_support = true

View File

@@ -131,16 +131,20 @@ module ActionDispatch
#
# Examples:
#
# url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :port => '8080' # => 'http://somehost.org:8080/tasks/testing'
# url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok'
# url_for :controller => 'tasks', :action => 'testing', :trailing_slash => true # => 'http://somehost.org/tasks/testing/'
# url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33'
# url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :port => '8080'
# # => 'http://somehost.org:8080/tasks/testing'
# url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :anchor => 'ok', :only_path => true
# # => '/tasks/testing#ok'
# url_for :controller => 'tasks', :action => 'testing', :trailing_slash => true
# # => 'http://somehost.org/tasks/testing/'
# url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :number => '33'
# # => 'http://somehost.org/tasks/testing?number=33'
def url_for(options = nil)
case options
when String
options
when nil, Hash
_routes.url_for((options || {}).reverse_merge!(url_options).symbolize_keys)
_routes.url_for((options || {}).reverse_merge(url_options).symbolize_keys)
else
polymorphic_url(options)
end

View File

@@ -4,6 +4,7 @@ require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/array/wrap'
require 'active_support/ordered_options'
require 'action_view/log_subscriber'
require 'active_support/core_ext/module/deprecation'
module ActionView #:nodoc:
# = Action View Base
@@ -161,6 +162,7 @@ module ActionView #:nodoc:
value.is_a?(PathSet) ?
value.dup : ActionView::PathSet.new(Array.wrap(value))
end
deprecate :process_view_paths
def xss_safe? #:nodoc:
true

View File

@@ -56,8 +56,8 @@ module ActionView
# form_tag('http://far.away.com/form', :authenticity_token => "cf50faa3fe97702ca1ae")
# # form with custom authenticity token
#
def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block)
html_options = html_options_for_form(url_for_options, options, *parameters_for_url)
def form_tag(url_for_options = {}, options = {}, &block)
html_options = html_options_for_form(url_for_options, options)
if block_given?
form_tag_in_block(html_options, &block)
else
@@ -177,9 +177,12 @@ module ActionView
# label_tag 'name', nil, :class => 'small_label'
# # => <label for="name" class="small_label">Name</label>
def label_tag(name = nil, content_or_options = nil, options = nil, &block)
options = content_or_options if block_given? && content_or_options.is_a?(Hash)
options ||= {}
options.stringify_keys!
if block_given? && content_or_options.is_a?(Hash)
options = content_or_options = content_or_options.stringify_keys
else
options ||= {}
options = options.stringify_keys
end
options["for"] = sanitize_to_id(name) unless name.blank? || options.has_key?("for")
content_tag :label, content_or_options || name.to_s.humanize, options, &block
end
@@ -604,12 +607,12 @@ module ActionView
end
private
def html_options_for_form(url_for_options, options, *parameters_for_url)
def html_options_for_form(url_for_options, options)
options.stringify_keys.tap do |html_options|
html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
# The following URL is unescaped, this is just a hash of options, and it is the
# responsibility of the caller to escape all the values.
html_options["action"] = url_for(url_for_options, *parameters_for_url)
html_options["action"] = url_for(url_for_options)
html_options["accept-charset"] = "UTF-8"
html_options["data-remote"] = true if html_options.delete("remote")
html_options["authenticity_token"] = html_options.delete("authenticity_token") if html_options.has_key?("authenticity_token")

View File

@@ -1,4 +1,5 @@
require 'action_view/helpers/tag_helper'
require 'active_support/core_ext/string/encoding'
module ActionView
module Helpers
@@ -10,15 +11,23 @@ module ActionView
"\n" => '\n',
"\r" => '\n',
'"' => '\\"',
"'" => "\\'" }
"'" => "\\'"
}
# Escape carrier returns and single and double quotes for JavaScript segments.
if "ruby".encoding_aware?
JS_ESCAPE_MAP["\342\200\250".force_encoding('UTF-8').encode!] = '&#x2028;'
else
JS_ESCAPE_MAP["\342\200\250"] = '&#x2028;'
end
# Escapes carriage returns and single and double quotes for JavaScript segments.
#
# Also available through the alias j(). This is particularly helpful in JavaScript responses, like:
#
# $('some_element').replaceWith('<%=j render 'some/element_template' %>');
def escape_javascript(javascript)
if javascript
result = javascript.gsub(/(\\|<\/|\r\n|[\n\r"'])/) {|match| JS_ESCAPE_MAP[match] }
result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] }
javascript.html_safe? ? result.html_safe : result
else
''

View File

@@ -268,7 +268,7 @@ module ActionView
# to change the HTTP verb used to submit the form.
#
# ==== Options
# The +options+ hash accepts the same options as url_for.
# The +options+ hash accepts the same options as +url_for+.
#
# There are a few special +html_options+:
# * <tt>:method</tt> - Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,

View File

@@ -78,7 +78,7 @@ module ActionView
# Whenever setting view paths, makes a copy so we can manipulate then in
# instance objects as we wish.
def view_paths=(paths)
@view_paths = ActionView::Base.process_view_paths(paths)
@view_paths = ActionView::PathSet.new(Array.wrap(paths))
end
def find(name, prefixes = [], partial = false, keys = [])

View File

@@ -1,11 +1,55 @@
module ActionView #:nodoc:
# = Action View PathSet
class PathSet < Array #:nodoc:
%w(initialize << concat insert push unshift).each do |method|
class PathSet #:nodoc:
include Enumerable
attr_reader :paths
def initialize(paths = [])
@paths = typecast paths
end
def initialize_copy(other)
@paths = other.paths.dup
self
end
def [](i)
paths[i]
end
def to_ary
paths.dup
end
def include?(item)
paths.include? item
end
def pop
paths.pop
end
def size
paths.size
end
def each(&block)
paths.each(&block)
end
def compact
PathSet.new paths.compact
end
def +(array)
PathSet.new(paths + array)
end
%w(<< concat push insert unshift).each do |method|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{method}(*args)
super
typecast!
paths.#{method}(*typecast(args))
end
METHOD
end
@@ -17,7 +61,7 @@ module ActionView #:nodoc:
def find_all(path, prefixes = [], *args)
prefixes = [prefixes] if String === prefixes
prefixes.each do |prefix|
each do |resolver|
paths.each do |resolver|
templates = resolver.find_all(path, prefix, *args)
return templates unless templates.empty?
end
@@ -25,17 +69,20 @@ module ActionView #:nodoc:
[]
end
def exists?(*args)
find_all(*args).any?
def exists?(path, prefixes, *args)
find_all(path, prefixes, *args).any?
end
protected
private
def typecast!
each_with_index do |path, i|
path = path.to_s if path.is_a?(Pathname)
next unless path.is_a?(String)
self[i] = OptimizedFileSystemResolver.new(path)
def typecast(paths)
paths.map do |path|
case path
when Pathname, String
OptimizedFileSystemResolver.new path.to_s
else
path
end
end
end
end

View File

@@ -1,5 +1,6 @@
require "pathname"
require "active_support/core_ext/class"
require "active_support/core_ext/io"
require "action_view/template"
module ActionView
@@ -68,7 +69,7 @@ module ActionView
# before returning it.
def cached(key, path_info, details, locals) #:nodoc:
name, prefix, partial = path_info
locals = sort_locals(locals)
locals = locals.map { |x| x.to_s }.sort!
if key && caching?
@cached[key][name][prefix][partial][locals] ||= decorate(yield, path_info, details, locals)
@@ -97,18 +98,6 @@ module ActionView
t.virtual_path ||= (cached ||= build_path(*path_info))
end
end
if :symbol.respond_to?("<=>")
def sort_locals(locals) #:nodoc:
locals.sort.freeze
end
else
def sort_locals(locals) #:nodoc:
locals = locals.map{ |l| l.to_s }
locals.sort!
locals.freeze
end
end
end
# An abstract class that implements a Resolver with path semantics.
@@ -130,27 +119,35 @@ module ActionView
def query(path, details, formats)
query = build_query(path, details)
templates = []
sanitizer = Hash.new { |h,k| h[k] = Dir["#{File.dirname(k)}/*"] }
Dir[query].each do |p|
next if File.directory?(p) || !sanitizer[p].include?(p)
# deals with case-insensitive file systems.
sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] }
handler, format = extract_handler_and_format(p, formats)
contents = File.open(p, "rb") { |io| io.read }
template_paths = Dir[query].reject { |filename|
File.directory?(filename) ||
!sanitizer[File.dirname(filename)].include?(filename)
}
templates << Template.new(contents, File.expand_path(p), handler,
:virtual_path => path.virtual, :format => format, :updated_at => mtime(p))
end
template_paths.map { |template|
handler, format = extract_handler_and_format(template, formats)
contents = File.binread template
templates
Template.new(contents, File.expand_path(template), handler,
:virtual_path => path.virtual,
:format => format,
:updated_at => mtime(template))
}
end
# Helper for building query glob string based on resolver's pattern.
def build_query(path, details)
query = @pattern.dup
query.gsub!(/\:prefix(\/)?/, path.prefix.empty? ? "" : "#{path.prefix}\\1") # prefix can be empty...
query.gsub!(/\:action/, path.partial? ? "_#{path.name}" : path.name)
prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
query.gsub!(/\:prefix(\/)?/, prefix)
partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
query.gsub!(/\:action/, partial)
details.each do |ext, variants|
query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
@@ -159,6 +156,10 @@ module ActionView
File.expand_path(query, @path)
end
def escape_entry(entry)
entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
end
# Returns the file mtime from the filesystem.
def mtime(p)
File.mtime(p)
@@ -235,15 +236,11 @@ module ActionView
class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
def build_query(path, details)
exts = EXTENSIONS.map { |ext| details[ext] }
query = File.join(@path, path)
query = escape_entry(File.join(@path, path))
exts.each do |ext|
query << "{"
ext.compact.each { |e| query << ".#{e}," }
query << "}"
end
query
query + exts.map { |ext|
"{#{ext.compact.uniq.map { |e| ".#{e}," }.join}}"
}.join
end
end

View File

@@ -1,24 +1,55 @@
namespace :assets do
# Ensures the RAILS_GROUPS environment variable is set
task :ensure_env do
ENV["RAILS_GROUPS"] ||= "assets"
end
desc "Compile all the assets named in config.assets.precompile"
task :precompile => :ensure_env do
Rake::Task["environment"].invoke
Sprockets::Helpers::RailsHelper
task :precompile do
# We need to do this dance because RAILS_GROUPS is used
# too early in the boot process and changing here is already too late.
if ENV["RAILS_GROUPS"].to_s.empty? || ENV["RAILS_ENV"].to_s.empty?
ENV["RAILS_GROUPS"] ||= "assets"
ENV["RAILS_ENV"] ||= "production"
Kernel.exec $0, *ARGV
else
Rake::Task["environment"].invoke
assets = Rails.application.config.assets.precompile
# Always perform caching so that asset_path appends the timestamps to file references.
Rails.application.config.action_controller.perform_caching = true
Rails.application.assets.precompile(*assets)
# Ensure that action view is loaded and the appropriate sprockets hooks get executed
ActionView::Base
# Always perform caching so that asset_path appends the timestamps to file references.
Rails.application.config.action_controller.perform_caching = true
config = Rails.application.config
env = Rails.application.assets
target = Rails.root.join("public#{config.assets.prefix}")
if env.respond_to?(:each_logical_path)
config.assets.precompile.each do |path|
env.each_logical_path do |logical_path|
if path.is_a?(Regexp)
next unless path.match(logical_path)
else
next unless File.fnmatch(path.to_s, logical_path)
end
if asset = env.find_asset(logical_path)
filename = target.join(asset.digest_path)
mkdir_p filename.dirname
asset.write_to(filename)
asset.write_to("#{filename}.gz") if filename.to_s =~ /\.(css|js)$/
end
end
end
else
# TODO: Remove this once we're depending on sprockets beta 15
assets = config.assets.precompile.dup
assets << {:to => target}
env.precompile(*assets)
end
end
end
desc "Remove compiled assets"
task :clean => :environment do
assets = Rails.application.config.assets
public_asset_path = Rails.public_path + assets.prefix
task :clean => [:environment, 'tmp:cache:clear'] do
config = Rails.application.config
public_asset_path = File.join(Rails.public_path, config.assets.prefix)
rm_rf public_asset_path, :secure => true
end
end

View File

@@ -1,21 +1,37 @@
module Sprockets
class NullCompressor
# An asset compressor which does nothing.
#
# This compressor simply returns the asset as-is, without any compression
# whatsoever. It is useful in development mode, when compression isn't
# needed but using the same asset pipeline as production is desired.
class NullCompressor #:nodoc:
def compress(content)
content
end
end
class LazyCompressor
# An asset compressor which only initializes the underlying compression
# engine when needed.
#
# This postpones the initialization of the compressor until
# <code>#compress</code> is called the first time.
class LazyCompressor #:nodoc:
# Initializes a new LazyCompressor.
#
# The block should return a compressor when called, i.e. an object
# which responds to <code>#compress</code>.
def initialize(&block)
@block = block
end
def compressor
@compressor ||= @block.call || NullCompressor.new
end
def compress(content)
compressor.compress(content)
end
private
def compressor
@compressor ||= (@block.call || NullCompressor.new)
end
end
end
end

View File

@@ -26,15 +26,10 @@ module Sprockets
sources.collect do |source|
if debug && asset = asset_paths.asset_for(source, 'js')
asset.to_a.map { |dep|
javascript_include_tag(dep, :debug => false, :body => true)
}.join("\n").html_safe
super(dep.to_s, { :src => asset_path(dep, 'js', true) }.merge!(options))
}
else
tag_options = {
'type' => "text/javascript",
'src' => asset_path(source, 'js', body)
}.merge(options.stringify_keys)
content_tag 'script', "", tag_options
super(source.to_s, { :src => asset_path(source, 'js', body) }.merge!(options))
end
end.join("\n").html_safe
end
@@ -47,17 +42,10 @@ module Sprockets
sources.collect do |source|
if debug && asset = asset_paths.asset_for(source, 'css')
asset.to_a.map { |dep|
stylesheet_link_tag(dep, :debug => false, :body => true)
}.join("\n").html_safe
super(dep.to_s, { :href => asset_path(dep, 'css', true, :request) }.merge!(options))
}
else
tag_options = {
'rel' => "stylesheet",
'type' => "text/css",
'media' => "screen",
'href' => asset_path(source, 'css', body, :request)
}.merge(options.stringify_keys)
tag 'link', tag_options
super(source.to_s, { :href => asset_path(source, 'css', body, :request) }.merge!(options))
end
end.join("\n").html_safe
end
@@ -70,10 +58,12 @@ module Sprockets
private
def debug_assets?
params[:debug_assets] == '1' ||
params[:debug_assets] == 'true'
rescue NoMethodError
false
begin
config = Rails.application.config.assets
config.allow_debugging && (config.debug || params[:debug_assets])
rescue NoMethodError
false
end
end
# Override to specify an alternative prefix for asset path generation.
@@ -112,11 +102,22 @@ module Sprockets
asset_environment[source]
end
def digest_for(logical_path)
if asset = asset_environment[logical_path]
return asset.digest_path
end
logical_path
end
def rewrite_asset_path(source, dir)
if source[0] == ?/
source
else
asset_environment.path(source, performing_caching?, dir)
source = digest_for(source) if performing_caching?
source = File.join(dir, source)
source = "/#{source}" unless source =~ /^\//
source
end
end
@@ -128,9 +129,14 @@ module Sprockets
end
end
# When included in Sprockets::Context, we need to ask the top-level config as the controller is not available
def performing_caching?
config.action_controller.present? ? config.action_controller.perform_caching : config.perform_caching
# When included in Sprockets::Context, we need to ask the
# top-level config as the controller is not available.
if config.action_controller.present?
config.action_controller.perform_caching
else
config.perform_caching
end
end
end
end

View File

@@ -18,8 +18,8 @@ module Sprockets
require 'sprockets'
app.assets = Sprockets::Environment.new(app.root.to_s) do |env|
env.static_root = File.join(app.root.join('public'), config.assets.prefix)
env.logger = ::Rails.logger
env.logger = ::Rails.logger
env.version = ::Rails.env + "-#{config.assets.version}"
if config.assets.cache_store != false
env.cache = ActiveSupport::Cache.lookup_store(config.assets.cache_store) || ::Rails.cache

View File

@@ -4,6 +4,11 @@ class WorkshopsController < ActionController::Base
end
class RedirectController < ActionController::Base
# empty method not used anywhere to ensure methods like
# `status` and `location` aren't called on `redirect_to` calls
def status; render :text => 'called status'; end
def location; render :text => 'called location'; end
def simple_redirect
redirect_to :action => "hello_world"
end

View File

@@ -405,6 +405,14 @@ class TestController < ActionController::Base
render :template => "test/hello_world"
end
def render_with_explicit_unescaped_template
render :template => "test/h*llo_world"
end
def render_with_explicit_escaped_template
render :template => "test/hello_w*rld"
end
def render_with_explicit_string_template
render "test/hello_world"
end
@@ -1057,6 +1065,12 @@ class RenderTest < ActionController::TestCase
assert_response :success
end
def test_render_with_explicit_unescaped_template
assert_raise(ActionView::MissingTemplate) { get :render_with_explicit_unescaped_template }
get :render_with_explicit_escaped_template
assert_equal "Hello w*rld!", @response.body
end
def test_render_with_explicit_string_template
get :render_with_explicit_string_template
assert_equal "<html>Hello world!</html>", @response.body

View File

@@ -1664,114 +1664,6 @@ class RackMountIntegrationTests < ActiveSupport::TestCase
assert_raise(ActionController::RoutingError) { @routes.recognize_path('/none', :method => :get) }
end
def test_generate
assert_equal '/admin/users', url_for(@routes, { :use_route => 'admin_users' })
assert_equal '/admin/users', url_for(@routes, { :controller => 'admin/users' })
assert_equal '/admin/users', url_for(@routes, { :controller => 'admin/users', :action => 'index' })
assert_equal '/admin/users', url_for(@routes, { :action => 'index' }, { :controller => 'admin/users' })
assert_equal '/admin/users', url_for(@routes, { :controller => 'users', :action => 'index' }, { :controller => 'admin/accounts' })
assert_equal '/people', url_for(@routes, { :controller => '/people', :action => 'index' }, { :controller => 'admin/accounts' })
assert_equal '/admin/posts', url_for(@routes, { :controller => 'admin/posts' })
assert_equal '/admin/posts/new', url_for(@routes, { :controller => 'admin/posts', :action => 'new' })
assert_equal '/blog/2009', url_for(@routes, { :controller => 'posts', :action => 'show_date', :year => 2009 })
assert_equal '/blog/2009/1', url_for(@routes, { :controller => 'posts', :action => 'show_date', :year => 2009, :month => 1 })
assert_equal '/blog/2009/1/1', url_for(@routes, { :controller => 'posts', :action => 'show_date', :year => 2009, :month => 1, :day => 1 })
assert_equal '/archive/2010', url_for(@routes, { :controller => 'archive', :action => 'index', :year => '2010' })
assert_equal '/archive', url_for(@routes, { :controller => 'archive', :action => 'index' })
assert_equal '/archive?year=january', url_for(@routes, { :controller => 'archive', :action => 'index', :year => 'january' })
assert_equal '/people', url_for(@routes, { :controller => 'people', :action => 'index' })
assert_equal '/people', url_for(@routes, { :action => 'index' }, { :controller => 'people' })
assert_equal '/people', url_for(@routes, { :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' })
assert_equal '/people', url_for(@routes, { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' })
assert_equal '/people', url_for(@routes, {}, { :controller => 'people', :action => 'index' })
assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'show', :id => '1' })
assert_equal '/people/new', url_for(@routes, { :use_route => 'new_person' })
assert_equal '/people/new', url_for(@routes, { :controller => 'people', :action => 'new' })
assert_equal '/people/1', url_for(@routes, { :use_route => 'person', :id => '1' })
assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show', :id => '1' })
assert_equal '/people/1.xml', url_for(@routes, { :controller => 'people', :action => 'show', :id => '1', :format => 'xml' })
assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show', :id => 1 })
assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show', :id => Model.new('1') })
assert_equal '/people/1', url_for(@routes, { :action => 'show', :id => '1' }, { :controller => 'people', :action => 'index' })
assert_equal '/people/1', url_for(@routes, { :action => 'show', :id => 1 }, { :controller => 'people', :action => 'show', :id => '1' })
assert_equal '/people', url_for(@routes, { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' })
assert_equal '/people/1', url_for(@routes, {}, { :controller => 'people', :action => 'show', :id => '1' })
assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'index', :id => '1' })
assert_equal '/people/1/edit', url_for(@routes, { :controller => 'people', :action => 'edit', :id => '1' })
assert_equal '/people/1/edit.xml', url_for(@routes, { :controller => 'people', :action => 'edit', :id => '1', :format => 'xml' })
assert_equal '/people/1/edit', url_for(@routes, { :use_route => 'edit_person', :id => '1' })
assert_equal '/people/1?legacy=true', url_for(@routes, { :controller => 'people', :action => 'show', :id => '1', :legacy => 'true' })
assert_equal '/people?legacy=true', url_for(@routes, { :controller => 'people', :action => 'index', :legacy => 'true' })
assert_equal '/id_default/2', url_for(@routes, { :controller => 'foo', :action => 'id_default', :id => '2' })
assert_equal '/id_default', url_for(@routes, { :controller => 'foo', :action => 'id_default', :id => '1' })
assert_equal '/id_default', url_for(@routes, { :controller => 'foo', :action => 'id_default', :id => 1 })
assert_equal '/id_default', url_for(@routes, { :controller => 'foo', :action => 'id_default' })
assert_equal '/optional/bar', url_for(@routes, { :controller => 'posts', :action => 'index', :optional => 'bar' })
assert_equal '/posts', url_for(@routes, { :controller => 'posts', :action => 'index' })
assert_equal '/project', url_for(@routes, { :controller => 'project', :action => 'index' })
assert_equal '/projects/1', url_for(@routes, { :controller => 'project', :action => 'index', :project_id => '1' })
assert_equal '/projects/1', url_for(@routes, { :controller => 'project', :action => 'index'}, {:project_id => '1' })
assert_raise(ActionController::RoutingError) { url_for(@routes, { :use_route => 'project', :controller => 'project', :action => 'index' }) }
assert_equal '/projects/1', url_for(@routes, { :use_route => 'project', :controller => 'project', :action => 'index', :project_id => '1' })
assert_equal '/projects/1', url_for(@routes, { :use_route => 'project', :controller => 'project', :action => 'index' }, { :project_id => '1' })
assert_equal '/clients', url_for(@routes, { :controller => 'projects', :action => 'index' })
assert_equal '/clients?project_id=1', url_for(@routes, { :controller => 'projects', :action => 'index', :project_id => '1' })
assert_equal '/clients', url_for(@routes, { :controller => 'projects', :action => 'index' }, { :project_id => '1' })
assert_equal '/clients', url_for(@routes, { :action => 'index' }, { :controller => 'projects', :action => 'index', :project_id => '1' })
assert_equal '/comment/20', url_for(@routes, { :id => 20 }, { :controller => 'comments', :action => 'show' })
assert_equal '/comment/20', url_for(@routes, { :controller => 'comments', :id => 20, :action => 'show' })
assert_equal '/comments/boo', url_for(@routes, { :controller => 'comments', :action => 'boo' })
assert_equal '/ws/posts/show/1', url_for(@routes, { :controller => 'posts', :action => 'show', :id => '1', :ws => true })
assert_equal '/ws/posts', url_for(@routes, { :controller => 'posts', :action => 'index', :ws => true })
assert_equal '/account', url_for(@routes, { :controller => 'account', :action => 'subscription' })
assert_equal '/account/billing', url_for(@routes, { :controller => 'account', :action => 'billing' })
assert_equal '/pages/1/notes/show/1', url_for(@routes, { :page_id => '1', :controller => 'notes', :action => 'show', :id => '1' })
assert_equal '/pages/1/notes/list', url_for(@routes, { :page_id => '1', :controller => 'notes', :action => 'list' })
assert_equal '/pages/1/notes', url_for(@routes, { :page_id => '1', :controller => 'notes', :action => 'index' })
assert_equal '/pages/1/notes', url_for(@routes, { :page_id => '1', :controller => 'notes' })
assert_equal '/notes', url_for(@routes, { :page_id => nil, :controller => 'notes' })
assert_equal '/notes', url_for(@routes, { :controller => 'notes' })
assert_equal '/notes/print', url_for(@routes, { :controller => 'notes', :action => 'print' })
assert_equal '/notes/print', url_for(@routes, {}, { :controller => 'notes', :action => 'print' })
assert_equal '/notes/index/1', url_for(@routes, { :controller => 'notes' }, { :controller => 'notes', :id => '1' })
assert_equal '/notes/index/1', url_for(@routes, { :controller => 'notes' }, { :controller => 'notes', :id => '1', :foo => 'bar' })
assert_equal '/notes/index/1', url_for(@routes, { :controller => 'notes' }, { :controller => 'notes', :id => '1' })
assert_equal '/notes/index/1', url_for(@routes, { :action => 'index' }, { :controller => 'notes', :id => '1' })
assert_equal '/notes/index/1', url_for(@routes, {}, { :controller => 'notes', :id => '1' })
assert_equal '/notes/show/1', url_for(@routes, {}, { :controller => 'notes', :action => 'show', :id => '1' })
assert_equal '/notes/index/1', url_for(@routes, { :controller => 'notes', :id => '1' }, { :foo => 'bar' })
assert_equal '/posts', url_for(@routes, { :controller => 'posts' }, { :controller => 'notes', :action => 'show', :id => '1' })
assert_equal '/notes/list', url_for(@routes, { :action => 'list' }, { :controller => 'notes', :action => 'show', :id => '1' })
assert_equal '/posts/ping', url_for(@routes, { :controller => 'posts', :action => 'ping' })
assert_equal '/posts/show/1', url_for(@routes, { :controller => 'posts', :action => 'show', :id => '1' })
assert_equal '/posts', url_for(@routes, { :controller => 'posts' })
assert_equal '/posts', url_for(@routes, { :controller => 'posts', :action => 'index' })
assert_equal '/posts', url_for(@routes, { :controller => 'posts' }, { :controller => 'posts', :action => 'index' })
assert_equal '/posts/create', url_for(@routes, { :action => 'create' }, { :controller => 'posts' })
assert_equal '/posts?foo=bar', url_for(@routes, { :controller => 'posts', :foo => 'bar' })
assert_equal '/posts?foo%5B%5D=bar&foo%5B%5D=baz', url_for(@routes, { :controller => 'posts', :foo => ['bar', 'baz'] })
assert_equal '/posts?page=2', url_for(@routes, { :controller => 'posts', :page => 2 })
assert_equal '/posts?q%5Bfoo%5D%5Ba%5D=b', url_for(@routes, { :controller => 'posts', :q => { :foo => { :a => 'b'}} })
assert_equal '/news.rss', url_for(@routes, { :controller => 'news', :action => 'index', :format => 'rss' })
assert_raise(ActionController::RoutingError) { url_for(@routes, { :action => 'index' }) }
end
def test_generate_extras
assert_equal ['/people', []], @routes.generate_extras(:controller => 'people')
assert_equal ['/people', [:foo]], @routes.generate_extras(:controller => 'people', :foo => 'bar')

View File

@@ -50,6 +50,10 @@ class TestTest < ActionController::TestCase
render :text => request.query_string
end
def test_protocol
render :text => request.protocol
end
def test_html_output
render :text => <<HTML
<html>
@@ -515,6 +519,12 @@ XML
)
end
def test_params_passing_doesnt_modify_in_place
page = {:name => "Page name", :month => 4, :year => 2004, :day => 6}
get :test_params, :page => page
assert_equal 2004, page[:year]
end
def test_id_converted_to_string
get :test_params, :id => 20, :foo => Object.new
assert_kind_of String, @request.path_parameters['id']
@@ -592,6 +602,19 @@ XML
assert_nil @request.symbolized_path_parameters[:id]
end
def test_request_protocol_is_reset_after_request
get :test_protocol
assert_equal "http://", @response.body
@request.env["HTTPS"] = "on"
get :test_protocol
assert_equal "https://", @response.body
@request.env.delete("HTTPS")
get :test_protocol
assert_equal "http://", @response.body
end
def test_should_have_knowledge_of_client_side_cookie_state_even_if_they_are_not_set
cookies['foo'] = 'bar'
get :no_op

View File

@@ -0,0 +1,183 @@
# encoding: utf-8
require 'abstract_unit'
require 'controller/fake_controllers'
require 'active_support/core_ext/object/with_options'
module RoutingTestHelpers
def url_for(set, options, recall = nil)
set.send(:url_for, options.merge(:only_path => true, :_path_segments => recall))
end
end
module ActionPack
class URLForIntegrationTest < ActiveSupport::TestCase
include RoutingTestHelpers
Model = Struct.new(:to_param)
Mapping = lambda {
namespace :admin do
resources :users, :posts
end
namespace 'api' do
root :to => 'users#index'
end
match '/blog(/:year(/:month(/:day)))' => 'posts#show_date',
:constraints => {
:year => /(19|20)\d\d/,
:month => /[01]?\d/,
:day => /[0-3]?\d/
},
:day => nil,
:month => nil
match 'archive/:year', :controller => 'archive', :action => 'index',
:defaults => { :year => nil },
:constraints => { :year => /\d{4}/ },
:as => "blog"
resources :people
#match 'legacy/people' => "people#index", :legacy => "true"
match 'symbols', :controller => :symbols, :action => :show, :name => :as_symbol
match 'id_default(/:id)' => "foo#id_default", :id => 1
match 'get_or_post' => "foo#get_or_post", :via => [:get, :post]
match 'optional/:optional' => "posts#index"
match 'projects/:project_id' => "project#index", :as => "project"
match 'clients' => "projects#index"
match 'ignorecase/geocode/:postalcode' => 'geocode#show', :postalcode => /hx\d\d-\d[a-z]{2}/i
match 'extended/geocode/:postalcode' => 'geocode#show',:constraints => {
:postalcode => /# Postcode format
\d{5} #Prefix
(-\d{4})? #Suffix
/x
}, :as => "geocode"
match 'news(.:format)' => "news#index"
match 'comment/:id(/:action)' => "comments#show"
match 'ws/:controller(/:action(/:id))', :ws => true
match 'account(/:action)' => "account#subscription"
match 'pages/:page_id/:controller(/:action(/:id))'
match ':controller/ping', :action => 'ping'
match ':controller(/:action(/:id))(.:format)'
root :to => "news#index"
}
def setup
@routes = ActionDispatch::Routing::RouteSet.new
@routes.draw(&Mapping)
end
[
['/admin/users',[ { :use_route => 'admin_users' }]],
['/admin/users',[ { :controller => 'admin/users' }]],
['/admin/users',[ { :controller => 'admin/users', :action => 'index' }]],
['/admin/users',[ { :action => 'index' }, { :controller => 'admin/users' }]],
['/admin/users',[ { :controller => 'users', :action => 'index' }, { :controller => 'admin/accounts' }]],
['/people',[ { :controller => '/people', :action => 'index' }, { :controller => 'admin/accounts' }]],
['/admin/posts',[ { :controller => 'admin/posts' }]],
['/admin/posts/new',[ { :controller => 'admin/posts', :action => 'new' }]],
['/blog/2009',[ { :controller => 'posts', :action => 'show_date', :year => 2009 }]],
['/blog/2009/1',[ { :controller => 'posts', :action => 'show_date', :year => 2009, :month => 1 }]],
['/blog/2009/1/1',[ { :controller => 'posts', :action => 'show_date', :year => 2009, :month => 1, :day => 1 }]],
['/archive/2010',[ { :controller => 'archive', :action => 'index', :year => '2010' }]],
['/archive',[ { :controller => 'archive', :action => 'index' }]],
['/archive?year=january',[ { :controller => 'archive', :action => 'index', :year => 'january' }]],
['/people',[ { :controller => 'people', :action => 'index' }]],
['/people',[ { :action => 'index' }, { :controller => 'people' }]],
['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]],
['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]],
['/people',[ {}, { :controller => 'people', :action => 'index' }]],
['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'show', :id => '1' }]],
['/people/new',[ { :use_route => 'new_person' }]],
['/people/new',[ { :controller => 'people', :action => 'new' }]],
['/people/1',[ { :use_route => 'person', :id => '1' }]],
['/people/1',[ { :controller => 'people', :action => 'show', :id => '1' }]],
['/people/1.xml',[ { :controller => 'people', :action => 'show', :id => '1', :format => 'xml' }]],
['/people/1',[ { :controller => 'people', :action => 'show', :id => 1 }]],
['/people/1',[ { :controller => 'people', :action => 'show', :id => Model.new('1') }]],
['/people/1',[ { :action => 'show', :id => '1' }, { :controller => 'people', :action => 'index' }]],
['/people/1',[ { :action => 'show', :id => 1 }, { :controller => 'people', :action => 'show', :id => '1' }]],
['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]],
['/people/1',[ {}, { :controller => 'people', :action => 'show', :id => '1' }]],
['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'index', :id => '1' }]],
['/people/1/edit',[ { :controller => 'people', :action => 'edit', :id => '1' }]],
['/people/1/edit.xml',[ { :controller => 'people', :action => 'edit', :id => '1', :format => 'xml' }]],
['/people/1/edit',[ { :use_route => 'edit_person', :id => '1' }]],
['/people/1?legacy=true',[ { :controller => 'people', :action => 'show', :id => '1', :legacy => 'true' }]],
['/people?legacy=true',[ { :controller => 'people', :action => 'index', :legacy => 'true' }]],
['/id_default/2',[ { :controller => 'foo', :action => 'id_default', :id => '2' }]],
['/id_default',[ { :controller => 'foo', :action => 'id_default', :id => '1' }]],
['/id_default',[ { :controller => 'foo', :action => 'id_default', :id => 1 }]],
['/id_default',[ { :controller => 'foo', :action => 'id_default' }]],
['/optional/bar',[ { :controller => 'posts', :action => 'index', :optional => 'bar' }]],
['/posts',[ { :controller => 'posts', :action => 'index' }]],
['/project',[ { :controller => 'project', :action => 'index' }]],
['/projects/1',[ { :controller => 'project', :action => 'index', :project_id => '1' }]],
['/projects/1',[ { :controller => 'project', :action => 'index'}, {:project_id => '1' }]],
['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index', :project_id => '1' }]],
['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index' }, { :project_id => '1' }]],
['/clients',[ { :controller => 'projects', :action => 'index' }]],
['/clients?project_id=1',[ { :controller => 'projects', :action => 'index', :project_id => '1' }]],
['/clients',[ { :controller => 'projects', :action => 'index' }, { :project_id => '1' }]],
['/clients',[ { :action => 'index' }, { :controller => 'projects', :action => 'index', :project_id => '1' }]],
['/comment/20',[ { :id => 20 }, { :controller => 'comments', :action => 'show' }]],
['/comment/20',[ { :controller => 'comments', :id => 20, :action => 'show' }]],
['/comments/boo',[ { :controller => 'comments', :action => 'boo' }]],
['/ws/posts/show/1',[ { :controller => 'posts', :action => 'show', :id => '1', :ws => true }]],
['/ws/posts',[ { :controller => 'posts', :action => 'index', :ws => true }]],
['/account',[ { :controller => 'account', :action => 'subscription' }]],
['/account/billing',[ { :controller => 'account', :action => 'billing' }]],
['/pages/1/notes/show/1',[ { :page_id => '1', :controller => 'notes', :action => 'show', :id => '1' }]],
['/pages/1/notes/list',[ { :page_id => '1', :controller => 'notes', :action => 'list' }]],
['/pages/1/notes',[ { :page_id => '1', :controller => 'notes', :action => 'index' }]],
['/pages/1/notes',[ { :page_id => '1', :controller => 'notes' }]],
['/notes',[ { :page_id => nil, :controller => 'notes' }]],
['/notes',[ { :controller => 'notes' }]],
['/notes/print',[ { :controller => 'notes', :action => 'print' }]],
['/notes/print',[ {}, { :controller => 'notes', :action => 'print' }]],
['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1' }]],
['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1', :foo => 'bar' }]],
['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1' }]],
['/notes/index/1',[ { :action => 'index' }, { :controller => 'notes', :id => '1' }]],
['/notes/index/1',[ {}, { :controller => 'notes', :id => '1' }]],
['/notes/show/1',[ {}, { :controller => 'notes', :action => 'show', :id => '1' }]],
['/notes/index/1',[ { :controller => 'notes', :id => '1' }, { :foo => 'bar' }]],
['/posts',[ { :controller => 'posts' }, { :controller => 'notes', :action => 'show', :id => '1' }]],
['/notes/list',[ { :action => 'list' }, { :controller => 'notes', :action => 'show', :id => '1' }]],
['/posts/ping',[ { :controller => 'posts', :action => 'ping' }]],
['/posts/show/1',[ { :controller => 'posts', :action => 'show', :id => '1' }]],
['/posts',[ { :controller => 'posts' }]],
['/posts',[ { :controller => 'posts', :action => 'index' }]],
['/posts',[ { :controller => 'posts' }, { :controller => 'posts', :action => 'index' }]],
['/posts/create',[ { :action => 'create' }, { :controller => 'posts' }]],
['/posts?foo=bar',[ { :controller => 'posts', :foo => 'bar' }]],
['/posts?foo%5B%5D=bar&foo%5B%5D=baz', [{ :controller => 'posts', :foo => ['bar', 'baz'] }]],
['/posts?page=2', [{ :controller => 'posts', :page => 2 }]],
['/posts?q%5Bfoo%5D%5Ba%5D=b', [{ :controller => 'posts', :q => { :foo => { :a => 'b'}} }]],
['/news.rss', [{ :controller => 'news', :action => 'index', :format => 'rss' }]],
].each_with_index do |(url, params), i|
define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do
assert_equal url, url_for(@routes, *params), params.inspect
end
end
end
end

View File

@@ -851,6 +851,18 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
# tests the use of dup in url_for
def test_url_for_with_no_side_effects
# without dup, additional (and possibly unwanted) values will be present in the options (eg. :host)
original_options = {:controller => 'projects', :action => 'status'}
options = original_options.dup
url_for options
# verify that the options passed in have not changed from the original ones
assert_equal original_options, options
end
def test_projects_status
with_test_routes do
assert_equal '/projects/status', url_for(:controller => 'projects', :action => 'status', :only_path => true)

View File

@@ -0,0 +1 @@
Hello w*rld!

View File

@@ -42,7 +42,7 @@ class CompiledTemplatesTest < Test::Unit::TestCase
def render_without_cache(*args)
path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
view_paths = ActionView::Base.process_view_paths(path)
view_paths = ActionView::PathSet.new([path])
ActionView::Base.new(view_paths, {}).render(*args)
end

View File

@@ -508,25 +508,31 @@ class FormTagHelperTest < ActionView::TestCase
def test_text_area_tag_options_symbolize_keys_side_effects
options = { :option => "random_option" }
actual = text_area_tag "body", "hello world", options
text_area_tag "body", "hello world", options
assert_equal options, { :option => "random_option" }
end
def test_submit_tag_options_symbolize_keys_side_effects
options = { :option => "random_option" }
actual = submit_tag "submit value", options
submit_tag "submit value", options
assert_equal options, { :option => "random_option" }
end
def test_button_tag_options_symbolize_keys_side_effects
options = { :option => "random_option" }
actual = button_tag "button value", options
button_tag "button value", options
assert_equal options, { :option => "random_option" }
end
def test_image_submit_tag_options_symbolize_keys_side_effects
options = { :option => "random_option" }
actual = image_submit_tag "submit source", options
image_submit_tag "submit source", options
assert_equal options, { :option => "random_option" }
end
def test_image_label_tag_options_symbolize_keys_side_effects
options = { :option => "random_option" }
label_tag "submit source", "title", options
assert_equal options, { :option => "random_option" }
end

View File

@@ -5,6 +5,13 @@ class SanitizerTest < ActionController::TestCase
@sanitizer = nil # used by assert_sanitizer
end
def test_strip_tags_with_quote
sanitizer = HTML::FullSanitizer.new
string = '<" <img src="trollface.gif" onload="alert(1)"> hi'
assert_equal ' hi', sanitizer.sanitize(string)
end
def test_strip_tags
sanitizer = HTML::FullSanitizer.new
assert_equal("<<<bad html", sanitizer.sanitize("<<<bad html"))

View File

@@ -1,4 +1,5 @@
require 'abstract_unit'
require 'active_support/core_ext/string/encoding'
class JavaScriptHelperTest < ActionView::TestCase
tests ActionView::Helpers::JavaScriptHelper
@@ -27,6 +28,11 @@ class JavaScriptHelperTest < ActionView::TestCase
assert_equal %(This \\"thing\\" is really\\n netos\\'), escape_javascript(%(This "thing" is really\n netos'))
assert_equal %(backslash\\\\test), escape_javascript( %(backslash\\test) )
assert_equal %(dont <\\/close> tags), escape_javascript(%(dont </close> tags))
if "ruby".encoding_aware?
assert_equal %(unicode &#x2028; newline), escape_javascript(%(unicode \342\200\250 newline).force_encoding('UTF-8').encode!)
else
assert_equal %(unicode &#x2028; newline), escape_javascript(%(unicode \342\200\250 newline))
end
assert_equal %(dont <\\/close> tags), j(%(dont </close> tags))
end

View File

@@ -380,7 +380,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase
# is not eager loaded
def setup
path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
view_paths = ActionView::Base.process_view_paths(path)
view_paths = ActionView::PathSet.new([path])
assert_equal ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH), view_paths.first
setup_view(view_paths)
end

View File

@@ -151,11 +151,16 @@ class SprocketsHelperTest < ActionView::TestCase
assert_equal '<script src="http://www.example.com/xmlhr" type="text/javascript"></script>',
javascript_include_tag("http://www.example.com/xmlhr")
assert_match %r{<script src=\"/assets/xmlhr-[0-9a-f]+.js" type=\"text/javascript\"></script>\n<script src=\"/assets/extra-[0-9a-f]+.js" type=\"text/javascript\"></script>},
javascript_include_tag("xmlhr", "extra")
assert_match %r{<script src="/assets/xmlhr-[0-9a-f]+.js\?body=1" type="text/javascript"></script>\n<script src="/assets/application-[0-9a-f]+.js\?body=1" type="text/javascript"></script>},
javascript_include_tag(:application, :debug => true)
assert_match %r{<script src=\"/assets/xmlhr-[0-9a-f]+.js\" type=\"text/javascript\"></script>\n<script src=\"/assets/extra-[0-9a-f]+.js\" type=\"text/javascript\"></script>},
javascript_include_tag("xmlhr", "extra")
@config.assets.allow_debugging = true
@config.assets.debug = true
assert_match %r{<script src="/assets/xmlhr-[0-9a-f]+.js\?body=1" type="text/javascript"></script>\n<script src="/assets/application-[0-9a-f]+.js\?body=1" type="text/javascript"></script>},
javascript_include_tag(:application)
end
test "stylesheet path" do
@@ -187,11 +192,19 @@ class SprocketsHelperTest < ActionView::TestCase
assert_match %r{<link href="/assets/style-[0-9a-f]+.css" media="print" rel="stylesheet" type="text/css" />},
stylesheet_link_tag("style", :media => "print")
assert_match %r{<link href="/assets/style-[0-9a-f]+.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/assets/extra-[0-9a-f]+.css" media="screen" rel="stylesheet" type="text/css" />},
stylesheet_link_tag("style", "extra")
assert_match %r{<link href="/assets/style-[0-9a-f]+.css\?body=1" media="screen" rel="stylesheet" type="text/css" />\n<link href="/assets/application-[0-9a-f]+.css\?body=1" media="screen" rel="stylesheet" type="text/css" />},
stylesheet_link_tag(:application, :debug => true)
assert_match %r{<link href="/assets/style-[0-9a-f]+.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/assets/extra-[0-9a-f]+.css" media="screen" rel="stylesheet" type="text/css" />},
stylesheet_link_tag("style", "extra")
@config.assets.allow_debugging = true
@config.assets.debug = true
assert_match %r{<link href="/assets/style-[0-9a-f]+.css\?body=1" media="screen" rel="stylesheet" type="text/css" />\n<link href="/assets/application-[0-9a-f]+.css\?body=1" media="screen" rel="stylesheet" type="text/css" />},
stylesheet_link_tag(:application)
assert_match %r{<link href="/assets/style-[0-9a-f]+.css\?body=1" media="print" rel="stylesheet" type="text/css" />\n<link href="/assets/application-[0-9a-f]+.css\?body=1" media="print" rel="stylesheet" type="text/css" />},
stylesheet_link_tag(:application, :media => "print")
end
test "alternate asset prefix" do
@@ -205,4 +218,17 @@ class SprocketsHelperTest < ActionView::TestCase
stubs(:asset_environment).returns(assets)
assert_match %r{/assets/style-[0-9a-f]+.css}, asset_path("style", "css")
end
test "alternate hash based on environment" do
assets = Sprockets::Environment.new
assets.version = 'development'
assets.append_path(FIXTURES.join("sprockets/alternate/stylesheets"))
stubs(:asset_environment).returns(assets)
dev_path = asset_path("style", "css")
assets.version = 'production'
prod_path = asset_path("style", "css")
assert_not_equal prod_path, dev_path
end
end

View File

@@ -1,3 +1,5 @@
* Add ability to define strict validation(with :strict => true option) that always raises exception when fails [Bogdan Gusiev]
* Deprecate "Model.model_name.partial_path" in favor of "model.to_partial_path" [Grant Hutchins, Peter Jaros]
* Provide mass_assignment_sanitizer as an easy API to replace the sanitizer behavior. Also support both :logger (default) and :strict sanitizer behavior [Bogdan Gusiev]

View File

@@ -19,5 +19,5 @@ Gem::Specification.new do |s|
s.add_dependency('activesupport', version)
s.add_dependency('builder', '~> 3.0.0')
s.add_dependency('i18n', '~> 0.6')
s.add_dependency('bcrypt-ruby', '~> 2.1.4')
s.add_dependency('bcrypt-ruby', '~> 3.0.0')
end

View File

@@ -63,7 +63,7 @@ module ActiveModel
class Errors
include Enumerable
CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank]
CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
attr_reader :messages
@@ -218,6 +218,9 @@ module ActiveModel
elsif message.is_a?(Proc)
message = message.call
end
if options[:strict]
raise ActiveModel::StrictValidationFailed, message
end
self[attribute] << message
end
@@ -319,4 +322,7 @@ module ActiveModel
I18n.translate(key, options)
end
end
class StrictValidationFailed < StandardError
end
end

View File

@@ -226,10 +226,10 @@ module ActiveModel
# Send observed_method(object) if the method exists and
# the observer is enabled for the given object's class.
def update(observed_method, object) #:nodoc:
def update(observed_method, object, &block) #:nodoc:
return unless respond_to?(observed_method)
return if disabled_for?(object)
send(observed_method, object)
send(observed_method, object, &block)
end
# Special method sent by the observed class when it is inherited.

View File

@@ -1,5 +1,7 @@
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/array/wrap'
module ActiveModel
# == Active Model Serialization

View File

@@ -58,6 +58,8 @@ module ActiveModel
# <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).
# The method, proc or string should return or evaluate to a true or
# false value.
# * <tt>:strict</tt> - Specifies whether validation should be strict.
# See <tt>ActiveModel::Validation#validates!</tt> for more information
def validates_acceptance_of(*attr_names)
validates_with AcceptanceValidator, _merge_attributes(attr_names)
end

View File

@@ -58,6 +58,8 @@ module ActiveModel
# <tt>:unless => :skip_validation</tt>, or
# <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
# * <tt>:strict</tt> - Specifies whether validation should be strict.
# See <tt>ActiveModel::Validation#validates!</tt> for more information
def validates_confirmation_of(*attr_names)
validates_with ConfirmationValidator, _merge_attributes(attr_names)
end

View File

@@ -59,6 +59,8 @@ module ActiveModel
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
# * <tt>:strict</tt> - Specifies whether validation should be strict.
# See <tt>ActiveModel::Validation#validates!</tt> for more information
def validates_exclusion_of(*attr_names)
validates_with ExclusionValidator, _merge_attributes(attr_names)
end

View File

@@ -84,6 +84,8 @@ module ActiveModel
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
# * <tt>:strict</tt> - Specifies whether validation should be strict.
# See <tt>ActiveModel::Validation#validates!</tt> for more information
def validates_format_of(*attr_names)
validates_with FormatValidator, _merge_attributes(attr_names)
end

View File

@@ -59,6 +59,8 @@ module ActiveModel
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
# * <tt>:strict</tt> - Specifies whether validation should be strict.
# See <tt>ActiveModel::Validation#validates!</tt> for more information
def validates_inclusion_of(*attr_names)
validates_with InclusionValidator, _merge_attributes(attr_names)
end

View File

@@ -96,6 +96,8 @@ module ActiveModel
# * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to
# count words as in above example.)
# Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters.
# * <tt>:strict</tt> - Specifies whether validation should be strict.
# See <tt>ActiveModel::Validation#validates!</tt> for more information
def validates_length_of(*attr_names)
validates_with LengthValidator, _merge_attributes(attr_names)
end

View File

@@ -107,6 +107,8 @@ module ActiveModel
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
# * <tt>:strict</tt> - Specifies whether validation should be strict.
# See <tt>ActiveModel::Validation#validates!</tt> for more information
#
# The following checks can also be supplied with a proc or a symbol which corresponds to a method:
# * <tt>:greater_than</tt>

View File

@@ -35,6 +35,8 @@ module ActiveModel
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).
# The method, proc or string should return or evaluate to a true or false value.
# * <tt>:strict</tt> - Specifies whether validation should be strict.
# See <tt>ActiveModel::Validation#validates!</tt> for more information
#
def validates_presence_of(*attr_names)
validates_with PresenceValidator, _merge_attributes(attr_names)

View File

@@ -70,8 +70,8 @@ module ActiveModel
# validator's initializer as +options[:in]+ while other types including
# regular expressions and strings are passed as +options[:with]+
#
# Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+ and +:allow_nil+ can be given
# to one specific validator, as a hash:
# Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+ and +:strict+
# can be given to one specific validator, as a hash:
#
# validates :password, :presence => { :if => :password_required? }, :confirmation => true
#
@@ -101,12 +101,24 @@ module ActiveModel
end
end
# This method is used to define validation that can not be corrected by end user
# and is considered exceptional.
# So each validator defined with bang or <tt>:strict</tt> option set to <tt>true</tt>
# will always raise <tt>ActiveModel::InternalValidationFailed</tt> instead of adding error
# when validation fails
# See <tt>validates</tt> for more information about validation itself.
def validates!(*attributes)
options = attributes.extract_options!
options[:strict] = true
validates(*(attributes << options))
end
protected
# When creating custom validators, it might be useful to be able to specify
# additional default keys. This can be done by overwriting this method.
def _validates_default_keys
[ :if, :unless, :on, :allow_blank, :allow_nil ]
[ :if, :unless, :on, :allow_blank, :allow_nil , :strict]
end
def _parse_validates_options(options) #:nodoc:

View File

@@ -61,7 +61,9 @@ module ActiveModel
# (e.g. <tt>:unless => :skip_validation</tt>, or
# <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).
# The method, proc or string should return or evaluate to a true or false value.
#
# * <tt>:strict</tt> - Specifies whether validation should be strict.
# See <tt>ActiveModel::Validation#validates!</tt> for more information
# If you pass any additional configuration options, they will be passed
# to the class and available as <tt>options</tt>:
#
@@ -140,4 +142,4 @@ module ActiveModel
end
end
end
end
end

View File

@@ -17,6 +17,10 @@ class FooObserver < ActiveModel::Observer
def on_spec(record)
stub.event_with(record) if stub
end
def around_save(record)
yield :in_around_save
end
end
class Foo
@@ -133,4 +137,12 @@ class ObserverTest < ActiveModel::TestCase
foo = Foo.new
Foo.send(:notify_observers, :whatever, foo)
end
test "update passes a block on to the observer" do
yielded_value = nil
FooObserver.instance.update(:around_save, Foo.new) do |val|
yielded_value = val
end
assert_equal :in_around_save, yielded_value
end
end

View File

@@ -114,8 +114,21 @@ class SerializationTest < ActiveModel::TestCase
@user.friends.first.friends = [@user]
expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
:friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male',
:friends => ["email"=>"david@example.com", "gender"=>"male", "name"=>"David"]},
:friends => [{"email"=>"david@example.com", "gender"=>"male", "name"=>"David"}]},
{"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female', :friends => []}]}
assert_equal expected , @user.serializable_hash(:include => {:friends => {:include => :friends}})
end
def test_only_include
expected = {"name"=>"David", :friends => [{"name" => "Joe"}, {"name" => "Sue"}]}
assert_equal expected , @user.serializable_hash(:only => :name, :include => {:friends => {:only => :name}})
end
def test_except_include
expected = {"name"=>"David", "email"=>"david@example.com",
:friends => [{"name" => 'Joe', "email" => 'joe@example.com'},
{"name" => "Sue", "email" => 'sue@example.com'}]}
assert_equal expected , @user.serializable_hash(:except => :gender, :include => {:friends => {:except => :gender}})
end
end

View File

@@ -297,4 +297,37 @@ class ValidationsTest < ActiveModel::TestCase
assert auto.valid?
end
def test_strict_validation_in_validates
Topic.validates :title, :strict => true, :presence => true
assert_raises ActiveModel::StrictValidationFailed do
Topic.new.valid?
end
end
def test_strict_validation_not_fails
Topic.validates :title, :strict => true, :presence => true
assert Topic.new(:title => "hello").valid?
end
def test_strict_validation_particular_validator
Topic.validates :title, :presence => {:strict => true}
assert_raises ActiveModel::StrictValidationFailed do
Topic.new.valid?
end
end
def test_strict_validation_in_custom_validator_helper
Topic.validates_presence_of :title, :strict => true
assert_raises ActiveModel::StrictValidationFailed do
Topic.new.valid?
end
end
def test_validates_with_bang
Topic.validates! :title, :presence => true
assert_raises ActiveModel::StrictValidationFailed do
Topic.new.valid?
end
end
end

View File

@@ -1,5 +1,7 @@
*Rails 3.2.0 (unreleased)*
* Support bulk change_table in mysql2 adapter, as well as the mysql one. [Jon Leighton]
* If multiple parameters are sent representing a date, and some are blank, the
resulting object is nil. In previous releases those values defaulted to 1. This
only affects existing but blank parameters, missing ones still raise an error.

View File

@@ -21,6 +21,6 @@ Gem::Specification.new do |s|
s.add_dependency('activesupport', version)
s.add_dependency('activemodel', version)
s.add_dependency('arel', '~> 2.1.3')
s.add_dependency('arel', '~> 2.2.1')
s.add_dependency('tzinfo', '~> 0.3.29')
end

View File

@@ -21,13 +21,6 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
activemodel_path = File.expand_path('../../../activemodel/lib', __FILE__)
$:.unshift(activemodel_path) if File.directory?(activemodel_path) && !$:.include?(activemodel_path)
require 'active_support'
require 'active_support/i18n'
require 'active_model'
@@ -42,7 +35,7 @@ module ActiveRecord
autoload :ActiveRecordError, 'active_record/errors'
autoload :ConnectionNotEstablished, 'active_record/errors'
autoload :ConnectionAdapters, 'active_record/connection_adapters/abstract_adapter'
autoload :Aggregations
autoload :Associations
autoload :AttributeMethods
@@ -72,6 +65,7 @@ module ActiveRecord
autoload :Persistence
autoload :QueryCache
autoload :Reflection
autoload :Result
autoload :Schema
autoload :SchemaDumper
autoload :Serialization

View File

@@ -1087,7 +1087,8 @@ module ActiveRecord
#
# [:finder_sql]
# Specify a complete SQL statement to fetch the association. This is a good way to go for complex
# associations that depend on multiple tables. Note: When this option is used, +find_in_collection+
# associations that depend on multiple tables. May be supplied as a string or a proc where interpolation is
# required. Note: When this option is used, +find_in_collection+
# is _not_ added.
# [:counter_sql]
# Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
@@ -1162,11 +1163,14 @@ module ActiveRecord
# has_many :tags, :as => :taggable
# has_many :reports, :readonly => true
# has_many :subscribers, :through => :subscriptions, :source => :user
# has_many :subscribers, :class_name => "Person", :finder_sql =>
# 'SELECT DISTINCT people.* ' +
# 'FROM people p, post_subscriptions ps ' +
# 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
# 'ORDER BY p.first_name'
# has_many :subscribers, :class_name => "Person", :finder_sql => Proc.new {
# %Q{
# SELECT DISTINCT people.*
# FROM people p, post_subscriptions ps
# WHERE ps.post_id = #{id} AND ps.person_id = p.id
# ORDER BY p.first_name
# }
# }
def has_many(name, options = {}, &extension)
Builder::HasMany.build(self, name, options, &extension)
end

View File

@@ -53,12 +53,18 @@ module ActiveRecord
# quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
quoted_name = connection.quote_table_name(name).downcase
table_joins.map { |join|
# Table names + table aliases
join.left.downcase.scan(
/join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
).size
}.sum
counts = table_joins.map do |join|
if join.is_a?(Arel::Nodes::StringJoin)
# Table names + table aliases
join.left.downcase.scan(
/join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
).size
else
join.left.table_name == name ? 1 : 0
end
end
counts.sum
end
def truncate(name)

View File

@@ -151,20 +151,20 @@ module ActiveRecord
reset
end
def interpolate(sql, record = nil)
if sql.respond_to?(:to_proc)
owner.send(:instance_exec, record, &sql)
else
sql
end
end
private
def find_target?
!loaded? && (!owner.new_record? || foreign_key_present?) && klass
end
def interpolate(sql, record = nil)
if sql.respond_to?(:to_proc)
owner.send(:instance_exec, record, &sql)
else
sql
end
end
def creation_attributes
attributes = {}

View File

@@ -2,6 +2,11 @@ module ActiveRecord
# = Active Record Belongs To Polymorphic Association
module Associations
class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc:
def klass
type = owner[reflection.foreign_type]
type.presence && type.constantize
end
private
def replace_keys(record)
@@ -17,11 +22,6 @@ module ActiveRecord
reflection.polymorphic_inverse_of(record.class)
end
def klass
type = owner[reflection.foreign_type]
type.presence && type.constantize
end
def raise_on_type_mismatch(record)
# A polymorphic association cannot have a type mismatch, by definition
end

View File

@@ -26,7 +26,7 @@ module ActiveRecord
join_table[reflection.association_foreign_key] => record.id
)
owner.connection.insert stmt.to_sql
owner.connection.insert stmt
end
record
@@ -46,7 +46,7 @@ module ActiveRecord
stmt = relation.where(relation[reflection.foreign_key].eq(owner.id).
and(relation[reflection.association_foreign_key].in(records.map { |x| x.id }.compact))
).compile_delete
owner.connection.delete stmt.to_sql
owner.connection.delete stmt
end
end

View File

@@ -188,13 +188,12 @@ module ActiveRecord
association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
set_target_and_inverse(join_part, association, record)
else
return if row[join_part.aliased_primary_key].nil?
association = join_part.instantiate(row)
association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
case macro
when :has_many, :has_and_belongs_to_many
other = record.association(join_part.reflection.name)
other.loaded!
other.target.push(association)
other.target.push(association) if association
other.set_inverse_instance(association)
when :belongs_to
set_target_and_inverse(join_part, association, record)

View File

@@ -13,7 +13,7 @@ module ActiveRecord
# access the aliased column on the join table
def records_for(ids)
scope = super
klass.connection.select_all(scope.arel.to_sql, 'SQL', scope.bind_values)
klass.connection.select_all(scope.arel, 'SQL', scope.bind_values)
end
def owner_key_name

View File

@@ -3,11 +3,10 @@ module ActiveRecord
module PrimaryKey
extend ActiveSupport::Concern
# Returns this record's primary key value wrapped in an Array or nil if
# the record is not persisted? or has just been destroyed.
# Returns this record's primary key value wrapped in an Array if one is available
def to_key
key = send(self.class.primary_key)
persisted? && key ? [key] : nil
[key] if key
end
module ClassMethods

View File

@@ -178,7 +178,7 @@ module ActiveRecord #:nodoc:
# <tt>Person.find_all_by_last_name(last_name)</tt>.
#
# It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an
# <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records,
# <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records,
# like <tt>Person.find_by_last_name!</tt>.
#
# It's also possible to use multiple attributes in the same find by separating them with "_and_".
@@ -624,6 +624,8 @@ module ActiveRecord #:nodoc:
# Computes the table name, (re)sets it internally, and returns it.
def reset_table_name #:nodoc:
return if abstract_class?
self.table_name = compute_table_name
end
@@ -941,17 +943,6 @@ module ActiveRecord #:nodoc:
self.current_scope = nil
end
# Specifies how the record is loaded by +Marshal+.
#
# +_load+ sets an instance variable for each key in the hash it takes as input.
# Override this method if you require more complex marshalling.
def _load(data)
record = allocate
record.init_with(Marshal.load(data))
record
end
# Finder methods must instantiate through this method to work with the
# single-table inheritance model that makes it possible to create
# objects of different types from the same table.
@@ -1271,27 +1262,43 @@ MSG
self.default_scopes = default_scopes + [scope]
end
# The @ignore_default_scope flag is used to prevent an infinite recursion situation where
# a default scope references a scope which has a default scope which references a scope...
def build_default_scope #:nodoc:
return if defined?(@ignore_default_scope) && @ignore_default_scope
@ignore_default_scope = true
if method(:default_scope).owner != Base.singleton_class
default_scope
evaluate_default_scope { default_scope }
elsif default_scopes.any?
default_scopes.inject(relation) do |default_scope, scope|
if scope.is_a?(Hash)
default_scope.apply_finder_options(scope)
elsif !scope.is_a?(Relation) && scope.respond_to?(:call)
default_scope.merge(scope.call)
else
default_scope.merge(scope)
evaluate_default_scope do
default_scopes.inject(relation) do |default_scope, scope|
if scope.is_a?(Hash)
default_scope.apply_finder_options(scope)
elsif !scope.is_a?(Relation) && scope.respond_to?(:call)
default_scope.merge(scope.call)
else
default_scope.merge(scope)
end
end
end
end
ensure
@ignore_default_scope = false
end
def ignore_default_scope? #:nodoc:
Thread.current["#{self}_ignore_default_scope"]
end
def ignore_default_scope=(ignore) #:nodoc:
Thread.current["#{self}_ignore_default_scope"] = ignore
end
# The ignore_default_scope flag is used to prevent an infinite recursion situation where
# a default scope references a scope which has a default scope which references a scope...
def evaluate_default_scope
return if ignore_default_scope?
begin
self.ignore_default_scope = true
yield
ensure
self.ignore_default_scope = false
end
end
# Returns the class type of the record using the current module as a prefix. So descendants of
@@ -1413,9 +1420,8 @@ MSG
attrs = expand_hash_conditions_for_aggregates(attrs)
table = Arel::Table.new(table_name).alias(default_table_name)
viz = Arel::Visitors.for(arel_engine)
PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b|
viz.accept b
connection.visitor.accept b
}.join(' AND ')
end
alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
@@ -1593,16 +1599,6 @@ MSG
self
end
# Specifies how the record is dumped by +Marshal+.
#
# +_dump+ emits a marshalled hash which has been passed to +encode_with+. Override this
# method if you require more complex marshalling.
def _dump(level)
dump = {}
encode_with(dump)
Marshal.dump(dump)
end
# Returns a String, which Action Pack uses for constructing an URL to this
# object. The default implementation returns this record's id as a String,
# or nil if this record's unsaved.
@@ -1853,7 +1849,7 @@ MSG
ensure_proper_type
populate_with_current_scope_attributes
clear_timestamp_attributes
super
end
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
@@ -2117,14 +2113,6 @@ MSG
send("#{att}=", value) if respond_to?("#{att}=")
end
end
# Clear attributes and changed_attributes
def clear_timestamp_attributes
all_timestamp_attributes_in_model.each do |attribute_name|
self[attribute_name] = nil
changed_attributes.delete(attribute_name)
end
end
end
Base.class_eval do
@@ -2169,4 +2157,5 @@ MSG
end
end
require 'active_record/connection_adapters/abstract/connection_specification'
ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)

View File

@@ -82,10 +82,11 @@ module ActiveRecord
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
@connections = []
@checked_out = []
@connections = []
@checked_out = []
@automatic_reconnect = true
@tables = {}
@tables = {}
@visitor = nil
@columns = Hash.new do |h, table_name|
h[table_name] = with_connection do |conn|
@@ -298,8 +299,18 @@ module ActiveRecord
:connected?, :disconnect!, :with => :@connection_mutex
private
def new_connection
ActiveRecord::Base.send(spec.adapter_method, spec.config)
connection = ActiveRecord::Base.send(spec.adapter_method, spec.config)
# TODO: This is a bit icky, and in the long term we may want to change the method
# signature for connections. Also, if we switch to have one visitor per
# connection (and therefore per thread), we can get rid of the thread-local
# variable in Arel::Visitors::ToSql.
@visitor ||= connection.class.visitor_for(self)
connection.visitor = @visitor
connection
end
def current_connection_id #:nodoc:

View File

@@ -1,30 +1,39 @@
module ActiveRecord
module ConnectionAdapters # :nodoc:
module DatabaseStatements
# Converts an arel AST to SQL
def to_sql(arel)
if arel.respond_to?(:ast)
visitor.accept(arel.ast)
else
arel
end
end
# Returns an array of record hashes with the column names as keys and
# column values as values.
def select_all(sql, name = nil, binds = [])
select(sql, name, binds)
def select_all(arel, name = nil, binds = [])
select(to_sql(arel), name, binds)
end
# Returns a record hash with the column names as keys and column values
# as values.
def select_one(sql, name = nil)
result = select_all(sql, name)
def select_one(arel, name = nil)
result = select_all(arel, name)
result.first if result
end
# Returns a single value from a record
def select_value(sql, name = nil)
if result = select_one(sql, name)
def select_value(arel, name = nil)
if result = select_one(arel, name)
result.values.first
end
end
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
def select_values(sql, name = nil)
result = select_rows(sql, name)
def select_values(arel, name = nil)
result = select_rows(to_sql(arel), name)
result.map { |v| v[0] }
end
@@ -74,20 +83,20 @@ module ActiveRecord
#
# If the next id was calculated in advance (as in Oracle), it should be
# passed in as +id_value+.
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
sql, binds = sql_for_insert(sql, pk, id_value, sequence_name, binds)
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
sql, binds = sql_for_insert(to_sql(arel), pk, id_value, sequence_name, binds)
value = exec_insert(sql, name, binds)
id_value || last_inserted_id(value)
end
# Executes the update statement and returns the number of rows affected.
def update(sql, name = nil, binds = [])
exec_update(sql, name, binds)
def update(arel, name = nil, binds = [])
exec_update(to_sql(arel), name, binds)
end
# Executes the delete statement and returns the number of rows affected.
def delete(sql, name = nil, binds = [])
exec_delete(sql, name, binds)
def delete(arel, name = nil, binds = [])
exec_delete(to_sql(arel), name, binds)
end
# Checks whether there is currently no transaction active. This is done
@@ -297,6 +306,16 @@ module ActiveRecord
end
end
# The default strategy for an UPDATE with joins is to use a subquery. This doesn't work
# on mysql (even when aliasing the tables), but mysql allows using JOIN directly in
# an UPDATE statement, so in the mysql adapters we redefine this to do that.
def join_to_update(update, select) #:nodoc:
subselect = select.clone
subselect.projections = [update.key]
update.where update.key.in(subselect)
end
protected
# Returns an array of record hashes with the column names as keys and
# column values as values.

View File

@@ -55,9 +55,10 @@ module ActiveRecord
@query_cache.clear
end
def select_all(sql, name = nil, binds = [])
def select_all(arel, name = nil, binds = [])
if @query_cache_enabled
cache_sql(sql, binds) { super }
sql = to_sql(arel)
cache_sql(sql, binds) { super(sql, name, binds) }
else
super
end

View File

@@ -2,21 +2,33 @@ require 'date'
require 'bigdecimal'
require 'bigdecimal/util'
require 'active_support/core_ext/benchmark'
# TODO: Autoload these files
require 'active_record/connection_adapters/column'
require 'active_record/connection_adapters/abstract/schema_definitions'
require 'active_record/connection_adapters/abstract/schema_statements'
require 'active_record/connection_adapters/abstract/database_statements'
require 'active_record/connection_adapters/abstract/quoting'
require 'active_record/connection_adapters/abstract/connection_pool'
require 'active_record/connection_adapters/abstract/connection_specification'
require 'active_record/connection_adapters/abstract/query_cache'
require 'active_record/connection_adapters/abstract/database_limits'
require 'active_record/result'
require 'active_support/deprecation'
module ActiveRecord
module ConnectionAdapters # :nodoc:
extend ActiveSupport::Autoload
autoload :Column
autoload_under 'abstract' do
autoload :IndexDefinition, 'active_record/connection_adapters/abstract/schema_definitions'
autoload :ColumnDefinition, 'active_record/connection_adapters/abstract/schema_definitions'
autoload :TableDefinition, 'active_record/connection_adapters/abstract/schema_definitions'
autoload :Table, 'active_record/connection_adapters/abstract/schema_definitions'
autoload :SchemaStatements
autoload :DatabaseStatements
autoload :DatabaseLimits
autoload :Quoting
autoload :ConnectionPool
autoload :ConnectionHandler, 'active_record/connection_adapters/abstract/connection_pool'
autoload :ConnectionManagement, 'active_record/connection_adapters/abstract/connection_pool'
autoload :ConnectionSpecification
autoload :QueryCache
end
# Active Record supports multiple database systems. AbstractAdapter and
# related classes form the abstraction layer which makes this possible.
# An AbstractAdapter represents a connection to a database, and provides an
@@ -38,6 +50,8 @@ module ActiveRecord
define_callbacks :checkout, :checkin
attr_accessor :visitor
def initialize(connection, logger = nil) #:nodoc:
@active = nil
@connection, @logger = connection, logger
@@ -45,6 +59,25 @@ module ActiveRecord
@query_cache = Hash.new { |h,sql| h[sql] = {} }
@open_transactions = 0
@instrumenter = ActiveSupport::Notifications.instrumenter
@visitor = nil
end
# Returns a visitor instance for this adaptor, which conforms to the Arel::ToSql interface
def self.visitor_for(pool) # :nodoc:
adapter = pool.spec.config[:adapter]
if Arel::Visitors::VISITORS[adapter]
ActiveSupport::Deprecation.warn(
"Arel::Visitors::VISITORS is deprecated and will be removed. Database adapters " \
"should define a visitor_for method which returns the appropriate visitor for " \
"the database. For example, MysqlAdapter.visitor_for(pool) returns " \
"Arel::Visitors::MySQL.new(pool)."
)
Arel::Visitors::VISITORS[adapter].new(pool)
else
Arel::Visitors::ToSql.new(pool)
end
end
# Returns the human-readable name of the adapter. Use mixed case - one

View File

@@ -0,0 +1,611 @@
require 'active_support/core_ext/object/blank'
module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
class Column < ConnectionAdapters::Column # :nodoc:
def extract_default(default)
if sql_type =~ /blob/i || type == :text
if default.blank?
return null ? nil : ''
else
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
end
elsif missing_default_forged_as_empty_string?(default)
nil
else
super
end
end
def has_default?
return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
super
end
# Must return the relevant concrete adapter
def adapter
raise NotImplementedError
end
private
def simplified_type(field_type)
return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
case field_type
when /enum/i, /set/i then :string
when /year/i then :integer
when /bit/i then :binary
else
super
end
end
def extract_limit(sql_type)
case sql_type
when /blob|text/i
case sql_type
when /tiny/i
255
when /medium/i
16777215
when /long/i
2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
else
super # we could return 65535 here, but we leave it undecorated by default
end
when /^bigint/i; 8
when /^int/i; 4
when /^mediumint/i; 3
when /^smallint/i; 2
when /^tinyint/i; 1
else
super
end
end
# MySQL misreports NOT NULL column default when none is given.
# We can't detect this for columns which may have a legitimate ''
# default (string) but we can for others (integer, datetime, boolean,
# and the rest).
#
# Test whether the column has default '', is not null, and is not
# a type allowing default ''.
def missing_default_forged_as_empty_string?(default)
type != :string && !null && default == ''
end
end
##
# :singleton-method:
# By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt>
# as boolean. If you wish to disable this emulation (which was the default
# behavior in versions 0.13.1 and earlier) you can add the following line
# to your application.rb file:
#
# ActiveRecord::ConnectionAdapters::Mysql[2]Adapter.emulate_booleans = false
class_attribute :emulate_booleans
self.emulate_booleans = true
LOST_CONNECTION_ERROR_MESSAGES = [
"Server shutdown in progress",
"Broken pipe",
"Lost connection to MySQL server during query",
"MySQL server has gone away" ]
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
NATIVE_DATABASE_TYPES = {
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int", :limit => 4 },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "datetime" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "blob" },
:boolean => { :name => "tinyint", :limit => 1 }
}
# FIXME: Make the first parameter more similar for the two adapters
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
end
def self.visitor_for(pool) # :nodoc:
Arel::Visitors::MySQL.new(pool)
end
def adapter_name #:nodoc:
self.class::ADAPTER_NAME
end
# Returns true, since this connection adapter supports migrations.
def supports_migrations?
true
end
def supports_primary_key?
true
end
# Returns true, since this connection adapter supports savepoints.
def supports_savepoints?
true
end
def supports_bulk_alter? #:nodoc:
true
end
def native_database_types
NATIVE_DATABASE_TYPES
end
# HELPER METHODS ===========================================
# The two drivers have slightly different ways of yielding hashes of results, so
# this method must be implemented to provide a uniform interface.
def each_hash(result) # :nodoc:
raise NotImplementedError
end
# Overridden by the adapters to instantiate their specific Column type.
def new_column(field, default, type, null) # :nodoc:
Column.new(field, default, type, null)
end
# Must return the Mysql error number from the exception, if the exception has an
# error number.
def error_number(exception) # :nodoc:
raise NotImplementedError
end
# QUOTING ==================================================
def quote(value, column = nil)
if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
s = column.class.string_to_binary(value).unpack("H*")[0]
"x'#{s}'"
elsif value.kind_of?(BigDecimal)
value.to_s("F")
else
super
end
end
def quote_column_name(name) #:nodoc:
@quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`"
end
def quote_table_name(name) #:nodoc:
@quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
end
def quoted_true
QUOTED_TRUE
end
def quoted_false
QUOTED_FALSE
end
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity(&block) #:nodoc:
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
begin
update("SET FOREIGN_KEY_CHECKS = 0")
yield
ensure
update("SET FOREIGN_KEY_CHECKS = #{old}")
end
end
# DATABASE STATEMENTS ======================================
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
if name == :skip_logging
@connection.query(sql)
else
log(sql, name) { @connection.query(sql) }
end
rescue ActiveRecord::StatementInvalid => exception
if exception.message.split(":").first =~ /Packets out of order/
raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
else
raise
end
end
# MysqlAdapter has to free a result after using it, so we use this method to write
# stuff in a abstract way without concerning ourselves about whether it needs to be
# explicitly freed or not.
def execute_and_free(sql, name = nil) #:nodoc:
yield execute(sql, name)
end
def update_sql(sql, name = nil) #:nodoc:
super
@connection.affected_rows
end
def begin_db_transaction
execute "BEGIN"
rescue Exception
# Transactions aren't supported
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
rescue Exception
# Transactions aren't supported
end
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
rescue Exception
# 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
# In the simple case, MySQL allows us to place JOINs directly into the UPDATE
# query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
# these, we must use a subquery. However, MySQL is too stupid to create a
# temporary table for this automatically, so we have to give it some prompting
# in the form of a subsubquery. Ugh!
def join_to_update(update, select) #:nodoc:
if select.limit || select.offset || select.orders.any?
subsubselect = select.clone
subsubselect.projections = [update.key]
subselect = Arel::SelectManager.new(select.engine)
subselect.project Arel.sql(update.key.name)
subselect.from subsubselect.as('__active_record_temp')
update.where update.key.in(subselect)
else
update.table select.source
update.wheres = select.constraints
end
end
# SCHEMA STATEMENTS ========================================
def structure_dump #:nodoc:
if supports_views?
sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
else
sql = "SHOW TABLES"
end
select_all(sql).map do |table|
table.delete('Table_type')
sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
exec_without_stmt(sql).first['Create Table'] + ";\n\n"
end.join("")
end
# Drops the database specified on the +name+ attribute
# and creates it again using the provided +options+.
def recreate_database(name, options = {})
drop_database(name)
create_database(name, options)
end
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
# Charset defaults to utf8.
#
# Example:
# create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
# create_database 'matt_development'
# create_database 'matt_development', :charset => :big5
def create_database(name, options = {})
if options[:collation]
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
else
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
end
end
# Drops a MySQL database.
#
# Example:
# drop_database('sebastian_development')
def drop_database(name) #:nodoc:
execute "DROP DATABASE IF EXISTS `#{name}`"
end
def current_database
select_value 'SELECT DATABASE() as db'
end
# Returns the database character set.
def charset
show_variable 'character_set_database'
end
# Returns the database collation strategy.
def collation
show_variable 'collation_database'
end
def tables(name = nil, database = nil) #:nodoc:
sql = ["SHOW TABLES", database].compact.join(' IN ')
execute_and_free(sql, 'SCHEMA') do |result|
result.collect { |field| field.first }
end
end
def table_exists?(name)
return true if super
name = name.to_s
schema, table = name.split('.', 2)
unless table # A table was provided without a schema
table = schema
schema = nil
end
tables(nil, schema).include? table
end
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil) #:nodoc:
indexes = []
current_index = nil
execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result|
each_hash(result) do |row|
if current_index != row[:Key_name]
next if row[:Key_name] == 'PRIMARY' # skip the primary key
current_index = row[:Key_name]
indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [])
end
indexes.last.columns << row[:Column_name]
indexes.last.lengths << row[:Sub_part]
end
end
indexes
end
# Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name, name = nil)#:nodoc:
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
execute_and_free(sql, 'SCHEMA') do |result|
each_hash(result).map do |field|
new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES")
end
end
end
def create_table(table_name, options = {}) #:nodoc:
super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
end
def bulk_change_table(table_name, operations) #:nodoc:
sqls = operations.map do |command, args|
table, arguments = args.shift, args
method = :"#{command}_sql"
if respond_to?(method)
send(method, table, *arguments)
else
raise "Unknown method called : #{method}(#{arguments.inspect})"
end
end.flatten.join(", ")
execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
end
# Renames a table.
#
# Example:
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
end
def add_column(table_name, column_name, type, options = {})
execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}")
end
def change_column_default(table_name, column_name, default)
column = column_for(table_name, column_name)
change_column table_name, column_name, column.sql_type, :default => default
end
def change_column_null(table_name, column_name, null, default = nil)
column = column_for(table_name, column_name)
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
change_column table_name, column_name, column.sql_type, :null => null
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
end
# Maps logical Rails types to MySQL-specific data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
return super unless type.to_s == 'integer'
case limit
when 1; 'tinyint'
when 2; 'smallint'
when 3; 'mediumint'
when nil, 4, 11; 'int(11)' # compatibility with MySQL default
when 5..8; 'bigint'
else raise(ActiveRecordError, "No integer type has byte size #{limit}")
end
end
def add_column_position!(sql, options)
if options[:first]
sql << " FIRST"
elsif options[:after]
sql << " AFTER #{quote_column_name(options[:after])}"
end
end
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
variables = select_all("SHOW VARIABLES LIKE '#{name}'")
variables.first['Value'] unless variables.empty?
end
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table)
execute_and_free("DESCRIBE #{quote_table_name(table)}", 'SCHEMA') do |result|
keys = each_hash(result).select { |row| row[:Key] == 'PRI' }.map { |row| row[:Field] }
keys.length == 1 ? [keys.first, nil] : nil
end
end
# Returns just a table's primary key
def primary_key(table)
pk_and_sequence = pk_and_sequence_for(table)
pk_and_sequence && pk_and_sequence.first
end
def case_sensitive_modifier(node)
Arel::Nodes::Bin.new(node)
end
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
where_sql
end
protected
def quoted_columns_for_index(column_names, options = {})
length = options[:length] if options.is_a?(Hash)
case length
when Hash
column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
when Fixnum
column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
else
column_names.map {|name| quote_column_name(name) }
end
end
def translate_exception(exception, message)
case error_number(exception)
when 1062
RecordNotUnique.new(message, exception)
when 1452
InvalidForeignKey.new(message, exception)
else
super
end
end
def add_column_sql(table_name, column_name, type, options = {})
add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(add_column_sql, options)
add_column_position!(add_column_sql, options)
add_column_sql
end
def change_column_sql(table_name, column_name, type, options = {})
column = column_for(table_name, column_name)
unless options_include_default?(options)
options[:default] = column.default
end
unless options.has_key?(:null)
options[:null] = column.null
end
change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(change_column_sql, options)
add_column_position!(change_column_sql, options)
change_column_sql
end
def rename_column_sql(table_name, column_name, new_column_name)
options = {}
if column = columns(table_name).find { |c| c.name == column_name.to_s }
options[:default] = column.default
options[:null] = column.null
else
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
end
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
add_column_options!(rename_column_sql, options)
rename_column_sql
end
def remove_column_sql(table_name, *column_names)
columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" }
end
alias :remove_columns_sql :remove_column
def add_index_sql(table_name, column_name, options = {})
index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
"ADD #{index_type} INDEX #{index_name} (#{index_columns})"
end
def remove_index_sql(table_name, options = {})
index_name = index_name_for_remove(table_name, options)
"DROP INDEX #{index_name}"
end
def add_timestamps_sql(table_name)
[add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
end
def remove_timestamps_sql(table_name)
[remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
end
private
def supports_views?
version[0] >= 5
end
def column_for(table_name, column_name)
unless column = columns(table_name).find { |c| c.name == column_name.to_s }
raise "No such column: #{table_name}.#{column_name}"
end
column
end
end
end
end

View File

@@ -1,4 +1,4 @@
# encoding: utf-8
require 'active_record/connection_adapters/abstract_mysql_adapter'
gem 'mysql2', '~> 0.3.6'
require 'mysql2'
@@ -20,187 +20,51 @@ module ActiveRecord
end
module ConnectionAdapters
class Mysql2IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
end
class Mysql2Adapter < AbstractMysqlAdapter
class Mysql2Column < Column
BOOL = "tinyint(1)"
def extract_default(default)
if sql_type =~ /blob/i || type == :text
if default.blank?
return null ? nil : ''
else
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
end
elsif missing_default_forged_as_empty_string?(default)
nil
else
super
class Column < AbstractMysqlAdapter::Column # :nodoc:
def adapter
Mysql2Adapter
end
end
def has_default?
return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
super
end
private
def simplified_type(field_type)
return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL)
case field_type
when /enum/i, /set/i then :string
when /year/i then :integer
when /bit/i then :binary
else
super
end
end
def extract_limit(sql_type)
case sql_type
when /blob|text/i
case sql_type
when /tiny/i
255
when /medium/i
16777215
when /long/i
2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
else
super # we could return 65535 here, but we leave it undecorated by default
end
when /^bigint/i; 8
when /^int/i; 4
when /^mediumint/i; 3
when /^smallint/i; 2
when /^tinyint/i; 1
else
super
end
end
# MySQL misreports NOT NULL column default when none is given.
# We can't detect this for columns which may have a legitimate ''
# default (string) but we can for others (integer, datetime, boolean,
# and the rest).
#
# Test whether the column has default '', is not null, and is not
# a type allowing default ''.
def missing_default_forged_as_empty_string?(default)
type != :string && !null && default == ''
end
end
class Mysql2Adapter < AbstractAdapter
cattr_accessor :emulate_booleans
self.emulate_booleans = true
ADAPTER_NAME = 'Mysql2'
PRIMARY = "PRIMARY"
LOST_CONNECTION_ERROR_MESSAGES = [
"Server shutdown in progress",
"Broken pipe",
"Lost connection to MySQL server during query",
"MySQL server has gone away" ]
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
NATIVE_DATABASE_TYPES = {
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int", :limit => 4 },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "datetime" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "blob" },
:boolean => { :name => "tinyint", :limit => 1 }
}
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
super
configure_connection
end
def adapter_name
ADAPTER_NAME
end
# HELPER METHODS ===========================================
# Returns true, since this connection adapter supports migrations.
def supports_migrations?
true
end
def supports_primary_key?
true
end
# Returns true, since this connection adapter supports savepoints.
def supports_savepoints?
true
end
def native_database_types
NATIVE_DATABASE_TYPES
end
# QUOTING ==================================================
def quote(value, column = nil)
if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
s = column.class.string_to_binary(value).unpack("H*")[0]
"x'#{s}'"
elsif value.kind_of?(BigDecimal)
value.to_s("F")
def each_hash(result) # :nodoc:
if block_given?
result.each(:as => :hash, :symbolize_keys => true) do |row|
yield row
end
else
super
to_enum(:each_hash, result)
end
end
def quote_column_name(name) #:nodoc:
@quoted_column_names[name] ||= "`#{name}`"
def new_column(field, default, type, null) # :nodoc:
Column.new(field, default, type, null)
end
def quote_table_name(name) #:nodoc:
@quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
def error_number(exception)
exception.error_number if exception.respond_to?(:error_number)
end
# QUOTING ==================================================
def quote_string(string)
@connection.escape(string)
end
def quoted_true
QUOTED_TRUE
end
def quoted_false
QUOTED_FALSE
end
def substitute_at(column, index)
Arel.sql "\0"
end
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity(&block) #:nodoc:
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
begin
update("SET FOREIGN_KEY_CHECKS = 0")
yield
ensure
update("SET FOREIGN_KEY_CHECKS = #{old}")
end
end
# CONNECTION MANAGEMENT ====================================
def active?
@@ -213,11 +77,6 @@ module ActiveRecord
connect
end
# this is set to true in 2.3, but we don't want it to be
def requires_reloading?
false
end
# Disconnects from the database if already connected.
# Otherwise, this method does nothing.
def disconnect!
@@ -273,17 +132,22 @@ module ActiveRecord
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
# made since we established the connection
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
if name == :skip_logging
@connection.query(sql)
else
log(sql, name) { @connection.query(sql) }
end
rescue ActiveRecord::StatementInvalid => exception
if exception.message.split(":").first =~ /Packets out of order/
raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
else
raise
end
super
end
def exec_query(sql, name = 'SQL', binds = [])
result = execute(sql, name)
ActiveRecord::Result.new(result.fields, result.to_a)
end
alias exec_without_stmt exec_query
# Returns an array of record hashes with the column names as keys and
# column values as values.
def select(sql, name = nil, binds = [])
binds = binds.dup
exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a
end
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
@@ -312,358 +176,35 @@ module ActiveRecord
@connection.last_id
end
def update_sql(sql, name = nil)
super
@connection.affected_rows
end
def begin_db_transaction
execute "BEGIN"
rescue Exception
# Transactions aren't supported
end
def commit_db_transaction
execute "COMMIT"
rescue Exception
# Transactions aren't supported
end
def rollback_db_transaction
execute "ROLLBACK"
rescue Exception
# 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
# SCHEMA STATEMENTS ========================================
def structure_dump
if supports_views?
sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
else
sql = "SHOW TABLES"
end
select_all(sql).inject("") do |structure, table|
table.delete('Table_type')
structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
end
end
# Drops the database specified on the +name+ attribute
# and creates it again using the provided +options+.
def recreate_database(name, options = {})
drop_database(name)
create_database(name, options)
end
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
# Charset defaults to utf8.
#
# Example:
# create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
# create_database 'matt_development'
# create_database 'matt_development', :charset => :big5
def create_database(name, options = {})
if options[:collation]
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
else
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
end
end
# Drops a MySQL database.
#
# Example:
# drop_database('sebastian_development')
def drop_database(name) #:nodoc:
execute "DROP DATABASE IF EXISTS `#{name}`"
end
def current_database
select_value 'SELECT DATABASE() as db'
end
# Returns the database character set.
def charset
show_variable 'character_set_database'
end
# Returns the database collation strategy.
def collation
show_variable 'collation_database'
end
def tables(name = nil, database = nil) #:nodoc:
sql = ["SHOW TABLES", database].compact.join(' IN ')
execute(sql, 'SCHEMA').collect do |field|
field.first
end
end
def table_exists?(name)
return true if super
name = name.to_s
schema, table = name.split('.', 2)
unless table # A table was provided without a schema
table = schema
schema = nil
end
tables(nil, schema).include? table
end
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil)
indexes = []
current_index = nil
result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA')
result.each(:symbolize_keys => true, :as => :hash) do |row|
if current_index != row[:Key_name]
next if row[:Key_name] == PRIMARY # skip the primary key
current_index = row[:Key_name]
indexes << Mysql2IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], [])
end
indexes.last.columns << row[:Column_name]
indexes.last.lengths << row[:Sub_part]
end
indexes
end
# Returns an array of +Mysql2Column+ objects for the table specified by +table_name+.
def columns(table_name, name = nil)
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
columns = []
result = execute(sql, 'SCHEMA')
result.each(:symbolize_keys => true, :as => :hash) { |field|
columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES")
}
columns
end
def create_table(table_name, options = {})
super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
end
# Renames a table.
#
# Example:
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
end
def add_column(table_name, column_name, type, options = {})
add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(add_column_sql, options)
add_column_position!(add_column_sql, options)
execute(add_column_sql)
end
def change_column_default(table_name, column_name, default)
column = column_for(table_name, column_name)
change_column table_name, column_name, column.sql_type, :default => default
end
def change_column_null(table_name, column_name, null, default = nil)
column = column_for(table_name, column_name)
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
change_column table_name, column_name, column.sql_type, :null => null
end
def change_column(table_name, column_name, type, options = {})
column = column_for(table_name, column_name)
unless options_include_default?(options)
options[:default] = column.default
end
unless options.has_key?(:null)
options[:null] = column.null
end
change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(change_column_sql, options)
add_column_position!(change_column_sql, options)
execute(change_column_sql)
end
def rename_column(table_name, column_name, new_column_name)
options = {}
if column = columns(table_name).find { |c| c.name == column_name.to_s }
options[:default] = column.default
options[:null] = column.null
else
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
end
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
add_column_options!(rename_column_sql, options)
execute(rename_column_sql)
end
# Maps logical Rails types to MySQL-specific data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
return super unless type.to_s == 'integer'
case limit
when 1; 'tinyint'
when 2; 'smallint'
when 3; 'mediumint'
when nil, 4, 11; 'int(11)' # compatibility with MySQL default
when 5..8; 'bigint'
else raise(ActiveRecordError, "No integer type has byte size #{limit}")
end
end
def add_column_position!(sql, options)
if options[:first]
sql << " FIRST"
elsif options[:after]
sql << " AFTER #{quote_column_name(options[:after])}"
end
end
# SHOW VARIABLES LIKE 'name'.
def show_variable(name)
variables = select_all("SHOW VARIABLES LIKE '#{name}'")
variables.first['Value'] unless variables.empty?
end
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table)
keys = []
result = execute("DESCRIBE #{quote_table_name(table)}", 'SCHEMA')
result.each(:symbolize_keys => true, :as => :hash) do |row|
keys << row[:Field] if row[:Key] == "PRI"
end
keys.length == 1 ? [keys.first, nil] : nil
end
# Returns just a table's primary key
def primary_key(table)
pk_and_sequence = pk_and_sequence_for(table)
pk_and_sequence && pk_and_sequence.first
end
def case_sensitive_modifier(node)
Arel::Nodes::Bin.new(node)
end
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
where_sql
end
protected
def quoted_columns_for_index(column_names, options = {})
length = options[:length] if options.is_a?(Hash)
case length
when Hash
column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
when Fixnum
column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
else
column_names.map {|name| quote_column_name(name) }
end
end
def translate_exception(exception, message)
return super unless exception.respond_to?(:error_number)
case exception.error_number
when 1062
RecordNotUnique.new(message, exception)
when 1452
InvalidForeignKey.new(message, exception)
else
super
end
end
private
def connect
@connection = Mysql2::Client.new(@config)
configure_connection
end
def configure_connection
@connection.query_options.merge!(:as => :array)
def connect
@connection = Mysql2::Client.new(@config)
configure_connection
end
# By default, MySQL 'where id is null' selects the last inserted id.
# Turn this off. http://dev.rubyonrails.org/ticket/6778
variable_assignments = ['SQL_AUTO_IS_NULL=0']
encoding = @config[:encoding]
def configure_connection
@connection.query_options.merge!(:as => :array)
# make sure we set the encoding
variable_assignments << "NAMES '#{encoding}'" if encoding
# By default, MySQL 'where id is null' selects the last inserted id.
# Turn this off. http://dev.rubyonrails.org/ticket/6778
variable_assignments = ['SQL_AUTO_IS_NULL=0']
encoding = @config[:encoding]
# increase timeout so mysql server doesn't disconnect us
wait_timeout = @config[:wait_timeout]
wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum)
variable_assignments << "@@wait_timeout = #{wait_timeout}"
# make sure we set the encoding
variable_assignments << "NAMES '#{encoding}'" if encoding
execute("SET #{variable_assignments.join(', ')}", :skip_logging)
end
# increase timeout so mysql server doesn't disconnect us
wait_timeout = @config[:wait_timeout]
wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum)
variable_assignments << "@@wait_timeout = #{wait_timeout}"
# Returns an array of record hashes with the column names as keys and
# column values as values.
def select(sql, name = nil, binds = [])
binds = binds.dup
exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a
end
execute("SET #{variable_assignments.join(', ')}", :skip_logging)
end
def exec_query(sql, name = 'SQL', binds = [])
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
log(sql, name, binds) do
begin
result = @connection.query(sql)
rescue ActiveRecord::StatementInvalid => exception
if exception.message.split(":").first =~ /Packets out of order/
raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
else
raise
end
end
ActiveRecord::Result.new(result.fields, result.to_a)
end
end
def supports_views?
version[0] >= 5
end
def version
@version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
def column_for(table_name, column_name)
unless column = columns(table_name).find { |c| c.name == column_name.to_s }
raise "No such column: #{table_name}.#{column_name}"
end
column
end
def version
@version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
end
end
end

View File

@@ -1,6 +1,5 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'active_support/core_ext/object/blank'
require 'set'
require 'active_record/connection_adapters/abstract_mysql_adapter'
require 'active_support/core_ext/hash/keys'
gem 'mysql', '~> 2.8.1'
require 'mysql'
@@ -40,92 +39,6 @@ module ActiveRecord
end
module ConnectionAdapters
class MysqlColumn < Column #:nodoc:
class << self
def string_to_time(value)
return super unless Mysql::Time === value
new_time(
value.year,
value.month,
value.day,
value.hour,
value.minute,
value.second,
value.second_part)
end
def string_to_dummy_time(v)
return super unless Mysql::Time === v
new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part)
end
def string_to_date(v)
return super unless Mysql::Time === v
new_date(v.year, v.month, v.day)
end
end
def extract_default(default)
if sql_type =~ /blob/i || type == :text
if default.blank?
return null ? nil : ''
else
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
end
elsif missing_default_forged_as_empty_string?(default)
nil
else
super
end
end
def has_default?
return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
super
end
private
def simplified_type(field_type)
return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
return :string if field_type =~ /enum/i
super
end
def extract_limit(sql_type)
case sql_type
when /blob|text/i
case sql_type
when /tiny/i
255
when /medium/i
16777215
when /long/i
2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
else
super # we could return 65535 here, but we leave it undecorated by default
end
when /^bigint/i; 8
when /^int/i; 4
when /^mediumint/i; 3
when /^smallint/i; 2
when /^tinyint/i; 1
else
super
end
end
# MySQL misreports NOT NULL column default when none is given.
# We can't detect this for columns which may have a legitimate ''
# default (string) but we can for others (integer, datetime, boolean,
# and the rest).
#
# Test whether the column has default '', is not null, and is not
# a type allowing default ''.
def missing_default_forged_as_empty_string?(default)
type != :string && !null && default == ''
end
end
# The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
# the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
#
@@ -145,139 +58,84 @@ module ActiveRecord
# * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
#
class MysqlAdapter < AbstractAdapter
class MysqlAdapter < AbstractMysqlAdapter
##
# :singleton-method:
# By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt>
# as boolean. If you wish to disable this emulation (which was the default
# behavior in versions 0.13.1 and earlier) you can add the following line
# to your application.rb file:
#
# ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
cattr_accessor :emulate_booleans
self.emulate_booleans = true
class Column < AbstractMysqlAdapter::Column #:nodoc:
def self.string_to_time(value)
return super unless Mysql::Time === value
new_time(
value.year,
value.month,
value.day,
value.hour,
value.minute,
value.second,
value.second_part)
end
def self.string_to_dummy_time(v)
return super unless Mysql::Time === v
new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part)
end
def self.string_to_date(v)
return super unless Mysql::Time === v
new_date(v.year, v.month, v.day)
end
def adapter
MysqlAdapter
end
end
ADAPTER_NAME = 'MySQL'
LOST_CONNECTION_ERROR_MESSAGES = [
"Server shutdown in progress",
"Broken pipe",
"Lost connection to MySQL server during query",
"MySQL server has gone away" ]
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
NATIVE_DATABASE_TYPES = {
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int", :limit => 4 },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
:timestamp => { :name => "datetime" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "blob" },
:boolean => { :name => "tinyint", :limit => 1 }
}
def initialize(connection, logger, connection_options, config)
super(connection, logger)
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
super
@statements = {}
@client_encoding = nil
connect
end
def adapter_name #:nodoc:
ADAPTER_NAME
end
def supports_bulk_alter? #:nodoc:
true
end
# Returns true, since this connection adapter supports prepared statement
# caching.
def supports_statement_cache?
true
end
# Returns true, since this connection adapter supports migrations.
def supports_migrations? #:nodoc:
true
end
# HELPER METHODS ===========================================
# Returns true.
def supports_primary_key? #:nodoc:
true
end
# Returns true, since this connection adapter supports savepoints.
def supports_savepoints? #:nodoc:
true
end
def native_database_types #:nodoc:
NATIVE_DATABASE_TYPES
end
# QUOTING ==================================================
def quote(value, column = nil)
if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
s = column.class.string_to_binary(value).unpack("H*")[0]
"x'#{s}'"
elsif value.kind_of?(BigDecimal)
value.to_s("F")
def each_hash(result) # :nodoc:
if block_given?
result.each_hash do |row|
row.symbolize_keys!
yield row
end
else
super
to_enum(:each_hash, result)
end
end
def new_column(field, default, type, null) # :nodoc:
Column.new(field, default, type, null)
end
def error_number(exception) # :nodoc:
exception.errno if exception.respond_to?(:errno)
end
# QUOTING ==================================================
def type_cast(value, column)
return super unless value == true || value == false
value ? 1 : 0
end
def quote_column_name(name) #:nodoc:
@quoted_column_names[name] ||= "`#{name}`"
end
def quote_table_name(name) #:nodoc:
@quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
end
def quote_string(string) #:nodoc:
@connection.quote(string)
end
def quoted_true
QUOTED_TRUE
end
def quoted_false
QUOTED_FALSE
end
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity #:nodoc:
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
begin
update("SET FOREIGN_KEY_CHECKS = 0")
yield
ensure
update("SET FOREIGN_KEY_CHECKS = #{old}")
end
end
# CONNECTION MANAGEMENT ====================================
def active?
@@ -421,20 +279,11 @@ module ActiveRecord
end
end
# Executes an SQL query and returns a MySQL::Result object. Note that you have to free
# the Result object after you're done using it.
def execute(sql, name = nil) #:nodoc:
if name == :skip_logging
@connection.query(sql)
else
log(sql, name) { @connection.query(sql) }
end
rescue ActiveRecord::StatementInvalid => exception
if exception.message.split(":").first =~ /Packets out of order/
raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
else
raise
end
def execute_and_free(sql, name = nil)
result = execute(sql, name)
ret = yield result
result.free
ret
end
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
@@ -443,11 +292,6 @@ module ActiveRecord
end
alias :create :insert_sql
def update_sql(sql, name = nil) #:nodoc:
super
@connection.affected_rows
end
def exec_delete(sql, name, binds)
log(sql, name, binds) do
exec_stmt(sql, name, binds) do |cols, stmt|
@@ -463,337 +307,8 @@ module ActiveRecord
# Transactions aren't supported
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
rescue Exception
# Transactions aren't supported
end
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
rescue Exception
# 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
# SCHEMA STATEMENTS ========================================
def structure_dump #:nodoc:
if supports_views?
sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
else
sql = "SHOW TABLES"
end
select_all(sql).map do |table|
table.delete('Table_type')
sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
exec_without_stmt(sql).first['Create Table'] + ";\n\n"
end.join("")
end
# Drops the database specified on the +name+ attribute
# and creates it again using the provided +options+.
def recreate_database(name, options = {}) #:nodoc:
drop_database(name)
create_database(name, options)
end
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
# Charset defaults to utf8.
#
# Example:
# create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
# create_database 'matt_development'
# create_database 'matt_development', :charset => :big5
def create_database(name, options = {})
if options[:collation]
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
else
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
end
end
# Drops a MySQL database.
#
# Example:
# drop_database 'sebastian_development'
def drop_database(name) #:nodoc:
execute "DROP DATABASE IF EXISTS `#{name}`"
end
def current_database
select_value 'SELECT DATABASE() as db'
end
# Returns the database character set.
def charset
show_variable 'character_set_database'
end
# Returns the database collation strategy.
def collation
show_variable 'collation_database'
end
def tables(name = nil, database = nil) #:nodoc:
result = execute(["SHOW TABLES", database].compact.join(' IN '), 'SCHEMA')
tables = result.collect { |field| field[0] }
result.free
tables
end
def table_exists?(name)
return true if super
name = name.to_s
schema, table = name.split('.', 2)
unless table # A table was provided without a schema
table = schema
schema = nil
end
tables(nil, schema).include? table
end
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil)#:nodoc:
indexes = []
current_index = nil
result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name)
result.each do |row|
if current_index != row[2]
next if row[2] == "PRIMARY" # skip the primary key
current_index = row[2]
indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [], [])
end
indexes.last.columns << row[4]
indexes.last.lengths << row[7]
end
result.free
indexes
end
# Returns an array of +MysqlColumn+ objects for the table specified by +table_name+.
def columns(table_name, name = nil)#:nodoc:
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
result = execute(sql, 'SCHEMA')
columns = result.collect { |field| MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
result.free
columns
end
def create_table(table_name, options = {}) #:nodoc:
super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
end
# Renames a table.
#
# Example:
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
end
def bulk_change_table(table_name, operations) #:nodoc:
sqls = operations.map do |command, args|
table, arguments = args.shift, args
method = :"#{command}_sql"
if respond_to?(method)
send(method, table, *arguments)
else
raise "Unknown method called : #{method}(#{arguments.inspect})"
end
end.flatten.join(", ")
execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
end
def add_column(table_name, column_name, type, options = {})
execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}")
end
def change_column_default(table_name, column_name, default) #:nodoc:
column = column_for(table_name, column_name)
change_column table_name, column_name, column.sql_type, :default => default
end
def change_column_null(table_name, column_name, null, default = nil)
column = column_for(table_name, column_name)
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
change_column table_name, column_name, column.sql_type, :null => null
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
end
# Maps logical Rails types to MySQL-specific data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
return super unless type.to_s == 'integer'
case limit
when 1; 'tinyint'
when 2; 'smallint'
when 3; 'mediumint'
when nil, 4, 11; 'int(11)' # compatibility with MySQL default
when 5..8; 'bigint'
else raise(ActiveRecordError, "No integer type has byte size #{limit}")
end
end
def add_column_position!(sql, options)
if options[:first]
sql << " FIRST"
elsif options[:after]
sql << " AFTER #{quote_column_name(options[:after])}"
end
end
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
variables = select_all("SHOW VARIABLES LIKE '#{name}'")
variables.first['Value'] unless variables.empty?
end
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table) #:nodoc:
keys = []
result = execute("describe #{quote_table_name(table)}", 'SCHEMA')
result.each_hash do |h|
keys << h["Field"]if h["Key"] == "PRI"
end
result.free
keys.length == 1 ? [keys.first, nil] : nil
end
# Returns just a table's primary key
def primary_key(table)
pk_and_sequence = pk_and_sequence_for(table)
pk_and_sequence && pk_and_sequence.first
end
def case_sensitive_modifier(node)
Arel::Nodes::Bin.new(node)
end
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
where_sql
end
protected
def quoted_columns_for_index(column_names, options = {})
length = options[:length] if options.is_a?(Hash)
case length
when Hash
column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
when Fixnum
column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
else
column_names.map {|name| quote_column_name(name) }
end
end
def translate_exception(exception, message)
return super unless exception.respond_to?(:errno)
case exception.errno
when 1062
RecordNotUnique.new(message, exception)
when 1452
InvalidForeignKey.new(message, exception)
else
super
end
end
def add_column_sql(table_name, column_name, type, options = {})
add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(add_column_sql, options)
add_column_position!(add_column_sql, options)
add_column_sql
end
def remove_column_sql(table_name, *column_names)
columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" }
end
alias :remove_columns_sql :remove_column
def change_column_sql(table_name, column_name, type, options = {})
column = column_for(table_name, column_name)
unless options_include_default?(options)
options[:default] = column.default
end
unless options.has_key?(:null)
options[:null] = column.null
end
change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(change_column_sql, options)
add_column_position!(change_column_sql, options)
change_column_sql
end
def rename_column_sql(table_name, column_name, new_column_name)
options = {}
if column = columns(table_name).find { |c| c.name == column_name.to_s }
options[:default] = column.default
options[:null] = column.null
else
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
end
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
add_column_options!(rename_column_sql, options)
rename_column_sql
end
def add_index_sql(table_name, column_name, options = {})
index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
"ADD #{index_type} INDEX #{index_name} (#{index_columns})"
end
def remove_index_sql(table_name, options = {})
index_name = index_name_for_remove(table_name, options)
"DROP INDEX #{index_name}"
end
def add_timestamps_sql(table_name)
[add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
end
def remove_timestamps_sql(table_name)
[remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
end
private
def exec_stmt(sql, name, binds)
cache = {}
if binds.empty?
@@ -805,7 +320,6 @@ module ActiveRecord
stmt = cache[:stmt]
end
begin
stmt.execute(*binds.map { |col, val| type_cast(val, col) })
rescue Mysql::Error => e
@@ -834,59 +348,48 @@ module ActiveRecord
result
end
def connect
encoding = @config[:encoding]
if encoding
@connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
end
if @config[:sslca] || @config[:sslkey]
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
end
@connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout]
@connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout]
@connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout]
@connection.real_connect(*@connection_options)
# reconnect must be set after real_connect is called, because real_connect sets it to false internally
@connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=)
configure_connection
def connect
encoding = @config[:encoding]
if encoding
@connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
end
def configure_connection
encoding = @config[:encoding]
execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
# By default, MySQL 'where id is null' selects the last inserted id.
# Turn this off. http://dev.rubyonrails.org/ticket/6778
execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
if @config[:sslca] || @config[:sslkey]
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
end
def select(sql, name = nil, binds = [])
@connection.query_with_result = true
rows = exec_query(sql, name, binds).to_a
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
rows
end
@connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout]
@connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout]
@connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout]
def supports_views?
version[0] >= 5
end
@connection.real_connect(*@connection_options)
# Returns the version of the connected MySQL server.
def version
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
# reconnect must be set after real_connect is called, because real_connect sets it to false internally
@connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=)
def column_for(table_name, column_name)
unless column = columns(table_name).find { |c| c.name == column_name.to_s }
raise "No such column: #{table_name}.#{column_name}"
end
column
end
configure_connection
end
def configure_connection
encoding = @config[:encoding]
execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
# By default, MySQL 'where id is null' selects the last inserted id.
# Turn this off. http://dev.rubyonrails.org/ticket/6778
execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
end
def select(sql, name = nil, binds = [])
@connection.query_with_result = true
rows = exec_query(sql, name, binds).to_a
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
rows
end
# Returns the version of the connected MySQL server.
def version
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
end
end
end

View File

@@ -265,6 +265,10 @@ module ActiveRecord
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
end
def self.visitor_for(pool) # :nodoc:
Arel::Visitors::PostgreSQL.new(pool)
end
# Clears the prepared statements cache.
def clear_cache!
@statements.each_value do |value|
@@ -950,6 +954,8 @@ module ActiveRecord
end
module Utils
extend self
# Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+.
# +schema_name+ is nil if not specified in +name+.
# +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+)
@@ -960,7 +966,7 @@ module ActiveRecord
# * <tt>schema_name.table_name</tt>
# * <tt>schema_name."table.name"</tt>
# * <tt>"schema.name"."table name"</tt>
def self.extract_schema_and_table(name)
def extract_schema_and_table(name)
table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse
[schema, table]
end

View File

@@ -1,4 +1,6 @@
require 'active_record/connection_adapters/sqlite_adapter'
gem 'sqlite3', '~> 1.3.4'
require 'sqlite3'
module ActiveRecord

View File

@@ -53,6 +53,10 @@ module ActiveRecord
@config = config
end
def self.visitor_for(pool) # :nodoc:
Arel::Visitors::SQLite.new(pool)
end
def adapter_name #:nodoc:
'SQLite'
end
@@ -144,7 +148,7 @@ module ActiveRecord
end
def quote_column_name(name) #:nodoc:
%Q("#{name}")
%Q("#{name.to_s.gsub('"', '""')}")
end
# Quote date/time values for use in SQL input. Includes microseconds
@@ -157,10 +161,25 @@ module ActiveRecord
end
end
def type_cast(value, column) # :nodoc:
return super unless BigDecimal === value
if "<3".encoding_aware?
def type_cast(value, column) # :nodoc:
return value.to_f if BigDecimal === value
return super unless String === value
return super unless column && value
value.to_f
value = super
if column.type == :string && value.encoding == Encoding::ASCII_8BIT
@logger.error "Binary data inserted for `string` type on column `#{column.name}`"
value.encode! 'utf-8'
end
value
end
else
def type_cast(value, column) # :nodoc:
return super unless BigDecimal === value
value.to_f
end
end
# DATABASE STATEMENTS ======================================
@@ -238,15 +257,15 @@ module ActiveRecord
end
def begin_db_transaction #:nodoc:
@connection.transaction
log('begin transaction',nil) { @connection.transaction }
end
def commit_db_transaction #:nodoc:
@connection.commit
log('commit transaction',nil) { @connection.commit }
end
def rollback_db_transaction #:nodoc:
@connection.rollback
log('rollback transaction',nil) { @connection.rollback }
end
# SCHEMA STATEMENTS ========================================

View File

@@ -33,7 +33,7 @@ module ActiveRecord
stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
arel_table[counter_name] => object.send(association).count
})
connection.update stmt.to_sql
connection.update stmt
end
return true
end

View File

@@ -70,7 +70,7 @@ module ActiveRecord
# If the locking column has no default value set,
# start the lock version at zero. Note we can't use
# <tt>locking_enabled?</tt> at this point as
# <tt>locking_enabled?</tt> at this point as
# <tt>@attributes</tt> may not have been initialized yet.
if result.key?(self.class.locking_column) && lock_optimistically
@@ -100,7 +100,7 @@ module ActiveRecord
)
).arel.compile_update(arel_attributes_values(false, false, attribute_names))
affected_rows = connection.update stmt.to_sql
affected_rows = connection.update stmt
unless affected_rows == 1
raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}"

View File

@@ -563,7 +563,7 @@ module ActiveRecord
def get_all_versions
table = Arel::Table.new(schema_migrations_table_name)
Base.connection.select_values(table.project(table['version']).to_sql).map{ |v| v.to_i }.sort
Base.connection.select_values(table.project(table['version'])).map{ |v| v.to_i }.sort
end
def current_version
@@ -720,11 +720,11 @@ module ActiveRecord
if down?
@migrated_versions.delete(version)
stmt = table.where(table["version"].eq(version.to_s)).compile_delete
Base.connection.delete stmt.to_sql
Base.connection.delete stmt
else
@migrated_versions.push(version).sort!
stmt = table.compile_insert table["version"] => version.to_s
Base.connection.insert stmt.to_sql
Base.connection.insert stmt
end
end

View File

@@ -111,7 +111,7 @@ module ActiveRecord
callback_meth = :"_notify_#{observer_name}_for_#{callback}"
unless klass.respond_to?(callback_meth)
klass.send(:define_method, callback_meth) do |&block|
observer.send(callback, self, &block)
observer.update(callback, self, &block)
end
klass.send(callback, callback_meth)
end

View File

@@ -304,7 +304,7 @@ module ActiveRecord
return 0 if attributes_with_values.empty?
klass = self.class
stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values)
klass.connection.update stmt.to_sql
klass.connection.update stmt
end
# Creates a record with values matching those of the instance attributes

View File

@@ -61,6 +61,12 @@ module ActiveRecord
status, headers, body = @app.call(env)
[status, headers, BodyProxy.new(old, body)]
rescue Exception => e
ActiveRecord::Base.connection.clear_query_cache
unless old
ActiveRecord::Base.connection.disable_query_cache!
end
raise e
end
end
end

View File

@@ -94,7 +94,7 @@ db_namespace = namespace :db do
"IDENTIFIED BY '#{config['password']}' WITH GRANT OPTION;"
ActiveRecord::Base.establish_connection(config.merge(
'database' => nil, 'username' => 'root', 'password' => root_password))
ActiveRecord::Base.connection.create_database(config['database'], creation_options)
ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config))
ActiveRecord::Base.connection.execute grant_statement
ActiveRecord::Base.establish_connection(config)
else

View File

@@ -68,7 +68,7 @@ module ActiveRecord
end
conn.insert(
im.to_sql,
im,
'SQL',
primary_key,
primary_key_value,
@@ -108,10 +108,10 @@ module ActiveRecord
if default_scoped.equal?(self)
@records = if @readonly_value.nil? && !@klass.locking_enabled?
eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values)
eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values)
else
IdentityMap.without do
eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values)
eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values)
end
end
@@ -216,15 +216,21 @@ module ActiveRecord
if conditions || options.present?
where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates)
else
stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)))
stmt = Arel::UpdateManager.new(arel.engine)
if limit = arel.limit
stmt.take limit
stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
stmt.table(table)
stmt.key = table[primary_key]
if joins_values.any?
@klass.connection.join_to_update(stmt, arel)
else
stmt.take(arel.limit)
stmt.order(*arel.orders)
stmt.wheres = arel.constraints
end
stmt.order(*arel.orders)
stmt.key = table[primary_key]
@klass.connection.update stmt.to_sql, 'SQL', bind_values
@klass.connection.update stmt, 'SQL', bind_values
end
end
@@ -341,8 +347,7 @@ module ActiveRecord
where(conditions).delete_all
else
statement = arel.compile_delete
affected = @klass.connection.delete(
statement.to_sql, 'SQL', bind_values)
affected = @klass.connection.delete(statement, 'SQL', bind_values)
reset
affected
@@ -388,7 +393,7 @@ module ActiveRecord
end
def to_sql
@to_sql ||= arel.to_sql
@to_sql ||= klass.connection.to_sql(arel)
end
def where_values_hash

View File

@@ -223,7 +223,7 @@ module ActiveRecord
query_builder = relation.arel
end
type_cast_calculated_value(@klass.connection.select_value(query_builder.to_sql), column_for(column_name), operation)
type_cast_calculated_value(@klass.connection.select_value(query_builder), column_for(column_name), operation)
end
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
@@ -259,7 +259,7 @@ module ActiveRecord
relation = except(:group).group(group.join(','))
relation.select_values = select_values
calculated_data = @klass.connection.select_all(relation.to_sql)
calculated_data = @klass.connection.select_all(relation)
if association
key_ids = calculated_data.collect { |row| row[group_aliases.first] }

View File

@@ -193,8 +193,8 @@ module ActiveRecord
else
relation = relation.where(table[primary_key].eq(id)) if id
end
connection.select_value(relation.to_sql, "#{name} Exists") ? true : false
connection.select_value(relation, "#{name} Exists") ? true : false
end
protected
@@ -202,7 +202,7 @@ module ActiveRecord
def find_with_associations
join_dependency = construct_join_dependency_for_association_find
relation = construct_relation_for_association_find(join_dependency)
rows = connection.select_all(relation.to_sql, 'SQL', relation.bind_values)
rows = connection.select_all(relation, 'SQL', relation.bind_values)
join_dependency.instantiate(rows)
rescue ThrowResult
[]

View File

@@ -19,7 +19,7 @@ module ActiveRecord
case value
when ActiveRecord::Relation
value.select_values = [value.klass.arel_table['id']] if value.select_values.empty?
value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
attribute.in(value.arel.ast)
when Array, ActiveRecord::Associations::CollectionProxy
values = value.to_a.map { |x|

View File

@@ -254,12 +254,12 @@ module ActiveRecord
association_joins = buckets['association_join'] || []
stashed_association_joins = buckets['stashed_join'] || []
join_nodes = buckets['join_node'] || []
join_nodes = (buckets['join_node'] || []).uniq
string_joins = (buckets['string_join'] || []).map { |x|
x.strip
}.uniq
join_list = custom_join_ast(manager, string_joins)
join_list = join_nodes + custom_join_ast(manager, string_joins)
join_dependency = ActiveRecord::Associations::JoinDependency.new(
@klass,
@@ -267,10 +267,6 @@ module ActiveRecord
join_list
)
join_nodes.each do |join|
join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase)
end
join_dependency.graft(*stashed_association_joins)
@implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
@@ -280,7 +276,6 @@ module ActiveRecord
association.join_to(manager)
end
manager.join_sources.concat join_nodes.uniq
manager.join_sources.concat join_list
manager

View File

@@ -37,6 +37,10 @@ module ActiveRecord
self.record_timestamps = true
end
def initialize_dup(other)
clear_timestamp_attributes
end
private
def create #:nodoc:
@@ -95,6 +99,13 @@ module ActiveRecord
def current_time_from_proper_timezone #:nodoc:
self.class.default_timezone == :utc ? Time.now.utc : Time.now
end
# Clear attributes and changed_attributes
def clear_timestamp_attributes
all_timestamp_attributes_in_model.each do |attribute_name|
self[attribute_name] = nil
changed_attributes.delete(attribute_name)
end
end
end
end

View File

@@ -151,7 +151,20 @@ class AdapterTest < ActiveRecord::TestCase
else
@connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)"
end
# should deleted created record as otherwise disable_referential_integrity will try to enable contraints after executed block
# and will fail (at least on Oracle)
@connection.execute "DELETE FROM fk_test_has_fk"
end
end
end
def test_deprecated_visitor_for
visitor_klass = Class.new(Arel::Visitors::ToSql)
Arel::Visitors::VISITORS['fuuu'] = visitor_klass
pool = stub(:spec => stub(:config => { :adapter => 'fuuu' }))
visitor = assert_deprecated {
ActiveRecord::ConnectionAdapters::AbstractAdapter.visitor_for(pool)
}
assert visitor.is_a?(visitor_klass)
end
end

View File

@@ -2,7 +2,7 @@ require "cases/helper"
class ActiveSchemaTest < ActiveRecord::TestCase
def setup
ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
alias_method :execute_without_stub, :execute
remove_method :execute
def execute(sql, name = nil) return sql end
@@ -10,7 +10,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
end
def teardown
ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
remove_method :execute
alias_method :execute, :execute_without_stub
end
@@ -99,7 +99,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
private
def with_real_execute
#we need to actually modify some data, so we make execute point to the original method
ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
alias_method :execute_with_stub, :execute
remove_method :execute
alias_method :execute, :execute_without_stub
@@ -107,7 +107,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
yield
ensure
#before finishing, we restore the alias to the mock-up method
ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
remove_method :execute
alias_method :execute, :execute_with_stub
end

View File

@@ -219,21 +219,6 @@ class SchemaTest < ActiveRecord::TestCase
end
end
def test_extract_schema_and_table
{
%(table_name) => [nil,'table_name'],
%("table.name") => [nil,'table.name'],
%(schema.table_name) => %w{schema table_name},
%("schema".table_name) => %w{schema table_name},
%(schema."table_name") => %w{schema table_name},
%("schema"."table_name") => %w{schema table_name},
%("even spaces".table) => ['even spaces','table'],
%(schema."table.name") => ['schema', 'table.name']
}.each do |given, expect|
assert_equal expect, ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils.extract_schema_and_table(given)
end
end
def test_current_schema
{
%('$user',public) => 'public',

Some files were not shown because too many files have changed in this diff Show More