diff options
author | Ryan Bigg <radarlistener@gmail.com> | 2008-10-01 21:38:16 +0930 |
---|---|---|
committer | Ryan Bigg <radarlistener@gmail.com> | 2008-10-01 21:38:16 +0930 |
commit | dbbd757edd896947e33fdf18ba870b8df5974d62 (patch) | |
tree | cc0428bee430a18200925acb830f3dc3946e31de /activerecord | |
parent | 6aca4458e3a1712a4a675ee7bf2cd35701ff75c9 (diff) | |
parent | 8292c7dfce4b893588860053e50ef60ae9a0609a (diff) | |
download | rails-dbbd757edd896947e33fdf18ba870b8df5974d62.tar.gz rails-dbbd757edd896947e33fdf18ba870b8df5974d62.tar.bz2 rails-dbbd757edd896947e33fdf18ba870b8df5974d62.zip |
Merge branch 'master' of git@github.com:lifo/docrails
Diffstat (limited to 'activerecord')
29 files changed, 463 insertions, 63 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 5bcc7ad3a9..6479cc5a9b 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,13 @@ *Edge* +* Add Model#delete instance method, similar to Model.delete class method. #1086 [Hongli Lai] + +* MySQL: cope with quirky default values for not-null text columns. #1043 [Frederick Cheung] + +* Multiparameter attributes skip time zone conversion for time-only columns [#1030 state:resolved] [Geoff Buesing] + +* Base.skip_time_zone_conversion_for_attributes uses class_inheritable_accessor, so that subclasses don't overwrite Base [#346 state:resolved] [miloops] + * Added find_last_by dynamic finder #762 [miloops] * Internal API: configurable association options and build_association method for reflections so plugins may extend and override. #985 [Hongli Lai] diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index a6bbd6fc82..219cd30f94 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -77,5 +77,5 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/schema_dumper' -I18n.load_translations File.dirname(__FILE__) + '/active_record/locale/en-US.yml' - +require 'active_record/i18n_interpolation_deprecation' +I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en-US.yml' diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 33457822ff..4d36216c34 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1026,7 +1026,7 @@ module ActiveRecord # Create the callbacks to update counter cache if options[:counter_cache] cache_column = options[:counter_cache] == true ? - "#{self.to_s.underscore.pluralize}_count" : + "#{self.to_s.demodulize.underscore.pluralize}_count" : options[:counter_cache] method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym @@ -1268,7 +1268,11 @@ module ActiveRecord if association_proxy_class == BelongsToAssociation define_method("#{reflection.primary_key_name}=") do |target_id| - instance_variable_get(ivar).reset if instance_variable_defined?(ivar) + if instance_variable_defined?(ivar) + if association = instance_variable_get(ivar) + association.reset + end + end write_attribute(reflection.primary_key_name, target_id) end end @@ -1424,15 +1428,23 @@ module ActiveRecord [] end + # Creates before_destroy callback methods that nullify, delete or destroy + # has_many associated objects, according to the defined :dependent rule. + # # See HasManyAssociation#delete_records. Dependent associations # delete children, otherwise foreign key is set to NULL. - def configure_dependency_for_has_many(reflection) + # + # The +extra_conditions+ parameter, which is not used within the main + # Active Record codebase, is meant to allow plugins to define extra + # finder conditions. + def configure_dependency_for_has_many(reflection, extra_conditions = nil) if reflection.options.include?(:dependent) # Add polymorphic type if the :as option is present dependent_conditions = [] dependent_conditions << "#{reflection.primary_key_name} = \#{record.quoted_id}" dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as] dependent_conditions << sanitize_sql(reflection.options[:conditions]) if reflection.options[:conditions] + dependent_conditions << extra_conditions if extra_conditions dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ") case reflection.options[:dependent] @@ -1443,9 +1455,24 @@ module ActiveRecord end before_destroy method_name when :delete_all - module_eval "before_destroy { |record| #{reflection.class_name}.delete_all(%(#{dependent_conditions})) }" + module_eval %Q{ + before_destroy do |record| + delete_all_has_many_dependencies(record, + "#{reflection.name}", + #{reflection.class_name}, + "#{dependent_conditions}") + end + } when :nullify - module_eval "before_destroy { |record| #{reflection.class_name}.update_all(%(#{reflection.primary_key_name} = NULL), %(#{dependent_conditions})) }" + module_eval %Q{ + before_destroy do |record| + nullify_has_many_dependencies(record, + "#{reflection.name}", + #{reflection.class_name}, + "#{reflection.primary_key_name}", + "#{dependent_conditions}") + end + } else raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, or :nullify (#{reflection.options[:dependent].inspect})" end @@ -1472,7 +1499,7 @@ module ActiveRecord # with foreign keys pointing to this object, and we only want # to delete the correct one, not all of them. association = send(reflection.name) - association.class.delete(association.id) unless association.nil? + association.delete unless association.nil? end before_destroy method_name when :nullify @@ -1502,7 +1529,7 @@ module ActiveRecord method_name = "belongs_to_dependent_delete_for_#{reflection.name}".to_sym define_method(method_name) do association = send(reflection.name) - association.class.delete(association.id) unless association.nil? + association.delete unless association.nil? end before_destroy method_name else @@ -1511,6 +1538,14 @@ module ActiveRecord end end + def delete_all_has_many_dependencies(record, reflection_name, association_class, dependent_conditions) + association_class.delete_all(dependent_conditions) + end + + def nullify_has_many_dependencies(record, reflection_name, association_class, primary_key_name, dependent_conditions) + association_class.update_all("#{primary_key_name} = NULL", dependent_conditions) + end + mattr_accessor :valid_keys_for_has_many_association @@valid_keys_for_has_many_association = [ :class_name, :table_name, :foreign_key, :primary_key, @@ -1757,12 +1792,12 @@ module ActiveRecord def create_extension_modules(association_id, block_extension, extensions) if block_extension - extension_module_name = "#{self.to_s}#{association_id.to_s.camelize}AssociationExtension" + extension_module_name = "#{self.to_s.demodulize}#{association_id.to_s.camelize}AssociationExtension" silence_warnings do - Object.const_set(extension_module_name, Module.new(&block_extension)) + self.parent.const_set(extension_module_name, Module.new(&block_extension)) end - Array(extensions).push(extension_module_name.constantize) + Array(extensions).push("#{self.parent}::#{extension_module_name}".constantize) else Array(extensions) end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 8de528f638..463de9d819 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -63,7 +63,7 @@ module ActiveRecord # Fetches the first one using SQL if possible. def first(*args) - if fetch_first_or_last_using_find? args + if fetch_first_or_last_using_find?(args) find(:first, *args) else load_target unless loaded? @@ -73,7 +73,7 @@ module ActiveRecord # Fetches the last one using SQL if possible. def last(*args) - if fetch_first_or_last_using_find? args + if fetch_first_or_last_using_find?(args) find(:last, *args) else load_target unless loaded? @@ -108,7 +108,7 @@ module ActiveRecord result = true load_target if @owner.new_record? - @owner.transaction do + transaction do flatten_deeper(records).each do |record| raise_on_type_mismatch(record) add_record_to_target_with_callbacks(record) do |r| @@ -123,6 +123,21 @@ module ActiveRecord alias_method :push, :<< alias_method :concat, :<< + # Starts a transaction in the association class's database connection. + # + # class Author < ActiveRecord::Base + # has_many :books + # end + # + # Author.find(:first).books.transaction do + # # same effect as calling Book.transaction + # end + def transaction(*args) + @reflection.klass.transaction(*args) do + yield + end + end + # Remove all records from this association def delete_all load_target @@ -173,7 +188,7 @@ module ActiveRecord records = flatten_deeper(records) records.each { |record| raise_on_type_mismatch(record) } - @owner.transaction do + transaction do records.each { |record| callback(:before_remove, record) } old_records = records.reject {|r| r.new_record? } @@ -200,7 +215,7 @@ module ActiveRecord end def destroy_all - @owner.transaction do + transaction do each { |record| record.destroy } end @@ -238,6 +253,8 @@ module ActiveRecord def size if @owner.new_record? || (loaded? && !@reflection.options[:uniq]) @target.size + elsif !loaded? && @reflection.options[:group] + load_target.size elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array) unsaved_records = @target.select { |r| r.new_record? } unsaved_records.size + count_records @@ -290,7 +307,7 @@ module ActiveRecord other = other_array.size < 100 ? other_array : other_array.to_set current = @target.size < 100 ? @target : @target.to_set - @owner.transaction do + transaction do delete(@target.select { |v| !other.include?(v) }) concat(other_array.select { |v| !current.include?(v) }) end @@ -418,7 +435,8 @@ module ActiveRecord end def fetch_first_or_last_using_find?(args) - args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] || !@target.blank? || args.first.kind_of?(Integer)) + args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] || + @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer)) end end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index dda22668c6..3b2f306637 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -17,7 +17,10 @@ module ActiveRecord # Returns the number of records in this collection. # # If the association has a counter cache it gets that value. Otherwise - # a count via SQL is performed, bounded to <tt>:limit</tt> if there's one. + # it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if + # there's one. Some configuration options like :group make it impossible + # to do a SQL count, in those cases the array count will be used. + # # That does not depend on whether the collection has already been loaded # or not. The +size+ method is the one that takes the loaded flag into # account and delegates to +count_records+ if needed. 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 ebd2bf768c..3171665e19 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -9,14 +9,14 @@ module ActiveRecord alias_method :new, :build def create!(attrs = nil) - @reflection.klass.transaction do + transaction do self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association! } : @reflection.create_association!) object end end def create(attrs = nil) - @reflection.klass.transaction do + transaction do self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association } : @reflection.create_association) object end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 0a1baff87d..e5486738f0 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -10,7 +10,7 @@ module ActiveRecord base.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT base.cattr_accessor :time_zone_aware_attributes, :instance_writer => false base.time_zone_aware_attributes = false - base.cattr_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false + base.class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false base.skip_time_zone_conversion_for_attributes = [] end @@ -232,6 +232,10 @@ module ActiveRecord def method_missing(method_id, *args, &block) method_name = method_id.to_s + if self.class.private_method_defined?(method_name) + raise NoMethodError("Attempt to call private method", method_name, args) + end + # If we haven't generated any methods yet, generate them, then # see if we've created the method we're looking for. if !self.class.generated_methods? @@ -334,10 +338,12 @@ module ActiveRecord # <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) + def respond_to?(method, include_private_methods = false) method_name = method.to_s if super return true + elsif self.private_methods.include?(method_name) && !include_private_methods + return false elsif !self.class.generated_methods? self.class.define_attribute_methods if self.class.generated_methods.include?(method_name) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index c0c9b8a9b3..ac15eed408 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1632,19 +1632,19 @@ 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}" + def merge_joins(*joins) + if joins.any?{|j| j.is_a?(String) || array_of_strings?(j) } + joins = joins.collect do |join| + join = [join] if join.is_a?(String) + unless array_of_strings?(join) + join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, join, nil) + join = join_dependency.join_associations.collect { |assoc| assoc.association_join } + end + join end + joins.flatten.uniq else - (safe_to_array(first) + safe_to_array(second)).uniq + joins.collect{|j| safe_to_array(j)}.flatten.uniq end end @@ -1660,6 +1660,10 @@ module ActiveRecord #:nodoc: end end + def array_of_strings?(o) + o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)} + end + def add_order!(sql, order, scope = :auto) scope = scope(:find) if :auto == scope scoped_order = scope[:order] if scope @@ -1708,8 +1712,12 @@ module ActiveRecord #:nodoc: 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} " + if array_of_strings?(merged_joins) + sql << merged_joins.join(' ') + " " + else + join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, merged_joins, nil) + sql << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} " + end when String sql << " #{merged_joins} " end @@ -2387,8 +2395,18 @@ module ActiveRecord #:nodoc: # Deletes the record in the database and freezes this instance to reflect that no changes should # be made (since they can't be persisted). # + # Unlike #destroy, this method doesn't run any +before_delete+ and +after_delete+ + # callbacks, nor will it enforce any association +:dependent+ rules. + # # In addition to deleting this record, any defined +before_delete+ and +after_delete+ # callbacks are run, and +:dependent+ rules defined on associations are run. + def delete + self.class.delete(id) unless new_record? + freeze + end + + # Deletes the record in the database and freezes this instance to reflect that no changes should + # be made (since they can't be persisted). def destroy unless new_record? connection.delete <<-end_sql, "#{self.class.name} Destroy" @@ -2828,7 +2846,7 @@ module ActiveRecord #:nodoc: end def instantiate_time_object(name, values) - if self.class.time_zone_aware_attributes && !self.class.skip_time_zone_conversion_for_attributes.include?(name.to_sym) + if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) Time.zone.local(*values) else Time.time_with_datetime_fallback(@@default_timezone, *values) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 22304edfc9..58992f91da 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -40,6 +40,10 @@ module ActiveRecord type == :integer || type == :float || type == :decimal end + def has_default? + !default.nil? + end + # Returns the Ruby class that corresponds to the abstract data type. def klass case type diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index c2a0fb72bf..a26fd02b90 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -80,7 +80,7 @@ module ActiveRecord def extract_default(default) if type == :binary || type == :text if default.blank? - nil + return null ? nil : '' else raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" end @@ -91,6 +91,11 @@ module ActiveRecord end end + def has_default? + return false if type == :binary || type == :text #mysql forbids defaults on blob and text columns + super + end + private def simplified_type(field_type) return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)") diff --git a/activerecord/lib/active_record/i18n_interpolation_deprecation.rb b/activerecord/lib/active_record/i18n_interpolation_deprecation.rb new file mode 100644 index 0000000000..cd634e1b8d --- /dev/null +++ b/activerecord/lib/active_record/i18n_interpolation_deprecation.rb @@ -0,0 +1,26 @@ +# Deprecates the use of the former message interpolation syntax in activerecord +# as in "must have %d characters". The new syntax uses explicit variable names +# as in "{{value}} must have {{count}} characters". + +require 'i18n/backend/simple' +module I18n + module Backend + class Simple + DEPRECATED_INTERPOLATORS = { '%d' => '{{count}}', '%s' => '{{value}}' } + + protected + def interpolate_with_deprecated_syntax(locale, string, values = {}) + return string unless string.is_a?(String) + + string = string.gsub(/%d|%s/) do |s| + instead = DEPRECATED_INTERPOLATORS[s] + ActiveSupport::Deprecation.warn "using #{s} in messages is deprecated; use #{instead} instead." + instead + end + + interpolate_without_deprecated_syntax(locale, string, values) + end + alias_method_chain :interpolate, :deprecated_syntax + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index b90ed88c6b..4f96e225c1 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -102,7 +102,7 @@ HEADER spec[:precision] = column.precision.inspect if !column.precision.nil? spec[:scale] = column.scale.inspect if !column.scale.nil? spec[:null] = 'false' if !column.null - spec[:default] = default_string(column.default) if !column.default.nil? + spec[:default] = default_string(column.default) if column.has_default? (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")} spec end.compact diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 8481706074..9220eae4d1 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -472,7 +472,7 @@ module ActiveRecord db_cols = begin column_names - rescue ActiveRecord::StatementInvalid + rescue Exception # To ignore both statement and connection errors [] end names = attr_names.reject { |name| db_cols.include?(name.to_s) } @@ -738,7 +738,7 @@ module ActiveRecord condition_params = [value] else condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}" - condition_params = [value.chars.downcase] + condition_params = [value.mb_chars.downcase] end if scope = configuration[:scope] diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index aaadef9979..2479b75789 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -1,7 +1,7 @@ module ActiveRecord module VERSION #:nodoc: MAJOR = 2 - MINOR = 1 + MINOR = 2 TINY = 0 STRING = [MAJOR, MINOR, TINY].join('.') diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 5c01c3c1f5..9390633d5b 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -3,6 +3,7 @@ require 'models/post' require 'models/comment' require 'models/project' require 'models/developer' +require 'models/company_in_module' class AssociationsExtensionsTest < ActiveRecord::TestCase fixtures :projects, :developers, :developers_projects, :comments, :posts @@ -44,4 +45,18 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase david = Marshal.load(Marshal.dump(david)) assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent end + + + def test_extension_name + extension = Proc.new {} + name = :association_name + + assert_equal 'DeveloperAssociationNameAssociationExtension', Developer.send(:create_extension_modules, name, extension, []).first.name + assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', +MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name + assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name + assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', MyApplication::Business::Developer.send(:create_extension_modules, name, extension, []).first.name + end + + end 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 9981f4c5d5..2949f1d304 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 @@ -253,7 +253,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert !devel.projects.loaded? assert_equal devel.projects.last, proj - assert devel.projects.loaded? + assert !devel.projects.loaded? assert !proj.new_record? assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated @@ -738,4 +738,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase # Array#count in Ruby >=1.8.7, which would raise an ArgumentError assert_nothing_raised { david.projects.count(:all, :conditions => '1=1') } end + + uses_mocha 'mocking Post.transaction' do + def test_association_proxy_transaction_method_starts_transaction_in_association_class + Post.expects(:transaction) + Category.find(:first).posts.transaction do + # nothing + end + end + 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 5bcbc5eb5b..315d77de07 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -249,7 +249,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_scoped_grouped + assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.size assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.length + assert_equal 2, companies(:first_firm).clients_grouped_by_name.size assert_equal 2, companies(:first_firm).clients_grouped_by_name.length end @@ -1007,6 +1009,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert firm.clients.loaded? end + def test_calling_first_or_last_on_existing_record_with_create_should_not_load_association + firm = companies(:first_firm) + firm.clients.create(:name => 'Foo') + assert !firm.clients.loaded? + + assert_queries 2 do + firm.clients.first + firm.clients.last + end + + assert !firm.clients.loaded? + end + def test_calling_first_or_last_on_new_record_should_not_run_queries firm = Firm.new @@ -1056,4 +1071,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase ActiveRecord::Base.store_full_sti_class = old end + uses_mocha 'mocking Comment.transaction' do + def test_association_proxy_transaction_method_starts_transaction_in_association_class + Comment.expects(:transaction) + Post.find(:first).comments.transaction do + # nothing + end + end + end + end 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 0be050ec81..12cce98c26 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -220,4 +220,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort assert !person.posts.loaded? end + + uses_mocha 'mocking Tag.transaction' do + def test_association_proxy_transaction_method_starts_transaction_in_association_class + Tag.expects(:transaction) + Post.find(:first).tags.transaction do + # nothing + end + end + end end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 2050d16179..cde451de0e 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -194,6 +194,14 @@ class AssociationProxyTest < ActiveRecord::TestCase assert_equal david, welcome.author end + def test_assigning_association_id_after_reload + welcome = posts(:welcome) + welcome.reload + assert_nothing_raised do + welcome.author_id = authors(:david).id + end + end + def test_reload_returns_assocition david = developers(:david) assert_nothing_raised do diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 7999e29264..160716f944 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/topic' +require 'models/minimalistic' class AttributeMethodsTest < ActiveRecord::TestCase fixtures :topics @@ -57,19 +58,19 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_kernel_methods_not_implemented_in_activerecord %w(test name display y).each do |method| - assert_equal false, ActiveRecord::Base.instance_method_already_implemented?(method), "##{method} is defined" + assert !ActiveRecord::Base.instance_method_already_implemented?(method), "##{method} is defined" end end def test_primary_key_implemented - assert_equal true, Class.new(ActiveRecord::Base).instance_method_already_implemented?('id') + assert Class.new(ActiveRecord::Base).instance_method_already_implemented?('id') end def test_defined_kernel_methods_implemented_in_model %w(test name display y).each do |method| klass = Class.new ActiveRecord::Base klass.class_eval "def #{method}() 'defined #{method}' end" - assert_equal true, klass.instance_method_already_implemented?(method), "##{method} is not defined" + assert klass.instance_method_already_implemented?(method), "##{method} is not defined" end end @@ -79,7 +80,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase abstract.class_eval "def #{method}() 'defined #{method}' end" abstract.abstract_class = true klass = Class.new abstract - assert_equal true, klass.instance_method_already_implemented?(method), "##{method} is not defined" + assert klass.instance_method_already_implemented?(method), "##{method} is not defined" end end @@ -219,6 +220,48 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_setting_time_zone_conversion_for_attributes_should_write_value_on_class_variable + Topic.skip_time_zone_conversion_for_attributes = [:field_a] + Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b] + + assert_equal [:field_a], Topic.skip_time_zone_conversion_for_attributes + assert_equal [:field_b], Minimalistic.skip_time_zone_conversion_for_attributes + end + + def test_read_attributes_respect_access_control + privatize("title") + + topic = @target.new(:title => "The pros and cons of programming naked.") + assert !topic.respond_to?(:title) + assert_raise(NoMethodError) { topic.title } + topic.send(:title) + end + + def test_write_attributes_respect_access_control + privatize("title=(value)") + + topic = @target.new + assert !topic.respond_to?(:title=) + assert_raise(NoMethodError) { topic.title = "Pants"} + topic.send(:title=, "Very large pants") + end + + def test_question_attributes_respect_access_control + privatize("title?") + + topic = @target.new(:title => "Isaac Newton's pants") + assert !topic.respond_to?(:title?) + assert_raise(NoMethodError) { topic.title? } + assert topic.send(:title?) + end + + def test_bulk_update_respects_access_control + privatize("title=(value)") + + assert_raise(ActiveRecord::UnknownAttributeError) { topic = @target.new(:title => "Rants about pants") } + assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } } + end + private def time_related_columns_on_topic Topic.columns.select{|c| [:time, :date, :datetime, :timestamp].include?(c.type)}.map(&:name) @@ -235,4 +278,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase Time.zone = old_zone ActiveRecord::Base.time_zone_aware_attributes = old_tz end + + def privatize(method_signature) + @target.class_eval <<-private_method + private + def #{method_signature} + "I'm private" + end + private_method + end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index bda6f346f0..d512834237 100644..100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -472,6 +472,18 @@ class BasicsTest < ActiveRecord::TestCase assert topic.instance_variable_get("@custom_approved") end + def test_delete + topic = Topic.find(1) + assert_equal topic, topic.delete, 'topic.delete did not return self' + assert topic.frozen?, 'topic not frozen after delete' + assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } + end + + def test_delete_doesnt_run_callbacks + Topic.find(1).delete + assert_not_nil Topic.find(2) + end + def test_destroy topic = Topic.find(1) assert_equal topic, topic.destroy, 'topic.destroy did not return self' @@ -820,6 +832,20 @@ class BasicsTest < ActiveRecord::TestCase assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ] end + def test_delete_new_record + client = Client.new + client.delete + assert client.frozen? + end + + def test_delete_record_with_associations + client = Client.find(3) + client.delete + assert client.frozen? + assert_kind_of Firm, client.firm + assert_raises(ActiveSupport::FrozenObjectError) { client.name = "something else" } + end + def test_destroy_new_record client = Client.new client.destroy @@ -1084,6 +1110,24 @@ class BasicsTest < ActiveRecord::TestCase Time.zone = nil Topic.skip_time_zone_conversion_for_attributes = [] end + + def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion + ActiveRecord::Base.time_zone_aware_attributes = true + ActiveRecord::Base.default_timezone = :utc + Time.zone = ActiveSupport::TimeZone[-28800] + attributes = { + "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1", + "bonus_time(4i)" => "16", "bonus_time(5i)" => "24" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time + assert topic.bonus_time.utc? + ensure + ActiveRecord::Base.time_zone_aware_attributes = false + ActiveRecord::Base.default_timezone = :local + Time.zone = nil + end def test_multiparameter_attributes_on_time_with_empty_seconds attributes = { @@ -1420,15 +1464,17 @@ class BasicsTest < ActiveRecord::TestCase if RUBY_VERSION < '1.9' def test_quote_chars - str = 'The Narrator' - topic = Topic.create(:author_name => str) - assert_equal str, topic.author_name + with_kcode('UTF8') do + str = 'The Narrator' + topic = Topic.create(:author_name => str) + assert_equal str, topic.author_name - assert_kind_of ActiveSupport::Multibyte::Chars, str.chars - topic = Topic.find_by_author_name(str.chars) + assert_kind_of ActiveSupport::Multibyte.proxy_class, str.mb_chars + topic = Topic.find_by_author_name(str.mb_chars) - assert_kind_of Topic, topic - assert_equal str, topic.author_name, "The right topic should have been found by name even with name passed as Chars" + assert_kind_of Topic, topic + assert_equal str, topic.author_name, "The right topic should have been found by name even with name passed as Chars" + end end end @@ -2021,4 +2067,18 @@ class BasicsTest < ActiveRecord::TestCase ensure ActiveRecord::Base.logger = original_logger end + + private + def with_kcode(kcode) + if RUBY_VERSION < '1.9' + orig_kcode, $KCODE = $KCODE, kcode + begin + yield + ensure + $KCODE = orig_kcode + end + else + yield + end + end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 3473b846a0..ee84cb8af8 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -19,6 +19,37 @@ class DefaultTest < ActiveRecord::TestCase end if current_adapter?(:MysqlAdapter) + + #MySQL 5 and higher is quirky with not null text/blob columns. + #With MySQL Text/blob columns cannot have defaults. If the column is not null MySQL will report that the column has a null default + #but it behaves as though the column had a default of '' + def test_mysql_text_not_null_defaults + klass = Class.new(ActiveRecord::Base) + klass.table_name = 'test_mysql_text_not_null_defaults' + klass.connection.create_table klass.table_name do |t| + t.column :non_null_text, :text, :null => false + t.column :non_null_blob, :blob, :null => false + t.column :null_text, :text, :null => true + t.column :null_blob, :blob, :null => true + end + assert_equal '', klass.columns_hash['non_null_blob'].default + assert_equal '', klass.columns_hash['non_null_text'].default + + assert_equal nil, klass.columns_hash['null_blob'].default + assert_equal nil, klass.columns_hash['null_text'].default + + assert_nothing_raised do + instance = klass.create! + assert_equal '', instance.non_null_text + assert_equal '', instance.non_null_blob + assert_nil instance.null_text + assert_nil instance.null_blob + end + ensure + klass.connection.drop_table(klass.table_name) rescue nil + end + + # MySQL uses an implicit default 0 rather than NULL unless in strict mode. # We use an implicit NULL so schema.rb is compatible with other databases. def test_mysql_integer_not_null_defaults diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index cbdff382fe..292b88edbc 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -465,8 +465,8 @@ class FinderTest < ActiveRecord::TestCase quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi") assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi".chars) - assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper".chars) + assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi".mb_chars) + assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper".mb_chars) end def test_bind_record @@ -935,6 +935,17 @@ class FinderTest < ActiveRecord::TestCase assert_equal 1, first.id end + def test_joins_with_string_array + person_with_reader_and_post = Post.find( + :all, + :joins => [ + "INNER JOIN categorizations ON categorizations.post_id = posts.id", + "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'" + ] + ) + assert_equal 1, person_with_reader_and_post.size + end + def test_find_by_id_with_conditions_with_or assert_nothing_raised do Post.find([1,2,3], diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index f30d58546e..f7bdac8013 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -46,3 +46,17 @@ end class << ActiveRecord::Base public :with_scope, :with_exclusive_scope end + +unless ENV['FIXTURE_DEBUG'] + module Test #:nodoc: + module Unit #:nodoc: + class << TestCase #:nodoc: + def try_to_load_dependency_with_silence(*args) + ActiveRecord::Base.logger.silence { try_to_load_dependency_without_silence(*args)} + end + + alias_method_chain :try_to_load_dependency, :silence + end + end + end +end
\ No newline at end of file diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index af6fcd32ad..ff10bfaf3e 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -138,6 +138,36 @@ class MethodScopingTest < ActiveRecord::TestCase assert_equal authors(:david).attributes, scoped_authors.first.attributes end + def test_scoped_find_merges_string_array_style_and_string_style_joins + scoped_authors = Author.with_scope(:find => { :joins => ["INNER JOIN posts ON posts.author_id = authors.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_string_array_style_and_hash_style_joins + scoped_authors = Author.with_scope(:find => { :joins => :posts}) 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_joins_and_eliminates_duplicate_string_joins + scoped_authors = Author.with_scope(:find => { :joins => 'INNER JOIN posts ON posts.author_id = authors.id'}) do + Author.find(:all, :select => 'DISTINCT authors.*', :joins => ["INNER JOIN posts ON posts.author_id = authors.id", "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_count_include # with the include, will retrieve only developers for the given project Developer.with_scope(:find => { :include => :projects }) do diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index c1a8da2270..ac44dd7ffe 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1133,7 +1133,11 @@ if ActiveRecord::Base.connection.supports_migrations? columns = Person.connection.columns(:binary_testings) data_column = columns.detect { |c| c.name == "data" } - assert_nil data_column.default + if current_adapter?(:MysqlAdapter) + assert_equal '', data_column.default + else + assert_nil data_column.default + end Person.connection.drop_table :binary_testings rescue nil end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 444debd255..64e899780c 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -271,4 +271,10 @@ class NamedScopeTest < ActiveRecord::TestCase topics.size # use loaded (no query) end end + + def test_chaining_with_duplicate_joins + join = "INNER JOIN comments ON comments.post_id = posts.id" + post = Post.find(1) + assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size + end end diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 0106572ced..817897ceac 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -8,18 +8,18 @@ class SanitizeTest < ActiveRecord::TestCase def test_sanitize_sql_array_handles_string_interpolation quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi") assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"]) - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi".chars]) + assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi".mb_chars]) quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper") assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper"]) - assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper".chars]) + assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper".mb_chars]) end def test_sanitize_sql_array_handles_bind_variables quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi"]) - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi".chars]) + assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi".mb_chars]) quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"]) - assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".chars]) + assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars]) end end diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb index e34890ef19..42246f18b6 100644 --- a/activerecord/test/cases/validations_i18n_test.rb +++ b/activerecord/test/cases/validations_i18n_test.rb @@ -6,12 +6,16 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase def setup reset_callbacks Topic @topic = Topic.new + @old_load_path, @old_backend = I18n.load_path, I18n.backend + I18n.load_path.clear + I18n.backend = I18n::Backend::Simple.new I18n.backend.store_translations('en-US', :activerecord => {:errors => {:messages => {:custom => nil}}}) end def teardown reset_callbacks Topic - I18n.load_translations File.dirname(__FILE__) + '/../../lib/active_record/locale/en-US.yml' + I18n.load_path.replace @old_load_path + I18n.backend = @old_backend end def unique_topic |