mirror of
https://github.com/github/rails.git
synced 2026-04-26 03:00:59 -04:00
Merge branch 'master' of git://github.com/rails/rails
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,4 +21,5 @@ railties/doc
|
||||
railties/guides/output
|
||||
railties/tmp
|
||||
.rvmrc
|
||||
.rbenv-version
|
||||
RDOC_MAIN.rdoc
|
||||
|
||||
@@ -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
|
||||
|
||||
9
Gemfile
9
Gemfile
@@ -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
|
||||
|
||||
12
README.rdoc
12
README.rdoc
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
10
Rakefile
10
Rakefile
@@ -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')
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
# @url = root_path # named route from the application.
|
||||
# end
|
||||
# end
|
||||
# =>
|
||||
#
|
||||
module ActionController
|
||||
module UrlFor
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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!] = '
'
|
||||
else
|
||||
JS_ESCAPE_MAP["\342\200\250"] = '
'
|
||||
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
|
||||
''
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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 = [])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
183
actionpack/test/controller/url_for_integration_test.rb
Normal file
183
actionpack/test/controller/url_for_integration_test.rb
Normal 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
|
||||
@@ -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)
|
||||
|
||||
1
actionpack/test/fixtures/test/hello_w*rld.erb
vendored
Normal file
1
actionpack/test/fixtures/test/hello_w*rld.erb
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Hello w*rld!
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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 
 newline), escape_javascript(%(unicode \342\200\250 newline).force_encoding('UTF-8').encode!)
|
||||
else
|
||||
assert_equal %(unicode 
 newline), escape_javascript(%(unicode \342\200\250 newline))
|
||||
end
|
||||
assert_equal %(dont <\\/close> tags), j(%(dont </close> tags))
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
require 'active_record/connection_adapters/sqlite_adapter'
|
||||
|
||||
gem 'sqlite3', '~> 1.3.4'
|
||||
require 'sqlite3'
|
||||
|
||||
module ActiveRecord
|
||||
|
||||
@@ -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 ========================================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] }
|
||||
|
||||
@@ -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
|
||||
[]
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user