diff options
90 files changed, 2151 insertions, 1542 deletions
@@ -40,7 +40,7 @@ group :doc do end # AS -gem 'dalli' +gem 'dalli', '>= 2.2.1' # Add your own local bundler stuff local_gemfile = File.dirname(__FILE__) + "/.Gemfile" diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index c33a24c1ba..ed8ee89617 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,5 +1,21 @@ ## Rails 4.0.0 (unreleased) ## +* Allow delivery method options to be set per mail instance *Aditya Sanghi* + + If your smtp delivery settings are dynamic, + you can now override settings per mail instance for e.g. + + def my_mailer(user,company) + mail to: user.email, subject: "Welcome!", + delivery_method_options: {user_name: company.smtp_user, + password: company.smtp_password} + end + + This will ensure that your default SMTP settings will be overridden + by the company specific ones. You only have to override the settings + that are dynamic and leave the static setting in your environment + configuration file (e.g. config/environments/production.rb) + * Allow to set default Action Mailer options via `config.action_mailer.default_options=` *Robert Pankowecki* * Raise an `ActionView::MissingTemplate` exception when no implicit template could be found. *Damien Mathieu* diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index f5c325179f..4a099553c0 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -683,7 +683,7 @@ module ActionMailer #:nodoc: m.charset = charset = headers[:charset] # Set configure delivery behavior - wrap_delivery_behavior!(headers.delete(:delivery_method)) + wrap_delivery_behavior!(headers.delete(:delivery_method),headers.delete(:delivery_method_options)) # Assign all headers except parts_order, content_type and body assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path) diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb index 3b38dbccc7..b795d4f80a 100644 --- a/actionmailer/lib/action_mailer/delivery_methods.rb +++ b/actionmailer/lib/action_mailer/delivery_methods.rb @@ -57,7 +57,7 @@ module ActionMailer self.delivery_methods = delivery_methods.merge(symbol.to_sym => klass).freeze end - def wrap_delivery_behavior(mail, method=nil) #:nodoc: + def wrap_delivery_behavior(mail, method=nil, options=nil) #:nodoc: method ||= self.delivery_method mail.delivery_handler = self @@ -66,7 +66,7 @@ module ActionMailer raise "Delivery method cannot be nil" when Symbol if klass = delivery_methods[method] - mail.delivery_method(klass, send(:"#{method}_settings")) + mail.delivery_method(klass,(send(:"#{method}_settings") || {}).merge!(options || {})) else raise "Invalid delivery method #{method.inspect}" end diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb index 08f84dbf3b..7109f23e4c 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -4,6 +4,13 @@ require 'mail' class MyCustomDelivery end +class MyOptionedDelivery + attr_reader :options + def initialize(options) + @options = options + end +end + class BogusDelivery def initialize(*) end @@ -115,6 +122,38 @@ class MailDeliveryTest < ActiveSupport::TestCase assert_instance_of Mail::TestMailer, email.delivery_method end + test "delivery method options default to class level options" do + default_options = {a: "b"} + ActionMailer::Base.add_delivery_method :optioned, MyOptionedDelivery, default_options + mail_instance = DeliveryMailer.welcome(:delivery_method => :optioned) + assert_equal default_options, mail_instance.delivery_method.options + end + + test "delivery method options can be overridden per mail instance" do + default_options = {a: "b"} + ActionMailer::Base.add_delivery_method :optioned, MyOptionedDelivery, default_options + overridden_options = {a: "a"} + mail_instance = DeliveryMailer.welcome(:delivery_method => :optioned, :delivery_method_options => overridden_options) + assert_equal overridden_options, mail_instance.delivery_method.options + end + + test "default delivery options can be overridden per mail instance" do + settings = { :address => "localhost", + :port => 25, + :domain => 'localhost.localdomain', + :user_name => nil, + :password => nil, + :authentication => nil, + :enable_starttls_auto => true } + assert_equal settings, ActionMailer::Base.smtp_settings + overridden_options = {user_name: "overridden", :password => "somethingobtuse"} + mail_instance = DeliveryMailer.welcome(:delivery_method_options => overridden_options) + delivery_method_instance = mail_instance.delivery_method + assert_equal "overridden", delivery_method_instance.settings[:user_name] + assert_equal "somethingobtuse", delivery_method_instance.settings[:password] + assert_equal delivery_method_instance.settings.merge(overridden_options), delivery_method_instance.settings + end + test "non registered delivery methods raises errors" do DeliveryMailer.delivery_method = :unknown assert_raise RuntimeError do diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 1232370439..8c7089cf57 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,12 @@ ## Rails 4.0.0 (unreleased) ## +* Add `separator` option for `ActionView::Helpers::TextHelper#excerpt`: + + excerpt('This is a very beautiful morning', 'very', :separator => ' ', :radius => 1) + # => ...a very beautiful... + + *Guirec Corbel* + * Added controller-level etag additions that will be part of the action etag computation *Jeremy Kemper/DHH* class InvoicesController < ApplicationController diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index dde51da497..fd09d3b55b 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('rack-cache', '~> 1.2') - s.add_dependency('builder', '~> 3.0.0') + s.add_dependency('builder', '~> 3.1.0') s.add_dependency('rack', '~> 1.4.1') s.add_dependency('rack-test', '~> 0.6.1') s.add_dependency('journey', '~> 2.0.0') diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index 0238135bc1..eb3aa05a25 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -132,7 +132,7 @@ module ActionController #:nodoc: options.values_at(:cache_path, :store_options, :layout) end - def filter(controller) + def around(controller) cache_layout = @cache_layout.respond_to?(:call) ? @cache_layout.call(controller) : @cache_layout path_options = if @cache_path.respond_to?(:call) diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 7521deaaca..12ef68ff26 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -22,7 +22,7 @@ module ActionController # # class InvoicesController < ApplicationController # etag { current_user.try :id } - # + # # def show # # Etag will differ even for the same invoice when it's viewed by a different current_user # @invoice = Invoice.find(params[:id]) @@ -71,7 +71,7 @@ module ActionController options.assert_valid_keys(:etag, :last_modified, :public) else record = record_or_options - options = { etag: record, last_modified: record.try(:updated_at) }.merge(additional_options) + options = { etag: record, last_modified: record.try(:updated_at) }.merge!(additional_options) end response.etag = combine_etags(options[:etag]) if options[:etag] @@ -162,8 +162,7 @@ module ActionController def expires_now #:doc: response.cache_control.replace(:no_cache => true) end - - + private def combine_etags(etag) [ etag, *etaggers.map { |etagger| instance_exec(&etagger) }.compact ] diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index d9c89a74f1..42a0959a58 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -102,7 +102,7 @@ module ActionController #:nodoc: # # def create # @project = Project.find(params[:project_id]) - # @task = @project.comments.build(params[:task]) + # @task = @project.tasks.build(params[:task]) # respond_with(@project, @task, :status => 201) do |format| # if @task.save # flash[:notice] = 'Task was successfully created.' diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index fd86966c50..f86ae26b8a 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,10 +1,11 @@ require 'set' require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/string/starts_ends_with' module Mime class Mimes < Array def symbols - @symbols ||= map {|m| m.to_sym } + @symbols ||= map { |m| m.to_sym } end %w(<< concat shift unshift push pop []= clear compact! collect! @@ -23,14 +24,16 @@ module Mime EXTENSION_LOOKUP = {} LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } - def self.[](type) - return type if type.is_a?(Type) - Type.lookup_by_extension(type.to_s) - end + class << self + def [](type) + return type if type.is_a?(Type) + Type.lookup_by_extension(type) + end - def self.fetch(type) - return type if type.is_a?(Type) - EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k } + def fetch(type) + return type if type.is_a?(Type) + EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k } + end end # Encapsulates the notion of a mime type. Can be used at render time, for example, with: @@ -61,32 +64,84 @@ module Mime # A simple helper class used in parsing the accept header class AcceptItem #:nodoc: - attr_accessor :order, :name, :q + attr_accessor :index, :name, :q + alias :to_s :name - def initialize(order, name, q=nil) - @order = order - @name = name.strip - q ||= 0.0 if @name == Mime::ALL # default wildcard match to end of list + def initialize(index, name, q = nil) + @index = index + @name = name + q ||= 0.0 if @name == Mime::ALL.to_s # default wildcard match to end of list @q = ((q || 1.0).to_f * 100).to_i end - def to_s - @name - end - def <=>(item) - result = item.q <=> q - result = order <=> item.order if result == 0 + result = item.q <=> @q + result = @index <=> item.index if result == 0 result end def ==(item) - name == (item.respond_to?(:name) ? item.name : item) + @name == item.to_s end end - class << self + class AcceptList < Array #:nodoc: + def assort! + sort! + + # Take care of the broken text/xml entry by renaming or deleting it + if text_xml_idx && app_xml_idx + app_xml.q = [text_xml.q, app_xml.q].max # set the q value to the max of the two + exchange_xml_items if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list + delete_at(text_xml_idx) # delete text_xml from the list + elsif text_xml_idx + text_xml.name = Mime::XML.to_s + end + + # Look for more specific XML-based types and sort them ahead of app/xml + if app_xml_idx + idx = app_xml_idx + + while idx < length + type = self[idx] + break if type.q < app_xml.q + + if type.name.ends_with? '+xml' + self[app_xml_idx], self[idx] = self[idx], app_xml + @app_xml_idx = idx + end + idx += 1 + end + end + map! { |i| Mime::Type.lookup(i.name) }.uniq! + to_a + end + + private + def text_xml_idx + @text_xml_idx ||= index('text/xml') + end + + def app_xml_idx + @app_xml_idx ||= index(Mime::XML.to_s) + end + + def text_xml + self[text_xml_idx] + end + + def app_xml + self[app_xml_idx] + end + + def exchange_xml_items + self[app_xml_idx], self[text_xml_idx] = text_xml, app_xml + @app_xml_idx, @text_xml_idx = text_xml_idx, app_xml_idx + end + end + + class << self TRAILING_STAR_REGEXP = /(text|application)\/\*/ PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/ @@ -125,75 +180,30 @@ module Mime def parse(accept_header) if accept_header !~ /,/ accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first - if accept_header =~ TRAILING_STAR_REGEXP - parse_data_with_trailing_star($1) - else - [Mime::Type.lookup(accept_header)] - end + parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)] else - # keep track of creation order to keep the subsequent sort stable - list, index = [], 0 - accept_header.split(/,/).each do |header| + list, index = AcceptList.new, 0 + accept_header.split(',').each do |header| params, q = header.split(PARAMETER_SEPARATOR_REGEXP) if params.present? params.strip! - if params =~ TRAILING_STAR_REGEXP - parse_data_with_trailing_star($1).each do |m| - list << AcceptItem.new(index, m.to_s, q) - index += 1 - end - else - list << AcceptItem.new(index, params, q) - index += 1 - end - end - end - list.sort! - - # Take care of the broken text/xml entry by renaming or deleting it - text_xml = list.index("text/xml") - app_xml = list.index(Mime::XML.to_s) - - if text_xml && app_xml - # set the q value to the max of the two - list[app_xml].q = [list[text_xml].q, list[app_xml].q].max - - # make sure app_xml is ahead of text_xml in the list - if app_xml > text_xml - list[app_xml], list[text_xml] = list[text_xml], list[app_xml] - app_xml, text_xml = text_xml, app_xml - end - - # delete text_xml from the list - list.delete_at(text_xml) + params = parse_trailing_star(params) || [params] - elsif text_xml - list[text_xml].name = Mime::XML.to_s - end - - # Look for more specific XML-based types and sort them ahead of app/xml - - if app_xml - idx = app_xml - app_xml_type = list[app_xml] - - while(idx < list.length) - type = list[idx] - break if type.q < app_xml_type.q - if type.name =~ /\+xml$/ - list[app_xml], list[idx] = list[idx], list[app_xml] - app_xml = idx + params.each do |m| + list << AcceptItem.new(index, m.to_s, q) + index += 1 end - idx += 1 end end - - list.map! { |i| Mime::Type.lookup(i.name) }.uniq! - list + list.assort! end end + def parse_trailing_star(accept_header) + parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP + end + # For an input of <tt>'text'</tt>, returns <tt>[Mime::JSON, Mime::XML, Mime::ICS, # Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT]</tt>. # @@ -273,18 +283,18 @@ module Mime @@html_types.include?(to_sym) || @string =~ /html/ end - def respond_to?(method, include_private = false) #:nodoc: - super || method.to_s =~ /(\w+)\?$/ - end - private def method_missing(method, *args) - if method.to_s =~ /(\w+)\?$/ - $1.downcase.to_sym == to_sym + if method.to_s.ends_with? '?' + method[0..-2].downcase.to_sym == to_sym else super end end + + def respond_to_missing?(method, include_private = false) #:nodoc: + method.to_s.ends_with? '?' + end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index d24c7c7f3f..b8ebeb408f 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -227,8 +227,11 @@ module ActionDispatch # TODO This should be broken apart into AD::Request::Session and probably # be included by the session middleware. def reset_session - session.destroy if session && session.respond_to?(:destroy) - self.session = {} + if session && session.respond_to?(:destroy) + session.destroy + else + self.session = {} + end @env['action_dispatch.request.flash_hash'] = nil end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 9b159b2caf..019849ef95 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -44,6 +44,14 @@ module ActionDispatch include StaleSessionCheck include SessionObject + # Override rack's method + def destroy_session(env, session_id, options) + new_sid = super + # Reset hash and Assign the new session id + env["action_dispatch.request.unsigned_session_cookie"] = new_sid ? { "session_id" => new_sid } : {} + new_sid + end + private def unpacked_cookie_data(env) diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb index 38a737cd2b..b4d6629c35 100644 --- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb @@ -1,15 +1,19 @@ require 'action_dispatch/middleware/session/abstract_store' -require 'rack/session/memcache' +begin + require 'rack/session/dalli' +rescue LoadError => e + $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install" + raise e +end module ActionDispatch module Session - class MemCacheStore < Rack::Session::Memcache + class MemCacheStore < Rack::Session::Dalli include Compatibility include StaleSessionCheck include SessionObject def initialize(app, options = {}) - require 'memcache' options[:expire_after] ||= options[:expires] super end diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 35d694b4a1..a05a23d953 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -70,9 +70,12 @@ module ActionDispatch def destroy clear options = self.options || {} - @by.send(:destroy_session, @env, options[:id], options) - options[:id] = nil + new_sid = @by.send(:destroy_session, @env, options[:id], options) + options[:id] = new_sid # Reset session id with a new value or nil + + # Load the new sid to be written with the response @loaded = false + load_for_write! end def [](key) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index b52f66faf1..ddb34a2394 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1585,7 +1585,7 @@ module ActionDispatch end end - # Routing Concerns allows you to declare common routes that can be reused + # Routing Concerns allow you to declare common routes that can be reused # inside others resources and routes. # # concern :commentable do @@ -1608,13 +1608,63 @@ module ActionDispatch module Concerns # Define a routing concern using a name. # - # concern :commentable do - # resources :comments + # Concerns may be defined inline, using a block, or handled by + # another object, by passing that object as the second parameter. + # + # The concern object, if supplied, should respond to <tt>call</tt>, + # which will receive two parameters: + # + # * The current mapper + # * A hash of options which the concern object may use + # + # Options may also be used by concerns defined in a block by accepting + # a block parameter. So, using a block, you might do something as + # simple as limit the actions available on certain resources, passing + # standard resource options through the concern: + # + # concern :commentable do |options| + # resources :comments, options # end # - # Any routing helpers can be used inside a concern. - def concern(name, &block) - @concerns[name] = block + # resources :posts, concerns: :commentable + # resources :archived_posts do + # # Don't allow comments on archived posts + # concerns :commentable, only: [:index, :show] + # end + # + # Or, using a callable object, you might implement something more + # specific to your application, which would be out of place in your + # routes file. + # + # # purchasable.rb + # class Purchasable + # def initialize(defaults = {}) + # @defaults = defaults + # end + # + # def call(mapper, options = {}) + # options = @defaults.merge(options) + # mapper.resources :purchases + # mapper.resources :receipts + # mapper.resources :returns if options[:returnable] + # end + # end + # + # # routes.rb + # concern :purchasable, Purchasable.new(returnable: true) + # + # resources :toys, concerns: :purchasable + # resources :electronics, concerns: :purchasable + # resources :pets do + # concerns :purchasable, returnable: false + # end + # + # Any routing helpers can be used inside a concern. If using a + # callable, they're accessible from the Mapper that's passed to + # <tt>call</tt>. + def concern(name, callable = nil, &block) + callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) } + @concerns[name] = callable end # Use the named concerns @@ -1628,10 +1678,11 @@ module ActionDispatch # namespace :posts do # concerns :commentable # end - def concerns(*names) - names.flatten.each do |name| + def concerns(*args) + options = args.extract_options! + args.flatten.each do |name| if concern = @concerns[name] - instance_eval(&concern) + concern.call(self, options) else raise ArgumentError, "No concern named #{name} was found!" end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 1fe40d8458..060d0bfa2f 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -319,7 +319,7 @@ module ActionDispatch MountedHelpers.class_eval(<<-RUBY, __FILE__, __LINE__ + 1) def #{name} - @#{name} ||= _#{name} + @_#{name} ||= _#{name} end RUBY end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index ab584abf68..a8b27ffafd 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -19,7 +19,7 @@ module ActionDispatch # - +headers+: Additional headers to pass, as a Hash. The headers will be # merged into the Rack env hash. # - # This method returns an Response object, which one can use to + # This method returns a Response object, which one can use to # inspect the details of the response. Furthermore, if this method was # called from an ActionDispatch::IntegrationTest object, then that # object's <tt>@response</tt> instance variable will point to the same diff --git a/actionpack/lib/action_view/digestor.rb b/actionpack/lib/action_view/digestor.rb index 899100d06c..5d3add4091 100644 --- a/actionpack/lib/action_view/digestor.rb +++ b/actionpack/lib/action_view/digestor.rb @@ -1,6 +1,3 @@ -require 'active_support/core_ext' -require 'logger' - module ActionView class Digestor EXPLICIT_DEPENDENCY = /# Template Dependency: ([^ ]+)/ @@ -21,8 +18,8 @@ module ActionView ([@a-z"'][@a-z_\/\."']+) # the template name itself -- 2nd capture /x - cattr_accessor(:cache) { Hash.new } - cattr_accessor(:logger, instance_reader: true) { ActionView::Base.logger } + cattr_reader(:cache) + @@cache = Hash.new def self.digest(name, format, finder, options = {}) cache["#{name}.#{format}"] ||= new(name, format, finder, options).digest @@ -35,7 +32,7 @@ module ActionView end def digest - Digest::MD5.hexdigest("#{name}.#{format}-#{source}-#{dependency_digest}").tap do |digest| + Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest| logger.try :info, "Cache digest for #{name}.#{format}: #{digest}" end rescue ActionView::MissingTemplate @@ -56,12 +53,16 @@ module ActionView end end - private + + def logger + ActionView::Base.logger + end + def logical_name name.gsub(%r|/_|, "/") end - + def directory name.split("/").first end @@ -74,7 +75,6 @@ module ActionView @source ||= finder.find(logical_name, [], partial?, formats: [ format ]).source end - def dependency_digest dependencies.collect do |template_name| Digestor.digest(template_name, format, finder, partial: true) @@ -92,7 +92,7 @@ module ActionView # render("headline") => render("message/headline") collect { |name| name.include?("/") ? name : "#{directory}/#{name}" }. - + # replace quotes from string renders collect { |name| name.gsub(/["']/, "") } end @@ -101,4 +101,4 @@ module ActionView source.scan(EXPLICIT_DEPENDENCY).flatten.uniq end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 795440cacc..387dfeab17 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -904,10 +904,13 @@ module ActionView # <option value="3">3</option> # <option value="5">5</option>..." def build_options(selected, options = {}) + options = { + leading_zeros: true, ampm: false, use_two_digit_numbers: false + }.merge!(options) + start = options.delete(:start) || 0 stop = options.delete(:end) || 59 step = options.delete(:step) || 1 - options.reverse_merge!({:leading_zeros => true, :ampm => false, :use_two_digit_numbers => false}) leading_zeros = options.delete(:leading_zeros) select_options = [] @@ -919,6 +922,7 @@ module ActionView text = options[:ampm] ? AMPM_TRANSLATION[i] : text select_options << content_tag(:option, text, tag_options) end + (select_options.join("\n") + "\n").html_safe end @@ -931,8 +935,8 @@ module ActionView select_options = { :id => input_id_from_type(type), :name => input_name_from_type(type) - }.merge(@html_options) - select_options.merge!(:disabled => 'disabled') if @options[:disabled] + }.merge!(@html_options) + select_options[:disabled] = 'disabled' if @options[:disabled] select_html = "\n" select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank] @@ -968,8 +972,8 @@ module ActionView :id => input_id_from_type(type), :name => input_name_from_type(type), :value => value - }.merge(@html_options.slice(:disabled)) - select_options.merge!(:disabled => 'disabled') if @options[:disabled] + }.merge!(@html_options.slice(:disabled)) + select_options[:disabled] = 'disabled' if @options[:disabled] tag(:input, select_options) + "\n".html_safe end diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 0f599d5f41..527bfe0cab 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -126,8 +126,9 @@ module ActionView # Extracts an excerpt from +text+ that matches the first instance of +phrase+. # The <tt>:radius</tt> option expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+, - # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. The resulting string - # will be stripped in any case. If the +phrase+ isn't found, nil is returned. + # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. The + # <tt>:separator</tt> enable to choose the delimation. The resulting string will be stripped in any case. If the +phrase+ + # isn't found, nil is returned. # # excerpt('This is an example', 'an', :radius => 5) # # => ...s is an exam... @@ -143,21 +144,32 @@ module ActionView # # excerpt('This is also an example', 'an', :radius => 8, :omission => '<chop> ') # # => <chop> is also an example + # + # excerpt('This is a very beautiful morning', 'very', :separator => ' ', :radius => 1) + # # => ...a very beautiful... def excerpt(text, phrase, options = {}) return unless text && phrase - radius = options.fetch(:radius, 100) - omission = options.fetch(:omission, "...") - phrase = Regexp.escape(phrase) - return unless found_pos = text =~ /(#{phrase})/i + separator = options.fetch(:separator, "") + phrase = Regexp.escape(phrase) + regex = /#{phrase}/i + + return unless matches = text.match(regex) + phrase = matches[0] + + text.split(separator).each do |value| + if value.match(regex) + regex = phrase = value + break + end + end - start_pos = [ found_pos - radius, 0 ].max - end_pos = [ [ found_pos + phrase.length + radius - 1, 0].max, text.length ].min + first_part, second_part = text.split(regex, 2) - prefix = start_pos > 0 ? omission : "" - postfix = end_pos < text.length - 1 ? omission : "" + prefix, first_part = cut_excerpt_part(:first, first_part, separator, options) + postfix, second_part = cut_excerpt_part(:second, second_part, separator, options) - prefix + text[start_pos..end_pos].strip + postfix + prefix + (first_part + separator + phrase + separator + second_part).strip + postfix end # Attempts to pluralize the +singular+ word unless +count+ is 1. If @@ -402,6 +414,26 @@ module ActionView t.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') || t end end + + def cut_excerpt_part(part_position, part, separator, options) + return "", "" unless part + + radius = options.fetch(:radius, 100) + omission = options.fetch(:omission, "...") + + part = part.split(separator) + part.delete("") + affix = part.size > radius ? omission : "" + + part = if part_position == :first + drop_index = [part.length - radius, 0].max + part.drop(drop_index) + else + part.first(radius) + end + + return affix, part.join(separator) + end end end end diff --git a/actionpack/lib/action_view/routing_url_for.rb b/actionpack/lib/action_view/routing_url_for.rb index 13fb6406e1..d1488e2332 100644 --- a/actionpack/lib/action_view/routing_url_for.rb +++ b/actionpack/lib/action_view/routing_url_for.rb @@ -99,7 +99,7 @@ module ActionView protected :_routes_context def optimize_routes_generation? #:nodoc: - controller.respond_to?(:optimize_routes_generation?) ? + controller.respond_to?(:optimize_routes_generation?, true) ? controller.optimize_routes_generation? : super end protected :optimize_routes_generation? diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index e5054a9eb8..4f5b2895c9 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -358,6 +358,7 @@ end class ThreadsController < ResourcesController; end class MessagesController < ResourcesController; end class CommentsController < ResourcesController; end +class ReviewsController < ResourcesController; end class AuthorsController < ResourcesController; end class LogosController < ResourcesController; end diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index afc00a3c9d..d203601771 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -193,7 +193,7 @@ class FilterTest < ActionController::TestCase end class ConditionalClassFilter - def self.filter(controller) controller.instance_variable_set(:"@ran_class_filter", true) end + def self.before(controller) controller.instance_variable_set(:"@ran_class_filter", true) end end class OnlyConditionClassController < ConditionalFilterController @@ -309,7 +309,7 @@ class FilterTest < ActionController::TestCase end class AuditFilter - def self.filter(controller) + def self.before(controller) controller.instance_variable_set(:"@was_audited", true) end end @@ -449,7 +449,7 @@ class FilterTest < ActionController::TestCase class ErrorToRescue < Exception; end class RescuingAroundFilterWithBlock - def filter(controller) + def around(controller) begin yield rescue ErrorToRescue => ex @@ -894,7 +894,7 @@ end class ControllerWithFilterClass < PostsController class YieldingFilter < DefaultFilter - def self.filter(controller) + def self.around(controller) yield raise After end @@ -905,7 +905,7 @@ end class ControllerWithFilterInstance < PostsController class YieldingFilter < DefaultFilter - def filter(controller) + def around(controller) yield raise After end @@ -916,13 +916,13 @@ end class ControllerWithFilterMethod < PostsController class YieldingFilter < DefaultFilter - def filter(controller) + def around(controller) yield raise After end end - around_filter YieldingFilter.new.method(:filter), :only => :raises_after + around_filter YieldingFilter.new.method(:around), :only => :raises_after end class ControllerWithProcFilter < PostsController diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index ed012093a7..3e83f3d4fa 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -92,7 +92,7 @@ class MimeTypeTest < ActiveSupport::TestCase # (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; InfoPath.1) test "parse other broken acceptlines" do accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, , pronto/1.00.00, sslvpn/1.00.00.00, */*" - expect = ['image/gif', 'image/x-xbitmap', 'image/jpeg','image/pjpeg', 'application/x-shockwave-flash', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/msword', 'pronto/1.00.00', 'sslvpn/1.00.00.00', Mime::ALL ] + expect = ['image/gif', 'image/x-xbitmap', 'image/jpeg','image/pjpeg', 'application/x-shockwave-flash', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/msword', 'pronto/1.00.00', 'sslvpn/1.00.00.00', Mime::ALL] assert_equal expect, Mime::Type.parse(accept).collect { |c| c.to_s } end diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index 6d75c5ec7a..cfbf970a37 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -57,6 +57,7 @@ module TestGenerationPrefix get "/polymorphic_path_for_engine", :to => "outside_engine_generating#polymorphic_path_for_engine" get "/polymorphic_with_url_for", :to => "outside_engine_generating#polymorphic_with_url_for" get "/conflicting_url", :to => "outside_engine_generating#conflicting" + get "/ivar_usage", :to => "outside_engine_generating#ivar_usage" root :to => "outside_engine_generating#index" end @@ -125,6 +126,11 @@ module TestGenerationPrefix def conflicting render :text => "application" end + + def ivar_usage + @blog_engine = "Not the engine route helper" + render :text => blog_engine.post_path(:id => 1) + end end class EngineObject @@ -203,6 +209,11 @@ module TestGenerationPrefix assert_equal "http://example.org/awesome/blog/posts/1", last_response.body end + test "[APP] instance variable with same name as engine" do + get "/ivar_usage" + assert_equal "/awesome/blog/posts/1", last_response.body + end + # Inside any Object test "[OBJECT] proxy route should override respond_to?() as expected" do assert_respond_to blog_engine, :named_helper_that_should_be_invoked_only_in_respond_to_test_path diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index 80d5a13171..3f36d4f1a9 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -52,6 +52,15 @@ module ActionDispatch assert_equal %w[ftw awesome], s.values end + def test_clear + env = {} + s = Session.create(store, env, {}) + s['rails'] = 'ftw' + s['adequate'] = 'awesome' + s.clear + assert_equal([], s.values) + end + private def store Class.new { diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 4d699bd739..185a9e9b18 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -178,33 +178,40 @@ class ResponseTest < ActiveSupport::TestCase end test "read x_frame_options, x_content_type_options and x_xss_protection" do - ActionDispatch::Response.default_headers = { - 'X-Frame-Options' => 'DENY', - 'X-Content-Type-Options' => 'nosniff', - 'X-XSS-Protection' => '1;' - } - resp = ActionDispatch::Response.new.tap { |response| - response.body = 'Hello' - } - resp.to_a - - assert_equal('DENY', resp.headers['X-Frame-Options']) - assert_equal('nosniff', resp.headers['X-Content-Type-Options']) - assert_equal('1;', resp.headers['X-XSS-Protection']) - end + begin + ActionDispatch::Response.default_headers = { + 'X-Frame-Options' => 'DENY', + 'X-Content-Type-Options' => 'nosniff', + 'X-XSS-Protection' => '1;' + } + resp = ActionDispatch::Response.new.tap { |response| + response.body = 'Hello' + } + resp.to_a + + assert_equal('DENY', resp.headers['X-Frame-Options']) + assert_equal('nosniff', resp.headers['X-Content-Type-Options']) + assert_equal('1;', resp.headers['X-XSS-Protection']) + ensure + ActionDispatch::Response.default_headers = nil + end + end test "read custom default_header" do - ActionDispatch::Response.default_headers = { - 'X-XX-XXXX' => 'Here is my phone number' - } - resp = ActionDispatch::Response.new.tap { |response| - response.body = 'Hello' - } - resp.to_a - - assert_equal('Here is my phone number', resp.headers['X-XX-XXXX']) - end - + begin + ActionDispatch::Response.default_headers = { + 'X-XX-XXXX' => 'Here is my phone number' + } + resp = ActionDispatch::Response.new.tap { |response| + response.body = 'Hello' + } + resp.to_a + + assert_equal('Here is my phone number', resp.headers['X-XX-XXXX']) + ensure + ActionDispatch::Response.default_headers = nil + end + end end class ResponseIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionpack/test/dispatch/routing/concerns_test.rb b/actionpack/test/dispatch/routing/concerns_test.rb index 21da3bd77a..9f37701656 100644 --- a/actionpack/test/dispatch/routing/concerns_test.rb +++ b/actionpack/test/dispatch/routing/concerns_test.rb @@ -1,18 +1,28 @@ require 'abstract_unit' class RoutingConcernsTest < ActionDispatch::IntegrationTest + class Reviewable + def self.call(mapper, options = {}) + mapper.resources :reviews, options + end + end + Routes = ActionDispatch::Routing::RouteSet.new.tap do |app| app.draw do - concern :commentable do - resources :comments + concern :commentable do |options| + resources :comments, options end concern :image_attachable do resources :images, only: :index end - resources :posts, concerns: [:commentable, :image_attachable] do - resource :video, concerns: :commentable + concern :reviewable, Reviewable + + resources :posts, concerns: [:commentable, :image_attachable, :reviewable] do + resource :video, concerns: :commentable do + concerns :reviewable, as: :video_reviews + end end resource :picture, concerns: :commentable do @@ -20,7 +30,7 @@ class RoutingConcernsTest < ActionDispatch::IntegrationTest end scope "/videos" do - concerns :commentable + concerns :commentable, except: :destroy end end end @@ -63,11 +73,28 @@ class RoutingConcernsTest < ActionDispatch::IntegrationTest assert_equal "404", @response.code end + def test_accessing_callable_concern_ + get "/posts/1/reviews/1" + assert_equal "200", @response.code + assert_equal "/posts/1/reviews/1", post_review_path(post_id: 1, id: 1) + end + + def test_callable_concerns_accept_options + get "/posts/1/video/reviews/1" + assert_equal "200", @response.code + assert_equal "/posts/1/video/reviews/1", post_video_video_review_path(post_id: 1, id: 1) + end + def test_accessing_concern_from_a_scope get "/videos/comments" assert_equal "200", @response.code end + def test_concerns_accept_options + delete "/videos/comments/1" + assert_equal "404", @response.code + end + def test_with_an_invalid_concern_name e = assert_raise ArgumentError do ActionDispatch::Routing::RouteSet.new.tap do |app| @@ -79,4 +106,14 @@ class RoutingConcernsTest < ActionDispatch::IntegrationTest assert_equal "No concern named foo was found!", e.message end + + def test_concerns_executes_block_in_context_of_current_mapper + mapper = ActionDispatch::Routing::Mapper.new(ActionDispatch::Routing::RouteSet.new) + mapper.concern :test_concern do + resources :things + return self + end + + assert_equal mapper, mapper.concerns(:test_concern) + end end diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index 631974d6c4..41fa036a92 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -30,6 +30,11 @@ class CookieStoreTest < ActionDispatch::IntegrationTest render :text => "id: #{request.session_options[:id]}" end + def get_class_after_reset_session + reset_session + render :text => "class: #{session.class}" + end + def call_session_clear session.clear head :ok @@ -187,6 +192,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest get '/call_reset_session' assert_response :success assert_not_equal [], headers['Set-Cookie'] + assert_not_nil session_payload assert_not_equal session_payload, cookies[SessionKey] get '/get_session_value' @@ -195,6 +201,20 @@ class CookieStoreTest < ActionDispatch::IntegrationTest end end + def test_class_type_after_session_reset + with_test_route_set do + get '/set_session_value' + assert_response :success + assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", + headers['Set-Cookie'] + + get '/get_class_after_reset_session' + assert_response :success + assert_not_equal [], headers['Set-Cookie'] + assert_equal 'class: ActionDispatch::Request::Session', response.body + end + end + def test_getting_from_nonexistent_session with_test_route_set do get '/get_session_value' diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb index 03234612ab..e53ce4195b 100644 --- a/actionpack/test/dispatch/session/mem_cache_store_test.rb +++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb @@ -34,9 +34,9 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest end begin - require 'memcache' - memcache = MemCache.new('localhost:11211') - memcache.set('ping', '') + require 'dalli' + ss = Dalli::Client.new('localhost:11211').stats + raise Dalli::DalliError unless ss['localhost:11211'] def test_setting_and_getting_session_value with_test_route_set do @@ -131,11 +131,6 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest get '/get_session_id' assert_response :success end - with_autoload_path "session_autoload_test" do - get '/get_session_value' - assert_response :success - assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class" - end end end @@ -165,7 +160,7 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest assert_not_equal session_id, cookies['_session_id'] end end - rescue LoadError, RuntimeError + rescue LoadError, RuntimeError, Dalli::DalliError $stderr.puts "Skipping MemCacheStoreTest tests. Start memcached and try again." end diff --git a/actionpack/test/template/digestor_test.rb b/actionpack/test/template/digestor_test.rb index 9c84f3107f..01b101cb49 100644 --- a/actionpack/test/template/digestor_test.rb +++ b/actionpack/test/template/digestor_test.rb @@ -3,7 +3,7 @@ require 'fileutils' class FixtureTemplate attr_reader :source - + def initialize(template_path) @source = File.read(template_path) rescue Errno::ENOENT @@ -14,7 +14,7 @@ end class FixtureFinder FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor" TMP_DIR = "#{File.dirname(__FILE__)}/../tmp" - + def find(logical_name, keys, partial, options) FixtureTemplate.new("#{TMP_DIR}/digestor/#{partial ? logical_name.gsub(%r|/([^/]+)$|, '/_\1') : logical_name}.#{options[:formats].first}.erb") end @@ -24,7 +24,7 @@ class TemplateDigestorTest < ActionView::TestCase def setup FileUtils.cp_r FixtureFinder::FIXTURES_DIR, FixtureFinder::TMP_DIR end - + def teardown FileUtils.rm_r File.join(FixtureFinder::TMP_DIR, "digestor") ActionView::Digestor.cache.clear @@ -71,7 +71,7 @@ class TemplateDigestorTest < ActionView::TestCase change_template("messages/actions/_move") end end - + def test_dont_generate_a_digest_for_missing_templates assert_equal '', digest("nothing/there") end @@ -85,7 +85,7 @@ class TemplateDigestorTest < ActionView::TestCase change_template("events/_event") end end - + def test_collection_derived_from_record_dependency assert_digest_difference("messages/show") do change_template("events/_event") @@ -124,15 +124,18 @@ class TemplateDigestorTest < ActionView::TestCase private def assert_logged(message) + old_logger = ActionView::Base.logger log = StringIO.new - ActionView::Digestor.logger = Logger.new(log) + ActionView::Base.logger = Logger.new(log) - yield - - log.rewind - assert_match message, log.read - - ActionView::Digestor.logger = nil + begin + yield + + log.rewind + assert_match message, log.read + ensure + ActionView::Base.logger = old_logger + end end def assert_digest_difference(template_name) @@ -144,11 +147,11 @@ class TemplateDigestorTest < ActionView::TestCase assert previous_digest != digest(template_name), "digest didn't change" ActionView::Digestor.cache.clear end - + def digest(template_name) ActionView::Digestor.digest(template_name, :html, FixtureFinder.new) end - + def change_template(template_name) File.open("#{FixtureFinder::TMP_DIR}/digestor/#{template_name}.html.erb", "w") do |f| f.write "\nTHIS WAS CHANGED!" diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb index c0f694b2bf..4525efe73c 100644 --- a/actionpack/test/template/text_helper_test.rb +++ b/actionpack/test/template/text_helper_test.rb @@ -303,6 +303,19 @@ class TextHelperTest < ActionView::TestCase assert_equal options, passed_options end + def test_excerpt_with_separator + options = { :separator => ' ', :radius => 1 } + assert_equal('...a very beautiful...', excerpt('This is a very beautiful morning', 'very', options)) + assert_equal('This is...', excerpt('This is a very beautiful morning', 'this', options)) + assert_equal('...beautiful morning', excerpt('This is a very beautiful morning', 'morning', options)) + + options = { :separator => "\n", :radius => 0 } + assert_equal("...very long...", excerpt("my very\nvery\nvery long\nstring", 'long', options)) + + options = { :separator => "\n", :radius => 1 } + assert_equal("...very\nvery long\nstring", excerpt("my very\nvery\nvery long\nstring", 'long', options)) + end + def test_word_wrap assert_equal("my very very\nvery long\nstring", word_wrap("my very very very long string", :line_width => 15)) end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 9d0ff5ba99..d7041055a4 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,5 +1,16 @@ ## Rails 4.0.0 (unreleased) ## +* Due to a change in builder, nil values and empty strings now generates + closed tags, so instead of this: + + <pseudonyms nil=\"true\"></pseudonyms> + + It generates this: + + <pseudonyms nil=\"true\"/> + + *Carlos Antonio da Silva* + * Changed inclusion and exclusion validators to accept a symbol for `:in` option. This allows to use dynamic inclusion/exclusion values using methods, besides the current lambda/proc support. diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 66f324a1a1..be5d5d3ca8 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -18,5 +18,5 @@ Gem::Specification.new do |s| s.require_path = 'lib' s.add_dependency('activesupport', version) - s.add_dependency('builder', '~> 3.0.0') + s.add_dependency('builder', '~> 3.1.0') end diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index 8c5a3c5efd..e2bb0dda0b 100755 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -133,7 +133,7 @@ class XmlSerializationTest < ActiveModel::TestCase end test "should serialize nil" do - assert_match %r{<pseudonyms nil=\"true\"></pseudonyms>}, @contact.to_xml(:methods => :pseudonyms) + assert_match %r{<pseudonyms nil=\"true\"/>}, @contact.to_xml(:methods => :pseudonyms) end test "should serialize integer" do diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 85880e97ea..6f7b2cb108 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,6 +1,50 @@ ## Rails 4.0.0 (unreleased) ## -* Allow to pass Symbol or Proc into :limit option of #accepts_nested_attributes_for +* Attribute predicate methods, such as `article.title?`, will now raise + `ActiveModel::MissingAttributeError` if the attribute being queried for + truthiness was not read from the database, instead of just returning false. + + *Ernie Miller* + +* `ActiveRecord::SchemaDumper` uses Ruby 1.9 style hash, which means that the + schema.rb file will be generated using this new syntax from now on. + + *Konstantin Shabanov* + +* Map interval with precision to string datatype in PostgreSQL. Fixes #7518. *Yves Senn* + +* Fix eagerly loading associations without primary keys. Fixes #4976. *Kelley Reynolds* + +* Rails now raise an exception when you're trying to run a migration that has an invalid + file name. Only lower case letters, numbers, and '_' are allowed in migration's file name. + Please see #7419 for more details. + + *Jan Bernacki* + +* Fix bug when call `store_accessor` multiple times. + Fixes #7532. + + *Matt Jones* + +* Fix store attributes that show the changes incorrectly. + Fixes #7532. + + *Matt Jones* + +* Fix `ActiveRecord::Relation#pluck` when columns or tables are reserved words. + + *Ian Lesperance* + +* Allow JSON columns to be created in PostgreSQL and properly encoded/decoded. + to/from database. + + *Dickson S. Guedes* + +* Fix time column type casting for invalid time string values to correctly return nil. + + *Adam Meehan* + +* Allow to pass Symbol or Proc into :limit option of #accepts_nested_attributes_for. *Mikhail Dieterle* @@ -41,10 +85,11 @@ *Dave Yeu* -* Fixed table name prefix that is generated in engines for namespaced models +* Fixed table name prefix that is generated in engines for namespaced models. + *Wojciech Wnętrzak* -* Make sure `:environment` task is executed before `db:schema:load` or `db:structure:load` +* Make sure `:environment` task is executed before `db:schema:load` or `db:structure:load`. Fixes #4772. *Seamus Abshere* @@ -69,7 +114,7 @@ *Jon Leighton* -* Add CollectionProxy#scope +* Add CollectionProxy#scope. This can be used to get a Relation from an association. @@ -87,7 +132,7 @@ *Jon Leighton* -* Add `Relation#load` +* Add `Relation#load`. This method explicitly loads the records and then returns `self`. @@ -103,7 +148,7 @@ *Jon Leighton* * `Model.all` now returns an `ActiveRecord::Relation`, rather than an - array of records. Use ``Relation#to_a` if you really want an array. + array of records. Use `Relation#to_a` if you really want an array. In some specific cases, this may cause breakage when upgrading. However in most cases the `ActiveRecord::Relation` will just act as a @@ -127,7 +172,7 @@ * `:insert_sql` and `:delete_sql` options on `has_and_belongs_to_many` associations are deprecated. Please transition to using `has_many - :through` + :through`. *Jon Leighton* @@ -616,104 +661,4 @@ * PostgreSQL hstore types are automatically deserialized from the database. -## Rails 3.2.8 (Aug 9, 2012) ## - -* Do not consider the numeric attribute as changed if the old value is zero and the new value - is not a string. - Fixes #7237. - - *Rafael Mendonça França* - -* Removes the deprecation of `update_attribute`. *fxn* - -* Reverted the deprecation of `composed_of`. *Rafael Mendonça França* - -* Reverted the deprecation of `*_sql` association options. They will - be deprecated in 4.0 instead. *Jon Leighton* - -* Do not eager load AR session store. ActiveRecord::SessionStore depends on the abstract store - in Action Pack. Eager loading this class would break client code that eager loads Active Record - standalone. - Fixes #7160 - - *Xavier Noria* - -* Do not set RAILS_ENV to "development" when using `db:test:prepare` and related rake tasks. - This was causing the truncation of the development database data when using RSpec. - Fixes #7175. - - *Rafael Mendonça França* - - -## Rails 3.2.7 (Jul 26, 2012) ## - -* `:finder_sql` and `:counter_sql` options on collection associations - are deprecated. Please transition to using scopes. - - *Jon Leighton* - -* `:insert_sql` and `:delete_sql` options on `has_and_belongs_to_many` - associations are deprecated. Please transition to using `has_many - :through` - - *Jon Leighton* - -* `composed_of` has been deprecated. You'll have to write your own accessor - and mutator methods if you'd like to use value objects to represent some - portion of your models. - - *Steve Klabnik* - -* `update_attribute` has been deprecated. Use `update_column` if - you want to bypass mass-assignment protection, validations, callbacks, - and touching of updated_at. Otherwise please use `update_attributes`. - - *Steve Klabnik* - - -## Rails 3.2.6 (Jun 12, 2012) ## - -* protect against the nesting of hashes changing the - table context in the next call to build_from_hash. This fix - covers this case as well. - - CVE-2012-2695 - -* Revert earlier 'perf fix' (see 3.2.4 changelog / GH #6289). This - change introduced a regression (GH #6609). assoc.clear and - assoc.delete_all have loaded the association before doing the delete - since at least Rails 2.3. Doing the delete without loading the - records means that the `before_remove` and `after_remove` callbacks do - not get invoked. Therefore, this change was less a fix a more an - optimisation, which should only have gone into master. - - *Jon Leighton* - - -## Rails 3.2.5 (Jun 1, 2012) ## - -* Restore behavior of Active Record 3.2.3 scopes. - A series of commits relating to preloading and scopes caused a regression. - - *Andrew White* - - -## Rails 3.2.4 (May 31, 2012) ## - -* Perf fix: Don't load the records when doing assoc.delete_all. - GH #6289. *Jon Leighton* - -* Association preloading shouldn't be affected by the current scoping. - This could cause infinite recursion and potentially other problems. - See GH #5667. *Jon Leighton* - -* Datetime attributes are forced to be changed. GH #3965 - -* Fix attribute casting. GH #5549 - -* Fix #5667. Preloading should ignore scoping. - -* Predicate builder should not recurse for determining where columns. - Thanks to Ben Murphy for reporting this! CVE-2012-2661 - Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 2b1d888a9a..711f7b3ce1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -54,7 +54,7 @@ module ActiveRecord unless @column_names_with_alias @column_names_with_alias = [] - ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i| + ([primary_key] + (column_names - [primary_key])).compact.each_with_index do |column_name, i| @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"] end end diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index a8b23abb7c..0f9723febb 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -1,4 +1,3 @@ - module ActiveRecord module AttributeMethods module Query @@ -9,7 +8,7 @@ module ActiveRecord end def query_attribute(attr_name) - value = read_attribute(attr_name) + value = read_attribute(attr_name) { |n| missing_attribute(n, caller) } case value when true then true diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 4e1f0e1d62..11e4d34de2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -193,7 +193,8 @@ module ActiveRecord rescue Exception => database_transaction_rollback if transaction_open && !outside_transaction? transaction_open = false - decrement_open_transactions + txn = decrement_open_transactions + txn.aborted! if open_transactions == 0 rollback_db_transaction rollback_transaction_records(true) @@ -208,9 +209,10 @@ module ActiveRecord @transaction_joinable = last_transaction_joinable if outside_transaction? - @open_transactions = 0 + @current_transaction = nil elsif transaction_open - decrement_open_transactions + txn = decrement_open_transactions + txn.committed! begin if open_transactions == 0 commit_db_transaction diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index b3f9187429..27700e4fd2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -69,6 +69,7 @@ module ActiveRecord @last_use = false @logger = logger @open_transactions = 0 + @current_transaction = nil @pool = pool @query_cache = Hash.new { |h,sql| h[sql] = {} } @query_cache_enabled = false @@ -236,14 +237,30 @@ module ActiveRecord @connection end - attr_reader :open_transactions + def open_transactions + count = 0 + txn = current_transaction + + while txn + count += 1 + txn = txn.next + end + + count + end + + attr_reader :current_transaction def increment_open_transactions - @open_transactions += 1 + @current_transaction = Transaction.new(current_transaction) end def decrement_open_transactions - @open_transactions -= 1 + return unless current_transaction + + txn = current_transaction + @current_transaction = txn.next + txn end def transaction_joinable=(joinable) diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 1445bb3b2f..0390168461 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -124,6 +124,7 @@ module ActiveRecord when :boolean then "#{klass}.value_to_boolean(#{var_name})" when :hstore then "#{klass}.string_to_hstore(#{var_name})" when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})" + when :json then "#{klass}.string_to_json(#{var_name})" else var_name end end @@ -178,7 +179,13 @@ module ActiveRecord return string unless string.is_a?(String) return nil if string.blank? - string_to_time "2000-01-01 #{string}" + dummy_time_string = "2000-01-01 #{string}" + + fast_string_to_time(dummy_time_string) || begin + time_hash = Date._parse(dummy_time_string) + return nil if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end end # convert something to a boolean diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb new file mode 100644 index 0000000000..b59195f98a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -0,0 +1,96 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLColumn < Column + module Cast + def string_to_time(string) + return string unless String === string + + case string + when 'infinity'; 1.0 / 0.0 + when '-infinity'; -1.0 / 0.0 + else + super + end + end + + def hstore_to_string(object) + if Hash === object + object.map { |k,v| + "#{escape_hstore(k)}=>#{escape_hstore(v)}" + }.join ',' + else + object + end + end + + def string_to_hstore(string) + if string.nil? + nil + elsif String === string + Hash[string.scan(HstorePair).map { |k,v| + v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') + [k,v] + }] + else + string + end + end + + def json_to_string(object) + if Hash === object + ActiveSupport::JSON.encode(object) + else + object + end + end + + def string_to_json(string) + if String === string + ActiveSupport::JSON.decode(string) + else + string + end + end + + def string_to_cidr(string) + if string.nil? + nil + elsif String === string + IPAddr.new(string) + else + string + end + end + + def cidr_to_string(object) + if IPAddr === object + "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + else + object + end + end + + private + + HstorePair = begin + quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ + unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ + /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ + end + + def escape_hstore(value) + if value.nil? + 'NULL' + else + if value == "" + '""' + else + '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb new file mode 100644 index 0000000000..eb3084e066 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -0,0 +1,234 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module DatabaseStatements + def explain(arel, binds = []) + sql = "EXPLAIN #{to_sql(arel, binds)}" + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + end + + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # PostgreSQL shell: + # + # QUERY PLAN + # ------------------------------------------------------------------------------ + # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) + # Join Filter: (posts.user_id = users.id) + # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) + # Index Cond: (id = 1) + # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) + # Filter: (posts.user_id = 1) + # (6 rows) + # + def pp(result) + header = result.columns.first + lines = result.rows.map(&:first) + + # We add 2 because there's one char of padding at both sides, note + # the extra hyphens in the example above. + width = [header, *lines].map(&:length).max + 2 + + pp = [] + + pp << header.center(width).rstrip + pp << '-' * width + + pp += lines.map {|line| " #{line}"} + + nrows = result.rows.length + rows_label = nrows == 1 ? 'row' : 'rows' + pp << "(#{nrows} #{rows_label})" + + pp.join("\n") + "\n" + end + end + + # Executes a SELECT query and returns an array of rows. Each row is an + # array of field values. + def select_rows(sql, name = nil) + select_raw(sql, name).last + end + + # Executes an INSERT query and returns the new record's ID + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + unless pk + # Extract the table from the insert sql. Yuck. + table_ref = extract_table_ref_from_insert_sql(sql) + pk = primary_key(table_ref) if table_ref + end + + if pk && use_insert_returning? + select_value("#{sql} RETURNING #{quote_column_name(pk)}") + elsif pk + super + last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk)) + else + super + end + end + + def create + super.insert + end + + # create a 2D array representing the result set + def result_as_array(res) #:nodoc: + # check if we have any binary column and if they need escaping + ftypes = Array.new(res.nfields) do |i| + [i, res.ftype(i)] + end + + rows = res.values + return rows unless ftypes.any? { |_, x| + x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID + } + + typehash = ftypes.group_by { |_, type| type } + binaries = typehash[BYTEA_COLUMN_TYPE_OID] || [] + monies = typehash[MONEY_COLUMN_TYPE_OID] || [] + + rows.each do |row| + # unescape string passed BYTEA field (OID == 17) + binaries.each do |index, _| + row[index] = unescape_bytea(row[index]) + end + + # If this is a money type column and there are any currency symbols, + # then strip them off. Indeed it would be prettier to do this in + # PostgreSQLColumn.string_to_decimal but would break form input + # fields that call value_before_type_cast. + monies.each do |index, _| + data = row[index] + # Because money output is formatted according to the locale, there are two + # cases to consider (note the decimal separators): + # (1) $12,345,678.12 + # (2) $12.345.678,12 + case data + when /^-?\D+[\d,]+\.\d{2}$/ # (1) + data.gsub!(/[^-\d.]/, '') + when /^-?\D+[\d.]+,\d{2}$/ # (2) + data.gsub!(/[^-\d,]/, '').sub!(/,/, '.') + end + end + end + end + + # Queries the database and returns the results in an Array-like object + def query(sql, name = nil) #:nodoc: + log(sql, name) do + result_as_array @connection.async_exec(sql) + end + end + + # Executes an SQL statement, returning a PGresult object on success + # or raising a PGError exception otherwise. + def execute(sql, name = nil) + log(sql, name) do + @connection.async_exec(sql) + end + end + + def substitute_at(column, index) + Arel::Nodes::BindParam.new "$#{index + 1}" + end + + def exec_query(sql, name = 'SQL', binds = []) + log(sql, name, binds) do + result = binds.empty? ? exec_no_cache(sql, binds) : + exec_cache(sql, binds) + + types = {} + result.fields.each_with_index do |fname, i| + ftype = result.ftype i + fmod = result.fmod i + types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| + warn "unknown OID: #{fname}(#{oid}) (#{sql})" + OID::Identity.new + } + end + + ret = ActiveRecord::Result.new(result.fields, result.values, types) + result.clear + return ret + end + end + + def exec_delete(sql, name = 'SQL', binds = []) + log(sql, name, binds) do + result = binds.empty? ? exec_no_cache(sql, binds) : + exec_cache(sql, binds) + affected = result.cmd_tuples + result.clear + affected + end + end + alias :exec_update :exec_delete + + def sql_for_insert(sql, pk, id_value, sequence_name, binds) + unless pk + # Extract the table from the insert sql. Yuck. + table_ref = extract_table_ref_from_insert_sql(sql) + pk = primary_key(table_ref) if table_ref + end + + if pk && use_insert_returning? + sql = "#{sql} RETURNING #{quote_column_name(pk)}" + end + + [sql, binds] + end + + def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + val = exec_query(sql, name, binds) + if !use_insert_returning? && pk + unless sequence_name + table_ref = extract_table_ref_from_insert_sql(sql) + sequence_name = default_sequence_name(table_ref, pk) + return val unless sequence_name + end + last_insert_id_result(sequence_name) + else + val + end + end + + # Executes an UPDATE query and returns the number of affected tuples. + def update_sql(sql, name = nil) + super.cmd_tuples + end + + # Begins a transaction. + def begin_db_transaction + execute "BEGIN" + end + + # Commits a transaction. + def commit_db_transaction + execute "COMMIT" + end + + # Aborts a transaction. + def rollback_db_transaction + execute "ROLLBACK" + end + + def outside_transaction? + @connection.transaction_status == PGconn::PQTRANS_IDLE + 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 + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 6657491c06..b8e7687b21 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -145,6 +145,14 @@ module ActiveRecord end end + class Json < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::PostgreSQLColumn.string_to_json value + end + end + class TypeMap def initialize @mapping = {} @@ -244,6 +252,7 @@ module ActiveRecord register_type 'polygon', OID::Identity.new register_type 'circle', OID::Identity.new register_type 'hstore', OID::Hstore.new + register_type 'json', OID::Json.new register_type 'cidr', OID::Cidr.new alias_type 'inet', 'cidr' diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb new file mode 100644 index 0000000000..152258a2f4 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -0,0 +1,124 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module Quoting + # Escapes binary strings for bytea input to the database. + def escape_bytea(value) + PGconn.escape_bytea(value) if value + end + + # Unescapes bytea output from a database to the binary string it represents. + # NOTE: This is NOT an inverse of escape_bytea! This is only to be used + # on escaped binary output from database drive. + def unescape_bytea(value) + PGconn.unescape_bytea(value) if value + end + + # Quotes PostgreSQL-specific data types for SQL input. + def quote(value, column = nil) #:nodoc: + return super unless column + + case value + when Hash + case column.sql_type + when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) + when 'json' then super(PostgreSQLColumn.json_to_string(value), column) + else super + end + when IPAddr + case column.sql_type + when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) + else super + end + when Float + if value.infinite? && column.type == :datetime + "'#{value.to_s.downcase}'" + elsif value.infinite? || value.nan? + "'#{value.to_s}'" + else + super + end + when Numeric + return super unless column.sql_type == 'money' + # Not truly string input, so doesn't require (or allow) escape string syntax. + "'#{value}'" + when String + case column.sql_type + when 'bytea' then "'#{escape_bytea(value)}'" + when 'xml' then "xml '#{quote_string(value)}'" + when /^bit/ + case value + when /^[01]*$/ then "B'#{value}'" # Bit-string notation + when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation + end + else + super + end + else + super + end + end + + def type_cast(value, column) + return super unless column + + case value + when String + return super unless 'bytea' == column.sql_type + { :value => value, :format => 1 } + when Hash + case column.sql_type + when 'hstore' then PostgreSQLColumn.hstore_to_string(value) + when 'json' then PostgreSQLColumn.json_to_string(value) + else super + end + when IPAddr + return super unless ['inet','cidr'].includes? column.sql_type + PostgreSQLColumn.cidr_to_string(value) + else + super + end + end + + # Quotes strings for use in SQL input. + def quote_string(s) #:nodoc: + @connection.escape(s) + end + + # Checks the following cases: + # + # - table_name + # - "table.name" + # - schema_name.table_name + # - schema_name."table.name" + # - "schema.name".table_name + # - "schema.name"."table.name" + def quote_table_name(name) + schema, name_part = extract_pg_identifier_from_name(name.to_s) + + unless name_part + quote_column_name(schema) + else + table_name, name_part = extract_pg_identifier_from_name(name_part) + "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" + end + end + + # Quotes column names for use in SQL queries. + def quote_column_name(name) #:nodoc: + PGconn.quote_ident(name.to_s) + end + + # Quote date/time values for use in SQL input. Includes microseconds + # if the value is a Time responding to usec. + def quoted_date(value) #:nodoc: + if value.acts_like?(:time) && value.respond_to?(:usec) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb new file mode 100644 index 0000000000..16da3ea732 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -0,0 +1,22 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module ReferentialIntegrity + def supports_disable_referential_integrity? #:nodoc: + true + end + + def disable_referential_integrity #:nodoc: + if supports_disable_referential_integrity? then + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) + end + yield + ensure + if supports_disable_referential_integrity? then + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb new file mode 100644 index 0000000000..60f01c297e --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -0,0 +1,446 @@ +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + module SchemaStatements + # 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 PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, + # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>, + # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses + # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). + # + # Example: + # create_database config[:database], config + # create_database 'foo_development', :encoding => 'unicode' + def create_database(name, options = {}) + options = options.reverse_merge(:encoding => "utf8") + + option_string = options.symbolize_keys.sum do |key, value| + case key + when :owner + " OWNER = \"#{value}\"" + when :template + " TEMPLATE = \"#{value}\"" + when :encoding + " ENCODING = '#{value}'" + when :collation + " LC_COLLATE = '#{value}'" + when :ctype + " LC_CTYPE = '#{value}'" + when :tablespace + " TABLESPACE = \"#{value}\"" + when :connection_limit + " CONNECTION LIMIT = #{value}" + else + "" + end + end + + execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}" + end + + # Drops a PostgreSQL database. + # + # Example: + # drop_database 'matt_development' + def drop_database(name) #:nodoc: + execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" + end + + # Returns the list of all tables in the schema search path or a specified schema. + def tables(name = nil) + query(<<-SQL, 'SCHEMA').map { |row| row[0] } + SELECT tablename + FROM pg_tables + WHERE schemaname = ANY (current_schemas(false)) + SQL + end + + # Returns true if table exists. + # If the schema is not specified as part of +name+ then it will only find tables within + # the current schema search path (regardless of permissions to access tables in other schemas) + def table_exists?(name) + schema, table = Utils.extract_schema_and_table(name.to_s) + return false unless table + + binds = [[nil, table]] + binds << [nil, schema] if schema + + exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + SELECT COUNT(*) + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind in ('v','r') + AND c.relname = '#{table.gsub(/(^"|"$)/,'')}' + AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} + SQL + end + + # Returns true if schema exists. + def schema_exists?(name) + exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + SELECT COUNT(*) + FROM pg_namespace + WHERE nspname = '#{name}' + SQL + end + + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + result = query(<<-SQL, 'SCHEMA') + SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid + FROM pg_class t + INNER JOIN pg_index d ON t.oid = d.indrelid + INNER JOIN pg_class i ON d.indexrelid = i.oid + WHERE i.relkind = 'i' + AND d.indisprimary = 'f' + AND t.relname = '#{table_name}' + AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + ORDER BY i.relname + SQL + + result.map do |row| + index_name = row[0] + unique = row[1] == 't' + indkey = row[2].split(" ") + inddef = row[3] + oid = row[4] + + columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")] + SELECT a.attnum, a.attname + FROM pg_attribute a + WHERE a.attrelid = #{oid} + AND a.attnum IN (#{indkey.join(",")}) + SQL + + column_names = columns.values_at(*indkey).compact + + # add info on sort order for columns (only desc order is explicitly specified, asc is the default) + desc_order_columns = inddef.scan(/(\w+) DESC/).flatten + orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} + where = inddef.scan(/WHERE (.+)$/).flatten[0] + + column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where) + end.compact + end + + # Returns the list of all column definitions for a table. + def columns(table_name) + # Limit, precision, and scale are all handled by the superclass. + column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| + oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) { + OID::Identity.new + } + PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') + end + end + + # Returns the current database name. + def current_database + query('select current_database()', 'SCHEMA')[0][0] + end + + # Returns the current schema name. + def current_schema + query('SELECT current_schema', 'SCHEMA')[0][0] + end + + # Returns the current database encoding format. + def encoding + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database + WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + + # Returns the current database collation. + def collation + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + + # Returns the current database ctype. + def ctype + query(<<-end_sql, 'SCHEMA')[0][0] + SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' + end_sql + end + + # Returns an array of schema names. + def schema_names + query(<<-SQL, 'SCHEMA').flatten + SELECT nspname + FROM pg_namespace + WHERE nspname !~ '^pg_.*' + AND nspname NOT IN ('information_schema') + ORDER by nspname; + SQL + end + + # Creates a schema for the given schema name. + def create_schema schema_name + execute "CREATE SCHEMA #{schema_name}" + end + + # Drops the schema for the given schema name. + def drop_schema schema_name + execute "DROP SCHEMA #{schema_name} CASCADE" + end + + # Sets the schema search path to a string of comma-separated schema names. + # Names beginning with $ have to be quoted (e.g. $user => '$user'). + # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html + # + # This should be not be called manually but set in database.yml. + def schema_search_path=(schema_csv) + if schema_csv + execute("SET search_path TO #{schema_csv}", 'SCHEMA') + @schema_search_path = schema_csv + end + end + + # Returns the active schema search path. + def schema_search_path + @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0] + end + + # Returns the current client message level. + def client_min_messages + query('SHOW client_min_messages', 'SCHEMA')[0][0] + end + + # Set the client message level. + def client_min_messages=(level) + execute("SET client_min_messages TO '#{level}'", 'SCHEMA') + end + + # Returns the sequence name for a table's primary key or some other specified key. + def default_sequence_name(table_name, pk = nil) #:nodoc: + result = serial_sequence(table_name, pk || 'id') + return nil unless result + result.split('.').last + rescue ActiveRecord::StatementInvalid + "#{table_name}_#{pk || 'id'}_seq" + end + + def serial_sequence(table, column) + result = exec_query(<<-eosql, 'SCHEMA') + SELECT pg_get_serial_sequence('#{table}', '#{column}') + eosql + result.rows.first.first + end + + # Resets the sequence of a table's primary key to the maximum value. + def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: + unless pk and sequence + default_pk, default_sequence = pk_and_sequence_for(table) + + pk ||= default_pk + sequence ||= default_sequence + end + + if @logger && pk && !sequence + @logger.warn "#{table} has primary key #{pk} with no default sequence" + end + + if pk && sequence + quoted_sequence = quote_table_name(sequence) + + select_value <<-end_sql, 'Reset sequence' + SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) + end_sql + end + end + + # Returns a table's primary key and belonging sequence. + def pk_and_sequence_for(table) #:nodoc: + # First try looking for a sequence with a dependency on the + # given table's primary key. + result = query(<<-end_sql, 'PK and serial sequence')[0] + SELECT attr.attname, seq.relname + FROM pg_class seq, + pg_attribute attr, + pg_depend dep, + pg_namespace name, + pg_constraint cons + WHERE seq.oid = dep.objid + AND seq.relkind = 'S' + AND attr.attrelid = dep.refobjid + AND attr.attnum = dep.refobjsubid + AND attr.attrelid = cons.conrelid + AND attr.attnum = cons.conkey[1] + AND cons.contype = 'p' + AND dep.refobjid = '#{quote_table_name(table)}'::regclass + end_sql + + if result.nil? or result.empty? + # If that fails, try parsing the primary key's default value. + # Support the 7.x and 8.0 nextval('foo'::text) as well as + # the 8.1+ nextval('foo'::regclass). + result = query(<<-end_sql, 'PK and custom sequence')[0] + SELECT attr.attname, + CASE + WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN + substr(split_part(def.adsrc, '''', 2), + strpos(split_part(def.adsrc, '''', 2), '.')+1) + ELSE split_part(def.adsrc, '''', 2) + END + FROM pg_class t + JOIN pg_attribute attr ON (t.oid = attrelid) + JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) + JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) + WHERE t.oid = '#{quote_table_name(table)}'::regclass + AND cons.contype = 'p' + AND def.adsrc ~* 'nextval' + end_sql + end + + [result.first, result.last] + rescue + nil + end + + # Returns just a table's primary key + def primary_key(table) + row = exec_query(<<-end_sql, 'SCHEMA').rows.first + SELECT DISTINCT(attr.attname) + FROM pg_attribute attr + INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid + INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] + WHERE cons.contype = 'p' + AND dep.refobjid = '#{table}'::regclass + end_sql + + row && row.first + end + + # Renames a table. + # Also renames a table's primary key sequence if the sequence name matches the + # Active Record default. + # + # Example: + # rename_table('octopuses', 'octopi') + def rename_table(name, new_name) + clear_cache! + execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + pk, seq = pk_and_sequence_for(new_name) + if seq == "#{name}_#{pk}_seq" + new_seq = "#{new_name}_#{pk}_seq" + execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" + end + end + + # Adds a new column to the named table. + # See TableDefinition#column for details of the options you can use. + def add_column(table_name, column_name, type, options = {}) + clear_cache! + add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + + execute add_column_sql + end + + # Changes the column of a table. + def change_column(table_name, column_name, type, options = {}) + clear_cache! + quoted_table_name = quote_table_name(table_name) + + execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + + change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) + change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + end + + # Changes the default value of a table column. + def change_column_default(table_name, column_name, default) + clear_cache! + execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" + end + + def change_column_null(table_name, column_name, null, default = nil) + clear_cache! + 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 + execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") + end + + # Renames a column in a table. + def rename_column(table_name, column_name, new_column_name) + clear_cache! + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" + end + + def remove_index!(table_name, index_name) #:nodoc: + execute "DROP INDEX #{quote_table_name(index_name)}" + end + + def rename_index(table_name, old_name, new_name) + execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" + end + + def index_name_length + 63 + end + + # Maps logical Rails types to PostgreSQL-specific data types. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) + case type.to_s + when 'binary' + # PostgreSQL doesn't support limits on binary (bytea) columns. + # The hard limit is 1Gb, because of a 32-bit size field, and TOAST. + case limit + when nil, 0..0x3fffffff; super(type) + else raise(ActiveRecordError, "No binary type has byte size #{limit}.") + end + when 'integer' + return 'integer' unless limit + + case limit + when 1, 2; 'smallint' + when 3, 4; 'integer' + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") + end + when 'datetime' + return super unless precision + + case precision + when 0..6; "timestamp(#{precision})" + else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6") + end + else + super + end + end + + # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. + # + # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and + # requires that the ORDER BY include the distinct column. + # + # distinct("posts.id", "posts.created_at desc") + def distinct(columns, orders) #:nodoc: + return "DISTINCT #{columns}" if orders.empty? + + # Construct a clean list of column names from the ORDER BY clause, removing + # any ASC/DESC modifiers + order_columns = orders.collect do |s| + s = s.to_sql unless s.is_a?(String) + s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') + end + order_columns.delete_if { |c| c.blank? } + order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } + + "DISTINCT #{columns}, #{order_columns * ', '}" + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index e1eabcde6b..d1751d70c6 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,6 +1,11 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/statement_pool' require 'active_record/connection_adapters/postgresql/oid' +require 'active_record/connection_adapters/postgresql/cast' +require 'active_record/connection_adapters/postgresql/quoting' +require 'active_record/connection_adapters/postgresql/schema_statements' +require 'active_record/connection_adapters/postgresql/database_statements' +require 'active_record/connection_adapters/postgresql/referential_integrity' require 'arel/visitors/bind_visitor' # Make sure we're using pg high enough for PGResult#values @@ -44,72 +49,9 @@ module ActiveRecord # :stopdoc: class << self - attr_accessor :money_precision - def string_to_time(string) - return string unless String === string - - case string - when 'infinity' then 1.0 / 0.0 - when '-infinity' then -1.0 / 0.0 - else - super - end - end - - def hstore_to_string(object) - if Hash === object - object.map { |k,v| - "#{escape_hstore(k)}=>#{escape_hstore(v)}" - }.join ',' - else - object - end - end - - def string_to_hstore(string) - if string.nil? - nil - elsif String === string - Hash[string.scan(HstorePair).map { |k,v| - v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') - k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1') - [k,v] - }] - else - string - end - end - - def string_to_cidr(string) - if string.nil? - nil - elsif String === string - IPAddr.new(string) - else - string - end - end + include ConnectionAdapters::PostgreSQLColumn::Cast - def cidr_to_string(object) - if IPAddr === object - "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" - else - object - end - end - - private - HstorePair = begin - quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ - unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ - /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ - end - - def escape_hstore(value) - value.nil? ? 'NULL' - : value == "" ? '""' - : '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') - end + attr_accessor :money_precision end # :startdoc: @@ -164,6 +106,9 @@ module ActiveRecord # Hstore when /\A'(.*)'::hstore\z/ $1 + # JSON + when /\A'(.*)'::json\z/ + $1 # Object identifier types when /\A-?\d+\z/ $1 @@ -182,90 +127,94 @@ module ActiveRecord end private - def extract_limit(sql_type) - case sql_type - when /^bigint/i; 8 - when /^smallint/i; 2 - when /^timestamp/i; nil - else super + + def extract_limit(sql_type) + case sql_type + when /^bigint/i; 8 + when /^smallint/i; 2 + when /^timestamp/i; nil + else super + end end - end - # Extracts the scale from PostgreSQL-specific data types. - def extract_scale(sql_type) - # Money type has a fixed scale of 2. - sql_type =~ /^money/ ? 2 : super - end + # Extracts the scale from PostgreSQL-specific data types. + def extract_scale(sql_type) + # Money type has a fixed scale of 2. + sql_type =~ /^money/ ? 2 : super + end - # Extracts the precision from PostgreSQL-specific data types. - def extract_precision(sql_type) - if sql_type == 'money' - self.class.money_precision - elsif sql_type =~ /timestamp/i - $1.to_i if sql_type =~ /\((\d+)\)/ - else - super + # Extracts the precision from PostgreSQL-specific data types. + def extract_precision(sql_type) + if sql_type == 'money' + self.class.money_precision + elsif sql_type =~ /timestamp/i + $1.to_i if sql_type =~ /\((\d+)\)/ + else + super + end end - end - # Maps PostgreSQL-specific data types to logical Rails types. - def simplified_type(field_type) - case field_type - # Numeric and monetary types - when /^(?:real|double precision)$/ - :float - # Monetary types - when 'money' - :decimal - when 'hstore' - :hstore - # Network address types - when 'inet' - :inet - when 'cidr' - :cidr - when 'macaddr' - :macaddr - # Character types - when /^(?:character varying|bpchar)(?:\(\d+\))?$/ - :string - # Binary data types - when 'bytea' - :binary - # Date/time types - when /^timestamp with(?:out)? time zone$/ - :datetime - when 'interval' - :string - # Geometric types - when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ - :string - # Bit strings - when /^bit(?: varying)?(?:\(\d+\))?$/ - :string - # XML type - when 'xml' - :xml - # tsvector type - when 'tsvector' - :tsvector - # Arrays - when /^\D+\[\]$/ - :string - # Object identifier types - when 'oid' - :integer - # UUID type - when 'uuid' - :uuid - # Small and big integer types - when /^(?:small|big)int$/ - :integer - # Pass through all types that are not specific to PostgreSQL. - else - super + # Maps PostgreSQL-specific data types to logical Rails types. + def simplified_type(field_type) + case field_type + # Numeric and monetary types + when /^(?:real|double precision)$/ + :float + # Monetary types + when 'money' + :decimal + when 'hstore' + :hstore + # Network address types + when 'inet' + :inet + when 'cidr' + :cidr + when 'macaddr' + :macaddr + # Character types + when /^(?:character varying|bpchar)(?:\(\d+\))?$/ + :string + # Binary data types + when 'bytea' + :binary + # Date/time types + when /^timestamp with(?:out)? time zone$/ + :datetime + when /^interval(?:|\(\d+\))$/ + :string + # Geometric types + when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ + :string + # Bit strings + when /^bit(?: varying)?(?:\(\d+\))?$/ + :string + # XML type + when 'xml' + :xml + # tsvector type + when 'tsvector' + :tsvector + # Arrays + when /^\D+\[\]$/ + :string + # Object identifier types + when 'oid' + :integer + # UUID type + when 'uuid' + :uuid + # JSON type + when 'json' + :json + # Small and big integer types + when /^(?:small|big)int$/ + :integer + # Pass through all types that are not specific to PostgreSQL. + else + super + end end - end end # The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver. @@ -324,32 +273,42 @@ module ActiveRecord def uuid(name, options = {}) column(name, 'uuid', options) end + + def json(name, options = {}) + column(name, 'json', options) + end end ADAPTER_NAME = 'PostgreSQL' NATIVE_DATABASE_TYPES = { - :primary_key => "serial primary key", - :string => { :name => "character varying", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "integer" }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "timestamp" }, - :timestamp => { :name => "timestamp" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "bytea" }, - :boolean => { :name => "boolean" }, - :xml => { :name => "xml" }, - :tsvector => { :name => "tsvector" }, - :hstore => { :name => "hstore" }, - :inet => { :name => "inet" }, - :cidr => { :name => "cidr" }, - :macaddr => { :name => "macaddr" }, - :uuid => { :name => "uuid" } + primary_key: "serial primary key", + string: { name: "character varying", limit: 255 }, + text: { name: "text" }, + integer: { name: "integer" }, + float: { name: "float" }, + decimal: { name: "decimal" }, + datetime: { name: "timestamp" }, + timestamp: { name: "timestamp" }, + time: { name: "time" }, + date: { name: "date" }, + binary: { name: "bytea" }, + boolean: { name: "boolean" }, + xml: { name: "xml" }, + tsvector: { name: "tsvector" }, + hstore: { name: "hstore" }, + inet: { name: "inet" }, + cidr: { name: "cidr" }, + macaddr: { name: "macaddr" }, + uuid: { name: "uuid" }, + json: { name: "json" } } + include Quoting + include ReferentialIntegrity + include SchemaStatements + include DatabaseStatements + # Returns 'PostgreSQL' as adapter name for identification purposes. def adapter_name ADAPTER_NAME @@ -406,19 +365,20 @@ module ActiveRecord end private - def cache - @cache[Process.pid] - end - def dealloc(key) - @connection.query "DEALLOCATE #{key}" if connection_active? - end + def cache + @cache[Process.pid] + end - def connection_active? - @connection.status == PGconn::CONNECTION_OK - rescue PGError - false - end + def dealloc(key) + @connection.query "DEALLOCATE #{key}" if connection_active? + end + + def connection_active? + @connection.status == PGconn::CONNECTION_OK + rescue PGError + false + end end class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc: @@ -534,812 +494,12 @@ module ActiveRecord @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i end - # QUOTING ================================================== - - # Escapes binary strings for bytea input to the database. - def escape_bytea(value) - PGconn.escape_bytea(value) if value - end - - # Unescapes bytea output from a database to the binary string it represents. - # NOTE: This is NOT an inverse of escape_bytea! This is only to be used - # on escaped binary output from database drive. - def unescape_bytea(value) - PGconn.unescape_bytea(value) if value - end - - # Quotes PostgreSQL-specific data types for SQL input. - def quote(value, column = nil) #:nodoc: - return super unless column - - case value - when Hash - case column.sql_type - when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) - else super - end - when IPAddr - case column.sql_type - when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) - else super - end - when Float - if value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" - elsif value.infinite? || value.nan? - "'#{value.to_s}'" - else - super - end - when Numeric - return super unless column.sql_type == 'money' - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" - when String - case column.sql_type - when 'bytea' then "'#{escape_bytea(value)}'" - when 'xml' then "xml '#{quote_string(value)}'" - when /^bit/ - case value - when /^[01]*$/ then "B'#{value}'" # Bit-string notation - when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation - end - else - super - end - else - super - end - end - - def type_cast(value, column) - return super unless column - - case value - when String - return super unless 'bytea' == column.sql_type - { :value => value, :format => 1 } - when Hash - return super unless 'hstore' == column.sql_type - PostgreSQLColumn.hstore_to_string(value) - when IPAddr - return super unless ['inet','cidr'].includes? column.sql_type - PostgreSQLColumn.cidr_to_string(value) - else - super - end - end - - # Quotes strings for use in SQL input. - def quote_string(s) #:nodoc: - @connection.escape(s) - end - - # Checks the following cases: - # - # - table_name - # - "table.name" - # - schema_name.table_name - # - schema_name."table.name" - # - "schema.name".table_name - # - "schema.name"."table.name" - def quote_table_name(name) - schema, name_part = extract_pg_identifier_from_name(name.to_s) - - unless name_part - quote_column_name(schema) - else - table_name, name_part = extract_pg_identifier_from_name(name_part) - "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" - end - end - - # Quotes column names for use in SQL queries. - def quote_column_name(name) #:nodoc: - PGconn.quote_ident(name.to_s) - end - - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. - def quoted_date(value) #:nodoc: - if value.acts_like?(:time) && value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - # Set the authorized user for this session def session_auth=(user) clear_cache! exec_query "SET SESSION AUTHORIZATION #{user}" end - # REFERENTIAL INTEGRITY ==================================== - - def supports_disable_referential_integrity? #:nodoc: - true - end - - def disable_referential_integrity #:nodoc: - if supports_disable_referential_integrity? then - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) - end - yield - ensure - if supports_disable_referential_integrity? then - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) - end - end - - # DATABASE STATEMENTS ====================================== - - def explain(arel, binds = []) - sql = "EXPLAIN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) - end - - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the - # PostgreSQL shell: - # - # QUERY PLAN - # ------------------------------------------------------------------------------ - # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) - # Join Filter: (posts.user_id = users.id) - # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) - # Index Cond: (id = 1) - # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) - # Filter: (posts.user_id = 1) - # (6 rows) - # - def pp(result) - header = result.columns.first - lines = result.rows.map(&:first) - - # We add 2 because there's one char of padding at both sides, note - # the extra hyphens in the example above. - width = [header, *lines].map(&:length).max + 2 - - pp = [] - - pp << header.center(width).rstrip - pp << '-' * width - - pp += lines.map {|line| " #{line}"} - - nrows = result.rows.length - rows_label = nrows == 1 ? 'row' : 'rows' - pp << "(#{nrows} #{rows_label})" - - pp.join("\n") + "\n" - end - end - - # Executes a SELECT query and returns an array of rows. Each row is an - # array of field values. - def select_rows(sql, name = nil) - select_raw(sql, name).last - end - - # Executes an INSERT query and returns the new record's ID - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - unless pk - # Extract the table from the insert sql. Yuck. - table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref - end - - if pk && use_insert_returning? - select_value("#{sql} RETURNING #{quote_column_name(pk)}") - elsif pk - super - last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk)) - else - super - end - end - alias :create :insert - - # create a 2D array representing the result set - def result_as_array(res) #:nodoc: - # check if we have any binary column and if they need escaping - ftypes = Array.new(res.nfields) do |i| - [i, res.ftype(i)] - end - - rows = res.values - return rows unless ftypes.any? { |_, x| - x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID - } - - typehash = ftypes.group_by { |_, type| type } - binaries = typehash[BYTEA_COLUMN_TYPE_OID] || [] - monies = typehash[MONEY_COLUMN_TYPE_OID] || [] - - rows.each do |row| - # unescape string passed BYTEA field (OID == 17) - binaries.each do |index, _| - row[index] = unescape_bytea(row[index]) - end - - # If this is a money type column and there are any currency symbols, - # then strip them off. Indeed it would be prettier to do this in - # PostgreSQLColumn.string_to_decimal but would break form input - # fields that call value_before_type_cast. - monies.each do |index, _| - data = row[index] - # Because money output is formatted according to the locale, there are two - # cases to consider (note the decimal separators): - # (1) $12,345,678.12 - # (2) $12.345.678,12 - case data - when /^-?\D+[\d,]+\.\d{2}$/ # (1) - data.gsub!(/[^-\d.]/, '') - when /^-?\D+[\d.]+,\d{2}$/ # (2) - data.gsub!(/[^-\d,]/, '').sub!(/,/, '.') - end - end - end - end - - - # Queries the database and returns the results in an Array-like object - def query(sql, name = nil) #:nodoc: - log(sql, name) do - result_as_array @connection.async_exec(sql) - end - end - - # Executes an SQL statement, returning a PGresult object on success - # or raising a PGError exception otherwise. - def execute(sql, name = nil) - log(sql, name) do - @connection.async_exec(sql) - end - end - - def substitute_at(column, index) - Arel::Nodes::BindParam.new "$#{index + 1}" - end - - def exec_query(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - result = binds.empty? ? exec_no_cache(sql, binds) : - exec_cache(sql, binds) - - types = {} - result.fields.each_with_index do |fname, i| - ftype = result.ftype i - fmod = result.fmod i - types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod| - warn "unknown OID: #{fname}(#{oid}) (#{sql})" - OID::Identity.new - } - end - - ret = ActiveRecord::Result.new(result.fields, result.values, types) - result.clear - return ret - end - end - - def exec_delete(sql, name = 'SQL', binds = []) - log(sql, name, binds) do - result = binds.empty? ? exec_no_cache(sql, binds) : - exec_cache(sql, binds) - affected = result.cmd_tuples - result.clear - affected - end - end - alias :exec_update :exec_delete - - def sql_for_insert(sql, pk, id_value, sequence_name, binds) - unless pk - # Extract the table from the insert sql. Yuck. - table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref - end - - if pk && use_insert_returning? - sql = "#{sql} RETURNING #{quote_column_name(pk)}" - end - - [sql, binds] - end - - def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) - val = exec_query(sql, name, binds) - if !use_insert_returning? && pk - unless sequence_name - table_ref = extract_table_ref_from_insert_sql(sql) - sequence_name = default_sequence_name(table_ref, pk) - return val unless sequence_name - end - last_insert_id_result(sequence_name) - else - val - end - end - - # Executes an UPDATE query and returns the number of affected tuples. - def update_sql(sql, name = nil) - super.cmd_tuples - end - - # Begins a transaction. - def begin_db_transaction - execute "BEGIN" - end - - # Commits a transaction. - def commit_db_transaction - execute "COMMIT" - end - - # Aborts a transaction. - def rollback_db_transaction - execute "ROLLBACK" - end - - def outside_transaction? - @connection.transaction_status == PGconn::PQTRANS_IDLE - 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 ======================================== - - # 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 PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, - # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>, - # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses - # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>). - # - # Example: - # create_database config[:database], config - # create_database 'foo_development', :encoding => 'unicode' - def create_database(name, options = {}) - options = options.reverse_merge(:encoding => "utf8") - - option_string = options.symbolize_keys.sum do |key, value| - case key - when :owner - " OWNER = \"#{value}\"" - when :template - " TEMPLATE = \"#{value}\"" - when :encoding - " ENCODING = '#{value}'" - when :collation - " LC_COLLATE = '#{value}'" - when :ctype - " LC_CTYPE = '#{value}'" - when :tablespace - " TABLESPACE = \"#{value}\"" - when :connection_limit - " CONNECTION LIMIT = #{value}" - else - "" - end - end - - execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}" - end - - # Drops a PostgreSQL database. - # - # Example: - # drop_database 'matt_development' - def drop_database(name) #:nodoc: - execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" - end - - # Returns the list of all tables in the schema search path or a specified schema. - def tables(name = nil) - query(<<-SQL, 'SCHEMA').map { |row| row[0] } - SELECT tablename - FROM pg_tables - WHERE schemaname = ANY (current_schemas(false)) - SQL - end - - # Returns true if table exists. - # If the schema is not specified as part of +name+ then it will only find tables within - # the current schema search path (regardless of permissions to access tables in other schemas) - def table_exists?(name) - schema, table = Utils.extract_schema_and_table(name.to_s) - return false unless table - - binds = [[nil, table]] - binds << [nil, schema] if schema - - exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind in ('v','r') - AND c.relname = '#{table.gsub(/(^"|"$)/,'')}' - AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} - SQL - end - - # Returns true if schema exists. - def schema_exists?(name) - exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_namespace - WHERE nspname = '#{name}' - SQL - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) - result = query(<<-SQL, 'SCHEMA') - SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid - FROM pg_class t - INNER JOIN pg_index d ON t.oid = d.indrelid - INNER JOIN pg_class i ON d.indexrelid = i.oid - WHERE i.relkind = 'i' - AND d.indisprimary = 'f' - AND t.relname = '#{table_name}' - AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) - ORDER BY i.relname - SQL - - result.map do |row| - index_name = row[0] - unique = row[1] == 't' - indkey = row[2].split(" ") - inddef = row[3] - oid = row[4] - - columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")] - SELECT a.attnum, a.attname - FROM pg_attribute a - WHERE a.attrelid = #{oid} - AND a.attnum IN (#{indkey.join(",")}) - SQL - - column_names = columns.values_at(*indkey).compact - - # add info on sort order for columns (only desc order is explicitly specified, asc is the default) - desc_order_columns = inddef.scan(/(\w+) DESC/).flatten - orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} - where = inddef.scan(/WHERE (.+)$/).flatten[0] - - column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where) - end.compact - end - - # Returns the list of all column definitions for a table. - def columns(table_name) - # Limit, precision, and scale are all handled by the superclass. - column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| - oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) { - OID::Identity.new - } - PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') - end - end - - # Returns the current database name. - def current_database - query('select current_database()', 'SCHEMA')[0][0] - end - - # Returns the current schema name. - def current_schema - query('SELECT current_schema', 'SCHEMA')[0][0] - end - - # Returns the current database encoding format. - def encoding - query(<<-end_sql, 'SCHEMA')[0][0] - SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database - WHERE pg_database.datname LIKE '#{current_database}' - end_sql - end - - # Returns the current database collation. - def collation - query(<<-end_sql, 'SCHEMA')[0][0] - SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' - end_sql - end - - # Returns the current database ctype. - def ctype - query(<<-end_sql, 'SCHEMA')[0][0] - SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' - end_sql - end - - # Returns an array of schema names. - def schema_names - query(<<-SQL, 'SCHEMA').flatten - SELECT nspname - FROM pg_namespace - WHERE nspname !~ '^pg_.*' - AND nspname NOT IN ('information_schema') - ORDER by nspname; - SQL - end - - # Creates a schema for the given schema name. - def create_schema schema_name - execute "CREATE SCHEMA #{schema_name}" - end - - # Drops the schema for the given schema name. - def drop_schema schema_name - execute "DROP SCHEMA #{schema_name} CASCADE" - end - - # Sets the schema search path to a string of comma-separated schema names. - # Names beginning with $ have to be quoted (e.g. $user => '$user'). - # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html - # - # This should be not be called manually but set in database.yml. - def schema_search_path=(schema_csv) - if schema_csv - execute("SET search_path TO #{schema_csv}", 'SCHEMA') - @schema_search_path = schema_csv - end - end - - # Returns the active schema search path. - def schema_search_path - @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0] - end - - # Returns the current client message level. - def client_min_messages - query('SHOW client_min_messages', 'SCHEMA')[0][0] - end - - # Set the client message level. - def client_min_messages=(level) - execute("SET client_min_messages TO '#{level}'", 'SCHEMA') - end - - # Returns the sequence name for a table's primary key or some other specified key. - def default_sequence_name(table_name, pk = nil) #:nodoc: - result = serial_sequence(table_name, pk || 'id') - return nil unless result - result.split('.').last - rescue ActiveRecord::StatementInvalid - "#{table_name}_#{pk || 'id'}_seq" - end - - def serial_sequence(table, column) - result = exec_query(<<-eosql, 'SCHEMA') - SELECT pg_get_serial_sequence('#{table}', '#{column}') - eosql - result.rows.first.first - end - - # Resets the sequence of a table's primary key to the maximum value. - def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: - unless pk and sequence - default_pk, default_sequence = pk_and_sequence_for(table) - - pk ||= default_pk - sequence ||= default_sequence - end - - if @logger && pk && !sequence - @logger.warn "#{table} has primary key #{pk} with no default sequence" - end - - if pk && sequence - quoted_sequence = quote_table_name(sequence) - - select_value <<-end_sql, 'Reset sequence' - SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) - end_sql - end - end - - # Returns a table's primary key and belonging sequence. - def pk_and_sequence_for(table) #:nodoc: - # First try looking for a sequence with a dependency on the - # given table's primary key. - result = query(<<-end_sql, 'PK and serial sequence')[0] - SELECT attr.attname, seq.relname - FROM pg_class seq, - pg_attribute attr, - pg_depend dep, - pg_namespace name, - pg_constraint cons - WHERE seq.oid = dep.objid - AND seq.relkind = 'S' - AND attr.attrelid = dep.refobjid - AND attr.attnum = dep.refobjsubid - AND attr.attrelid = cons.conrelid - AND attr.attnum = cons.conkey[1] - AND cons.contype = 'p' - AND dep.refobjid = '#{quote_table_name(table)}'::regclass - end_sql - - if result.nil? or result.empty? - # If that fails, try parsing the primary key's default value. - # Support the 7.x and 8.0 nextval('foo'::text) as well as - # the 8.1+ nextval('foo'::regclass). - result = query(<<-end_sql, 'PK and custom sequence')[0] - SELECT attr.attname, - CASE - WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN - substr(split_part(def.adsrc, '''', 2), - strpos(split_part(def.adsrc, '''', 2), '.')+1) - ELSE split_part(def.adsrc, '''', 2) - END - FROM pg_class t - JOIN pg_attribute attr ON (t.oid = attrelid) - JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) - JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) - WHERE t.oid = '#{quote_table_name(table)}'::regclass - AND cons.contype = 'p' - AND def.adsrc ~* 'nextval' - end_sql - end - - [result.first, result.last] - rescue - nil - end - - # Returns just a table's primary key - def primary_key(table) - row = exec_query(<<-end_sql, 'SCHEMA').rows.first - SELECT DISTINCT(attr.attname) - FROM pg_attribute attr - INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] - WHERE cons.contype = 'p' - AND dep.refobjid = '#{table}'::regclass - end_sql - - row && row.first - end - - # Renames a table. - # Also renames a table's primary key sequence if the sequence name matches the - # Active Record default. - # - # Example: - # rename_table('octopuses', 'octopi') - def rename_table(name, new_name) - clear_cache! - execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" - pk, seq = pk_and_sequence_for(new_name) - if seq == "#{name}_#{pk}_seq" - new_seq = "#{new_name}_#{pk}_seq" - execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" - end - end - - # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. - def add_column(table_name, column_name, type, options = {}) - clear_cache! - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - - execute add_column_sql - end - - # Changes the column of a table. - def change_column(table_name, column_name, type, options = {}) - clear_cache! - quoted_table_name = quote_table_name(table_name) - - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - - change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) - change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) - end - - # Changes the default value of a table column. - def change_column_default(table_name, column_name, default) - clear_cache! - execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" - end - - def change_column_null(table_name, column_name, null, default = nil) - clear_cache! - 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 - execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") - end - - # Renames a column in a table. - def rename_column(table_name, column_name, new_column_name) - clear_cache! - execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" - end - - def remove_index!(table_name, index_name) #:nodoc: - execute "DROP INDEX #{quote_table_name(index_name)}" - end - - def rename_index(table_name, old_name, new_name) - execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" - end - - def index_name_length - 63 - end - - # Maps logical Rails types to PostgreSQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) - case type.to_s - when 'binary' - # PostgreSQL doesn't support limits on binary (bytea) columns. - # The hard limit is 1Gb, because of a 32-bit size field, and TOAST. - case limit - when nil, 0..0x3fffffff; super(type) - else raise(ActiveRecordError, "No binary type has byte size #{limit}.") - end - when 'integer' - return 'integer' unless limit - - case limit - when 1, 2; 'smallint' - when 3, 4; 'integer' - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") - end - when 'datetime' - return super unless precision - - case precision - when 0..6; "timestamp(#{precision})" - else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6") - end - else - super - end - end - - # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. - # - # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and - # requires that the ORDER BY include the distinct column. - # - # distinct("posts.id", "posts.created_at desc") - def distinct(columns, orders) #:nodoc: - return "DISTINCT #{columns}" if orders.empty? - - # Construct a clean list of column names from the ORDER BY clause, removing - # any ASC/DESC modifiers - order_columns = orders.collect do |s| - s = s.to_sql unless s.is_a?(String) - s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') - end - order_columns.delete_if { |c| c.blank? } - order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } - - "DISTINCT #{columns}, #{order_columns * ', '}" - end - module Utils extend self @@ -1364,6 +524,7 @@ module ActiveRecord end protected + # Returns the version of the connected PostgreSQL server. def postgresql_version @connection.server_version @@ -1385,21 +546,22 @@ module ActiveRecord end private - def initialize_type_map - result = execute('SELECT oid, typname, typelem, typdelim FROM pg_type', 'SCHEMA') - leaves, nodes = result.partition { |row| row['typelem'] == '0' } - # populate the leaf nodes - leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| - OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']] - end + def initialize_type_map + result = execute('SELECT oid, typname, typelem, typdelim FROM pg_type', 'SCHEMA') + leaves, nodes = result.partition { |row| row['typelem'] == '0' } - # populate composite types - nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| - vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i] - OID::TYPE_MAP[row['oid'].to_i] = vector + # populate the leaf nodes + leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| + OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']] + end + + # populate composite types + nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row| + vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i] + OID::TYPE_MAP[row['oid'].to_i] = vector + end end - end FEATURE_NOT_SUPPORTED = "0A000" # :nodoc: diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index aad21b8e37..cf64985ddb 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -180,9 +180,7 @@ module ActiveRecord @columns_hash = self.class.column_types.dup init_internals - ensure_proper_type - populate_with_current_scope_attributes assign_attributes(attributes, options) if attributes @@ -202,7 +200,7 @@ module ActiveRecord # post.init_with('attributes' => { 'title' => 'hello world' }) # post.title # => 'hello world' def init_with(coder) - @attributes = self.class.initialize_attributes(coder['attributes']) + @attributes = self.class.initialize_attributes(coder['attributes']) @columns_hash = self.class.column_types.merge(coder['column_types'] || {}) init_internals @@ -246,12 +244,10 @@ module ActiveRecord cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) self.class.initialize_attributes(cloned_attributes, :serialized => false) - cloned_attributes.delete(self.class.primary_key) - @attributes = cloned_attributes @attributes[self.class.primary_key] = nil - run_callbacks(:initialize) if _initialize_callbacks.any? + run_callbacks(:initialize) unless _initialize_callbacks.empty? @changed_attributes = {} self.class.column_defaults.each do |attr, orig_value| @@ -310,7 +306,8 @@ module ActiveRecord # Freeze the attributes hash such that associations are still accessible, even on destroyed records. def freeze - @attributes.freeze; self + @attributes.freeze + self end # Returns +true+ if the attributes hash has been frozen. @@ -322,8 +319,6 @@ module ActiveRecord def <=>(other_object) if other_object.is_a?(self.class) self.to_key <=> other_object.to_key - else - nil end end @@ -380,7 +375,6 @@ module ActiveRecord def init_internals pk = self.class.primary_key - @attributes[pk] = nil unless @attributes.key?(pk) @aggregation_cache = {} @@ -393,6 +387,7 @@ module ActiveRecord @marked_for_destruction = false @new_record = true @mass_assignment_options = nil + @txn = nil @_start_transaction_state = {} end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index f91e41b535..3005dc042c 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -389,17 +389,19 @@ module ActiveRecord raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" end - limit = case options[:limit] - when Symbol - send(options[:limit]) - when Proc - options[:limit].call - else - options[:limit] - end + if limit = options[:limit] + limit = case limit + when Symbol + send(limit) + when Proc + limit.call + else + limit + end - if limit && attributes_collection.size > limit - raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." + if limit && attributes_collection.size > limit + raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." + end end if attributes_collection.is_a? Hash @@ -438,7 +440,6 @@ module ActiveRecord else association.add_to_target(existing_record) end - end if !call_reject_if(association_name, attributes) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index ffb8513624..7bd65c180d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -161,7 +161,7 @@ module ActiveRecord became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) became.instance_variable_set("@errors", errors) - became.type = klass.name unless self.class.descends_from_active_record? + became.public_send("#{klass.inheritance_column}=", klass.name) unless self.class.descends_from_active_record? became end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index ecf8547e67..a9f80ccd5f 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -71,7 +71,7 @@ module ActiveRecord end end - initializer "active_record.check_schema_cache_dump" do |app| + initializer "active_record.check_schema_cache_dump" do if config.active_record.delete(:use_schema_cache_dump) config.after_initialize do |app| ActiveSupport.on_load(:active_record) do diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index d93e7c8997..7c43d844d0 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -156,7 +156,7 @@ module ActiveRecord def pluck(*column_names) column_names.map! do |column_name| if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s) - "#{table_name}.#{column_name}" + "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}" else column_name end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 690409d62c..5c74c07ad1 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -1,4 +1,3 @@ - module ActiveRecord module Sanitization extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index a540bc0a3b..eaa4aa7086 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -11,16 +11,16 @@ module ActiveRecord # # ActiveRecord::Schema.define do # create_table :authors do |t| - # t.string :name, :null => false + # t.string :name, null: false # end # # add_index :authors, :name, :unique # # create_table :posts do |t| - # t.integer :author_id, :null => false + # t.integer :author_id, null: false # t.string :subject # t.text :body - # t.boolean :private, :default => false + # t.boolean :private, default: false # end # # add_index :posts, :author_id @@ -50,7 +50,7 @@ module ActiveRecord # The +info+ hash is optional, and if given is used to define metadata # about the current schema (currently, only the schema's version): # - # ActiveRecord::Schema.define(:version => 20380119000001) do + # ActiveRecord::Schema.define(version: 20380119000001) do # ... # end def self.define(info={}, &block) diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index a25a8d79bd..310b4c1459 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -38,7 +38,7 @@ module ActiveRecord end def header(stream) - define_params = @version ? ":version => #{@version}" : "" + define_params = @version ? "version: #{@version}" : "" if stream.respond_to?(:external_encoding) && stream.external_encoding stream.puts "# encoding: #{stream.external_encoding.name}" @@ -95,12 +95,12 @@ HEADER tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" if columns.detect { |c| c.name == pk } if pk != 'id' - tbl.print %Q(, :primary_key => "#{pk}") + tbl.print %Q(, primary_key: "#{pk}") end else - tbl.print ", :id => false" + tbl.print ", id: false" end - tbl.print ", :force => true" + tbl.print ", force: true" tbl.puts " do |t|" # then dump all non-primary key columns @@ -122,7 +122,7 @@ HEADER spec[:scale] = column.scale.inspect if column.scale spec[:null] = 'false' unless column.null spec[:default] = default_string(column.default) if column.has_default? - (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")} + (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")} spec end.compact @@ -187,17 +187,17 @@ HEADER statement_parts = [ ('add_index ' + remove_prefix_and_suffix(index.table).inspect), index.columns.inspect, - (':name => ' + index.name.inspect), + ('name: ' + index.name.inspect), ] - statement_parts << ':unique => true' if index.unique + statement_parts << 'unique: true' if index.unique index_lengths = (index.lengths || []).compact - statement_parts << (':length => ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty? + statement_parts << ('length: ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty? index_orders = (index.orders || {}) - statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty? + statement_parts << ('order: ' + index.orders.inspect) unless index_orders.empty? - statement_parts << (':where => ' + index.where.inspect) if index.where + statement_parts << ('where: ' + index.where.inspect) if index.where ' ' + statement_parts.join(', ') end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index c13fd01654..8ea0ea239f 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -57,8 +57,8 @@ module ActiveRecord define_method("#{key}=") do |value| attribute = initialize_store_attribute(store_attribute) if value != attribute[key] - attribute[key] = value send :"#{store_attribute}_will_change!" + attribute[key] = value end end @@ -67,7 +67,8 @@ module ActiveRecord end end - self.stored_attributes[store_attribute] = keys + self.stored_attributes[store_attribute] ||= [] + self.stored_attributes[store_attribute] |= keys end end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index bf62dfd5b5..85d08402f9 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -54,11 +54,15 @@ module ActiveRecord end def structure_load(filename) - establish_connection(configuration) - connection.execute('SET foreign_key_checks = 0') - IO.read(filename).split("\n\n").each do |table| - connection.execute(table) + args = ['mysql'] + args.concat(['--user', configuration['username']]) if configuration['username'] + args << "--password=#{configuration['password']}" if configuration['password'] + args.concat(['--default-character-set', configuration['charset']]) if configuration['charset'] + configuration.slice('host', 'port', 'socket', 'database').each do |k, v| + args.concat([ "--#{k}", v ]) if v end + args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}]) + Kernel.system(*args) end private diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 09318879d5..e008b32170 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -1,6 +1,32 @@ require 'thread' module ActiveRecord + class Transaction + attr_reader :next + + def initialize(txn = nil) + @next = txn + @committed = false + @aborted = false + end + + def committed! + @committed = true + end + + def aborted! + @aborted = true + end + + def committed? + @committed + end + + def aborted? + @aborted + end + end + # See ActiveRecord::Transactions::ClassMethods for documentation. module Transactions extend ActiveSupport::Concern @@ -307,11 +333,11 @@ module ActiveRecord def with_transaction_returning_status status = nil self.class.transaction do + @txn = self.class.connection.current_transaction add_to_transaction begin status = yield rescue ActiveRecord::Rollback - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 status = nil end @@ -327,20 +353,17 @@ module ActiveRecord @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) @_start_transaction_state[:new_record] = @new_record @_start_transaction_state[:destroyed] = @destroyed - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 end # Clear the new record state and id of a record. def clear_transaction_record_state #:nodoc: - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - @_start_transaction_state.clear if @_start_transaction_state[:level] < 1 + @_start_transaction_state.clear if @txn.committed? end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. def restore_transaction_record_state(force = false) #:nodoc: unless @_start_transaction_state.empty? - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - if @_start_transaction_state[:level] < 1 || force + if @txn.aborted? || force restore_state = @_start_transaction_state was_frozen = @attributes.frozen? @attributes = @attributes.dup if was_frozen diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index f6a432c6e5..a3c274d9b9 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -7,6 +7,7 @@ module ActiveRecord def create_migration_file set_local_assigns! + validate_file_name! migration_template "migration.rb", "db/migrate/#{file_name}.rb" end @@ -41,6 +42,14 @@ module ActiveRecord attribute.name.singularize.foreign_key end.to_sym end + + private + + def validate_file_name! + unless file_name =~ /^[_a-z0-9]+$/ + raise IllegalMigrationNameError.new(file_name) + end + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 113c27b194..1b4f4a5fc9 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -3,8 +3,6 @@ require 'cases/helper' class PostgresqlActiveSchemaTest < ActiveRecord::TestCase def setup ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do - alias_method :real_execute, :execute - remove_method :execute def execute(sql, name = nil) sql end end end @@ -12,7 +10,6 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase def teardown ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do remove_method :execute - alias_method :execute, :real_execute end end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index a4d9286d52..a7f6d9c580 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -51,7 +51,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase @connection.execute("INSERT INTO postgresql_numbers (single, double) VALUES (123.456, 123456.789)") @first_number = PostgresqlNumber.find(1) - @connection.execute("INSERT INTO postgresql_times (time_interval) VALUES ('1 year 2 days ago')") + @connection.execute("INSERT INTO postgresql_times (time_interval, scaled_time_interval) VALUES ('1 year 2 days ago', '3 weeks ago')") @first_time = PostgresqlTime.find(1) @connection.execute("INSERT INTO postgresql_network_addresses (cidr_address, inet_address, mac_address) VALUES('192.168.0/24', '172.16.1.254/32', '01:23:45:67:89:0a')") @@ -89,6 +89,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase def test_data_type_of_time_types assert_equal :string, @first_time.column_for_attribute(:time_interval).type + assert_equal :string, @first_time.column_for_attribute(:scaled_time_interval).type end def test_data_type_of_network_address_types @@ -142,6 +143,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase def test_time_values assert_equal '-1 years -2 days', @first_time.time_interval + assert_equal '-21 days', @first_time.scaled_time_interval end def test_network_address_values_ipaddr diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb new file mode 100644 index 0000000000..d64037eec0 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -0,0 +1,71 @@ +# encoding: utf-8 + +require "cases/helper" +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlJSONTest < ActiveRecord::TestCase + class JsonDataType < ActiveRecord::Base + self.table_name = 'json_data_type' + end + + def setup + @connection = ActiveRecord::Base.connection + begin + @connection.transaction do + @connection.create_table('json_data_type') do |t| + t.json 'payload', :default => {} + end + end + rescue ActiveRecord::StatementInvalid + return skip "do not test on PG without json" + end + @column = JsonDataType.columns.find { |c| c.name == 'payload' } + end + + def teardown + @connection.execute 'drop table if exists json_data_type' + end + + def test_column + assert_equal :json, @column.type + end + + def test_type_cast_json + assert @column + + data = "{\"a_key\":\"a_value\"}" + hash = @column.class.string_to_json data + assert_equal({'a_key' => 'a_value'}, hash) + assert_equal({'a_key' => 'a_value'}, @column.type_cast(data)) + + assert_equal({}, @column.type_cast("{}")) + assert_equal({'key'=>nil}, @column.type_cast('{"key": null}')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q({"c":"}", "\"a\"":"b \"a b"}))) + end + + def test_rewrite + @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" + x = JsonDataType.first + x.payload = { '"a\'' => 'b' } + assert x.save! + end + + def test_select + @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" + x = JsonDataType.first + assert_equal({'k' => 'v'}, x.payload) + end + + def test_select_multikey + @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| + x = JsonDataType.first + assert_equal({'k1' => 'v1', 'k2' => 'v2', 'k3' => [1,2,3]}, x.payload) + end + + def test_null_json + @connection.execute %q|insert into json_data_type (payload) VALUES(null)| + x = JsonDataType.first + assert_equal(nil, x.payload) + end +end diff --git a/activerecord/test/cases/associations/join_dependency_test.rb b/activerecord/test/cases/associations/join_dependency_test.rb new file mode 100644 index 0000000000..08c166dc33 --- /dev/null +++ b/activerecord/test/cases/associations/join_dependency_test.rb @@ -0,0 +1,8 @@ +require "cases/helper" +require 'models/edge' + +class JoinDependencyTest < ActiveRecord::TestCase + def test_column_names_with_alias_handles_nil_primary_key + assert_equal Edge.column_names, ActiveRecord::Associations::JoinDependency::JoinBase.new(Edge).column_names_with_alias.map(&:first) + end +end
\ No newline at end of file diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index ea58a624a1..d08b157011 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -395,7 +395,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_query_attribute_with_custom_fields object = Company.find_by_sql(<<-SQL).first - SELECT c1.*, c2.ruby_type as string_value, c2.rating as int_value + SELECT c1.*, c2.type as string_value, c2.rating as int_value FROM companies c1, companies c2 WHERE c1.firm_id = c2.id AND c1.id = 2 diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index c6f7e8cf0f..b9d480d9ce 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -970,6 +970,18 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time end + def test_attributes_on_dummy_time_with_invalid_time + # Oracle, and Sybase do not have a TIME datatype. + return true if current_adapter?(:OracleAdapter, :SybaseAdapter) + + attributes = { + "bonus_time" => "not a time" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_nil topic.bonus_time + end + def test_boolean b_nil = Boolean.create({ "value" => nil }) nil_id = b_nil.id @@ -1735,7 +1747,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_attribute_names - assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"], + assert_equal ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"], Company.attribute_names end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 40e712072f..6cb6c469d2 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -1,10 +1,11 @@ require "cases/helper" +require 'models/club' require 'models/company' require "models/contract" -require 'models/topic' require 'models/edge' -require 'models/club' require 'models/organization' +require 'models/possession' +require 'models/topic' Company.has_many :accounts @@ -576,4 +577,10 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal ["37signals", nil], companies_and_developers.first assert_equal ["test", 7], companies_and_developers.last end + + def test_pluck_with_reserved_words + Possession.create!(:where => "Over There") + + assert_equal ["Over There"], Possession.pluck(:where) + end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 20c8e8894d..d44ac21b05 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -276,6 +276,7 @@ class FinderTest < ActiveRecord::TestCase def test_find_only_some_columns topic = Topic.all.merge!(:select => "author_name").find(1) assert_raise(ActiveModel::MissingAttributeError) {topic.title} + assert_raise(ActiveModel::MissingAttributeError) {topic.title?} assert_nil topic.read_attribute("title") assert_equal "David", topic.author_name assert !topic.attribute_present?("title") diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index e80259a7f1..8fded9159f 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -5,9 +5,10 @@ require 'models/post' require 'models/project' require 'models/subscriber' require 'models/teapot' +require 'models/vegetables' class InheritanceTest < ActiveRecord::TestCase - fixtures :companies, :projects, :subscribers, :accounts + fixtures :companies, :projects, :subscribers, :accounts, :vegetables def test_class_with_store_full_sti_class_returns_full_name old = ActiveRecord::Base.store_full_sti_class @@ -122,9 +123,17 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_inheritance_find - switch_to_alt_inheritance_column - test_inheritance_find - switch_to_default_inheritance_column + assert_kind_of Cucumber, Vegetable.find(1) + assert_kind_of Cucumber, Cucumber.find(1) + assert_kind_of Cabbage, Vegetable.find(2) + assert_kind_of Cabbage, Cabbage.find(2) + end + + def test_alt_becomes_works_with_sti + vegetable = Vegetable.find(1) + assert_kind_of Vegetable, vegetable + cabbage = vegetable.becomes(Cabbage) + assert_kind_of Cabbage, cabbage end def test_inheritance_find_all @@ -134,9 +143,9 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_inheritance_find_all - switch_to_alt_inheritance_column - test_inheritance_find_all - switch_to_default_inheritance_column + companies = Vegetable.all.merge!(:order => 'id').to_a + assert_kind_of Cucumber, companies[0] + assert_kind_of Cabbage, companies[1] end def test_inheritance_save @@ -149,9 +158,11 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_inheritance_save - switch_to_alt_inheritance_column - test_inheritance_save - switch_to_default_inheritance_column + cabbage = Cabbage.new(:name => 'Savoy') + cabbage.save! + + savoy = Vegetable.find(cabbage.id) + assert_kind_of Cabbage, savoy end def test_inheritance_condition @@ -161,9 +172,9 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_inheritance_condition - switch_to_alt_inheritance_column - test_inheritance_condition - switch_to_default_inheritance_column + assert_equal 4, Vegetable.count + assert_equal 1, Cucumber.count + assert_equal 3, Cabbage.count end def test_finding_incorrect_type_data @@ -172,9 +183,8 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_finding_incorrect_type_data - switch_to_alt_inheritance_column - test_finding_incorrect_type_data - switch_to_default_inheritance_column + assert_raise(ActiveRecord::RecordNotFound) { Cucumber.find(2) } + assert_nothing_raised { Cucumber.find(1) } end def test_update_all_within_inheritance @@ -185,9 +195,9 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_update_all_within_inheritance - switch_to_alt_inheritance_column - test_update_all_within_inheritance - switch_to_default_inheritance_column + Cabbage.update_all "name = 'the cabbage'" + assert_equal "the cabbage", Cabbage.first.name + assert_equal ["my cucumber"], Cucumber.all.map(&:name).uniq end def test_destroy_all_within_inheritance @@ -197,9 +207,9 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_destroy_all_within_inheritance - switch_to_alt_inheritance_column - test_destroy_all_within_inheritance - switch_to_default_inheritance_column + Cabbage.destroy_all + assert_equal 0, Cabbage.count + assert_equal 1, Cucumber.count end def test_find_first_within_inheritance @@ -209,9 +219,9 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_find_first_within_inheritance - switch_to_alt_inheritance_column - test_find_first_within_inheritance - switch_to_default_inheritance_column + assert_kind_of Cabbage, Vegetable.all.merge!(:where => "name = 'his cabbage'").first + assert_kind_of Cabbage, Cabbage.all.merge!(:where => "name = 'his cabbage'").first + assert_nil Cucumber.all.merge!(:where => "name = 'his cabbage'").first end def test_complex_inheritance @@ -225,9 +235,13 @@ class InheritanceTest < ActiveRecord::TestCase end def test_alt_complex_inheritance - switch_to_alt_inheritance_column - test_complex_inheritance - switch_to_default_inheritance_column + king_cole = KingCole.create("name" => "uniform heads") + assert_equal king_cole, KingCole.where("name = 'uniform heads'").first + assert_equal king_cole, GreenCabbage.all.merge!(:where => "name = 'uniform heads'").first + assert_equal king_cole, Cabbage.all.merge!(:where => "name = 'uniform heads'").first + assert_equal king_cole, Vegetable.all.merge!(:where => "name = 'uniform heads'").first + assert_equal 1, Cabbage.all.merge!(:where => "name = 'his cabbage'").to_a.size + assert_equal king_cole, Cabbage.find(king_cole.id) end def test_eager_load_belongs_to_something_inherited @@ -235,6 +249,11 @@ class InheritanceTest < ActiveRecord::TestCase assert account.association_cache.key?(:firm), "nil proves eager load failed" end + def test_alt_eager_loading + cabbage = RedCabbage.all.merge!(:includes => :seller).find(4) + assert cabbage.association_cache.key?(:seller), "nil proves eager load failed" + end + def test_eager_load_belongs_to_primary_key_quoting con = Account.connection assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)/) do @@ -242,12 +261,6 @@ class InheritanceTest < ActiveRecord::TestCase end end - def test_alt_eager_loading - switch_to_alt_inheritance_column - test_eager_load_belongs_to_something_inherited - switch_to_default_inheritance_column - end - def test_inherits_custom_primary_key assert_equal Subscriber.primary_key, SpecialSubscriber.primary_key end @@ -256,21 +269,6 @@ class InheritanceTest < ActiveRecord::TestCase assert_kind_of SpecialSubscriber, SpecialSubscriber.find("webster132") assert_nothing_raised { s = SpecialSubscriber.new("name" => "And breaaaaathe!"); s.id = 'roger'; s.save } end - - private - def switch_to_alt_inheritance_column - # we don't want misleading test results, so get rid of the values in the type column - Company.all.merge!(:order => 'id').to_a.each do |c| - c['type'] = nil - c.save - end - [ Company, Firm, Client].each { |klass| klass.reset_column_information } - Company.inheritance_column = 'ruby_type' - end - def switch_to_default_inheritance_column - [ Company, Firm, Client].each { |klass| klass.reset_column_information } - Company.inheritance_column = 'type' - end end @@ -290,7 +288,7 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase def test_instantiation_doesnt_try_to_require_corresponding_file ActiveRecord::Base.store_full_sti_class = false foo = Firm.first.clone - foo.ruby_type = foo.type = 'FirmOnTheFly' + foo.type = 'FirmOnTheFly' foo.save! # Should fail without FirmOnTheFly in the type condition. diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 01dd25a9df..80d2670f94 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -96,7 +96,7 @@ class SchemaDumperTest < ActiveRecord::TestCase ActiveRecord::SchemaDumper.ignore_tables = [/^[^r]/] ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) output = stream.string - assert_match %r{:null => false}, output + assert_match %r{null: false}, output end def test_schema_dump_includes_limit_constraint_for_integer_columns @@ -107,46 +107,46 @@ class SchemaDumperTest < ActiveRecord::TestCase output = stream.string if current_adapter?(:PostgreSQLAdapter) - assert_match %r{c_int_1.*:limit => 2}, output - assert_match %r{c_int_2.*:limit => 2}, output + assert_match %r{c_int_1.*limit: 2}, output + assert_match %r{c_int_2.*limit: 2}, output # int 3 is 4 bytes in postgresql assert_match %r{c_int_3.*}, output - assert_no_match %r{c_int_3.*:limit}, output + assert_no_match %r{c_int_3.*limit:}, output assert_match %r{c_int_4.*}, output - assert_no_match %r{c_int_4.*:limit}, output + assert_no_match %r{c_int_4.*limit:}, output elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) - assert_match %r{c_int_1.*:limit => 1}, output - assert_match %r{c_int_2.*:limit => 2}, output - assert_match %r{c_int_3.*:limit => 3}, output + assert_match %r{c_int_1.*limit: 1}, output + assert_match %r{c_int_2.*limit: 2}, output + assert_match %r{c_int_3.*limit: 3}, output assert_match %r{c_int_4.*}, output assert_no_match %r{c_int_4.*:limit}, output elsif current_adapter?(:SQLite3Adapter) - assert_match %r{c_int_1.*:limit => 1}, output - assert_match %r{c_int_2.*:limit => 2}, output - assert_match %r{c_int_3.*:limit => 3}, output - assert_match %r{c_int_4.*:limit => 4}, output + assert_match %r{c_int_1.*limit: 1}, output + assert_match %r{c_int_2.*limit: 2}, output + assert_match %r{c_int_3.*limit: 3}, output + assert_match %r{c_int_4.*limit: 4}, output end assert_match %r{c_int_without_limit.*}, output - assert_no_match %r{c_int_without_limit.*:limit}, output + assert_no_match %r{c_int_without_limit.*limit:}, output if current_adapter?(:SQLite3Adapter) - assert_match %r{c_int_5.*:limit => 5}, output - assert_match %r{c_int_6.*:limit => 6}, output - assert_match %r{c_int_7.*:limit => 7}, output - assert_match %r{c_int_8.*:limit => 8}, output + assert_match %r{c_int_5.*limit: 5}, output + assert_match %r{c_int_6.*limit: 6}, output + assert_match %r{c_int_7.*limit: 7}, output + assert_match %r{c_int_8.*limit: 8}, output elsif current_adapter?(:OracleAdapter) - assert_match %r{c_int_5.*:limit => 5}, output - assert_match %r{c_int_6.*:limit => 6}, output - assert_match %r{c_int_7.*:limit => 7}, output - assert_match %r{c_int_8.*:limit => 8}, output + assert_match %r{c_int_5.*limit: 5}, output + assert_match %r{c_int_6.*limit: 6}, output + assert_match %r{c_int_7.*limit: 7}, output + assert_match %r{c_int_8.*limit: 8}, output else - assert_match %r{c_int_5.*:limit => 8}, output - assert_match %r{c_int_6.*:limit => 8}, output - assert_match %r{c_int_7.*:limit => 8}, output - assert_match %r{c_int_8.*:limit => 8}, output + assert_match %r{c_int_5.*limit: 8}, output + assert_match %r{c_int_6.*limit: 8}, output + assert_match %r{c_int_7.*limit: 8}, output + assert_match %r{c_int_8.*limit: 8}, output end end @@ -182,15 +182,15 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_index_columns_in_right_order index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip - assert_equal 'add_index "companies", ["firm_id", "type", "rating", "ruby_type"], :name => "company_index"', index_definition + assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition end def test_schema_dumps_partial_indices index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip if current_adapter?(:PostgreSQLAdapter) - assert_equal 'add_index "companies", ["firm_id", "type"], :name => "company_partial_index", :where => "(rating > 10)"', index_definition + assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)"', index_definition else - assert_equal 'add_index "companies", ["firm_id", "type"], :name => "company_partial_index"', index_definition + assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition end end @@ -198,25 +198,25 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump match = output.match(%r{create_table "movies"(.*)do}) assert_not_nil(match, "nonstandardpk table not found") - assert_match %r(:primary_key => "movieid"), match[1], "non-standard primary key not preserved" + assert_match %r(primary_key: "movieid"), match[1], "non-standard primary key not preserved" end if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) def test_schema_dump_should_not_add_default_value_for_mysql_text_field output = standard_dump - assert_match %r{t.text\s+"body",\s+:null => false$}, output + assert_match %r{t.text\s+"body",\s+null: false$}, output end def test_schema_dump_includes_length_for_mysql_blob_and_text_fields output = standard_dump - assert_match %r{t.binary\s+"tiny_blob",\s+:limit => 255$}, output + assert_match %r{t.binary\s+"tiny_blob",\s+limit: 255$}, output assert_match %r{t.binary\s+"normal_blob"$}, output - assert_match %r{t.binary\s+"medium_blob",\s+:limit => 16777215$}, output - assert_match %r{t.binary\s+"long_blob",\s+:limit => 2147483647$}, output - assert_match %r{t.text\s+"tiny_text",\s+:limit => 255$}, output + assert_match %r{t.binary\s+"medium_blob",\s+limit: 16777215$}, output + assert_match %r{t.binary\s+"long_blob",\s+limit: 2147483647$}, output + assert_match %r{t.text\s+"tiny_text",\s+limit: 255$}, output assert_match %r{t.text\s+"normal_text"$}, output - assert_match %r{t.text\s+"medium_text",\s+:limit => 16777215$}, output - assert_match %r{t.text\s+"long_text",\s+:limit => 2147483647$}, output + assert_match %r{t.text\s+"medium_text",\s+limit: 16777215$}, output + assert_match %r{t.text\s+"long_text",\s+limit: 2147483647$}, output end end @@ -225,7 +225,7 @@ class SchemaDumperTest < ActiveRecord::TestCase ActiveRecord::SchemaDumper.ignore_tables = [/^[^n]/] ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) output = stream.string - assert_match %r{:precision => 3,[[:space:]]+:scale => 2,[[:space:]]+:default => 2.78}, output + assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: 2.78}, output end if current_adapter?(:PostgreSQLAdapter) @@ -236,6 +236,13 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dump_includes_json_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_json_data_type"} =~ output + assert_match %r|t.json "json_data", default: {}|, output + end + end + def test_schema_dump_includes_inet_shorthand_definition output = standard_dump if %r{create_table "postgresql_network_address"} =~ output @@ -267,7 +274,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_includes_hstores_shorthand_definition output = standard_dump if %r{create_table "postgresql_hstores"} =~ output - assert_match %r[t.hstore "hash_store", :default => {}], output + assert_match %r[t.hstore "hash_store", default: {}], output end end @@ -283,9 +290,9 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump # Oracle supports precision up to 38 and it identifies decimals with scale 0 as integers if current_adapter?(:OracleAdapter) - assert_match %r{t.integer\s+"atoms_in_universe",\s+:precision => 38,\s+:scale => 0}, output + assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38,\s+scale: 0}, output else - assert_match %r{t.decimal\s+"atoms_in_universe",\s+:precision => 55,\s+:scale => 0}, output + assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55,\s+scale: 0}, output end end @@ -293,13 +300,13 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump match = output.match(%r{create_table "goofy_string_id"(.*)do.*\n(.*)\n}) assert_not_nil(match, "goofy_string_id table not found") - assert_match %r(:id => false), match[1], "no table id not preserved" - assert_match %r{t.string[[:space:]]+"id",[[:space:]]+:null => false$}, match[2], "non-primary key id column not preserved" + assert_match %r(id: false), match[1], "no table id not preserved" + assert_match %r{t.string[[:space:]]+"id",[[:space:]]+null: false$}, match[2], "non-primary key id column not preserved" end def test_schema_dump_keeps_id_false_when_id_is_false_and_unique_not_null_column_added output = standard_dump - assert_match %r{create_table "subscribers", :id => false}, output + assert_match %r{create_table "subscribers", id: false}, output end class CreateDogMigration < ActiveRecord::Migration diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index fb0d116c08..2741f223da 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -34,6 +34,12 @@ class StoreTest < ActiveRecord::TestCase assert @john.settings_changed? end + test "updating the store populates the changed array correctly" do + @john.color = 'red' + assert_equal 'black', @john.settings_change[0]['color'] + assert_equal 'red', @john.settings_change[1]['color'] + end + test "updating the store won't mark it as changed if an attribute isn't changed" do @john.color = @john.color assert !@john.settings_changed? @@ -117,8 +123,8 @@ class StoreTest < ActiveRecord::TestCase assert_equal false, @john.is_a_good_guy end - test "stored attributes are returned" do - assert_equal [:color, :homepage], Admin::User.stored_attributes[:settings] + test "all stored attributes are returned" do + assert_equal [:color, :homepage, :favorite_food], Admin::User.stored_attributes[:settings] end test "stores_attributes are class level settings" do diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index b49561d858..be591da8d6 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -251,17 +251,14 @@ module ActiveRecord ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) + Kernel.stubs(:system) end def test_structure_load filename = "awesome-file.sql" - ActiveRecord::Base.expects(:establish_connection).with(@configuration) - @connection.expects(:execute).twice + Kernel.expects(:system).with('mysql', '--database', 'test-db', '--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}) - open(filename, 'w') { |f| f.puts("SELECT CURDATE();") } ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) - ensure - FileUtils.rm(filename) end end diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index 7249ae9e4d..68fa15de50 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -185,18 +185,18 @@ class NilXmlSerializationTest < ActiveRecord::TestCase end def test_should_serialize_string - assert_match %r{<name nil="true"></name>}, @xml + assert_match %r{<name nil="true"/>}, @xml end def test_should_serialize_integer - assert %r{<age (.*)></age>}.match(@xml) + assert %r{<age (.*)/>}.match(@xml) attributes = $1 assert_match %r{nil="true"}, attributes assert_match %r{type="integer"}, attributes end def test_should_serialize_binary - assert %r{<avatar (.*)></avatar>}.match(@xml) + assert %r{<avatar (.*)/>}.match(@xml) attributes = $1 assert_match %r{type="binary"}, attributes assert_match %r{encoding="base64"}, attributes @@ -204,21 +204,21 @@ class NilXmlSerializationTest < ActiveRecord::TestCase end def test_should_serialize_datetime - assert %r{<created-at (.*)></created-at>}.match(@xml) + assert %r{<created-at (.*)/>}.match(@xml) attributes = $1 assert_match %r{nil="true"}, attributes assert_match %r{type="dateTime"}, attributes end def test_should_serialize_boolean - assert %r{<awesome (.*)></awesome>}.match(@xml) + assert %r{<awesome (.*)/>}.match(@xml) attributes = $1 assert_match %r{type="boolean"}, attributes assert_match %r{nil="true"}, attributes end def test_should_serialize_yaml - assert_match %r{<preferences nil=\"true\"></preferences>}, @xml + assert_match %r{<preferences nil=\"true\"/>}, @xml end end diff --git a/activerecord/test/fixtures/companies.yml b/activerecord/test/fixtures/companies.yml index a982d3921d..0766e92027 100644 --- a/activerecord/test/fixtures/companies.yml +++ b/activerecord/test/fixtures/companies.yml @@ -4,14 +4,12 @@ first_client: firm_id: 1 client_of: 2 name: Summit - ruby_type: Client firm_name: 37signals first_firm: id: 1 type: Firm name: 37signals - ruby_type: Firm firm_id: 1 second_client: @@ -20,13 +18,11 @@ second_client: firm_id: 1 client_of: 1 name: Microsoft - ruby_type: Client another_firm: id: 4 type: Firm name: Flamboyant Software - ruby_type: Firm another_client: id: 5 @@ -34,7 +30,6 @@ another_client: firm_id: 4 client_of: 4 name: Ex Nihilo - ruby_type: Client a_third_client: id: 10 @@ -42,7 +37,6 @@ a_third_client: firm_id: 4 client_of: 4 name: Ex Nihilo Part Deux - ruby_type: Client rails_core: id: 6 diff --git a/activerecord/test/fixtures/vegetables.yml b/activerecord/test/fixtures/vegetables.yml new file mode 100644 index 0000000000..b9afbfbb05 --- /dev/null +++ b/activerecord/test/fixtures/vegetables.yml @@ -0,0 +1,20 @@ +first_cucumber: + id: 1 + custom_type: Cucumber + name: 'my cucumber' + +first_cabbage: + id: 2 + custom_type: Cabbage + name: 'my cabbage' + +second_cabbage: + id: 3 + custom_type: Cabbage + name: 'his cabbage' + +red_cabbage: + id: 4 + custom_type: RedCabbage + name: 'red cabbage' + seller_id: 3
\ No newline at end of file diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index ad30039304..6c4eb03b06 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -1,6 +1,7 @@ class Admin::User < ActiveRecord::Base belongs_to :account store :settings, :accessors => [ :color, :homepage ] + store_accessor :settings, :favorite_food store :preferences, :accessors => [ :remember_login ] store :json_data, :accessors => [ :height, :weight ], :coder => JSON store :json_data_empty, :accessors => [ :is_a_good_guy ], :coder => JSON diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 75f38d275c..9bdce6e729 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -173,10 +173,6 @@ class Client < Company before_destroy :overwrite_to_raise # Used to test that read and question methods are not generated for these attributes - def ruby_type - read_attribute :ruby_type - end - def rating? query_attribute :rating end diff --git a/activerecord/test/models/possession.rb b/activerecord/test/models/possession.rb new file mode 100644 index 0000000000..ddf759113b --- /dev/null +++ b/activerecord/test/models/possession.rb @@ -0,0 +1,3 @@ +class Possession < ActiveRecord::Base + self.table_name = 'having' +end diff --git a/activerecord/test/models/vegetables.rb b/activerecord/test/models/vegetables.rb new file mode 100644 index 0000000000..1f41cde3a5 --- /dev/null +++ b/activerecord/test/models/vegetables.rb @@ -0,0 +1,24 @@ +class Vegetable < ActiveRecord::Base + + validates_presence_of :name + + def self.inheritance_column + 'custom_type' + end +end + +class Cucumber < Vegetable +end + +class Cabbage < Vegetable +end + +class GreenCabbage < Cabbage +end + +class KingCole < GreenCabbage +end + +class RedCabbage < Cabbage + belongs_to :seller, :class_name => 'Company' +end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 5f01f1fc50..2cd9f30b59 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,7 +1,7 @@ ActiveRecord::Schema.define do %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids - postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| + postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type).each do |table_name| execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end @@ -82,6 +82,15 @@ _SQL _SQL end + if 't' == select_value("select 'json'=ANY(select typname from pg_type)") + execute <<_SQL + CREATE TABLE postgresql_json_data_type ( + id SERIAL PRIMARY KEY, + json_data json default '{}'::json + ); +_SQL + end + execute <<_SQL CREATE TABLE postgresql_moneys ( id SERIAL PRIMARY KEY, @@ -100,7 +109,8 @@ _SQL execute <<_SQL CREATE TABLE postgresql_times ( id SERIAL PRIMARY KEY, - time_interval INTERVAL + time_interval INTERVAL, + scaled_time_interval INTERVAL(6) ); _SQL diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7c45ca27c0..b4e611cb09 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -171,7 +171,6 @@ ActiveRecord::Schema.define do create_table :companies, :force => true do |t| t.string :type - t.string :ruby_type t.integer :firm_id t.string :firm_name t.string :name @@ -181,9 +180,15 @@ ActiveRecord::Schema.define do t.string :description, :default => "" end - add_index :companies, [:firm_id, :type, :rating, :ruby_type], :name => "company_index" + add_index :companies, [:firm_id, :type, :rating], :name => "company_index" add_index :companies, [:firm_id, :type], :name => "company_partial_index", :where => "rating > 10" + create_table :vegetables, :force => true do |t| + t.string :name + t.integer :seller_id + t.string :custom_type + end + create_table :computers, :force => true do |t| t.integer :developer, :null => false t.integer :extendedWarranty, :null => false @@ -280,6 +285,10 @@ ActiveRecord::Schema.define do t.string :info end + create_table :having, :force => true do |t| + t.string :where + end + create_table :guids, :force => true do |t| t.column :key, :string end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 8dd88f9f62..47280c3dc8 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,5 +1,33 @@ ## Rails 4.0.0 (unreleased) ## +* `ActiveSupport::Callbacks`: deprecate monkey patch of object callbacks. + Using the #filter method like this: + + before_filter MyFilter.new + + class MyFilter + def filter(controller) + end + end + + Is now deprecated with recommendation to use the corresponding filter type + (`#before`, `#after` or `#around`): + + before_filter MyFilter.new + + class MyFilter + def before(controller) + end + end + + *Bogdan Gusiev* + +* An optional block can be passed to `HashWithIndifferentAccess#update` and `#merge`. + The block will be invoked for each duplicated key, and used to resolve the conflict, + thus replicating the behaviour of the corresponding methods on the `Hash` class. + + *Leo Cassarani* + * Remove `j` alias for `ERB::Util#json_escape`. The `j` alias is already used for `ActionView::Helpers::JavaScriptHelper#escape_javascript` and both modules are included in the view context that would confuse the developers. @@ -15,8 +43,8 @@ *Carlos Antonio da Silva* -* ActiveSupport::JSON::Variable is deprecated. Define your own #as_json and - #encode_json methods for custom JSON string literals. +* `ActiveSupport::JSON::Variable` is deprecated. Define your own `#as_json` and + `#encode_json` methods for custom JSON string literals. *Erich Menge* diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index a55f68497c..7166c21268 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -279,6 +279,7 @@ module ActiveSupport def _normalize_legacy_filter(kind, filter) if !filter.respond_to?(kind) && filter.respond_to?(:filter) + ActiveSupport::Deprecation.warn("Filter object with #filter method is deprecated. Define method corresponding to filter type (#before, #after or #around).") filter.singleton_class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{kind}(context, &block) filter(context, &block) end RUBY_EVAL diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 45c9f0472a..48be96f176 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -169,47 +169,12 @@ module ActiveSupport #:nodoc: end def const_missing(const_name) - klass_name = name.presence || "Object" - - # Since Ruby does not pass the nesting at the point the unknown - # constant triggered the callback we cannot fully emulate constant - # name lookup and need to make a trade-off: we are going to assume - # that the nesting in the body of Foo::Bar is [Foo::Bar, Foo] even - # though it might not be. Counterexamples are - # - # class Foo::Bar - # Module.nesting # => [Foo::Bar] - # end - # - # or - # - # module M::N - # module S::T - # Module.nesting # => [S::T, M::N] - # end - # end - # - # for example. - nesting = [] - klass_name.to_s.scan(/::|$/) { nesting.unshift $` } - - # If there are multiple levels of nesting to search under, the top - # level is the one we want to report as the lookup fail. - error = nil - - nesting.each do |namespace| - begin - return Dependencies.load_missing_constant Inflector.constantize(namespace), const_name - rescue NoMethodError then raise - rescue NameError => e - error ||= e - end - end - - # Raise the first error for this set. If this const_missing came from an - # earlier const_missing, this will result in the real error bubbling - # all the way up - raise error + # The interpreter does not pass nesting information, and in the + # case of anonymous modules we cannot even make the trade-off of + # assuming their name reflects the nesting. Resort to Object as + # the only meaningful guess we can make. + from_mod = anonymous? ? ::Object : self + Dependencies.load_missing_constant(from_mod, const_name) end def unloadable(const_desc = self) @@ -487,7 +452,7 @@ module ActiveSupport #:nodoc: raise "Circular dependency detected while autoloading constant #{qualified_name}" else require_or_load(expanded) - raise LoadError, "Expected #{file_path} to define #{qualified_name}" unless from_mod.const_defined?(const_name, false) + raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it" unless from_mod.const_defined?(const_name, false) return from_mod.const_get(const_name) end elsif mod = autoload_module!(from_mod, const_name, qualified_name, path_suffix) @@ -499,6 +464,25 @@ module ActiveSupport #:nodoc: # const_missing must be due to from_mod::const_name, which should not # return constants from from_mod's parents. begin + # Since Ruby does not pass the nesting at the point the unknown + # constant triggered the callback we cannot fully emulate constant + # name lookup and need to make a trade-off: we are going to assume + # that the nesting in the body of Foo::Bar is [Foo::Bar, Foo] even + # though it might not be. Counterexamples are + # + # class Foo::Bar + # Module.nesting # => [Foo::Bar] + # end + # + # or + # + # module M::N + # module S::T + # Module.nesting # => [S::T, M::N] + # end + # end + # + # for example. return parent.const_missing(const_name) rescue NameError => e raise unless e.missing_name? qualified_name_for(parent, const_name) diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 5fd20673d8..71713644a7 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -95,10 +95,10 @@ module ActiveSupport alias_method :store, :[]= - # Updates the receiver in-place merging in the hash passed as argument: + # Updates the receiver in-place, merging in the hash passed as argument: # # hash_1 = ActiveSupport::HashWithIndifferentAccess.new - # hash_2[:key] = "value" + # hash_1[:key] = "value" # # hash_2 = ActiveSupport::HashWithIndifferentAccess.new # hash_2[:key] = "New Value!" @@ -110,12 +110,27 @@ module ActiveSupport # In either case the merge respects the semantics of indifferent access. # # If the argument is a regular hash with keys +:key+ and +"key"+ only one - # of the values end up in the receiver, but which was is unespecified. + # of the values end up in the receiver, but which one is unspecified. + # + # When given a block, the value for duplicated keys will be determined + # by the result of invoking the block with the duplicated key, the value + # in the receiver, and the value in +other_hash+. The rules for duplicated + # keys follow the semantics of indifferent access: + # + # hash_1[:key] = 10 + # hash_2['key'] = 12 + # hash_1.update(hash_2) { |key, old, new| old + new } # => {"key"=>22} + # def update(other_hash) if other_hash.is_a? HashWithIndifferentAccess super(other_hash) else - other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) } + other_hash.each_pair do |key, value| + if block_given? && key?(key) + value = yield(convert_key(key), self[key], value) + end + regular_writer(convert_key(key), convert_value(value)) + end self end end @@ -173,8 +188,8 @@ module ActiveSupport # This method has the same semantics of +update+, except it does not # modify the receiver but rather returns a new hash with indifferent # access with the result of the merge. - def merge(hash) - self.dup.update(hash) + def merge(hash, &block) + self.dup.update(hash, &block) end # Like +merge+ but the other way around: Merges the receiver into the diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 4dc9f57038..94463cc311 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -428,6 +428,29 @@ class HashExtTest < ActiveSupport::TestCase assert_equal 2, hash['b'] end + def test_indifferent_merging_with_block + hash = HashWithIndifferentAccess.new + hash[:a] = 1 + hash['b'] = 3 + + other = { 'a' => 4, :b => 2, 'c' => 10 } + + merged = hash.merge(other) { |key, old, new| old > new ? old : new } + + assert_equal HashWithIndifferentAccess, merged.class + assert_equal 4, merged[:a] + assert_equal 3, merged['b'] + assert_equal 10, merged[:c] + + other_indifferent = HashWithIndifferentAccess.new('a' => 9, :b => 2) + + merged = hash.merge(other_indifferent) { |key, old, new| old + new } + + assert_equal HashWithIndifferentAccess, merged.class + assert_equal 10, merged[:a] + assert_equal 5, merged[:b] + end + def test_indifferent_reverse_merging hash = HashWithIndifferentAccess.new('some' => 'value', 'other' => 'value') hash.reverse_merge!(:some => 'noclobber', :another => 'clobber') @@ -825,7 +848,7 @@ class HashToXmlTest < ActiveSupport::TestCase assert_equal "<person>", xml.first(8) assert xml.include?(%(<street>Paulina</street>)) assert xml.include?(%(<name>David</name>)) - assert xml.include?(%(<age nil="true"></age>)) + assert xml.include?(%(<age nil="true"/>)) end def test_one_level_with_skipping_types @@ -833,7 +856,7 @@ class HashToXmlTest < ActiveSupport::TestCase assert_equal "<person>", xml.first(8) assert xml.include?(%(<street>Paulina</street>)) assert xml.include?(%(<name>David</name>)) - assert xml.include?(%(<age nil="true"></age>)) + assert xml.include?(%(<age nil="true"/>)) end def test_one_level_with_yielding diff --git a/guides/source/migrations.textile b/guides/source/migrations.textile index 0173f8ecc6..e89bde7a83 100644 --- a/guides/source/migrations.textile +++ b/guides/source/migrations.textile @@ -912,14 +912,14 @@ If +:ruby+ is selected then the schema is stored in +db/schema.rb+. If you look at this file you'll find that it looks an awful lot like one very big migration: <ruby> -ActiveRecord::Schema.define(:version => 20080906171750) do - create_table "authors", :force => true do |t| +ActiveRecord::Schema.define(version: 20080906171750) do + create_table "authors", force: true do |t| t.string "name" t.datetime "created_at" t.datetime "updated_at" end - create_table "products", :force => true do |t| + create_table "products", force: true do |t| t.string "name" t.text "description" t.datetime "created_at" diff --git a/guides/source/upgrading_ruby_on_rails.textile b/guides/source/upgrading_ruby_on_rails.textile index 52c582ffbc..b750cc2ffb 100644 --- a/guides/source/upgrading_ruby_on_rails.textile +++ b/guides/source/upgrading_ruby_on_rails.textile @@ -54,11 +54,21 @@ h4(#action_pack4_0). Action Pack Rails 4.0 changed how <tt>assert_generates</tt>, <tt>assert_recognizes</tt>, and <tt>assert_routing</tt> work. Now all these assertions raise <tt>Assertion</tt> instead of <tt>ActionController::RoutingError</tt>. -Rails 4.0 also changed the way unicode character routes are drawn. Now you can draw unicode character routes directly. If you already draw such routes, you must change them, e.g. <tt>get Rack::Utils.escape('こんにちは'), :controller => 'welcome', :action => 'index'</tt> to <tt>get 'こんにちは', :controller => 'welcome', :action => 'index'</tt>. +Rails 4.0 also changed the way unicode character routes are drawn. Now you can draw unicode character routes directly. If you already draw such routes, you must change them, for example: + +<ruby> +get Rack::Utils.escape('こんにちは'), :controller => 'welcome', :action => 'index' +</ruby> + +becomes + +<ruby> +get 'こんにちは', :controller => 'welcome', :action => 'index' +</ruby> h4(#active_support4_0). Active Support -Rails 4.0 Removed the `j` alias for `ERB::Util#json_escape` since `j` is already used for `ActionView::Helpers::JavaScriptHelper#escape_javascript`. +Rails 4.0 Removed the <tt>j</tt> alias for <tt>ERB::Util#json_escape</tt> since <tt>j</tt> is already used for <tt>ActionView::Helpers::JavaScriptHelper#escape_javascript</tt>. h4(#helpers_order). Helpers Loading Order diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 774038c0e1..15e5a0b92b 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -28,6 +28,13 @@ class MigrationGeneratorTest < Rails::Generators::TestCase run_generator [migration] assert_migration "db/migrate/change_title_body_from_posts.rb", /class #{migration} < ActiveRecord::Migration/ end + + def test_migration_with_invalid_file_name + migration = "add_something:datetime" + assert_raise ActiveRecord::IllegalMigrationNameError do + run_generator [migration] + end + end def test_add_migration_with_attributes migration = "add_title_body_to_posts" |