aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--actionmailer/CHANGELOG.md16
-rw-r--r--actionmailer/lib/action_mailer/base.rb2
-rw-r--r--actionmailer/lib/action_mailer/delivery_methods.rb4
-rw-r--r--actionmailer/test/delivery_methods_test.rb39
-rw-r--r--actionpack/CHANGELOG.md7
-rw-r--r--actionpack/actionpack.gemspec2
-rw-r--r--actionpack/lib/action_controller/caching/actions.rb2
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb7
-rw-r--r--actionpack/lib/action_controller/metal/responder.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb178
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb7
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb10
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb7
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb69
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb2
-rw-r--r--actionpack/lib/action_view/digestor.rb22
-rw-r--r--actionpack/lib/action_view/helpers/date_helper.rb14
-rw-r--r--actionpack/lib/action_view/helpers/text_helper.rb54
-rw-r--r--actionpack/lib/action_view/routing_url_for.rb2
-rw-r--r--actionpack/test/abstract_unit.rb1
-rw-r--r--actionpack/test/controller/filters_test.rb14
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb2
-rw-r--r--actionpack/test/dispatch/prefix_generation_test.rb11
-rw-r--r--actionpack/test/dispatch/request/session_test.rb9
-rw-r--r--actionpack/test/dispatch/response_test.rb57
-rw-r--r--actionpack/test/dispatch/routing/concerns_test.rb47
-rw-r--r--actionpack/test/dispatch/session/cookie_store_test.rb20
-rw-r--r--actionpack/test/dispatch/session/mem_cache_store_test.rb13
-rw-r--r--actionpack/test/template/digestor_test.rb31
-rw-r--r--actionpack/test/template/text_helper_test.rb13
-rw-r--r--activemodel/CHANGELOG.md11
-rw-r--r--activemodel/activemodel.gemspec2
-rwxr-xr-xactivemodel/test/cases/serializers/xml_serialization_test.rb2
-rw-r--r--activerecord/CHANGELOG.md159
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb96
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb234
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb124
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb446
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb1132
-rw-r--r--activerecord/lib/active_record/core.rb15
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb23
-rw-r--r--activerecord/lib/active_record/persistence.rb2
-rw-r--r--activerecord/lib/active_record/railtie.rb2
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb2
-rw-r--r--activerecord/lib/active_record/sanitization.rb1
-rw-r--r--activerecord/lib/active_record/schema.rb8
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb20
-rw-r--r--activerecord/lib/active_record/store.rb5
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb12
-rw-r--r--activerecord/lib/active_record/transactions.rb35
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb9
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb3
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb71
-rw-r--r--activerecord/test/cases/associations/join_dependency_test.rb8
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb2
-rw-r--r--activerecord/test/cases/base_test.rb14
-rw-r--r--activerecord/test/cases/calculations_test.rb11
-rw-r--r--activerecord/test/cases/finder_test.rb1
-rw-r--r--activerecord/test/cases/inheritance_test.rb98
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb93
-rw-r--r--activerecord/test/cases/store_test.rb10
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb7
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb12
-rw-r--r--activerecord/test/fixtures/companies.yml6
-rw-r--r--activerecord/test/fixtures/vegetables.yml20
-rw-r--r--activerecord/test/models/admin/user.rb1
-rw-r--r--activerecord/test/models/company.rb4
-rw-r--r--activerecord/test/models/possession.rb3
-rw-r--r--activerecord/test/models/vegetables.rb24
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb14
-rw-r--r--activerecord/test/schema/schema.rb13
-rw-r--r--activesupport/CHANGELOG.md32
-rw-r--r--activesupport/lib/active_support/callbacks.rb1
-rw-r--r--activesupport/lib/active_support/dependencies.rb68
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb27
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb27
-rw-r--r--guides/source/migrations.textile6
-rw-r--r--guides/source/upgrading_ruby_on_rails.textile14
-rw-r--r--railties/test/generators/migration_generator_test.rb7
90 files changed, 2151 insertions, 1542 deletions
diff --git a/Gemfile b/Gemfile
index 3a446f1c13..3364af787e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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"