diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2008-09-03 17:44:58 +0100 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2008-09-03 17:44:58 +0100 |
commit | 2933f4481f8b70b3b809fab6e818d80c2af1b919 (patch) | |
tree | c83d1308545cc82215b90ab37208cc81b4712276 /activerecord | |
parent | 36ee17d458b86c5f3f371810160e8839d318bbf1 (diff) | |
parent | 10fe6a6d8940300dd6698ec38e9c9573404e687d (diff) | |
download | rails-2933f4481f8b70b3b809fab6e818d80c2af1b919.tar.gz rails-2933f4481f8b70b3b809fab6e818d80c2af1b919.tar.bz2 rails-2933f4481f8b70b3b809fab6e818d80c2af1b919.zip |
Merge commit 'mainstream/master'
Conflicts:
actionpack/lib/action_controller/resources.rb
Diffstat (limited to 'activerecord')
29 files changed, 816 insertions, 430 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 12295ac799..58d0669770 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,9 @@ *Edge* +* Connection pooling. #936 [Nick Sieger] + +* Merge scoped :joins together instead of overwriting them. May expose scoping bugs in your code! #501 [Andrew White] + * before_save, before_validation and before_destroy callbacks that return false will now ROLLBACK the transaction. Previously this would have been committed before the processing was aborted. #891 [Xavier Noria] * Transactional migrations for databases which support them. #834 [divoxx, Adam Wiggins, Tarmo Tänav] diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 1af8ddcaf8..6405071354 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -878,10 +878,10 @@ module ActiveRecord method_name = "has_one_after_save_for_#{reflection.name}".to_sym define_method(method_name) do - association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}") + association = instance_variable_get(ivar) if instance_variable_defined?(ivar) - if !association.nil? && (new_record? || association.new_record? || association["#{reflection.primary_key_name}"] != id) - association["#{reflection.primary_key_name}"] = id + if !association.nil? && (new_record? || association.new_record? || association[reflection.primary_key_name] != id) + association[reflection.primary_key_name] = id association.save(true) end end @@ -994,7 +994,7 @@ module ActiveRecord method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym define_method(method_name) do - association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}") + association = instance_variable_get(ivar) if instance_variable_defined?(ivar) if association && association.target if association.new_record? @@ -1002,8 +1002,8 @@ module ActiveRecord end if association.updated? - self["#{reflection.primary_key_name}"] = association.id - self["#{reflection.options[:foreign_type]}"] = association.class.base_class.name.to_s + self[reflection.primary_key_name] = association.id + self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s end end end @@ -1015,7 +1015,7 @@ module ActiveRecord method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym define_method(method_name) do - association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}") + association = instance_variable_get(ivar) if instance_variable_defined?(ivar) if !association.nil? if association.new_record? @@ -1023,7 +1023,7 @@ module ActiveRecord end if association.updated? - self["#{reflection.primary_key_name}"] = association.id + self[reflection.primary_key_name] = association.id end end end @@ -1038,15 +1038,15 @@ module ActiveRecord method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym define_method(method_name) do - association = send("#{reflection.name}") - association.class.increment_counter("#{cache_column}", send("#{reflection.primary_key_name}")) unless association.nil? + association = send(reflection.name) + association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil? end after_create method_name method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym define_method(method_name) do - association = send("#{reflection.name}") - association.class.decrement_counter("#{cache_column}", send("#{reflection.primary_key_name}")) unless association.nil? + association = send(reflection.name) + association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil? end before_destroy method_name @@ -1164,6 +1164,9 @@ module ActiveRecord # If true, duplicate associated objects will be ignored by accessors and query methods. # [:finder_sql] # Overwrite the default generated SQL statement used to fetch the association with a manual statement + # [:counter_sql] + # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is + # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>. # [:delete_sql] # Overwrite the default generated SQL statement used to remove links between the associated # classes with a manual statement. @@ -1300,7 +1303,11 @@ module ActiveRecord end define_method("#{reflection.name.to_s.singularize}_ids") do - send(reflection.name).map { |record| record.id } + if send(reflection.name).loaded? + send(reflection.name).map(&:id) + else + send(reflection.name).all(:select => "#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map(&:id) + end end end @@ -1321,19 +1328,19 @@ module ActiveRecord end end end - + def add_single_associated_validation_callbacks(association_name) method_name = "validate_associated_records_for_#{association_name}".to_sym define_method(method_name) do association = instance_variable_get("@#{association_name}") if !association.nil? - errors.add "#{association_name}" unless association.target.nil? || association.valid? + errors.add association_name unless association.target.nil? || association.valid? end end - + validate method_name end - + def add_multiple_associated_validation_callbacks(association_name) method_name = "validate_associated_records_for_#{association_name}".to_sym ivar = "@#{association_name}" @@ -1349,7 +1356,7 @@ module ActiveRecord else association.target.select { |record| record.new_record? } end.each do |record| - errors.add "#{association_name}" unless record.valid? + errors.add association_name unless record.valid? end end end @@ -1369,7 +1376,7 @@ module ActiveRecord method_name = "after_create_or_update_associated_records_for_#{association_name}".to_sym define_method(method_name) do - association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}") + association = instance_variable_get(ivar) if instance_variable_defined?(ivar) records_to_save = if @new_record_before_save association @@ -1436,7 +1443,7 @@ module ActiveRecord when :destroy method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym define_method(method_name) do - send("#{reflection.name}").each { |o| o.destroy } + send(reflection.name).each { |o| o.destroy } end before_destroy method_name when :delete_all @@ -1455,22 +1462,22 @@ module ActiveRecord when :destroy method_name = "has_one_dependent_destroy_for_#{reflection.name}".to_sym define_method(method_name) do - association = send("#{reflection.name}") + association = send(reflection.name) association.destroy unless association.nil? end before_destroy method_name when :delete method_name = "has_one_dependent_delete_for_#{reflection.name}".to_sym define_method(method_name) do - association = send("#{reflection.name}") + association = send(reflection.name) association.class.delete(association.id) unless association.nil? end before_destroy method_name when :nullify method_name = "has_one_dependent_nullify_for_#{reflection.name}".to_sym define_method(method_name) do - association = send("#{reflection.name}") - association.update_attribute("#{reflection.primary_key_name}", nil) unless association.nil? + association = send(reflection.name) + association.update_attribute(reflection.primary_key_name, nil) unless association.nil? end before_destroy method_name else @@ -1485,14 +1492,14 @@ module ActiveRecord when :destroy method_name = "belongs_to_dependent_destroy_for_#{reflection.name}".to_sym define_method(method_name) do - association = send("#{reflection.name}") + association = send(reflection.name) association.destroy unless association.nil? end before_destroy method_name when :delete method_name = "belongs_to_dependent_delete_for_#{reflection.name}".to_sym define_method(method_name) do - association = send("#{reflection.name}") + association = send(reflection.name) association.class.delete(association.id) unless association.nil? end before_destroy method_name @@ -1527,7 +1534,7 @@ module ActiveRecord create_reflection(:has_one, association_id, options, self) end - + def create_has_one_through_reflection(association_id, options) options.assert_valid_keys( :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate @@ -1595,7 +1602,7 @@ module ActiveRecord sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} " sql << join_dependency.join_associations.collect{|join| join.association_join }.join - add_joins!(sql, options, scope) + add_joins!(sql, options[:joins], scope) add_conditions!(sql, options[:conditions], scope) add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit]) @@ -1651,7 +1658,7 @@ module ActiveRecord if is_distinct sql << distinct_join_associations.collect { |assoc| assoc.association_join }.join - add_joins!(sql, options, scope) + add_joins!(sql, options[:joins], scope) end add_conditions!(sql, options[:conditions], scope) @@ -1919,7 +1926,7 @@ module ActiveRecord end def aliased_primary_key - "#{ aliased_prefix }_r0" + "#{aliased_prefix}_r0" end def aliased_table_name @@ -1931,7 +1938,7 @@ module ActiveRecord @column_names_with_alias = [] ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i| - @column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"] + @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"] end end @@ -1968,11 +1975,11 @@ module ActiveRecord @aliased_prefix = "t#{ join_dependency.joins.size }" @parent_table_name = parent.active_record.table_name @aliased_table_name = aliased_table_name_for(table_name) - + if reflection.macro == :has_and_belongs_to_many @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") end - + if [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through] @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join") end @@ -2109,7 +2116,7 @@ module ActiveRecord end protected - + def aliased_table_name_for(name, suffix = nil) if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{name.downcase}\son} @join_dependency.table_aliases[name] += 1 @@ -2127,7 +2134,7 @@ module ActiveRecord name end - + def pluralize(table_name) ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index a12d4face4..168443e092 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -141,6 +141,35 @@ 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+. + def count(*args) + if @reflection.options[:counter_sql] + @reflection.klass.count_by_sql(@counter_sql) + else + column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args) + if @reflection.options[:uniq] + # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. + column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all + options.merge!(:distinct => true) + end + + value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) } + + limit = @reflection.options[:limit] + offset = @reflection.options[:offset] + + if limit || offset + [ [value - offset.to_i, 0].max, limit.to_i ].min + else + value + end + end + end + + # Remove +records+ from this association. Does not destroy +records+. def delete(*records) records = flatten_deeper(records) 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 e7e433b6b6..3d689098b5 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 @@ -78,6 +78,16 @@ module ActiveRecord 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}" + + 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 (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } + @counter_sql = interpolate_sql(@reflection.options[:counter_sql]) + else + @counter_sql = @finder_sql + end end def construct_scope diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 451bf8faa9..dda22668c6 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -5,32 +5,6 @@ module ActiveRecord # If the association has a <tt>:through</tt> option further specialization # is provided by its child HasManyThroughAssociation. class HasManyAssociation < AssociationCollection #:nodoc: - # Count the number of associated records. All arguments are optional. - def count(*args) - if @reflection.options[:counter_sql] - @reflection.klass.count_by_sql(@counter_sql) - elsif @reflection.options[:finder_sql] - @reflection.klass.count_by_sql(@finder_sql) - else - column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args) - options[:conditions] = options[:conditions].blank? ? - @finder_sql : - @finder_sql + " AND (#{sanitize_sql(options[:conditions])})" - options[:include] ||= @reflection.options[:include] - - value = @reflection.klass.count(column_name, options) - - limit = @reflection.options[:limit] - offset = @reflection.options[:offset] - - if limit || offset - [ [value - offset.to_i, 0].max, limit.to_i ].min - else - value - end - end - end - protected def owner_quoted_id if @reflection.options[:primary_key] 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 24b02efc35..84fa900f46 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -31,16 +31,6 @@ module ActiveRecord return count end - def count(*args) - column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args) - if @reflection.options[:uniq] - # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL statement. - column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all - options.merge!(:distinct => true) - end - @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) } - end - protected def construct_find_options!(options) options[:select] = construct_select(options[:select]) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index fab16a4446..0a1baff87d 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -214,7 +214,7 @@ module ActiveRecord if logger logger.warn "Exception occurred during reader method compilation." logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?" - logger.warn "#{err.message}" + logger.warn err.message end end end @@ -330,8 +330,8 @@ module ActiveRecord end end - # A Person object with a name attribute can ask <tt>person.respond_to?("name")</tt>, - # <tt>person.respond_to?("name=")</tt>, and <tt>person.respond_to?("name?")</tt> + # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, + # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt> # which will all return +true+. alias :respond_to_without_attributes? :respond_to? def respond_to?(method, include_priv = false) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 4a23c8c690..3419aad580 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -452,13 +452,6 @@ module ActiveRecord #:nodoc: cattr_accessor :default_timezone, :instance_writer => false @@default_timezone = :local - # Determines whether to use a connection for each thread, or a single shared connection for all threads. - # Defaults to false. If you're writing a threaded application, set to true - # and periodically call verify_active_connections! to clear out connections - # assigned to stale threads. - cattr_accessor :allow_concurrency, :instance_writer => false - @@allow_concurrency = false - # Specifies the format to use when dumping the database schema with Rails' # Rakefile. If :sql, the schema is dumped as (potentially database- # specific) SQL statements. If :ruby, the schema is dumped as an @@ -930,12 +923,12 @@ module ActiveRecord #:nodoc: # To start from an all-closed default and enable attributes as needed, # have a look at +attr_accessible+. def attr_protected(*attributes) - write_inheritable_attribute("attr_protected", Set.new(attributes.map(&:to_s)) + (protected_attributes || [])) + write_inheritable_attribute(:attr_protected, Set.new(attributes.map(&:to_s)) + (protected_attributes || [])) end # Returns an array of all the attributes that have been protected from mass-assignment. def protected_attributes # :nodoc: - read_inheritable_attribute("attr_protected") + read_inheritable_attribute(:attr_protected) end # Specifies a white list of model attributes that can be set via @@ -963,22 +956,22 @@ module ActiveRecord #:nodoc: # customer.credit_rating = "Average" # customer.credit_rating # => "Average" def attr_accessible(*attributes) - write_inheritable_attribute("attr_accessible", Set.new(attributes.map(&:to_s)) + (accessible_attributes || [])) + write_inheritable_attribute(:attr_accessible, Set.new(attributes.map(&:to_s)) + (accessible_attributes || [])) end # Returns an array of all the attributes that have been made accessible to mass-assignment. def accessible_attributes # :nodoc: - read_inheritable_attribute("attr_accessible") + read_inheritable_attribute(:attr_accessible) end # Attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards. def attr_readonly(*attributes) - write_inheritable_attribute("attr_readonly", Set.new(attributes.map(&:to_s)) + (readonly_attributes || [])) + write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || [])) end # Returns an array of all the attributes that have been specified as readonly. def readonly_attributes - read_inheritable_attribute("attr_readonly") + read_inheritable_attribute(:attr_readonly) end # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, @@ -1002,7 +995,7 @@ module ActiveRecord #:nodoc: # Returns a hash of all the attributes that have been specified for serialization as keys and their class restriction as values. def serialized_attributes - read_inheritable_attribute("attr_serialized") or write_inheritable_attribute("attr_serialized", {}) + read_inheritable_attribute(:attr_serialized) or write_inheritable_attribute(:attr_serialized, {}) end @@ -1552,7 +1545,7 @@ module ActiveRecord #:nodoc: sql = "SELECT #{options[:select] || (scope && scope[:select]) || ((options[:joins] || (scope && scope[:joins])) && quoted_table_name + '.*') || '*'} " sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} " - add_joins!(sql, options, scope) + add_joins!(sql, options[:joins], scope) add_conditions!(sql, options[:conditions], scope) add_group!(sql, options[:group], scope) @@ -1568,6 +1561,22 @@ module ActiveRecord #:nodoc: (safe_to_array(first) + safe_to_array(second)).uniq end + def merge_joins(first, second) + if first.is_a?(String) && second.is_a?(String) + "#{first} #{second}" + elsif first.is_a?(String) || second.is_a?(String) + if first.is_a?(String) + join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, second, nil) + "#{first} #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join}" + else + join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, first, nil) + "#{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} #{second}" + end + else + (safe_to_array(first) + safe_to_array(second)).uniq + end + end + # Object#to_a is deprecated, though it does have the desired behavior def safe_to_array(o) case o @@ -1623,16 +1632,15 @@ module ActiveRecord #:nodoc: end # The optional scope argument is for the current <tt>:find</tt> scope. - def add_joins!(sql, options, scope = :auto) + def add_joins!(sql, joins, scope = :auto) scope = scope(:find) if :auto == scope - [(scope && scope[:joins]), options[:joins]].each do |join| - case join - when Symbol, Hash, Array - join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, join, nil) - sql << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} " - else - sql << " #{join} " - end + merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins]) + case merged_joins + when Symbol, Hash, Array + join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, merged_joins, nil) + sql << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} " + when String + sql << " #{merged_joins} " end end @@ -1882,6 +1890,8 @@ module ActiveRecord #:nodoc: hash[method][key] = merge_conditions(params[key], hash[method][key]) elsif key == :include && merge hash[method][key] = merge_includes(hash[method][key], params[key]).uniq + elsif key == :joins && merge + hash[method][key] = merge_joins(params[key], hash[method][key]) else hash[method][key] = hash[method][key] || params[key] end @@ -1929,22 +1939,11 @@ module ActiveRecord #:nodoc: end end - def thread_safe_scoped_methods #:nodoc: + def scoped_methods #:nodoc: scoped_methods = (Thread.current[:scoped_methods] ||= {}) scoped_methods[self] ||= [] end - def single_threaded_scoped_methods #:nodoc: - @scoped_methods ||= [] - end - - # pick up the correct scoped_methods version from @@allow_concurrency - if @@allow_concurrency - alias_method :scoped_methods, :thread_safe_scoped_methods - else - alias_method :scoped_methods, :single_threaded_scoped_methods - end - def current_scoped_methods #:nodoc: scoped_methods.last end diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index 26acbf53b5..a675af4787 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -188,7 +188,7 @@ module ActiveRecord end joins = "" - add_joins!(joins, options, scope) + add_joins!(joins, options[:joins], scope) if merged_includes.any? join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, joins) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb new file mode 100644 index 0000000000..838b0434b0 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -0,0 +1,281 @@ +require 'monitor' +require 'set' + +module ActiveRecord + # Raised when a connection could not be obtained within the connection + # acquisition timeout period. + class ConnectionTimeoutError < ConnectionNotEstablished + end + + module ConnectionAdapters + # Connection pool base class for managing ActiveRecord database + # connections. + # + # Connections can be obtained and used from a connection pool in several + # ways: + # + # 1. Simply use ActiveRecord::Base.connection as with ActiveRecord 2.1 and + # earlier (pre-connection-pooling). Eventually, when you're done with + # the connection(s) and wish it to be returned to the pool, you call + # ActiveRecord::Base.clear_active_connections!. This will be the + # default behavior for ActiveRecord when used in conjunction with + # ActionPack's request handling cycle. + # 2. Manually check out a connection from the pool with + # ActiveRecord::Base.connection_pool.checkout. You are responsible for + # returning this connection to the pool when finished by calling + # ActiveRecord::Base.connection_pool.checkin(connection). + # 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which + # obtains a connection, yields it as the sole argument to the block, + # and returns it to the pool after the block completes. + # + # There are two connection-pooling-related options that you can add to + # your database connection configuration: + # + # * +pool+: number indicating size of connection pool (default 5) + # * +wait_timeout+: number of seconds to block and wait for a connection + # before giving up and raising a timeout error (default 5 seconds). + class ConnectionPool + delegate :verification_timeout, :to => "::ActiveRecord::Base" + attr_reader :spec + + def initialize(spec) + @spec = spec + # The cache of reserved connections mapped to threads + @reserved_connections = {} + # The mutex used to synchronize pool access + @connection_mutex = Monitor.new + @queue = @connection_mutex.new_cond + # default 5 second timeout + @timeout = spec.config[:wait_timeout] || 5 + # default max pool size to 5 + @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 + @connections = [] + @checked_out = [] + end + + # Retrieve the connection associated with the current thread, or call + # #checkout to obtain one if necessary. + # + # #connection can be called any number of times; the connection is + # held in a hash keyed by the thread id. + def connection + if conn = @reserved_connections[current_connection_id] + conn.verify!(verification_timeout) + conn + else + @reserved_connections[current_connection_id] = checkout + end + end + + # Signal that the thread is finished with the current connection. + # #release_connection releases the connection-thread association + # and returns the connection to the pool. + def release_connection + conn = @reserved_connections.delete(current_connection_id) + checkin conn if conn + end + + # Reserve a connection, and yield it to a block. Ensure the connection is + # checked back in when finished. + def with_connection + conn = checkout + yield conn + ensure + checkin conn + end + + # Returns true if a connection has already been opened. + def connected? + !@connections.empty? + end + + # Disconnect all connections in the pool. + def disconnect! + @reserved_connections.each do |name,conn| + checkin conn + end + @reserved_connections = {} + @connections.each do |conn| + conn.disconnect! + end + @connections = [] + end + + # Clears the cache which maps classes + def clear_reloadable_connections! + @reserved_connections.each do |name, conn| + checkin conn + end + @reserved_connections = {} + @connections.each do |conn| + conn.disconnect! if conn.requires_reloading? + end + @connections = [] + end + + # Verify active connections and remove and disconnect connections + # associated with stale threads. + def verify_active_connections! #:nodoc: + clear_stale_cached_connections! + @connections.each do |connection| + connection.verify!(verification_timeout) + end + end + + # Return any checked-out connections back to the pool by threads that + # are no longer alive. + def clear_stale_cached_connections! + remove_stale_cached_threads!(@reserved_connections) do |name, conn| + checkin conn + end + end + + # Check-out a database connection from the pool. + def checkout + # Checkout an available connection + conn = @connection_mutex.synchronize do + if @checked_out.size < @connections.size + checkout_existing_connection + elsif @connections.size < @size + checkout_new_connection + end + end + return conn if conn + + # No connections available; wait for one + @connection_mutex.synchronize do + if @queue.wait(@timeout) + checkout_existing_connection + else + raise ConnectionTimeoutError, "could not obtain a database connection in a timely fashion" + end + end + end + + # Check-in a database connection back into the pool. + def checkin(conn) + @connection_mutex.synchronize do + conn.run_callbacks :checkin + @checked_out.delete conn + @queue.signal + end + end + + synchronize :clear_reloadable_connections!, :verify_active_connections!, + :connected?, :disconnect!, :with => :@connection_mutex + + private + def new_connection + config = spec.config.reverse_merge(:allow_concurrency => true) + ActiveRecord::Base.send(spec.adapter_method, config) + end + + def current_connection_id #:nodoc: + Thread.current.object_id + end + + # Remove stale threads from the cache. + def remove_stale_cached_threads!(cache, &block) + keys = Set.new(cache.keys) + + Thread.list.each do |thread| + keys.delete(thread.object_id) if thread.alive? + end + keys.each do |key| + next unless cache.has_key?(key) + block.call(key, cache[key]) + cache.delete(key) + end + end + + def checkout_new_connection + c = new_connection + @connections << c + checkout_and_verify(c) + end + + def checkout_existing_connection + c = (@connections - @checked_out).first + checkout_and_verify(c) + end + + def checkout_and_verify(c) + c.run_callbacks :checkout + c.verify!(verification_timeout) + @checked_out << c + c + end + end + + class ConnectionHandler + def initialize(pools = {}) + @connection_pools = pools + end + + def connection_pools + @connection_pools ||= {} + end + + def establish_connection(name, spec) + @connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec) + end + + # Returns any connections in use by the current thread back to the pool, + # and also returns connections to the pool cached by threads that are no + # longer alive. + def clear_active_connections! + @connection_pools.each_value do |pool| + pool.release_connection + pool.clear_stale_cached_connections! + end + end + + # Clears the cache which maps classes + def clear_reloadable_connections! + @connection_pools.each_value {|pool| pool.clear_reloadable_connections! } + end + + def clear_all_connections! + @connection_pools.each_value {|pool| pool.disconnect! } + end + + # Verify active connections. + def verify_active_connections! #:nodoc: + @connection_pools.each_value {|pool| pool.verify_active_connections! } + end + + # Locate the connection of the nearest super class. This can be an + # active or defined connection: if it is the latter, it will be + # opened and set as the active connection for the class it was defined + # for (not necessarily the current class). + def retrieve_connection(klass) #:nodoc: + pool = retrieve_connection_pool(klass) + (pool && pool.connection) or raise ConnectionNotEstablished + end + + # Returns true if a connection that's accessible to this class has + # already been opened. + def connected?(klass) + retrieve_connection_pool(klass).connected? + end + + # Remove the connection for this class. This will close the active + # connection and the defined connection (if they exist). The result + # can be used as an argument for establish_connection, for easily + # re-establishing the connection. + def remove_connection(klass) + pool = @connection_pools[klass.name] + @connection_pools.delete_if { |key, value| value == pool } + pool.disconnect! if pool + pool.spec.config if pool + end + + def retrieve_connection_pool(klass) + pool = @connection_pools[klass.name] + return pool if pool + return nil if ActiveRecord::Base == klass + retrieve_connection_pool klass.superclass + end + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index 2a8807fb78..417a333aab 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -1,5 +1,3 @@ -require 'set' - module ActiveRecord class Base class ConnectionSpecification #:nodoc: @@ -14,158 +12,9 @@ module ActiveRecord cattr_accessor :verification_timeout, :instance_writer => false @@verification_timeout = 0 - # The class -> [adapter_method, config] map - @@defined_connections = {} - - # The class -> thread id -> adapter cache. (class -> adapter if not allow_concurrency) - @@active_connections = {} - - class << self - # Retrieve the connection cache. - def thread_safe_active_connections #:nodoc: - @@active_connections[Thread.current.object_id] ||= {} - end - - def single_threaded_active_connections #:nodoc: - @@active_connections - end - - # pick up the right active_connection method from @@allow_concurrency - if @@allow_concurrency - alias_method :active_connections, :thread_safe_active_connections - else - alias_method :active_connections, :single_threaded_active_connections - end - - # set concurrency support flag (not thread safe, like most of the methods in this file) - def allow_concurrency=(threaded) #:nodoc: - logger.debug "allow_concurrency=#{threaded}" if logger - return if @@allow_concurrency == threaded - clear_all_cached_connections! - @@allow_concurrency = threaded - method_prefix = threaded ? "thread_safe" : "single_threaded" - sing = (class << self; self; end) - [:active_connections, :scoped_methods].each do |method| - sing.send(:alias_method, method, "#{method_prefix}_#{method}") - end - log_connections if logger - end - - def active_connection_name #:nodoc: - @active_connection_name ||= - if active_connections[name] || @@defined_connections[name] - name - elsif self == ActiveRecord::Base - nil - else - superclass.active_connection_name - end - end - - def clear_active_connection_name #:nodoc: - @active_connection_name = nil - subclasses.each { |klass| klass.clear_active_connection_name } - end - - # Returns the connection currently associated with the class. This can - # also be used to "borrow" the connection to do database work unrelated - # to any of the specific Active Records. - def connection - if defined?(@active_connection_name) && (conn = active_connections[@active_connection_name]) - conn - else - # retrieve_connection sets the cache key. - conn = retrieve_connection - active_connections[@active_connection_name] = conn - end - end - - # Clears the cache which maps classes to connections. - def clear_active_connections! - clear_cache!(@@active_connections) do |name, conn| - conn.disconnect! - end - end - - # Clears the cache which maps classes - def clear_reloadable_connections! - if @@allow_concurrency - # With concurrent connections @@active_connections is - # a hash keyed by thread id. - @@active_connections.each do |thread_id, conns| - conns.each do |name, conn| - if conn.requires_reloading? - conn.disconnect! - @@active_connections[thread_id].delete(name) - end - end - end - else - @@active_connections.each do |name, conn| - if conn.requires_reloading? - conn.disconnect! - @@active_connections.delete(name) - end - end - end - end - - # Verify active connections. - def verify_active_connections! #:nodoc: - if @@allow_concurrency - remove_stale_cached_threads!(@@active_connections) do |name, conn| - conn.disconnect! - end - end - - active_connections.each_value do |connection| - connection.verify!(@@verification_timeout) - end - end - - private - def clear_cache!(cache, thread_id = nil, &block) - if cache - if @@allow_concurrency - thread_id ||= Thread.current.object_id - thread_cache, cache = cache, cache[thread_id] - return unless cache - end - - cache.each(&block) if block_given? - cache.clear - end - ensure - if thread_cache && @@allow_concurrency - thread_cache.delete(thread_id) - end - end - - # Remove stale threads from the cache. - def remove_stale_cached_threads!(cache, &block) - stale = Set.new(cache.keys) - - Thread.list.each do |thread| - stale.delete(thread.object_id) if thread.alive? - end - - stale.each do |thread_id| - clear_cache!(cache, thread_id, &block) - end - end - - def clear_all_cached_connections! - if @@allow_concurrency - @@active_connections.each_value do |connection_hash_for_thread| - connection_hash_for_thread.each_value {|conn| conn.disconnect! } - connection_hash_for_thread.clear - end - else - @@active_connections.each_value {|conn| conn.disconnect! } - end - @@active_connections.clear - end - end + # The connection handler + cattr_accessor :connection_handler, :instance_writer => false + @@connection_handler = ConnectionAdapters::ConnectionHandler.new # Returns the connection currently associated with the class. This can # also be used to "borrow" the connection to do database work that isn't @@ -208,9 +57,7 @@ module ActiveRecord raise AdapterNotSpecified unless defined? RAILS_ENV establish_connection(RAILS_ENV) when ConnectionSpecification - clear_active_connection_name - @active_connection_name = name - @@defined_connections[name] = spec + @@connection_handler.establish_connection(name, spec) when Symbol, String if configuration = configurations[spec.to_s] establish_connection(configuration) @@ -243,67 +90,42 @@ module ActiveRecord end end - # Locate the connection of the nearest super class. This can be an - # active or defined connection: if it is the latter, it will be - # opened and set as the active connection for the class it was defined - # for (not necessarily the current class). - def self.retrieve_connection #:nodoc: - # Name is nil if establish_connection hasn't been called for - # some class along the inheritance chain up to AR::Base yet. - if name = active_connection_name - if conn = active_connections[name] - # Verify the connection. - conn.verify!(@@verification_timeout) - elsif spec = @@defined_connections[name] - # Activate this connection specification. - klass = name.constantize - klass.connection = spec - conn = active_connections[name] - end + class << self + # Deprecated and no longer has any effect. + def allow_concurrency + ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency has been deprecated and no longer has any effect. Please remove all references to allow_concurrency.") end - conn or raise ConnectionNotEstablished - end + # Deprecated and no longer has any effect. + def allow_concurrency=(flag) + ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency= has been deprecated and no longer has any effect. Please remove all references to allow_concurrency=.") + end - # Returns true if a connection that's accessible to this class has already been opened. - def self.connected? - active_connections[active_connection_name] ? true : false - end + # Returns the connection currently associated with the class. This can + # also be used to "borrow" the connection to do database work unrelated + # to any of the specific Active Records. + def connection + retrieve_connection + end - # Remove the connection for this class. This will close the active - # connection and the defined connection (if they exist). The result - # can be used as an argument for establish_connection, for easily - # re-establishing the connection. - def self.remove_connection(klass=self) - spec = @@defined_connections[klass.name] - konn = active_connections[klass.name] - @@defined_connections.delete_if { |key, value| value == spec } - active_connections.delete_if { |key, value| value == konn } - konn.disconnect! if konn - spec.config if spec - end + def connection_pool + connection_handler.retrieve_connection_pool(self) + end - # Set the connection for the class. - def self.connection=(spec) #:nodoc: - if spec.kind_of?(ActiveRecord::ConnectionAdapters::AbstractAdapter) - active_connections[name] = spec - elsif spec.kind_of?(ConnectionSpecification) - config = spec.config.reverse_merge(:allow_concurrency => @@allow_concurrency) - self.connection = self.send(spec.adapter_method, config) - elsif spec.nil? - raise ConnectionNotEstablished - else - establish_connection spec + def retrieve_connection + connection_handler.retrieve_connection(self) end - end - # connection state logging - def self.log_connections #:nodoc: - if logger - logger.info "Defined connections: #{@@defined_connections.inspect}" - logger.info "Active connections: #{active_connections.inspect}" - logger.info "Active connection name: #{@active_connection_name}" + def connected? + connection_handler.connected?(self) end + + def remove_connection(klass = self) + connection_handler.remove_connection(klass) + end + + delegate :clear_active_connections!, :clear_reloadable_connections!, + :clear_all_connections!,:verify_active_connections!, :to => :connection_handler end end end 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 2afd6064ad..2fc50b9bfa 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -4,7 +4,6 @@ module ActiveRecord class << self def included(base) base.class_eval do - attr_accessor :query_cache_enabled alias_method_chain :columns, :query_cache alias_method_chain :select_all, :query_cache end @@ -16,7 +15,7 @@ module ActiveRecord method_names.each do |method_name| base.class_eval <<-end_code, __FILE__, __LINE__ def #{method_name}_with_query_dirty(*args) - clear_query_cache if @query_cache_enabled + clear_query_cache if query_cache_enabled #{method_name}_without_query_dirty(*args) end @@ -26,22 +25,38 @@ module ActiveRecord end end + def query_cache_enabled + Thread.current['query_cache_enabled'] + end + + def query_cache_enabled=(flag) + Thread.current['query_cache_enabled'] = flag + end + + def query_cache + Thread.current['query_cache'] + end + + def query_cache=(cache) + Thread.current['query_cache'] = cache + end + # Enable the query cache within the block. def cache - old, @query_cache_enabled = @query_cache_enabled, true - @query_cache ||= {} + old, self.query_cache_enabled = query_cache_enabled, true + self.query_cache ||= {} yield ensure clear_query_cache - @query_cache_enabled = old + self.query_cache_enabled = old end # Disable the query cache within the block. def uncached - old, @query_cache_enabled = @query_cache_enabled, false + old, self.query_cache_enabled = query_cache_enabled, false yield ensure - @query_cache_enabled = old + self.query_cache_enabled = old end # Clears the query cache. @@ -51,11 +66,11 @@ module ActiveRecord # the same SQL query and repeatedly return the same result each time, silently # undermining the randomness you were expecting. def clear_query_cache - @query_cache.clear if @query_cache + query_cache.clear if query_cache end def select_all_with_query_cache(*args) - if @query_cache_enabled + if query_cache_enabled cache_sql(args.first) { select_all_without_query_cache(*args) } else select_all_without_query_cache(*args) @@ -63,8 +78,8 @@ module ActiveRecord end def columns_with_query_cache(*args) - if @query_cache_enabled - @query_cache["SHOW FIELDS FROM #{args.first}"] ||= columns_without_query_cache(*args) + if query_cache_enabled + query_cache["SHOW FIELDS FROM #{args.first}"] ||= columns_without_query_cache(*args) else columns_without_query_cache(*args) end @@ -73,11 +88,11 @@ module ActiveRecord private def cache_sql(sql) result = - if @query_cache.has_key?(sql) + if query_cache.has_key?(sql) log_info(sql, "CACHE", 0.0) - @query_cache[sql] + query_cache[sql] else - @query_cache[sql] = yield + query_cache[sql] = yield end if Array === result diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 6924bb7e6f..005be9d72f 100644..100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -7,6 +7,7 @@ require 'active_record/connection_adapters/abstract/schema_definitions' require 'active_record/connection_adapters/abstract/schema_statements' require 'active_record/connection_adapters/abstract/database_statements' require 'active_record/connection_adapters/abstract/quoting' +require 'active_record/connection_adapters/abstract/connection_pool' require 'active_record/connection_adapters/abstract/connection_specification' require 'active_record/connection_adapters/abstract/query_cache' @@ -24,6 +25,9 @@ module ActiveRecord class AbstractAdapter include Quoting, DatabaseStatements, SchemaStatements include QueryCache + include ActiveSupport::Callbacks + define_callbacks :checkout, :checkin + checkout :reset! @@row_even = true def initialize(connection, logger = nil) #:nodoc: @@ -102,14 +106,25 @@ module ActiveRecord @active = false end + # Reset the state of this connection, directing the DBMS to clear + # transactions and other connection-related server-side state. Usually a + # database-dependent operation; the default method simply executes a + # ROLLBACK and swallows any exceptions which is probably not enough to + # ensure the connection is clean. + def reset! + silence_stderr do # postgres prints on stderr when you do this w/o a txn + execute "ROLLBACK" rescue nil + end + end + # Returns true if its safe 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? false end - # Lazily verify this connection, calling <tt>active?</tt> only if it hasn't - # been called for +timeout+ seconds. + # Lazily verify this connection, calling <tt>active?</tt> only if it + # hasn't been called for +timeout+ seconds. def verify!(timeout) now = Time.now.to_i if (now - @last_verification) > timeout diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 204ebaa2e2..14c76ac455 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -280,6 +280,16 @@ module ActiveRecord @connection.close rescue nil end + def reset! + if @connection.respond_to?(:change_user) + # See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to + # reset the connection is to change the user to the same user. + @connection.change_user(@config[:username], @config[:password], @config[:database]) + configure_connection + else + super + end + end # DATABASE STATEMENTS ====================================== @@ -529,7 +539,11 @@ module ActiveRecord end @connection.real_connect(*@connection_options) + configure_connection + end + def configure_connection + encoding = @config[:encoding] execute("SET NAMES '#{encoding}'") if encoding # By default, MySQL 'where id is null' selects the last inserted id. diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 622cfc3c3f..114141a646 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -955,7 +955,7 @@ module Test #:nodoc: ActiveRecord::Base.connection.rollback_db_transaction ActiveRecord::Base.connection.decrement_open_transactions end - ActiveRecord::Base.verify_active_connections! + ActiveRecord::Base.clear_active_connections! end private diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 46eaa42a55..83043c2c22 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -101,9 +101,9 @@ module ActiveRecord class Scope attr_reader :proxy_scope, :proxy_options - + NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set [].methods.each do |m| - unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty?|any?|respond_to?)/ + unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s) delegate m, :to => :proxy_found end end @@ -136,6 +136,10 @@ module ActiveRecord end end + def size + @found ? @found.length : count + end + def empty? @found ? @found.empty? : count.zero? end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 687f1261cd..577e30ec86 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -54,7 +54,7 @@ module ActiveRecord def add_on_empty(attributes, custom_message = nil) for attr in [attributes].flatten value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] - is_empty = value.respond_to?("empty?") ? value.empty? : false + is_empty = value.respond_to?(:empty?) ? value.empty? : false add(attr, :empty, :default => custom_message) unless !value.nil? && !is_empty end end @@ -87,6 +87,8 @@ module ActiveRecord # </ol> def generate_message(attribute, message = :invalid, options = {}) + message, options[:default] = options[:default], message if options[:default].is_a?(Symbol) + defaults = @base.class.self_and_descendents_from_active_record.map do |klass| [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", :"models.#{klass.name.underscore}.#{message}" ] @@ -95,7 +97,6 @@ module ActiveRecord defaults << options.delete(:default) defaults = defaults.compact.flatten << :"messages.#{message}" - model_name = @base.class.name key = defaults.shift value = @base.respond_to?(attribute) ? @base.send(attribute) : nil @@ -664,7 +665,7 @@ module ActiveRecord condition_params = [value] else condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}" - condition_params = [value.downcase] + condition_params = [value.chars.downcase] end if scope = configuration[:scope] @@ -750,7 +751,7 @@ module ActiveRecord enum = configuration[:in] || configuration[:within] - raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") + raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?) validates_each(attr_names, configuration) do |record, attr_name, value| unless enum.include?(value) @@ -784,7 +785,7 @@ module ActiveRecord enum = configuration[:in] || configuration[:within] - raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") + raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?) validates_each(attr_names, configuration) do |record, attr_name, value| if enum.include?(value) diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 0572418e3b..edca3c622b 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -223,10 +223,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase devel = Developer.find(1) proj = assert_no_queries { devel.projects.build("name" => "Projekt") } assert !devel.projects.loaded? - + assert_equal devel.projects.last, proj assert devel.projects.loaded? - + assert proj.new_record? devel.save assert !proj.new_record? @@ -251,10 +251,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase devel = Developer.find(1) proj = devel.projects.create("name" => "Projekt") assert !devel.projects.loaded? - + assert_equal devel.projects.last, proj assert devel.projects.loaded? - + assert !proj.new_record? assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end @@ -274,10 +274,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_creation_respects_hash_condition post = categories(:general).post_with_conditions.build(:body => '') - + assert post.save assert_equal 'Yet Another Testing Title', post.title - + another_post = categories(:general).post_with_conditions.create(:body => '') assert !another_post.new_record? @@ -288,7 +288,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase dev = developers(:jamis) dev.projects << projects(:active_record) dev.projects << projects(:active_record) - + assert_equal 3, dev.projects.size assert_equal 1, dev.projects.uniq.size end @@ -415,13 +415,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase project.developers.class # force load target developer = project.developers.first - + assert_no_queries do assert project.developers.loaded? assert project.developers.include?(developer) end end - + def test_include_checks_if_record_exists_if_target_not_loaded project = projects(:active_record) developer = project.developers.first @@ -641,6 +641,22 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal [projects(:active_record).id], developers(:jamis).project_ids end + def test_get_ids_for_loaded_associations + developer = developers(:david) + developer.projects(true) + assert_queries(0) do + developer.project_ids + developer.project_ids + end + end + + def test_get_ids_for_unloaded_associations_does_not_load_them + developer = developers(:david) + assert !developer.projects.loaded? + assert_equal projects(:active_record, :action_controller).map(&:id).sort, developer.project_ids.sort + assert !developer.projects.loaded? + end + def test_assign_ids developer = Developer.new("name" => "Joe") developer.project_ids = projects(:active_record, :action_controller).map(&:id) @@ -703,4 +719,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase # due to Unknown column 'authors.id' assert Category.find(1).posts_with_authors_sorted_by_author_id.find_by_title('Welcome to the weblog') end + + def test_counting_on_habtm_association_and_not_array + david = Developer.find(1) + # Extra parameter just to make sure we aren't falling back to + # Array#count in Ruby >=1.8.7, which would raise an ArgumentError + assert_nothing_raised { david.projects.count(:all, :conditions => '1=1') } + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 17fd88ce65..feac4b002b 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -384,7 +384,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase company = companies(:first_firm) new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } assert !company.clients_of_firm.loaded? - + assert_equal "Another Client", new_client.name assert new_client.new_record? assert_equal new_client, company.clients_of_firm.last @@ -416,7 +416,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_many company = companies(:first_firm) new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) } - + assert_equal 2, new_clients.size company.name += '-changed' assert_queries(3) { assert company.save } @@ -655,10 +655,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_creation_respects_hash_condition ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build - + assert ms_client.save assert_equal 'Microsoft', ms_client.name - + another_ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.create assert !another_ms_client.new_record? @@ -830,6 +830,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids end + def test_get_ids_for_loaded_associations + company = companies(:first_firm) + company.clients(true) + assert_queries(0) do + company.client_ids + company.client_ids + end + end + + def test_get_ids_for_unloaded_associations_does_not_load_them + company = companies(:first_firm) + assert !company.clients.loaded? + assert_equal [companies(:first_client).id, companies(:second_client).id], company.client_ids + assert !company.clients.loaded? + end + def test_assign_ids firm = Firm.new("name" => "Apple") firm.client_ids = [companies(:first_client).id, companies(:second_client).id] @@ -900,7 +916,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 4, authors(:david).limited_comments.find(:all, :conditions => "comments.type = 'SpecialComment'", :limit => 9_000).length assert_equal 4, authors(:david).limited_comments.find_all_by_type('SpecialComment', :limit => 9_000).length end - + def test_find_all_include_over_the_same_table_for_through assert_equal 2, people(:michael).posts.find(:all, :include => :people).length end @@ -937,13 +953,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_include_loads_collection_if_target_uses_finder_sql firm = companies(:first_firm) client = firm.clients_using_sql.first - + firm.reload assert ! firm.clients_using_sql.loaded? assert firm.clients_using_sql.include?(client) assert firm.clients_using_sql.loaded? end - + def test_include_returns_false_for_non_matching_record_to_verify_scoping firm = companies(:first_firm) diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 353262c81b..0be050ec81 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -200,4 +200,24 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_count_with_include_should_alias_join_table assert_equal 2, people(:michael).posts.count(:include => :readers) end + + def test_get_ids + assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort + end + + def test_get_ids_for_loaded_associations + person = people(:michael) + person.posts(true) + assert_queries(0) do + person.post_ids + person.post_ids + end + end + + def test_get_ids_for_unloaded_associations_does_not_load_them + person = people(:michael) + assert !person.posts.loaded? + assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort + assert !person.posts.loaded? + end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index c8111358e3..ac9081e003 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -880,7 +880,7 @@ class BasicsTest < ActiveRecord::TestCase def test_mass_assignment_protection_against_class_attribute_writers [:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names, :colorize_logging, - :default_timezone, :allow_concurrency, :schema_format, :verification_timeout, :lock_optimistically, :record_timestamps].each do |method| + :default_timezone, :schema_format, :verification_timeout, :lock_optimistically, :record_timestamps].each do |method| assert Task.respond_to?(method) assert Task.respond_to?("#{method}=") assert Task.new.respond_to?(method) diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 701187223f..bbe8582466 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -210,13 +210,6 @@ unless current_adapter?(:SQLServerAdapter, :SybaseAdapter, :OpenBaseAdapter) def setup # Avoid introspection queries during tests. Person.columns; Reader.columns - - @allow_concurrency = ActiveRecord::Base.allow_concurrency - ActiveRecord::Base.allow_concurrency = true - end - - def teardown - ActiveRecord::Base.allow_concurrency = @allow_concurrency end # Test typical find. diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index ee66ac948d..af6fcd32ad 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'models/author' require 'models/developer' require 'models/project' require 'models/comment' @@ -6,7 +7,7 @@ require 'models/post' require 'models/category' class MethodScopingTest < ActiveRecord::TestCase - fixtures :developers, :projects, :comments, :posts, :developers_projects + fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects def test_set_conditions Developer.with_scope(:find => { :conditions => 'just a test...' }) do @@ -97,6 +98,46 @@ class MethodScopingTest < ActiveRecord::TestCase assert_equal developers(:david).attributes, scoped_developers.first.attributes end + def test_scoped_find_using_new_style_joins + scoped_developers = Developer.with_scope(:find => { :joins => :projects }) do + Developer.find(:all, :conditions => 'projects.id = 2') + end + assert scoped_developers.include?(developers(:david)) + assert !scoped_developers.include?(developers(:jamis)) + assert_equal 1, scoped_developers.size + assert_equal developers(:david).attributes, scoped_developers.first.attributes + end + + def test_scoped_find_merges_old_style_joins + scoped_authors = Author.with_scope(:find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id ' }) do + Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'INNER JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1') + end + assert scoped_authors.include?(authors(:david)) + assert !scoped_authors.include?(authors(:mary)) + assert_equal 1, scoped_authors.size + assert_equal authors(:david).attributes, scoped_authors.first.attributes + end + + def test_scoped_find_merges_new_style_joins + scoped_authors = Author.with_scope(:find => { :joins => :posts }) do + Author.find(:all, :select => 'DISTINCT authors.*', :joins => :comments, :conditions => 'comments.id = 1') + end + assert scoped_authors.include?(authors(:david)) + assert !scoped_authors.include?(authors(:mary)) + assert_equal 1, scoped_authors.size + assert_equal authors(:david).attributes, scoped_authors.first.attributes + end + + def test_scoped_find_merges_new_and_old_style_joins + scoped_authors = Author.with_scope(:find => { :joins => :posts }) do + Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1') + end + assert scoped_authors.include?(authors(:david)) + assert !scoped_authors.include?(authors(:mary)) + assert_equal 1, scoped_authors.size + assert_equal authors(:david).attributes, scoped_authors.first.attributes + end + def test_scoped_count_include # with the include, will retrieve only developers for the given project Developer.with_scope(:find => { :include => :projects }) do @@ -152,7 +193,7 @@ class MethodScopingTest < ActiveRecord::TestCase end class NestedScopingTest < ActiveRecord::TestCase - fixtures :developers, :projects, :comments, :posts + fixtures :authors, :developers, :projects, :comments, :posts def test_merge_options Developer.with_scope(:find => { :conditions => 'salary = 80000' }) do @@ -357,6 +398,42 @@ class NestedScopingTest < ActiveRecord::TestCase assert_equal scoped_methods, Developer.instance_eval('current_scoped_methods') end end + + def test_nested_scoped_find_merges_old_style_joins + scoped_authors = Author.with_scope(:find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id' }) do + Author.with_scope(:find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do + Author.find(:all, :select => 'DISTINCT authors.*', :conditions => 'comments.id = 1') + end + end + assert scoped_authors.include?(authors(:david)) + assert !scoped_authors.include?(authors(:mary)) + assert_equal 1, scoped_authors.size + assert_equal authors(:david).attributes, scoped_authors.first.attributes + end + + def test_nested_scoped_find_merges_new_style_joins + scoped_authors = Author.with_scope(:find => { :joins => :posts }) do + Author.with_scope(:find => { :joins => :comments }) do + Author.find(:all, :select => 'DISTINCT authors.*', :conditions => 'comments.id = 1') + end + end + assert scoped_authors.include?(authors(:david)) + assert !scoped_authors.include?(authors(:mary)) + assert_equal 1, scoped_authors.size + assert_equal authors(:david).attributes, scoped_authors.first.attributes + end + + def test_nested_scoped_find_merges_new_and_old_style_joins + scoped_authors = Author.with_scope(:find => { :joins => :posts }) do + Author.with_scope(:find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do + Author.find(:all, :select => 'DISTINCT authors.*', :joins => '', :conditions => 'comments.id = 1') + end + end + assert scoped_authors.include?(authors(:david)) + assert !scoped_authors.include?(authors(:mary)) + assert_equal 1, scoped_authors.size + assert_equal authors(:david).attributes, scoped_authors.first.attributes + end end class HasManyScopingTest< ActiveRecord::TestCase diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 6f6ea1cbe9..444debd255 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -249,7 +249,26 @@ class NamedScopeTest < ActiveRecord::TestCase assert_equal Topic.base.select(&:approved), Topic.base.find_all(&:approved) end + def test_rand_should_select_a_random_object_from_proxy + assert Topic.approved.rand.is_a?(Topic) + end + def test_should_use_where_in_query_for_named_scope assert_equal Developer.find_all_by_name('Jamis'), Developer.find_all_by_id(Developer.jamises) end + + def test_size_should_use_count_when_results_are_not_loaded + topics = Topic.base + assert_queries(1) do + assert_sql(/COUNT/i) { topics.size } + end + end + + def test_size_should_use_length_when_results_are_loaded + topics = Topic.base + topics.reload # force load + assert_no_queries do + topics.size # use loaded (no query) + end + end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb new file mode 100644 index 0000000000..078ca1d679 --- /dev/null +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -0,0 +1,87 @@ +require "cases/helper" + +class PooledConnectionsTest < ActiveRecord::TestCase + def setup + super + @connection = ActiveRecord::Base.remove_connection + end + + def teardown + ActiveRecord::Base.clear_all_connections! + ActiveRecord::Base.establish_connection(@connection) + super + end + + def checkout_connections + ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :wait_timeout => 0.3})) + @connections = [] + @timed_out = 0 + + 4.times do + Thread.new do + begin + @connections << ActiveRecord::Base.connection_pool.checkout + rescue ActiveRecord::ConnectionTimeoutError + @timed_out += 1 + end + end.join + end + end + + def test_pooled_connection_checkout + checkout_connections + assert_equal @connections.length, 2 + assert_equal @timed_out, 2 + end + + def checkout_checkin_connections(pool_size, threads) + ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :wait_timeout => 0.5})) + @connection_count = 0 + @timed_out = 0 + threads.times do + Thread.new do + begin + conn = ActiveRecord::Base.connection_pool.checkout + sleep 0.1 + ActiveRecord::Base.connection_pool.checkin conn + @connection_count += 1 + rescue ActiveRecord::ConnectionTimeoutError + @timed_out += 1 + end + end.join + end + end + + def test_pooled_connection_checkin_one + checkout_checkin_connections 1, 2 + assert_equal 2, @connection_count + assert_equal 0, @timed_out + end + + def test_pooled_connection_checkin_two + checkout_checkin_connections 2, 3 + assert_equal 3, @connection_count + assert_equal 0, @timed_out + end + + def test_pooled_connection_checkout_existing_first + ActiveRecord::Base.establish_connection(@connection.merge({:pool => 1})) + conn_pool = ActiveRecord::Base.connection_pool + conn = conn_pool.checkout + conn_pool.checkin(conn) + conn = conn_pool.checkout + assert ActiveRecord::ConnectionAdapters::AbstractAdapter === conn + conn_pool.checkin(conn) + end +end unless %w(FrontBase).include? ActiveRecord::Base.connection.adapter_name + +class AllowConcurrencyDeprecatedTest < ActiveRecord::TestCase + def test_allow_concurrency_is_deprecated + assert_deprecated('ActiveRecord::Base.allow_concurrency') do + ActiveRecord::Base.allow_concurrency + end + assert_deprecated('ActiveRecord::Base.allow_concurrency=') do + ActiveRecord::Base.allow_concurrency = true + end + end +end diff --git a/activerecord/test/cases/threaded_connections_test.rb b/activerecord/test/cases/threaded_connections_test.rb deleted file mode 100644 index 28f8302367..0000000000 --- a/activerecord/test/cases/threaded_connections_test.rb +++ /dev/null @@ -1,48 +0,0 @@ -require "cases/helper" -require 'models/topic' -require 'models/reply' - -unless %w(FrontBase).include? ActiveRecord::Base.connection.adapter_name - class ThreadedConnectionsTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false - - fixtures :topics - - def setup - @connection = ActiveRecord::Base.remove_connection - @connections = [] - @allow_concurrency = ActiveRecord::Base.allow_concurrency - end - - def teardown - # clear the connection cache - ActiveRecord::Base.send(:clear_all_cached_connections!) - # set allow_concurrency to saved value - ActiveRecord::Base.allow_concurrency = @allow_concurrency - # reestablish old connection - ActiveRecord::Base.establish_connection(@connection) - end - - def gather_connections(use_threaded_connections) - ActiveRecord::Base.allow_concurrency = use_threaded_connections - ActiveRecord::Base.establish_connection(@connection) - - 5.times do - Thread.new do - Topic.find :first - @connections << ActiveRecord::Base.active_connections.values.first - end.join - end - end - - def test_threaded_connections - gather_connections(true) - assert_equal @connections.uniq.length, 5 - end - - def test_unthreaded_connections - gather_connections(false) - assert_equal @connections.uniq.length, 1 - end - end -end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index af3ee6ddba..8383ba58e9 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -283,17 +283,6 @@ end if current_adapter?(:PostgreSQLAdapter) class ConcurrentTransactionTest < TransactionTest - def setup - @allow_concurrency = ActiveRecord::Base.allow_concurrency - ActiveRecord::Base.allow_concurrency = true - super - end - - def teardown - super - ActiveRecord::Base.allow_concurrency = @allow_concurrency - end - # This will cause transactions to overlap and fail unless they are performed on # separate database connections. def test_transaction_per_thread diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb index 43592bcee3..090f347a20 100644 --- a/activerecord/test/cases/validations_i18n_test.rb +++ b/activerecord/test/cases/validations_i18n_test.rb @@ -675,6 +675,38 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase replied_topic.valid? assert_equal 'global message', replied_topic.errors.on(:replies) end + + def test_validations_with_message_symbol_must_translate + I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:custom_error => "I am a custom error"}}} + Topic.validates_presence_of :title, :message => :custom_error + @topic.title = nil + @topic.valid? + assert_equal "I am a custom error", @topic.errors.on(:title) + end + + def test_validates_with_message_symbol_must_translate_per_attribute + I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:custom_error => "I am a custom error"}}}}}} + Topic.validates_presence_of :title, :message => :custom_error + @topic.title = nil + @topic.valid? + assert_equal "I am a custom error", @topic.errors.on(:title) + end + + def test_validates_with_message_symbol_must_translate_per_model + I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:custom_error => "I am a custom error"}}}} + Topic.validates_presence_of :title, :message => :custom_error + @topic.title = nil + @topic.valid? + assert_equal "I am a custom error", @topic.errors.on(:title) + end + + def test_validates_with_message_string + Topic.validates_presence_of :title, :message => "I am a custom error" + @topic.title = nil + @topic.valid? + assert_equal "I am a custom error", @topic.errors.on(:title) + end + end class ActiveRecordValidationsGenerateMessageI18nTests < Test::Unit::TestCase @@ -855,4 +887,5 @@ class ActiveRecordValidationsGenerateMessageI18nTests < Test::Unit::TestCase def test_generate_message_even_with_default_message assert_equal "must be even", @topic.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10) end + end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index a40bda2533..4999d93a86 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -451,6 +451,18 @@ class ValidationsTest < ActiveRecord::TestCase t2.title = nil assert t2.valid?, "should validate with nil" assert t2.save, "should save with nil" + + with_kcode('UTF8') do + t_utf8 = Topic.new("title" => "Я тоже уникальный!") + assert t_utf8.save, "Should save t_utf8 as unique" + + # If database hasn't UTF-8 character set, this test fails + if Topic.find(t_utf8, :select => 'LOWER(title) AS title').title == "я тоже уникальный!" + t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!") + assert !t2_utf8.valid?, "Shouldn't be valid" + assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique" + end + end end def test_validate_case_sensitive_uniqueness |