diff options
53 files changed, 890 insertions, 329 deletions
@@ -31,10 +31,6 @@ platforms :mri_18 do gem 'ruby-prof' end -platforms :mri_19 do - gem "ruby-debug19" -end - platforms :ruby do gem 'json' gem 'yajl-ruby' diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 042ad43319..29b5813785 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -20,5 +20,5 @@ Gem::Specification.new do |s| s.has_rdoc = true s.add_dependency('actionpack', version) - s.add_dependency('mail', '~> 2.2.6.1') + s.add_dependency('mail', '~> 2.2.9') end diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 40eafd1a2f..c2af3dfaf5 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -22,9 +22,8 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) s.add_dependency('rack-cache', '~> 0.5.3') - s.add_dependency('rack-cache-purge', '~> 0.0.1') s.add_dependency('builder', '~> 2.1.2') - s.add_dependency('i18n', '~> 0.4.1') + s.add_dependency('i18n', '~> 0.4.2') s.add_dependency('rack', '~> 1.2.1') s.add_dependency('rack-test', '~> 0.5.6') s.add_dependency('rack-mount', '~> 0.6.13') diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 1c63fb2d14..475785a4b4 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -46,14 +46,14 @@ module AbstractController @view_context_class ||= begin controller = self Class.new(ActionView::Base) do + if controller.respond_to?(:_routes) && controller._routes + include controller._routes.url_helpers + include controller._routes.mounted_helpers + end + if controller.respond_to?(:_helpers) include controller._helpers - if controller.respond_to?(:_routes) && controller._routes - include controller._routes.url_helpers - include controller._routes.mounted_helpers - end - # TODO: Fix RJS to not require this self.helpers = controller._helpers end diff --git a/actionpack/lib/action_dispatch/http/rack_cache.rb b/actionpack/lib/action_dispatch/http/rack_cache.rb index e5914abc81..b5c1435903 100644 --- a/actionpack/lib/action_dispatch/http/rack_cache.rb +++ b/actionpack/lib/action_dispatch/http/rack_cache.rb @@ -21,11 +21,6 @@ module ActionDispatch @store.write(key, value) end - def purge(key) - @store.delete(key) - nil - end - ::Rack::Cache::MetaStore::RAILS = self end @@ -58,10 +53,6 @@ module ActionDispatch [key, size] end - def purge(key) - @store.delete(key) - end - ::Rack::Cache::EntityStore::RAILS = self end end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index cfee95eb4b..4da82f958a 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -8,11 +8,6 @@ module ActionDispatch protocol + host_with_port + fullpath end - # Returns 'https' if this is an SSL request and 'http' otherwise. - def scheme - ssl? ? 'https' : 'http' - end - # Returns 'https://' if this is an SSL request and 'http://' otherwise. def protocol @protocol ||= ssl? ? 'https://' : 'http://' @@ -99,4 +94,4 @@ module ActionDispatch end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 75c8cc3dd0..c4c2e1acd7 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -98,17 +98,19 @@ module ActionDispatch def self.build(request) secret = request.env[TOKEN_KEY] host = request.host + secure = request.ssl? - new(secret, host).tap do |hash| + new(secret, host, secure).tap do |hash| hash.update(request.cookies) end end - def initialize(secret = nil, host = nil) + def initialize(secret = nil, host = nil, secure = false) @secret = secret @set_cookies = {} @delete_cookies = {} @host = host + @secure = secure super() end @@ -193,9 +195,15 @@ module ActionDispatch end def write(headers) - @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) } + @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } end + + private + + def write_cookie?(cookie) + @secure || !cookie[:secure] || defined?(Rails.env) && Rails.env.development? + end end class PermanentCookieJar < CookieJar #:nodoc: diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 470b36dbe2..92597e40ff 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -47,6 +47,11 @@ end require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late module Rails + class << self + def env + @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test") + end + end end ActiveSupport::Dependencies.hook! diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index efdc1f5d93..faeae91f6b 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -135,11 +135,25 @@ class CookiesTest < ActionController::TestCase end def test_setting_cookie_with_secure + @request.env["HTTPS"] = "on" get :authenticate_with_secure assert_cookie_header "user_name=david; path=/; secure" assert_equal({"user_name" => "david"}, @response.cookies) end + def test_setting_cookie_with_secure_in_development + Rails.env.stubs(:development?).returns(true) + get :authenticate_with_secure + assert_cookie_header "user_name=david; path=/; secure" + assert_equal({"user_name" => "david"}, @response.cookies) + end + + def test_not_setting_cookie_with_secure + get :authenticate_with_secure + assert_not_cookie_header "user_name=david; path=/; secure" + assert_not_equal({"user_name" => "david"}, @response.cookies) + end + def test_multiple_cookies get :set_multiple_cookies assert_equal 2, @response.cookies.size @@ -286,4 +300,13 @@ class CookiesTest < ActionController::TestCase assert_equal expected.split("\n"), header end end + + def assert_not_cookie_header(expected) + header = @response.headers["Set-Cookie"] + if header.respond_to?(:to_str) + assert_not_equal expected.split("\n").sort, header.split("\n").sort + else + assert_not_equal expected.split("\n"), header + end + end end diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index a6fdf806a4..bc2548e06c 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -433,6 +433,10 @@ class UrlHelperControllerTest < ActionController::TestCase match 'url_helper_controller_test/url_helper/normalize_recall_params', :to => UrlHelperController.action(:normalize_recall), :as => :normalize_recall_params + + match '/url_helper_controller_test/url_helper/override_url_helper/default', + :to => 'url_helper_controller_test/url_helper#override_url_helper', + :as => :override_url_helper end def show @@ -468,6 +472,15 @@ class UrlHelperControllerTest < ActionController::TestCase end def rescue_action(e) raise e end + + def override_url_helper + render :inline => '<%= override_url_helper_path %>' + end + + def override_url_helper_path + '/url_helper_controller_test/url_helper/override_url_helper/override' + end + helper_method :override_url_helper_path end tests UrlHelperController @@ -527,6 +540,11 @@ class UrlHelperControllerTest < ActionController::TestCase get :show, :name => '123' assert_equal 'ok', @response.body end + + def test_url_helper_can_be_overriden + get :override_url_helper + assert_equal '/url_helper_controller_test/url_helper/override_url_helper/override', @response.body + end end class TasksController < ActionController::Base diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index c483ecbc3c..2d1263407f 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -21,5 +21,5 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('builder', '~> 2.1.2') - s.add_dependency('i18n', '~> 0.4.1') + s.add_dependency('i18n', '~> 0.4.2') end diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index adb71f788f..44aedc8efd 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,5 +1,6 @@ require 'active_support/inflector' require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/module/introspection' module ActiveModel class Name < String diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index e6b367790b..911a5155fd 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -256,9 +256,6 @@ module ActiveRecord end def preload_through_records(records, reflection, through_association) - through_reflection = reflections[through_association] - - through_records = [] if reflection.options[:source_type] interface = reflection.source_reflection.options[:foreign_type] preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]} @@ -267,16 +264,15 @@ module ActiveRecord records.first.class.preload_associations(records, through_association, preload_options) # Dont cache the association - we would only be caching a subset - records.each do |record| + records.map { |record| proxy = record.send(through_association) if proxy.respond_to?(:target) - through_records.concat Array.wrap(proxy.target) - proxy.reset + Array.wrap(proxy.target).tap { proxy.reset } else # this is a has_one :through reflection - through_records << proxy if proxy + [proxy].compact end - end + }.flatten(1) else options = {} options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions] @@ -284,11 +280,10 @@ module ActiveRecord options[:conditions] = reflection.options[:conditions] records.first.class.preload_associations(records, through_association, options) - records.each do |record| - through_records.concat Array.wrap(record.send(through_association)) - end + records.map { |record| + Array.wrap(record.send(through_association)) + }.flatten(1) end - through_records end def preload_belongs_to_association(records, reflection, preload_options={}) @@ -325,7 +320,7 @@ module ActiveRecord klass = klass_name.constantize table_name = klass.quoted_table_name - primary_key = reflection.options[:primary_key] || klass.primary_key + primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s column_type = klass.columns.detect{|c| c.name == primary_key}.type ids = _id_map.keys.map do |id| diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 59d328f207..dc466eafc2 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1837,7 +1837,7 @@ module ActiveRecord def initialize(base, associations, joins) @join_parts = [JoinBase.new(base, joins)] - @associations = associations + @associations = {} @reflections = [] @base_records_hash = {} @base_records_in_order = [] @@ -1917,30 +1917,58 @@ module ActiveRecord protected + def cache_joined_association(association) + associations = [] + parent = association.parent + while parent != join_base + associations.unshift(parent.reflection.name) + parent = parent.parent + end + ref = @associations + associations.each do |key| + ref = ref[key] + end + ref[association.reflection.name] ||= {} + end + def build(associations, parent = nil, join_type = Arel::InnerJoin) parent ||= join_parts.last case associations when Symbol, String reflection = parent.reflections[associations.to_s.intern] or raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?" - @reflections << reflection - join_association = build_join_association(reflection, parent) - join_association.join_type = join_type - @join_parts << join_association + unless join_association = find_join_association(reflection, parent) + @reflections << reflection + join_association = build_join_association(reflection, parent) + join_association.join_type = join_type + @join_parts << join_association + cache_joined_association(join_association) + end + join_association when Array associations.each do |association| build(association, parent, join_type) end when Hash associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| - build(name, parent, join_type) - build(associations[name], nil, join_type) + join_association = build(name, parent, join_type) + build(associations[name], join_association, join_type) end else raise ConfigurationError, associations.inspect end end + def find_join_association(name_or_reflection, parent) + if String === name_or_reflection + name_or_reflection = name_or_reflection.to_sym + end + + join_associations.detect { |j| + j.reflection == name_or_reflection && j.parent == parent + } + end + def remove_uniq_by_reflection(reflection, records) if reflection && reflection.collection? records.each { |record| record.send(reflection.name).target.uniq! } @@ -2013,7 +2041,7 @@ module ActiveRecord end # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited - # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which + # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which # everything else is being joined onto. A JoinAssociation represents an association which # is joining to the base. A JoinAssociation may result in more than one actual join # operations (for example a has_and_belongs_to_many JoinAssociation would result in @@ -2092,8 +2120,7 @@ module ActiveRecord def ==(other) other.class == self.class && - other.active_record == active_record && - other.table_joins == table_joins + other.active_record == active_record end def aliased_prefix @@ -2114,7 +2141,7 @@ module ActiveRecord attr_reader :reflection # The JoinDependency object which this JoinAssociation exists within. This is mainly - # relevant for generating aliases which do not conflict with other joins which are + # relevant for generating aliases which do not conflict with other joins which are # part of the query. attr_reader :join_dependency diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index cb2d9e0a79..896e18af01 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -19,11 +19,6 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class AssociationCollection < AssociationProxy #:nodoc: - def initialize(owner, reflection) - super - construct_sql - end - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped def select(select = nil) @@ -36,7 +31,7 @@ module ActiveRecord end def scoped - with_scope(construct_scope) { @reflection.klass.scoped } + with_scope(@scope) { @reflection.klass.scoped } end def find(*args) @@ -58,9 +53,7 @@ module ActiveRecord merge_options_from_reflection!(options) construct_find_options!(options) - find_scope = construct_scope[:find].slice(:conditions, :order) - - with_scope(:find => find_scope) do + with_scope(:find => @scope[:find].slice(:conditions, :order)) do relation = @reflection.klass.send(:construct_finder_arel, options, @reflection.klass.send(:current_scoped_methods)) case args.first @@ -178,17 +171,18 @@ module ActiveRecord end end - # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will - # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the - # descendant's +construct_sql+ method will have set :counter_sql automatically. - # Otherwise, construct options and pass them with scope to the target class's +count+. + # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the + # association, it will be used for the query. Otherwise, construct options and pass them with + # scope to the target class's +count+. def count(column_name = nil, options = {}) column_name, options = nil, column_name if column_name.is_a?(Hash) - if @reflection.options[:counter_sql] && !options.blank? - raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" - elsif @reflection.options[:counter_sql] - @reflection.klass.count_by_sql(@counter_sql) + if @reflection.options[:counter_sql] || @reflection.options[:finder_sql] + unless options.blank? + raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" + end + + @reflection.klass.count_by_sql(custom_counter_sql) else if @reflection.options[:uniq] @@ -197,7 +191,7 @@ module ActiveRecord options.merge!(:distinct => true) end - value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) } + value = @reflection.klass.send(:with_scope, @scope) { @reflection.klass.count(column_name, options) } limit = @reflection.options[:limit] offset = @reflection.options[:offset] @@ -377,18 +371,6 @@ module ActiveRecord def construct_find_options!(options) end - def construct_counter_sql - if @reflection.options[:counter_sql] - @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) - elsif @reflection.options[:finder_sql] - # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ - @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } - @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) - else - @counter_sql = @finder_sql - end - end - def load_target if !@owner.new_record? || foreign_key_present begin @@ -434,9 +416,9 @@ module ActiveRecord elsif @reflection.klass.scopes[method] @_named_scopes_cache ||= {} @_named_scopes_cache[method] ||= {} - @_named_scopes_cache[method][args] ||= with_scope(construct_scope) { @reflection.klass.send(method, *args) } + @_named_scopes_cache[method][args] ||= with_scope(@scope) { @reflection.klass.send(method, *args) } else - with_scope(construct_scope) do + with_scope(@scope) do if block_given? @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) } else @@ -446,9 +428,19 @@ module ActiveRecord end end - # overloaded in derived Association classes to provide useful scoping depending on association type. - def construct_scope - {} + def custom_counter_sql + if @reflection.options[:counter_sql] + counter_sql = @reflection.options[:counter_sql] + else + # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ + counter_sql = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } + end + + interpolate_sql(counter_sql) + end + + def custom_finder_sql + interpolate_sql(@reflection.options[:finder_sql]) end def reset_target! @@ -462,7 +454,7 @@ module ActiveRecord def find_target records = if @reflection.options[:finder_sql] - @reflection.klass.find_by_sql(@finder_sql) + @reflection.klass.find_by_sql(custom_finder_sql) else find(:all) end @@ -494,7 +486,7 @@ module ActiveRecord ensure_owner_is_not_new scoped_where = scoped.where_values_hash - create_scope = scoped_where ? construct_scope[:create].merge(scoped_where) : construct_scope[:create] + create_scope = scoped_where ? @scope[:create].merge(scoped_where) : @scope[:create] record = @reflection.klass.send(:with_scope, :create => create_scope) do @reflection.build_association(attrs) end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index f333f4d603..0c12c3737d 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -61,6 +61,7 @@ module ActiveRecord reflection.check_validity! Array.wrap(reflection.options[:extend]).each { |ext| proxy_extend(ext) } reset + construct_scope end # Returns the owner of the proxy. @@ -203,6 +204,24 @@ module ActiveRecord @reflection.klass.send :with_scope, *args, &block end + # Construct the scope used for find/create queries on the target + def construct_scope + @scope = { + :find => construct_find_scope, + :create => construct_create_scope + } + end + + # Implemented by subclasses + def construct_find_scope + raise NotImplementedError + end + + # Implemented by (some) subclasses + def construct_create_scope + {} + end + private # Forwards any missing method call to the \target. def method_missing(method, *args) diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 2eb56e5cd3..34b6cd5576 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -50,19 +50,21 @@ module ActiveRecord "find" end - options = @reflection.options.dup - (options.keys - [:select, :include, :readonly]).each do |key| - options.delete key - end - options[:conditions] = conditions + options = @reflection.options.dup.slice(:select, :include, :readonly) - the_target = @reflection.klass.send(find_method, - @owner[@reflection.primary_key_name], - options - ) if @owner[@reflection.primary_key_name] + the_target = with_scope(:find => @scope[:find]) do + @reflection.klass.send(find_method, + @owner[@reflection.primary_key_name], + options + ) if @owner[@reflection.primary_key_name] + end set_inverse_instance(the_target, @owner) the_target end + + def construct_find_scope + { :conditions => conditions } + end def foreign_key_present !@owner[@reflection.primary_key_name].nil? diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index e429806b0c..a0df860623 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -44,20 +44,20 @@ module ActiveRecord end end + def construct_find_scope + { :conditions => conditions } + end + def find_target return nil if association_class.nil? - target = - if @reflection.options[:conditions] - association_class.find( - @owner[@reflection.primary_key_name], - :select => @reflection.options[:select], - :conditions => conditions, - :include => @reflection.options[:include] - ) - else - association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include]) - end + target = association_class.send(:with_scope, :find => @scope[:find]) do + association_class.find( + @owner[@reflection.primary_key_name], + :select => @reflection.options[:select], + :include => @reflection.options[:include] + ) + end set_inverse_instance(target, @owner) target end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index eb65234dfb..1fc9aba5cf 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -24,7 +24,7 @@ module ActiveRecord protected def construct_find_options!(options) - options[:joins] = Arel::SqlLiteral.new @join_sql + options[:joins] = Arel::SqlLiteral.new(@scope[:find][:joins]) options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select]) options[:select] ||= (@reflection.options[:select] || Arel::SqlLiteral.new('*')) end @@ -80,27 +80,26 @@ module ActiveRecord ).delete end end + + def construct_joins + "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}" + end - def construct_sql - if @reflection.options[:finder_sql] - @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) - else - @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} " - @finder_sql << " AND (#{conditions})" if conditions - end - - @join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}" - - construct_counter_sql + def construct_conditions + sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} " + sql << " AND (#{conditions})" if conditions + sql end - def construct_scope - { :find => { :conditions => @finder_sql, - :joins => @join_sql, - :readonly => false, - :order => @reflection.options[:order], - :include => @reflection.options[:include], - :limit => @reflection.options[:limit] } } + def construct_find_scope + { + :conditions => construct_conditions, + :joins => construct_joins, + :readonly => false, + :order => @reflection.options[:order], + :include => @reflection.options[:include], + :limit => @reflection.options[:limit] + } end # Join tables with additional columns on top of the two foreign keys must be considered diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 978fc74560..830a82980d 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -6,14 +6,10 @@ module ActiveRecord # If the association has a <tt>:through</tt> option further specialization # is provided by its child HasManyThroughAssociation. class HasManyAssociation < AssociationCollection #:nodoc: - def initialize(owner, reflection) - @finder_sql = nil - super - end protected def owner_quoted_id if @reflection.options[:primary_key] - quote_value(@owner.send(@reflection.options[:primary_key])) + @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) else @owner.quoted_id end @@ -35,10 +31,10 @@ module ActiveRecord def count_records count = if has_cached_counter? @owner.send(:read_attribute, cached_counter_attribute_name) - elsif @reflection.options[:counter_sql] - @reflection.klass.count_by_sql(@counter_sql) + elsif @reflection.options[:counter_sql] || @reflection.options[:finder_sql] + @reflection.klass.count_by_sql(custom_counter_sql) else - @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include]) + @reflection.klass.count(@scope[:find].slice(:conditions, :joins, :include)) end # If there's nothing in the database and @target has no new records @@ -87,36 +83,32 @@ module ActiveRecord false end - def construct_sql - case - when @reflection.options[:finder_sql] - @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) - - when @reflection.options[:as] - @finder_sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" - @finder_sql << " AND (#{conditions})" if conditions - - else - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" - @finder_sql << " AND (#{conditions})" if conditions + def construct_conditions + if @reflection.options[:as] + sql = + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" + else + sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" end + sql << " AND (#{conditions})" if conditions + sql + end - construct_counter_sql + def construct_find_scope + { + :conditions => construct_conditions, + :readonly => false, + :order => @reflection.options[:order], + :limit => @reflection.options[:limit], + :include => @reflection.options[:include] + } end - def construct_scope + def construct_create_scope create_scoping = {} set_belongs_to_association_for(create_scoping) - { - :find => { :conditions => @finder_sql, - :readonly => false, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :include => @reflection.options[:include]}, - :create => create_scoping - } + create_scoping end def we_can_set_the_inverse_on_this?(record) diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 97883d8393..437c8b1fd6 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -82,21 +82,7 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - with_scope(construct_scope) { @reflection.klass.find(:all) } - end - - def construct_sql - case - when @reflection.options[:finder_sql] - @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) - - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" - @finder_sql << " AND (#{conditions})" if conditions - else - @finder_sql = construct_conditions - end - - construct_counter_sql + with_scope(@scope) { @reflection.klass.find(:all) } end def has_cached_counter? diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index a6e6bfa356..17901387e9 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -2,11 +2,6 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < AssociationProxy #:nodoc: - def initialize(owner, reflection) - super - construct_sql - end - def create(attrs = {}, replace_existing = true) new_record(replace_existing) do |reflection| attrs = merge_with_conditions(attrs) @@ -79,33 +74,31 @@ module ActiveRecord private def find_target - options = @reflection.options.dup - (options.keys - [:select, :order, :include, :readonly]).each do |key| - options.delete key - end - options[:conditions] = @finder_sql + options = @reflection.options.dup.slice(:select, :order, :include, :readonly) - the_target = @reflection.klass.find(:first, options) + the_target = with_scope(:find => @scope[:find]) do + @reflection.klass.find(:first, options) + end set_inverse_instance(the_target, @owner) the_target end - def construct_sql - case - when @reflection.options[:as] - @finder_sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" - else - @finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" + def construct_find_scope + if @reflection.options[:as] + sql = + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" + else + sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" end - @finder_sql << " AND (#{conditions})" if conditions + sql << " AND (#{conditions})" if conditions + { :conditions => sql } end - def construct_scope + def construct_create_scope create_scoping = {} set_belongs_to_association_for(create_scoping) - { :create => create_scoping } + create_scoping end def new_record(replace_existing) @@ -113,7 +106,7 @@ module ActiveRecord # instance. Otherwise, if the target has not previously been loaded # elsewhere, the instance we create will get orphaned. load_target if replace_existing - record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do + record = @reflection.klass.send(:with_scope, :create => @scope[:create]) do yield @reflection end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index fba0a2bfcc..7f28abf464 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -33,7 +33,7 @@ module ActiveRecord private def find_target - with_scope(construct_scope) { @reflection.klass.find(:first) } + with_scope(@scope) { @reflection.klass.find(:first) } end end end diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index cabb33c4a8..bd8e304e99 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -5,16 +5,20 @@ module ActiveRecord protected - def construct_scope - { :create => construct_owner_attributes(@reflection), - :find => { :conditions => construct_conditions, - :joins => construct_joins, - :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], - :select => construct_select, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :readonly => @reflection.options[:readonly], - } } + def construct_find_scope + { + :conditions => construct_conditions, + :joins => construct_joins, + :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], + :select => construct_select, + :order => @reflection.options[:order], + :limit => @reflection.options[:limit], + :readonly => @reflection.options[:readonly] + } + end + + def construct_create_scope + construct_owner_attributes(@reflection) end # Build SQL conditions from attributes, qualified by table name. diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 4a2c078e91..0b89a49896 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -322,8 +322,8 @@ module ActiveRecord end end - # reconstruct the SQL queries now that we know the owner's id - association.send(:construct_sql) if association.respond_to?(:construct_sql) + # reconstruct the scope now that we know the owner's id + association.send(:construct_scope) if association.respond_to?(:construct_scope) end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 053b796991..06a388cd21 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -448,8 +448,8 @@ module ActiveRecord #:nodoc: # # You can use the same string replacement techniques as you can with ActiveRecord#find # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] # > [#<Post:0x36bff9c @attributes={"first_name"=>"The Cheap Man Buys Twice"}>, ...] - def find_by_sql(sql) - connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) } + def find_by_sql(sql, binds = []) + connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) } end # Creates an object (or multiple objects) and saves it to the database, if validations pass. @@ -718,6 +718,7 @@ module ActiveRecord #:nodoc: # end # end def reset_column_information + connection.clear_cache! undefine_attribute_methods @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil @arel_engine = @relation = @arel_table = nil 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 646a78622c..6178130c00 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -3,8 +3,16 @@ module ActiveRecord module DatabaseStatements # Returns an array of record hashes with the column names as keys and # column values as values. - def select_all(sql, name = nil) - select(sql, name) + def select_all(sql, name = nil, binds = []) + if supports_statement_cache? + select(sql, name, binds) + else + return select(sql, name) if binds.empty? + binds = binds.dup + select sql.gsub('?') { + quote(*binds.shift.reverse) + }, name + end end # Returns a record hash with the column names as keys and column values @@ -39,6 +47,12 @@ module ActiveRecord end undef_method :execute + # Executes +sql+ statement in the context of this connection using + # +binds+ as the bind substitutes. +name+ is logged along with + # the executed +sql+ statement. + def exec(sql, name = 'SQL', binds = []) + end + # Returns the last auto-generated ID from the affected table. def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) insert_sql(sql, name, pk, id_value, sequence_name) @@ -68,6 +82,12 @@ module ActiveRecord nil end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + false + end + # Runs the given block in a database transaction, and returns the result # of the block. # @@ -254,7 +274,7 @@ module ActiveRecord protected # Returns an array of record hashes with the column names as keys and # column values as values. - def select(sql, name = nil) + def select(sql, name = nil, binds = []) end undef_method :select diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 0ee61d0b6f..d555308485 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/duplicable' - module ActiveRecord module ConnectionAdapters # :nodoc: module QueryCache @@ -49,32 +47,26 @@ module ActiveRecord @query_cache.clear end - def select_all(*args) + def select_all(sql, name = nil, binds = []) if @query_cache_enabled - cache_sql(args.first) { super } + cache_sql(sql, binds) { super } else super end end private - def cache_sql(sql) + def cache_sql(sql, binds) result = - if @query_cache.has_key?(sql) + if @query_cache[sql].key?(binds) ActiveSupport::Notifications.instrument("sql.active_record", :sql => sql, :name => "CACHE", :connection_id => self.object_id) - @query_cache[sql] + @query_cache[sql][binds] else - @query_cache[sql] = yield + @query_cache[sql][binds] = yield end - if Array === result - result.collect { |row| row.dup } - else - result.duplicable? ? result.dup : result - end - rescue TypeError - result + result.collect { |row| row.dup } end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index d8c92d0ad3..f3fba9a3a9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -12,6 +12,7 @@ require 'active_record/connection_adapters/abstract/connection_pool' require 'active_record/connection_adapters/abstract/connection_specification' require 'active_record/connection_adapters/abstract/query_cache' require 'active_record/connection_adapters/abstract/database_limits' +require 'active_record/result' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -40,7 +41,7 @@ module ActiveRecord @active = nil @connection, @logger = connection, logger @query_cache_enabled = false - @query_cache = {} + @query_cache = Hash.new { |h,sql| h[sql] = {} } @instrumenter = ActiveSupport::Notifications.instrumenter end @@ -97,6 +98,12 @@ module ActiveRecord quote_column_name(name) end + # Returns a bind substitution value given a +column+ and list of current + # +binds+ + def substitute_for(column, binds) + Arel.sql '?' + end + # REFERENTIAL INTEGRITY ==================================== # Override to turn off referential integrity while executing <tt>&block</tt>. @@ -135,6 +142,13 @@ module ActiveRecord # this should be overridden by concrete adapters end + ### + # Clear any caching the database adapter may be doing, for example + # clearing the prepared statement cache. This is database specific. + def clear_cache! + # this should be overridden by concrete adapters + end + # Returns true if its required to reload the connection between requests for development mode. # This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite. def requires_reloading? diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index a4b336dfaf..7d47d06ae1 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -3,6 +3,28 @@ require 'active_support/core_ext/kernel/requires' require 'active_support/core_ext/object/blank' require 'set' +begin + require 'mysql' +rescue LoadError + raise "!!! Missing the mysql gem. Add it to your Gemfile: gem 'mysql'" +end + +unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash) + raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'. Or use gem 'mysql2'" +end + +class Mysql + class Time + ### + # This monkey patch is for test_additional_columns_from_join_table + def to_date + Date.new(year, month, day) + end + end + class Stmt; include Enumerable end + class Result; include Enumerable end +end + module ActiveRecord class Base # Establishes a connection to the database that's used by all Active Record objects. @@ -15,18 +37,6 @@ module ActiveRecord password = config[:password].to_s database = config[:database] - unless defined? Mysql - begin - require 'mysql' - rescue LoadError - raise "!!! Missing the mysql2 gem. Add it to your Gemfile: gem 'mysql2'" - end - - unless defined?(Mysql::Result) && Mysql::Result.method_defined?(:each_hash) - raise "!!! Outdated mysql gem. Upgrade to 2.8.1 or later. In your Gemfile: gem 'mysql', '2.8.1'. Or use gem 'mysql2'" - end - end - mysql = Mysql.init mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey] @@ -39,6 +49,30 @@ module ActiveRecord module ConnectionAdapters class MysqlColumn < Column #:nodoc: + class << self + def string_to_time(value) + return super unless Mysql::Time === value + new_time( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.second_part) + end + + def string_to_dummy_time(v) + return super unless Mysql::Time === v + new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part) + end + + def string_to_date(v) + return super unless Mysql::Time === v + new_date(v.year, v.month, v.day) + end + end + def extract_default(default) if sql_type =~ /blob/i || type == :text if default.blank? @@ -161,6 +195,7 @@ module ActiveRecord super(connection, logger) @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} + @statements = {} connect end @@ -168,6 +203,12 @@ module ActiveRecord ADAPTER_NAME end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + true + end + def supports_migrations? #:nodoc: true end @@ -252,6 +293,7 @@ module ActiveRecord def reconnect! disconnect! + clear_cache! connect end @@ -272,14 +314,63 @@ module ActiveRecord def select_rows(sql, name = nil) @connection.query_with_result = true - result = execute(sql, name) - rows = [] - result.each { |row| rows << row } - result.free + rows = exec_without_stmt(sql, name).rows @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end + def clear_cache! + @statements.values.each do |cache| + cache[:stmt].close + end + @statements.clear + end + + def exec(sql, name = 'SQL', binds = []) + log(sql, name) do + result = nil + + cache = {} + if binds.empty? + stmt = @connection.prepare(sql) + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + end + + stmt.execute(*binds.map { |col, val| + col ? col.type_cast(val) : val + }) + if metadata = stmt.result_metadata + cols = cache[:cols] ||= metadata.fetch_fields.map { |field| + field.name + } + + metadata.free + result = ActiveRecord::Result.new(cols, stmt.to_a) + end + + stmt.free_result + stmt.close if binds.empty? + + result + end + end + + def exec_without_stmt(sql, name = 'SQL') # :nodoc: + # Some queries, like SHOW CREATE TABLE don't work through the prepared + # statement API. For those queries, we need to use this method. :'( + log(sql, name) do + result = @connection.query(sql) + cols = result.fetch_fields.map { |field| field.name } + rows = result.to_a + result.free + ActiveRecord::Result.new(cols, rows) + end + end + # Executes an SQL query and returns a MySQL::Result object. Note that you have to free # the Result object after you're done using it. def execute(sql, name = nil) #:nodoc: @@ -308,7 +399,7 @@ module ActiveRecord end def begin_db_transaction #:nodoc: - execute "BEGIN" + exec_without_stmt "BEGIN" rescue Exception # Transactions aren't supported end @@ -360,7 +451,8 @@ module ActiveRecord select_all(sql).map do |table| table.delete('Table_type') - select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n" + sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" + exec_without_stmt(sql).first['Create Table'] + ";\n\n" end.join("") end @@ -614,12 +706,9 @@ module ActiveRecord execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) end - def select(sql, name = nil) + def select(sql, name = nil, binds = []) @connection.query_with_result = true - result = execute(sql, name) - rows = [] - result.each_hash { |row| rows << row } - result.free + rows = exec(sql, name, binds).to_a @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5949985e4d..58c0f85dc2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -211,6 +211,12 @@ module ActiveRecord ADAPTER_NAME end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + true + end + # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) super(connection, logger) @@ -220,11 +226,19 @@ module ActiveRecord @local_tz = nil @table_alias_length = nil @postgresql_version = nil + @statements = {} connect @local_tz = execute('SHOW TIME ZONE').first["TimeZone"] end + def clear_cache! + @statements.each_value do |value| + @connection.query "DEALLOCATE #{value}" + end + @statements.clear + end + # Is this connection alive and ready for queries? def active? if @connection.respond_to?(:status) @@ -242,6 +256,7 @@ module ActiveRecord # Close then reopen the connection. def reconnect! if @connection.respond_to?(:reset) + clear_cache! @connection.reset configure_connection else @@ -250,8 +265,14 @@ module ActiveRecord end end + def reset! + clear_cache! + super + end + # Close the connection. def disconnect! + clear_cache! @connection.close rescue nil end @@ -374,6 +395,12 @@ module ActiveRecord end end + # Set the authorized user for this session + def session_auth=(user) + clear_cache! + exec "SET SESSION AUTHORIZATION #{user}" + end + # REFERENTIAL INTEGRITY ==================================== def supports_disable_referential_integrity?() #:nodoc: @@ -501,6 +528,35 @@ module ActiveRecord end end + def substitute_for(column, current_values) + Arel.sql("$#{current_values.length + 1}") + end + + def exec(sql, name = 'SQL', binds = []) + return exec_no_cache(sql, name) if binds.empty? + + log(sql, name) do + unless @statements.key? sql + nextkey = "a#{@statements.length + 1}" + @connection.prepare nextkey, sql + @statements[sql] = nextkey + end + + key = @statements[sql] + + # Clear the queue + @connection.get_last_result + @connection.send_query_prepared(key, binds.map { |col, val| + col ? col.type_cast(val) : val + }) + @connection.block + result = @connection.get_last_result + ret = ActiveRecord::Result.new(result.fields, result_as_array(result)) + result.clear + return ret + end + end + # Executes an UPDATE query and returns the number of affected tuples. def update_sql(sql, name = nil) super.cmd_tuples @@ -920,6 +976,15 @@ module ActiveRecord end private + def exec_no_cache(sql, name) + log(sql, name) do + result = @connection.async_exec(sql) + ret = ActiveRecord::Result.new(result.fields, result_as_array(result)) + result.clear + ret + end + end + # The internal PostgreSQL identifier of the money data type. MONEY_COLUMN_TYPE_OID = 790 #:nodoc: # The internal PostgreSQL identifier of the BYTEA data type. @@ -974,11 +1039,8 @@ module ActiveRecord # Executes a SELECT query and returns the results, performing any data type # conversions that are required to be performed here instead of in PostgreSQLColumn. - def select(sql, name = nil) - fields, rows = select_raw(sql, name) - rows.map do |row| - Hash[fields.zip(row)] - end + def select(sql, name = nil, binds = []) + exec(sql, name, binds).to_a end def select_raw(sql, name = nil) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 69bf2c0dcd..fc4d84a4f2 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -50,6 +50,7 @@ module ActiveRecord def initialize(connection, logger, config) super(connection, logger) + @statements = {} @config = config end @@ -61,6 +62,12 @@ module ActiveRecord sqlite_version >= '2.0.0' end + # Returns +true+ when the connection adapter supports prepared statement + # caching, otherwise returns +false+ + def supports_statement_cache? + true + end + def supports_migrations? #:nodoc: true end @@ -79,9 +86,14 @@ module ActiveRecord def disconnect! super + clear_cache! @connection.close rescue nil end + def clear_cache! + @statements.clear + end + def supports_count_distinct? #:nodoc: sqlite_version >= '3.2.6' end @@ -131,6 +143,29 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== + def exec(sql, name = nil, binds = []) + log(sql, name) do + + # Don't cache statements without bind values + if binds.empty? + stmt = @connection.prepare(sql) + cols = stmt.columns + else + cache = @statements[sql] ||= { + :stmt => @connection.prepare(sql) + } + stmt = cache[:stmt] + cols = cache[:cols] ||= stmt.columns + stmt.reset! + stmt.bind_params binds.map { |col, val| + col ? col.type_cast(val) : val + } + end + + ActiveRecord::Result.new(cols, stmt.to_a) + end + end + def execute(sql, name = nil) #:nodoc: log(sql, name) { @connection.execute(sql) } end @@ -151,9 +186,7 @@ module ActiveRecord alias :create :insert_sql def select_rows(sql, name = nil) - execute(sql, name).map do |row| - (0...(row.size / 2)).map { |i| row[i] } - end + exec(sql, name).rows end def begin_db_transaction #:nodoc: @@ -177,7 +210,7 @@ module ActiveRecord WHERE type = 'table' AND NOT name = 'sqlite_sequence' SQL - execute(sql, name).map do |row| + exec(sql, name).map do |row| row['name'] end end @@ -189,12 +222,12 @@ module ActiveRecord end def indexes(table_name, name = nil) #:nodoc: - execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| + exec("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row| IndexDefinition.new( table_name, row['name'], - row['unique'].to_i != 0, - execute("PRAGMA index_info('#{row['name']}')").map { |col| + row['unique'] != 0, + exec("PRAGMA index_info('#{row['name']}')").map { |col| col['name'] }) end @@ -202,17 +235,17 @@ module ActiveRecord def primary_key(table_name) #:nodoc: column = table_structure(table_name).find { |field| - field['pk'].to_i == 1 + field['pk'] == 1 } column && column['name'] end def remove_index!(table_name, index_name) #:nodoc: - execute "DROP INDEX #{quote_column_name(index_name)}" + exec "DROP INDEX #{quote_column_name(index_name)}" end def rename_table(name, new_name) - execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + exec "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" end # See: http://www.sqlite.org/lang_altertable.html @@ -249,7 +282,7 @@ module ActiveRecord def change_column_null(table_name, column_name, null, default = nil) 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") + exec("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") end alter_table(table_name) do |definition| definition[column_name].null = null @@ -280,14 +313,13 @@ module ActiveRecord end protected - def select(sql, name = nil) #:nodoc: - execute(sql, name).map do |row| - record = {} - row.each do |key, value| - record[key.sub(/^"?\w+"?\./, '')] = value if key.is_a?(String) - end - record - end + def select(sql, name = nil, binds = []) #:nodoc: + result = exec(sql, name, binds) + columns = result.columns.map { |column| + column.sub(/^"?\w+"?\./, '') + } + + result.rows.map { |row| Hash[columns.zip(row)] } end def table_structure(table_name) @@ -367,11 +399,11 @@ module ActiveRecord quoted_columns = columns.map { |col| quote_column_name(col) } * ',' quoted_to = quote_table_name(to) - @connection.execute "SELECT * FROM #{quote_table_name(from)}" do |row| + exec("SELECT * FROM #{quote_table_name(from)}").each do |row| sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' sql << ')' - @connection.execute sql + exec sql end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index f129b54f9a..3b22be78cb 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -5,7 +5,7 @@ module ActiveRecord class Relation JoinOperation = Struct.new(:relation, :join_class, :on) ASSOCIATION_METHODS = [:includes, :eager_load, :preload] - MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having] + MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from] include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches @@ -61,7 +61,7 @@ module ActiveRecord def to_a return @records if loaded? - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values) preload = @preload_values preload += @includes_values unless eager_loading? @@ -319,8 +319,13 @@ module ActiveRecord end def where_values_hash - Hash[@where_values.find_all {|w| w.respond_to?(:operator) && w.operator == :== }.map {|where| - [where.operand1.name, where.operand2.respond_to?(:value) ? where.operand2.value : where.operand2] + Hash[@where_values.find_all { |w| + w.respond_to?(:operator) && w.operator == :== && w.left.relation.name == table_name + }.map { |where| + [ + where.left.name, + where.right.respond_to?(:value) ? where.right.value : where.right + ] }] end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index b763e22ec6..fe1ef2e2e3 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -288,7 +288,12 @@ module ActiveRecord def find_one(id) id = id.id if ActiveRecord::Base === id - record = where(primary_key.eq(id)).first + column = primary_key.column + + substitute = connection.substitute_for(column, @bind_values) + relation = where(primary_key.eq(substitute)) + relation.bind_values = [[column, id]] + record = relation.first unless record conditions = arel.where_sql diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 64d2cb0203..9c399d3333 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -6,7 +6,8 @@ module ActiveRecord extend ActiveSupport::Concern attr_accessor :includes_values, :eager_load_values, :preload_values, - :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values, + :select_values, :group_values, :order_values, :joins_values, + :where_values, :having_values, :bind_values, :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value def includes(*args) @@ -62,6 +63,12 @@ module ActiveRecord relation end + def bind(value) + relation = clone + relation.bind_values += [value] + relation + end + def where(opts, *rest) relation = clone relation.where_values += build_where(opts, rest) unless opts.blank? diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 648a02f1cc..a61a3bd41c 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -28,17 +28,20 @@ module ActiveRecord merged_wheres = @where_values + r.where_values - # Remove duplicates, last one wins. - seen = {} - merged_wheres = merged_wheres.reverse.reject { |w| - nuke = false - if w.respond_to?(:operator) && w.operator == :== - name = w.left.name - nuke = seen[name] - seen[name] = true - end - nuke - }.reverse + unless @where_values.empty? + # Remove duplicates, last one wins. + seen = Hash.new { |h,table| h[table] = {} } + merged_wheres = merged_wheres.reverse.reject { |w| + nuke = false + if w.respond_to?(:operator) && w.operator == :== + name = w.left.name + table = w.left.relation.name + nuke = seen[table][name] + seen[table][name] = true + end + nuke + }.reverse + end merged_relation.where_values = merged_wheres diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb new file mode 100644 index 0000000000..8deff1478f --- /dev/null +++ b/activerecord/lib/active_record/result.rb @@ -0,0 +1,30 @@ +module ActiveRecord + ### + # This class encapsulates a Result returned from calling +exec+ on any + # database connection adapter. For example: + # + # x = ActiveRecord::Base.connection.exec('SELECT * FROM foo') + # x # => #<ActiveRecord::Result:0xdeadbeef> + class Result + include Enumerable + + attr_reader :columns, :rows + + def initialize(columns, rows) + @columns = columns + @rows = rows + @hash_rows = nil + end + + def each + hash_rows.each { |row| yield row } + end + + private + def hash_rows + @hash_rows ||= @rows.map { |row| + ActiveSupport::OrderedHash[@columns.zip(row)] + } + end + end +end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index f76a23a8ad..67bd8ec7e0 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -43,6 +43,64 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert @connection.active? end + def test_bind_value_substitute + bind_param = @connection.substitute_for('foo', []) + assert_equal Arel.sql('?'), bind_param + end + + def test_exec_no_binds + @connection.exec('drop table if exists ex') + @connection.exec(<<-eosql) + CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, + `data` varchar(255)) + eosql + result = @connection.exec('SELECT id, data FROM ex') + assert_equal 0, result.rows.length + assert_equal 2, result.columns.length + assert_equal %w{ id data }, result.columns + + @connection.exec('INSERT INTO ex (id, data) VALUES (1, "foo")') + result = @connection.exec('SELECT id, data FROM ex') + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, 'foo']], result.rows + end + + def test_exec_with_binds + @connection.exec('drop table if exists ex') + @connection.exec(<<-eosql) + CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, + `data` varchar(255)) + eosql + @connection.exec('INSERT INTO ex (id, data) VALUES (1, "foo")') + result = @connection.exec( + 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, 'foo']], result.rows + end + + def test_exec_typecasts_bind_vals + @connection.exec('drop table if exists ex') + @connection.exec(<<-eosql) + CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, + `data` varchar(255)) + eosql + @connection.exec('INSERT INTO ex (id, data) VALUES (1, "foo")') + column = @connection.columns('ex').find { |col| col.name == 'id' } + + result = @connection.exec( + 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, 'foo']], result.rows + end + # Test that MySQL allows multiple results for stored procedures if Mysql.const_defined?(:CLIENT_MULTI_RESULTS) def test_multi_results diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 7b72151b57..b0fd2273df 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -5,6 +5,8 @@ module ActiveRecord class PostgreSQLAdapterTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection + @connection.exec('drop table if exists ex') + @connection.exec('create table ex(id serial primary key, data character varying(255))') end def test_table_alias_length @@ -12,6 +14,55 @@ module ActiveRecord @connection.table_alias_length end end + + def test_exec_no_binds + result = @connection.exec('SELECT id, data FROM ex') + assert_equal 0, result.rows.length + assert_equal 2, result.columns.length + assert_equal %w{ id data }, result.columns + + string = @connection.quote('foo') + @connection.exec("INSERT INTO ex (id, data) VALUES (1, #{string})") + result = @connection.exec('SELECT id, data FROM ex') + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [['1', 'foo']], result.rows + end + + def test_exec_with_binds + string = @connection.quote('foo') + @connection.exec("INSERT INTO ex (id, data) VALUES (1, #{string})") + result = @connection.exec( + 'SELECT id, data FROM ex WHERE id = $1', nil, [[nil, 1]]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [['1', 'foo']], result.rows + end + + def test_exec_typecasts_bind_vals + string = @connection.quote('foo') + @connection.exec("INSERT INTO ex (id, data) VALUES (1, #{string})") + + column = @connection.columns('ex').find { |col| col.name == 'id' } + result = @connection.exec( + 'SELECT id, data FROM ex WHERE id = $1', nil, [[column, '1-fuu']]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [['1', 'foo']], result.rows + end + + def test_substitute_for + bind = @connection.substitute_for(nil, []) + assert_equal Arel.sql('$1'), bind + + bind = @connection.substitute_for(nil, [nil]) + assert_equal Arel.sql('$2'), bind + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index 6f372edc38..881631fb19 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -43,6 +43,36 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase end end + def test_session_auth= + assert_raise(ActiveRecord::StatementInvalid) do + @connection.session_auth = 'DEFAULT' + @connection.execute "SELECT * FROM #{TABLE_NAME}" + end + end + + def test_setting_auth_clears_stmt_cache + assert_nothing_raised do + set_session_auth + USERS.each do |u| + set_session_auth u + assert_equal u, @connection.exec("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [[nil, 1]]).first['name'] + set_session_auth + end + end + end + + def test_auth_with_bind + assert_nothing_raised do + set_session_auth + USERS.each do |u| + @connection.clear_cache! + set_session_auth u + assert_equal u, @connection.exec("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [[nil, 1]]).first['name'] + set_session_auth + end + end + end + def test_schema_uniqueness assert_nothing_raised do set_session_auth @@ -78,7 +108,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase private def set_session_auth auth = nil - @connection.execute "SET SESSION AUTHORIZATION #{auth || 'default'}" + @connection.session_auth = auth || 'default' end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 934cf72f72..973c51f3d7 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -3,6 +3,12 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class SQLite3AdapterTest < ActiveRecord::TestCase + def setup + @conn = Base.sqlite3_connection :database => ':memory:', + :adapter => 'sqlite3', + :timeout => 100 + end + def test_connection_no_db assert_raises(ArgumentError) do Base.sqlite3_connection {} @@ -38,18 +44,58 @@ module ActiveRecord end def test_connect - conn = Base.sqlite3_connection :database => ':memory:', - :adapter => 'sqlite3', - :timeout => 100 - assert conn, 'should have connection' + assert @conn, 'should have connection' end # sqlite3 defaults to UTF-8 encoding def test_encoding - conn = Base.sqlite3_connection :database => ':memory:', - :adapter => 'sqlite3', - :timeout => 100 - assert_equal 'UTF-8', conn.encoding + assert_equal 'UTF-8', @conn.encoding + end + + def test_bind_value_substitute + bind_param = @conn.substitute_for('foo', []) + assert_equal Arel.sql('?'), bind_param + end + + def test_exec_no_binds + @conn.exec('create table ex(id int, data string)') + result = @conn.exec('SELECT id, data FROM ex') + assert_equal 0, result.rows.length + assert_equal 2, result.columns.length + assert_equal %w{ id data }, result.columns + + @conn.exec('INSERT INTO ex (id, data) VALUES (1, "foo")') + result = @conn.exec('SELECT id, data FROM ex') + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, 'foo']], result.rows + end + + def test_exec_with_binds + @conn.exec('create table ex(id int, data string)') + @conn.exec('INSERT INTO ex (id, data) VALUES (1, "foo")') + result = @conn.exec( + 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, 'foo']], result.rows + end + + def test_exec_typecasts_bind_vals + @conn.exec('create table ex(id int, data string)') + @conn.exec('INSERT INTO ex (id, data) VALUES (1, "foo")') + column = @conn.columns('ex').find { |col| col.name == 'id' } + + result = @conn.exec( + 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length + + assert_equal [[1, 'foo']], result.rows end end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index cbaa4990f7..0fa4328826 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -81,6 +81,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_not_nil citibank_result.instance_variable_get("@firm_with_primary_key") end + def test_eager_loading_with_primary_key_as_symbol + Firm.create("name" => "Apple") + Client.create("name" => "Citibank", :firm_name => "Apple") + citibank_result = Client.find(:first, :conditions => {:name => "Citibank"}, :include => :firm_with_primary_key_symbols) + assert_not_nil citibank_result.instance_variable_get("@firm_with_primary_key_symbols") + end + def test_no_unexpected_aliasing first_firm = companies(:first_firm) another_firm = companies(:another_firm) diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index c7c32da9f3..271bb92ee8 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -3,6 +3,7 @@ require 'models/post' require 'models/comment' require 'models/author' require 'models/categorization' +require 'models/category' require 'models/company' require 'models/topic' require 'models/reply' @@ -45,6 +46,31 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal people(:michael), Person.eager_load(:primary_contact => :primary_contact).where('primary_contacts_people_2.first_name = ?', 'Susan').order('people.id').first end + def test_cascaded_eager_association_loading_with_join_for_count + categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors]) + + assert_nothing_raised do + assert_equal 2, categories.count + assert_equal 2, categories.all.uniq.size # Must uniq since instantiating with inner joins will get dupes + end + end + + def test_cascaded_eager_association_loading_with_duplicated_includes + categories = Category.includes(:categorizations).includes(:categorizations => :author).where("categorizations.id is not null") + assert_nothing_raised do + assert_equal 2, categories.count + assert_equal 2, categories.all.size + end + end + + def test_cascaded_eager_association_loading_with_twice_includes_edge_cases + categories = Category.includes(:categorizations => :author).includes(:categorizations => :post).where("posts.id is not null") + assert_nothing_raised do + assert_equal 2, categories.count + assert_equal 2, categories.all.size + end + end + def test_eager_association_loading_with_join_for_count authors = Author.joins(:special_posts).includes([:posts, :categorizations]) diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 1750bf004a..ab9a65944f 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -103,7 +103,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase if current_adapter?(:MysqlAdapter) def test_read_attributes_before_type_cast_on_boolean bool = Boolean.create({ "value" => false }) - assert_equal "0", bool.reload.attributes_before_type_cast["value"] + assert_equal 0, bool.reload.attributes_before_type_cast["value"] end end @@ -112,7 +112,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase developer = Developer.find(:first) # Oracle adapter returns Time before type cast unless current_adapter?(:OracleAdapter) - assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"] + assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"].to_s else assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"].to_s(:db) @@ -121,7 +121,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal developer.created_at, nil developer.created_at = "2010-03-21 21:23:32" - assert_equal developer.created_at_before_type_cast, "2010-03-21 21:23:32" + assert_equal developer.created_at_before_type_cast.to_s, "2010-03-21 21:23:32" assert_equal developer.created_at, Time.parse("2010-03-21 21:23:32") end end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 2d3047c875..f6ef155d66 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -55,6 +55,14 @@ ActiveRecord::Base.connection.class.class_eval do end alias_method_chain :execute, :query_record + + def exec_with_query_record(sql, name = nil, binds = [], &block) + $queries_executed ||= [] + $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r } + exec_without_query_record(sql, name, binds, &block) + end + + alias_method_chain :exec, :query_record end ActiveRecord::Base.connection.class.class_eval { diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index f3d3d62830..0ffd0e2ab3 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -226,6 +226,12 @@ class MethodScopingTest < ActiveRecord::TestCase assert Post.find(1).comments.include?(new_comment) end + def test_scoped_create_with_join_and_merge + (Comment.where(:body => "but Who's Buying?").joins(:post) & Post.where(:body => 'Peace Sells...')).with_scope do + assert_equal({:body => "but Who's Buying?"}, Comment.scoped.scope_for_create) + end + end + def test_immutable_scope options = { :conditions => "name = 'David'" } Developer.send(:with_scope, :find => options) do diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 594db1d0ab..5bb21a54bd 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -22,6 +22,12 @@ class QueryCacheTest < ActiveRecord::TestCase end end + def test_find_queries_with_cache_multi_record + Task.cache do + assert_queries(2) { Task.find(1); Task.find(1); Task.find(2) } + end + end + def test_count_queries_with_cache Task.cache do assert_queries(1) { Task.count; Task.count } @@ -57,7 +63,7 @@ class QueryCacheTest < ActiveRecord::TestCase # Oracle adapter returns count() as Fixnum or Float if current_adapter?(:OracleAdapter) assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter) && SQLite3::Version::VERSION > '1.2.5' or current_adapter?(:Mysql2Adapter) + elsif current_adapter?(:SQLite3Adapter) && SQLite3::Version::VERSION > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index b01a8bbef1..f4f3dc4d5a 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -19,6 +19,15 @@ class RelationTest < ActiveRecord::TestCase fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, :taggings, :cars + def test_bind_values + relation = Post.scoped + assert_equal [], relation.bind_values + + relation2 = relation.bind 'foo' + assert_equal %w{ foo }, relation2.bind_values + assert_equal [], relation.bind_values + end + def test_two_named_scopes_with_includes_should_not_drop_any_include car = Car.incl_engines.incl_tyres.first assert_no_queries { car.tyres.length } @@ -491,6 +500,11 @@ class RelationTest < ActiveRecord::TestCase end end + def test_relation_merging_with_joins + comments = Comment.joins(:post).where(:body => 'Thank you for the welcome') & Post.where(:body => 'Such a lovely day') + assert_equal 1, comments.count + end + def test_count posts = Post.scoped diff --git a/activerecord/test/connections/native_oracle/connection.rb b/activerecord/test/connections/native_oracle/connection.rb index 9a717018b2..c942036128 100644 --- a/activerecord/test/connections/native_oracle/connection.rb +++ b/activerecord/test/connections/native_oracle/connection.rb @@ -37,10 +37,10 @@ Course.establish_connection 'arunit2' ActiveRecord::Base.connection.class.class_eval do IGNORED_SELECT_SQL = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from ((all|user)_tab_columns|(all|user)_triggers|(all|user)_constraints)/im] - def select_with_query_record(sql, name = nil, return_column_names = false) + def select_with_query_record(sql, name = nil) $queries_executed ||= [] $queries_executed << sql unless IGNORED_SELECT_SQL.any? { |r| sql =~ r } - select_without_query_record(sql, name, return_column_names) + select_without_query_record(sql, name) end alias_method_chain :select, :query_record diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index be6dd71e3b..ee5f77b613 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -107,6 +107,7 @@ class Client < Company belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of" belongs_to :firm_with_condition, :class_name => "Firm", :foreign_key => "client_of", :conditions => ["1 = ?", 1] belongs_to :firm_with_primary_key, :class_name => "Firm", :primary_key => "name", :foreign_key => "firm_name" + belongs_to :firm_with_primary_key_symbols, :class_name => "Firm", :primary_key => :name, :foreign_key => :firm_name belongs_to :readonly_firm, :class_name => "Firm", :foreign_key => "firm_id", :readonly => true # Record destruction so we can test whether firm.clients.clear has diff --git a/railties/guides/assets/stylesheets/main.css b/railties/guides/assets/stylesheets/main.css index 611fb6b7e0..932ee402c4 100644 --- a/railties/guides/assets/stylesheets/main.css +++ b/railties/guides/assets/stylesheets/main.css @@ -23,8 +23,12 @@ dl { margin: 0 0 1.5em 0; } dl dt { font-weight: bold; } dd { margin-left: 1.5em;} -pre,code { margin: 1.5em 0; overflow: auto; color: #333;} -pre,code,tt { font: 1em 'Anonymous Pro', 'Inconsolata', 'Menlo', 'Consolas', 'Andale Mono', 'Lucida Console', monospace; line-height: 1.5; } +pre,code { margin: 1.5em 0; overflow: auto; color: #222;} +pre,code,tt { + font-size: 1em; + font-family: "Anonymous Pro", "Inconsolata", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + line-height: 1.5; +} abbr, acronym { border-bottom: 1px dotted #666; } address { margin: 0 0 1.5em; font-style: italic; } @@ -361,10 +365,6 @@ h6 { font-weight: normal; } -tt { - font-family: monaco, "Bitstream Vera Sans Mono", "Courier New", courier, monospace; -} - div.code_container { background: #EEE url(../images/tab_grey.gif) no-repeat left top; padding: 0.25em 1em 0.5em 48px; diff --git a/railties/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css b/railties/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css index c36a4bd2ba..6d2edb2eb8 100644 --- a/railties/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css +++ b/railties/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css @@ -3,6 +3,9 @@ */ .syntaxhighlighter { background-color: #eee !important; + font-family: "Anonymous Pro", "Inconsolata", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace !important; + overflow-y: hidden !important; + overflow-x: auto !important; } .syntaxhighlighter .line.alt1 { background-color: #eee !important; @@ -17,7 +20,7 @@ color: #eee !important; } .syntaxhighlighter table caption { - color: #333 !important; + color: #222 !important; } .syntaxhighlighter .gutter { color: #787878 !important; @@ -58,7 +61,7 @@ color: red !important; } .syntaxhighlighter .plain, .syntaxhighlighter .plain a { - color: #333 !important; + color: #222 !important; } .syntaxhighlighter .comments, .syntaxhighlighter .comments a { color: #708090 !important; @@ -74,7 +77,7 @@ color: #646464 !important; } .syntaxhighlighter .variable { - color: #333 !important; + color: #222 !important; } .syntaxhighlighter .value { color: #009900 !important; @@ -86,14 +89,14 @@ color: #0066cc !important; } .syntaxhighlighter .script { - color: #333 !important; + color: #222 !important; background-color: none !important; } .syntaxhighlighter .color1, .syntaxhighlighter .color1 a { color: gray !important; } .syntaxhighlighter .color2, .syntaxhighlighter .color2 a { - color: #333 !important; + color: #222 !important; font-weight: bold !important; } .syntaxhighlighter .color3, .syntaxhighlighter .color3 a { diff --git a/railties/guides/rails_guides/generator.rb b/railties/guides/rails_guides/generator.rb index 22d402ad21..0a2170a09e 100644 --- a/railties/guides/rails_guides/generator.rb +++ b/railties/guides/rails_guides/generator.rb @@ -223,8 +223,8 @@ module RailsGuides code_blocks.push(<<HTML) <notextile> <div class="code_container"> -<pre class="brush: #{brush}; gutter: false"> -#{ERB::Util.h($2).chomp} +<pre class="brush: #{brush}; gutter: false; toolbar: false"> +#{ERB::Util.h($2).strip} </pre> </div> </notextile> |