diff options
Diffstat (limited to 'activerecord')
20 files changed, 609 insertions, 293 deletions
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 2059d8acdf..9a6896dd55 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -69,12 +69,12 @@ module ActiveRecord::Associations::Builder def define_restrict_dependency_method name = self.name mixin.redefine_method(dependency_method_name) do - # has_many or has_one associations - if send(name).respond_to?(:exists?) ? send(name).exists? : !send(name).nil? + has_one_macro = association(name).reflection.macro == :has_one + if has_one_macro ? !send(name).nil? : send(name).exists? if dependent_restrict_raises? raise ActiveRecord::DeleteRestrictionError.new(name) else - key = association(name).reflection.macro == :has_one ? "one" : "many" + key = has_one_macro ? "one" : "many" errors.add(:base, :"restrict_dependent_destroy.#{key}", :record => self.class.human_attribute_name(name).downcase) return false diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 0b634ab944..30fc44b4c2 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -18,7 +18,7 @@ module ActiveRecord::Associations::Builder model.send(:include, Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 def destroy_associations - association(#{name.to_sym.inspect}).delete_all_on_destroy + association(#{name.to_sym.inspect}).delete_all super end RUBY diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 9ddfd433e4..d37d4e9d33 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -42,7 +42,7 @@ module ActiveRecord::Associations::Builder def define_delete_all_dependency_method name = self.name mixin.redefine_method(dependency_method_name) do - association(name).delete_all_on_destroy + association(name).delete_all end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 3af5ff3eab..4ec176e641 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -6,6 +6,15 @@ module ActiveRecord # ease the implementation of association proxies that represent # collections. See the class hierarchy in AssociationProxy. # + # CollectionAssociation: + # HasAndBelongsToManyAssociation => has_and_belongs_to_many + # HasManyAssociation => has_many + # HasManyThroughAssociation + ThroughAssociation => has_many :through + # + # CollectionAssociation class provides common methods to the collections + # defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with + # +:through association+ option. + # # You need to be careful with assumptions regarding the target: The proxy # does not fetch records from the database until it needs them, but new # ones created with +build+ are added to the target. So, the target may be @@ -115,8 +124,9 @@ module ActiveRecord create_record(attributes, options, true, &block) end - # Add +records+ to this association. Returns +self+ so method calls may be chained. - # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. + # Add +records+ to this association. Returns +self+ so method calls may + # be chained. Since << flattens its argument list and inserts each record, + # +push+ and +concat+ behave identically. def concat(*records) load_target if owner.new_record? @@ -142,23 +152,16 @@ module ActiveRecord end end - # Remove all records from this association + # Remove all records from this association. # # See delete for more info. def delete_all - delete(load_target).tap do + delete(:all).tap do reset loaded! end end - # Called when the association is declared as :dependent => :delete_all. This is - # an optimised version which avoids loading the records into memory. Not really - # for public consumption. - def delete_all_on_destroy - scoped.delete_all - end - # Destroy all the records from this association. # # See destroy for more info. @@ -169,7 +172,7 @@ module ActiveRecord end end - # Calculate sum using SQL, not Enumerable + # Calculate sum using SQL, not Enumerable. def sum(*args) if block_given? scoped.sum(*args) { |*block_args| yield(*block_args) } @@ -218,7 +221,17 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) - delete_or_destroy(records, options[:dependent]) + dependent = options[:dependent] + + if records.first == :all + if loaded? || dependent == :destroy + delete_or_destroy(load_target, dependent) + else + delete_records(:all, dependent) + end + else + delete_or_destroy(records, dependent) + end end # Destroy +records+ and remove them from this association calling @@ -267,13 +280,16 @@ module ActiveRecord load_target.size end - # Equivalent to <tt>collection.size.zero?</tt>. If the collection has - # not been already loaded and you are going to fetch the records anyway - # it is better to check <tt>collection.length.zero?</tt>. + # Returns true if the collection is empty. Equivalent to + # <tt>collection.size.zero?</tt>. If the collection has not been already + # loaded and you are going to fetch the records anyway it is better to + # check <tt>collection.length.zero?</tt>. def empty? size.zero? end + # Returns true if the collections is not empty. + # Equivalent to +!collection.empty?+. def any? if block_given? load_target.any? { |*block_args| yield(*block_args) } @@ -282,7 +298,8 @@ module ActiveRecord end end - # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1. + # Returns true if the collection has more than 1 record. + # Equivalent to +collection.size > 1+. def many? if block_given? load_target.many? { |*block_args| yield(*block_args) } @@ -298,8 +315,8 @@ module ActiveRecord end end - # Replace this collection with +other_array+ - # This will perform a diff and delete/add only records that have changed. + # Replace this collection with +other_array+. This will perform a diff + # and delete/add only records that have changed. def replace(other_array) other_array.each { |val| raise_on_type_mismatch(val) } original_target = load_target.dup @@ -473,7 +490,7 @@ module ActiveRecord "new records could not be saved." end - new_target + target end def concat_records(records) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index cf4cc98f38..fa316a8c9d 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -33,9 +33,231 @@ module ActiveRecord # # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. - class CollectionProxy < Relation # :nodoc: + class CollectionProxy < Relation delegate :target, :load_target, :loaded?, :to => :@association + ## + # :method: first + # Returns the first record, or the first +n+ records, from the collection. + # If the collection is empty, the first form returns nil, and the second + # form returns an empty array. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.first # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.first(2) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1> + # # ] + # + # another_person_without.pets # => [] + # another_person_without.pets.first # => nil + # another_person_without.pets.first(3) # => [] + + ## + # :method: last + # Returns the last record, or the last +n+ records, from the collection. + # If the collection is empty, the first form returns nil, and the second + # form returns an empty array. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.last # => #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # + # person.pets.last(2) + # # => [ + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # another_person_without.pets # => [] + # another_person_without.pets.last # => nil + # another_person_without.pets.last(3) # => [] + + ## + # :method: concat + # Add one or more records to the collection by setting their foreign keys + # to the association's primary key. Since << flattens its argument list and + # inserts each record, +push+ and +concat+ behave identically. Returns +self+ + # so method calls may be chained. + # + # class Person < ActiveRecord::Base + # pets :has_many + # end + # + # person.pets.size # => 0 + # person.pets.concat(Pet.new(name: 'Fancy-Fancy')) + # person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')) + # person.pets.size # => 3 + # + # person.id # => 1 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')]) + # person.pets.size # => 5 + + ## + # :method: replace + # Replace this collection with +other_array+. This will perform a diff + # and delete/add only records that have changed. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [#<Pet id: 1, name: "Gorby", group: "cats", person_id: 1>] + # + # other_pets = [Pet.new(name: 'Puff', group: 'celebrities'] + # + # person.pets.replace(other_pets) + # + # person.pets + # # => [#<Pet id: 2, name: "Puff", group: "celebrities", person_id: 1>] + # + # If the supplied array has an incorrect association type, it raises + # an <tt>ActiveRecord::AssociationTypeMismatch</tt> error: + # + # person.pets.replace(["doo", "ggie", "gaga"]) + # # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String + + ## + # :method: destroy_all + # Destroy all the records from this association. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 3 + # + # person.pets.destroy_all + # + # person.pets.size # => 0 + # person.pets # => [] + + ## + # :method: empty? + # Returns true if the collection is empty. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count # => 1 + # person.pets.empty? # => false + # + # person.pets.delete_all + # + # person.pets.count # => 0 + # person.pets.empty? # => true + + ## + # :method: any? + # Returns true if the collection is not empty. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count # => 0 + # person.pets.any? # => false + # + # person.pets << Pet.new(name: 'Snoop') + # person.pets.count # => 0 + # person.pets.any? # => true + # + # You can also pass a block to define criteria. The behaviour + # is the same, it returns true if the collection based on the + # criteria is not empty. + # + # person.pets + # # => [#<Pet name: "Snoop", group: "dogs">] + # + # person.pets.any? do |pet| + # pet.group == 'cats' + # end + # # => false + # + # person.pets.any? do |pet| + # pet.group == 'dogs' + # end + # # => true + + ## + # :method: many? + # Returns true if the collection has more than one record. + # Equivalent to +collection.size > 1+. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.count #=> 1 + # person.pets.many? #=> false + # + # person.pets << Pet.new(name: 'Snoopy') + # person.pets.count #=> 2 + # person.pets.many? #=> true + # + # You can also pass a block to define criteria. The + # behaviour is the same, it returns true if the collection + # based on the criteria has more than one record. + # + # person.pets + # # => [ + # # #<Pet name: "Gorby", group: "cats">, + # # #<Pet name: "Puff", group: "cats">, + # # #<Pet name: "Snoop", group: "dogs"> + # # ] + # + # person.pets.many? do |pet| + # pet.group == 'dogs' + # end + # # => false + # + # person.pets.many? do |pet| + # pet.group == 'cats' + # end + # # => true + + ## + # :method: include? + # Returns true if the given object is present in the collection. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets # => [#<Pet id: 20, name: "Snoop">] + # + # person.pets.include?(Pet.find(20)) # => true + # person.pets.include?(Pet.find(21)) # => false delegate :select, :find, :first, :last, :build, :create, :create!, :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq, @@ -84,11 +306,57 @@ module ActiveRecord end alias_method :to_a, :to_ary + # Adds one or more +records+ to the collection by setting their foreign keys + # to the association‘s primary key. Returns +self+, so several appends may be + # chained together. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets.size # => 0 + # person.pets << Pet.new(name: 'Fancy-Fancy') + # person.pets << [Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')] + # person.pets.size # => 3 + # + # person.id # => 1 + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] def <<(*records) proxy_association.concat(records) && self end alias_method :push, :<< + # Removes every object from the collection. This does not destroy + # the objects, it sets their foreign keys to +NULL+. Returns +self+ + # so methods can be chained. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # person.pets.clear # => [] + # person.pets.size # => 0 + # + # Pet.find(1) # => #<Pet id: 1, name: "Snoop", group: "dogs", person_id: nil> + # + # If they are associated with +dependent: :destroy+ option, it deletes + # them directly from the database. + # + # class Person < ActiveRecord::Base + # has_many :pets, dependent: :destroy + # end + # + # person.pets # => [#<Pet id: 2, name: "Gorby", group: "cats", person_id: 2>] + # person.pets.clear # => [] + # person.pets.size # => 0 + # + # Pet.find(2) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=2 def clear delete_all self 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 a4cea99372..58d041ec1d 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 @@ -32,10 +32,6 @@ module ActiveRecord record end - # ActiveRecord::Relation#delete_all needs to support joins before we can use a - # SQL-only implementation. - alias delete_all_on_destroy delete_all - private def count_records @@ -44,13 +40,20 @@ module ActiveRecord def delete_records(records, method) if sql = options[:delete_sql] + records = load_target if records == :all records.each { |record| owner.connection.delete(interpolate(sql, record)) } else - relation = join_table - stmt = relation.where(relation[reflection.foreign_key].eq(owner.id). - and(relation[reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) - ).compile_delete - owner.connection.delete stmt + relation = join_table + condition = relation[reflection.foreign_key].eq(owner.id) + + unless records == :all + condition = condition.and( + relation[reflection.association_foreign_key] + .in(records.map { |x| x.id }.compact) + ) + end + + owner.connection.delete(relation.where(condition).compile_delete) 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 059e6c77bc..e631579087 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -89,8 +89,12 @@ module ActiveRecord records.each { |r| r.destroy } update_counter(-records.length) unless inverse_updates_counter_cache? else - keys = records.map { |r| r[reflection.association_primary_key] } - scope = scoped.where(reflection.association_primary_key => keys) + if records == :all + scope = scoped + else + keys = records.map { |r| r[reflection.association_primary_key] } + scope = scoped.where(reflection.association_primary_key => keys) + end if method == :delete_all update_counter(-scope.delete_all) 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 53d49fef2e..2683aaf5da 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -54,10 +54,6 @@ module ActiveRecord record end - # ActiveRecord::Relation#delete_all needs to support joins before we can use a - # SQL-only implementation. - alias delete_all_on_destroy delete_all - private def through_association @@ -126,7 +122,12 @@ module ActiveRecord def delete_records(records, method) ensure_not_nested - scope = through_association.scoped.where(construct_join_attributes(*records)) + # This is unoptimised; it will load all the target records + # even when we just want to delete everything. + records = load_target if records == :all + + scope = through_association.scoped + scope.where! construct_join_attributes(*records) case method when :destroy diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 46c7fc71ac..c6699737b4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -139,14 +139,18 @@ module ActiveRecord # #connection can be called any number of times; the connection is # held in a hash keyed by the thread id. def connection - @reserved_connections[current_connection_id] ||= checkout + synchronize do + @reserved_connections[current_connection_id] ||= checkout + end end # Is there an open connection that is being used for the current thread? def active_connection? - @reserved_connections.fetch(current_connection_id) { - return false - }.in_use? + synchronize do + @reserved_connections.fetch(current_connection_id) { + return false + }.in_use? + end end # Signal that the thread is finished with the current connection. diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 14bc95abfe..15c3d7be36 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1336,11 +1336,15 @@ module ActiveRecord @connection.server_version end + # See http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html + FOREIGN_KEY_VIOLATION = "23503" + UNIQUE_VIOLATION = "23505" + def translate_exception(exception, message) - case exception.message - when /duplicate key value violates unique constraint/ + case exception.result.error_field(PGresult::PG_DIAG_SQLSTATE) + when UNIQUE_VIOLATION RecordNotUnique.new(message, exception) - when /violates foreign key constraint/ + when FOREIGN_KEY_VIOLATION InvalidForeignKey.new(message, exception) else super diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index b2ed606e5f..f2833fbf3c 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -127,7 +127,7 @@ module ActiveRecord object.is_a?(self) end - # Returns an instance of <tt>Arel::Table</tt> loaded with the curent table name. + # Returns an instance of <tt>Arel::Table</tt> loaded with the current table name. # # class Post < ActiveRecord::Base # scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0)) diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 2c42f4cca5..23c272ef12 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -39,7 +39,7 @@ module ActiveRecord when new_record? "#{self.class.model_name.cache_key}/new" when timestamp = self[:updated_at] - timestamp = timestamp.utc.to_s(:number) + timestamp = timestamp.utc.to_s(:nsec) "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" else "#{self.class.model_name.cache_key}/#{id}" diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 779e052e3c..05ced3299b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -370,17 +370,12 @@ module ActiveRecord end end - # Deletes the records matching +conditions+ without instantiating the records first, and hence not - # calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that - # goes straight to the database, much more efficient than +destroy_all+. Be careful with relations - # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns - # the number of rows affected. - # - # ==== Parameters - # - # * +conditions+ - Conditions are specified the same way as with +find+ method. - # - # ==== Example + # Deletes the records matching +conditions+ without instantiating the records + # first, and hence not calling the +destroy+ method nor invoking callbacks. This + # is a single SQL DELETE statement that goes straight to the database, much more + # efficient than +destroy_all+. Be careful with relations though, in particular + # <tt>:dependent</tt> rules defined on associations are not honored. Returns the + # number of rows affected. # # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')") # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else']) @@ -389,6 +384,11 @@ module ActiveRecord # Both calls delete the affected posts all at once with a single DELETE statement. # If you need to destroy dependent associations or call your <tt>before_*</tt> or # +after_destroy+ callbacks, use the +destroy_all+ method instead. + # + # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error: + # + # Post.limit(100).delete_all + # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope def delete_all(conditions = nil) raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index b9b5ec7956..b1a0f83b5f 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -12,10 +12,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration def up <% attributes.each do |attribute| -%> <%- if migration_action -%> - <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'add' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %> - <%- if attribute.has_index? && migration_action == 'add' -%> - add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> - <%- end -%> + <%= migration_action %>_column :<%= table_name %>, :<%= attribute.name %> <%- end -%> <%- end -%> end @@ -23,8 +20,8 @@ class <%= migration_class_name %> < ActiveRecord::Migration def down <% attributes.reverse.each do |attribute| -%> <%- if migration_action -%> - <%= migration_action == 'add' ? 'remove' : 'add' %>_column :<%= table_name %>, :<%= attribute.name %><% if migration_action == 'remove' %>, :<%= attribute.type %><%= attribute.inject_options %><% end %> - <%- if attribute.has_index? && migration_action == 'remove' -%> + add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> + <%- if attribute.has_index? -%> add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> <%- 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 0e361456f0..ed1caa2ef5 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 @@ -380,6 +380,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, active_record.developers_by_sql(true).size end + def test_deleting_all_with_sql + project = Project.find(1) + project.developers_by_sql.delete_all + assert_equal 0, project.developers_by_sql.size + end + def test_deleting_all david = Developer.find(1) david.projects.reload diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 5bd7ed5a1b..8b384c2513 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1572,14 +1572,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [bulb2], car.reload.bulbs end - def test_replace_returns_new_target + def test_replace_returns_target car = Car.create(:name => 'honda') bulb1 = car.bulbs.create bulb2 = car.bulbs.create bulb3 = Bulb.create assert_equal [bulb1, bulb2], car.bulbs - result = car.bulbs.replace([bulb1, bulb3]) + result = car.bulbs.replace([bulb3, bulb1]) assert_equal [bulb1, bulb3], car.bulbs assert_equal [bulb1, bulb3], result end @@ -1614,4 +1614,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [client], firm.clients_of_firm assert_equal [client], firm.reload.clients_of_firm end + + test "delete_all, when not loaded, doesn't load the records" do + post = posts(:welcome) + + assert post.taggings_with_delete_all.count > 0 + assert !post.taggings_with_delete_all.loaded? + + # 2 queries: one DELETE and another to update the counter cache + assert_queries(2) do + post.taggings_with_delete_all.delete_all + end + end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index a7c1881561..619fb881fa 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1921,7 +1921,7 @@ class BasicsTest < ActiveRecord::TestCase def test_cache_key_format_for_existing_record_with_updated_at dev = Developer.first - assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:number)}", dev.cache_key + assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key end def test_cache_key_format_for_existing_record_with_nil_updated_at diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 20279f814b..37fa13f771 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -2,6 +2,7 @@ require File.expand_path('../../../../load_paths', __FILE__) require 'config' +gem 'minitest' require 'minitest/autorun' require 'stringio' require 'mocha' diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb new file mode 100644 index 0000000000..063209389f --- /dev/null +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -0,0 +1,221 @@ +require "cases/migration/helper" + +module ActiveRecord + class Migration + class TableTest < ActiveRecord::TestCase + class MockConnection < MiniTest::Mock + def native_database_types + { + :string => 'varchar(255)', + :integer => 'integer', + } + end + + def type_to_sql(type, limit, precision, scale) + native_database_types[type] + end + end + + def setup + @connection = MockConnection.new + end + + def teardown + assert @connection.verify + end + + def with_change_table + yield ConnectionAdapters::Table.new(:delete_me, @connection) + end + + def test_references_column_type_adds_id + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, 'customer_id', :integer, {}] + t.references :customer + end + end + + def test_remove_references_column_type_removes_id + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, 'customer_id'] + t.remove_references :customer + end + end + + def test_add_belongs_to_works_like_add_references + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, 'customer_id', :integer, {}] + t.belongs_to :customer + end + end + + def test_remove_belongs_to_works_like_remove_references + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, 'customer_id'] + t.remove_belongs_to :customer + end + end + + def test_references_column_type_with_polymorphic_adds_type + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, 'taggable_id', :integer, {}] + @connection.expect :add_column, nil, [:delete_me, 'taggable_type', :string, {}] + t.references :taggable, :polymorphic => true + end + end + + def test_remove_references_column_type_with_polymorphic_removes_type + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, 'taggable_id'] + @connection.expect :remove_column, nil, [:delete_me, 'taggable_type'] + t.remove_references :taggable, :polymorphic => true + end + end + + def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, 'taggable_id', :integer, {:null => false}] + @connection.expect :add_column, nil, [:delete_me, 'taggable_type', :string, {:null => false}] + t.references :taggable, :polymorphic => true, :null => false + end + end + + def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, 'taggable_id'] + @connection.expect :remove_column, nil, [:delete_me, 'taggable_type'] + t.remove_references :taggable, :polymorphic => true, :null => false + end + end + + def test_timestamps_creates_updated_at_and_created_at + with_change_table do |t| + @connection.expect :add_timestamps, nil, [:delete_me] + t.timestamps + end + end + + def test_remove_timestamps_creates_updated_at_and_created_at + with_change_table do |t| + @connection.expect :remove_timestamps, nil, [:delete_me] + t.remove_timestamps + end + end + + def string_column + @connection.native_database_types[:string] + end + + def integer_column + @connection.native_database_types[:integer] + end + + def test_integer_creates_integer_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, integer_column, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, integer_column, {}] + t.integer :foo, :bar + end + end + + def test_string_creates_string_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, string_column, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, string_column, {}] + t.string :foo, :bar + end + end + + def test_column_creates_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}] + t.column :bar, :integer + end + end + + def test_column_creates_column_with_options + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {:null => false}] + t.column :bar, :integer, :null => false + end + end + + def test_index_creates_index + with_change_table do |t| + @connection.expect :add_index, nil, [:delete_me, :bar, {}] + t.index :bar + end + end + + def test_index_creates_index_with_options + with_change_table do |t| + @connection.expect :add_index, nil, [:delete_me, :bar, {:unique => true}] + t.index :bar, :unique => true + end + end + + def test_index_exists + with_change_table do |t| + @connection.expect :index_exists?, nil, [:delete_me, :bar, {}] + t.index_exists?(:bar) + end + end + + def test_index_exists_with_options + with_change_table do |t| + @connection.expect :index_exists?, nil, [:delete_me, :bar, {:unique => true}] + t.index_exists?(:bar, :unique => true) + end + end + + def test_change_changes_column + with_change_table do |t| + @connection.expect :change_column, nil, [:delete_me, :bar, :string, {}] + t.change :bar, :string + end + end + + def test_change_changes_column_with_options + with_change_table do |t| + @connection.expect :change_column, nil, [:delete_me, :bar, :string, {:null => true}] + t.change :bar, :string, :null => true + end + end + + def test_change_default_changes_column + with_change_table do |t| + @connection.expect :change_column_default, nil, [:delete_me, :bar, :string] + t.change_default :bar, :string + end + end + + def test_remove_drops_single_column + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, :bar] + t.remove :bar + end + end + + def test_remove_drops_multiple_columns + with_change_table do |t| + @connection.expect :remove_column, nil, [:delete_me, :bar, :baz] + t.remove :bar, :baz + end + end + + def test_remove_index_removes_index_with_options + with_change_table do |t| + @connection.expect :remove_index, nil, [:delete_me, {:unique => true}] + t.remove_index :unique => true + end + end + + def test_rename_renames_column + with_change_table do |t| + @connection.expect :rename_column, nil, [:delete_me, :bar, :baz] + t.rename :bar, :baz + end + end + end + end +end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index cad93936c9..5d1bad0d54 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -421,228 +421,6 @@ class ReservedWordsMigrationTest < ActiveRecord::TestCase end end - -class ChangeTableMigrationsTest < ActiveRecord::TestCase - def setup - @connection = Person.connection - @connection.stubs(:add_index) - @connection.create_table :delete_me, :force => true do |t| - end - end - - def teardown - Person.connection.drop_table :delete_me rescue nil - end - - def test_references_column_type_adds_id - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {}) - t.references :customer - end - end - - def test_remove_references_column_type_removes_id - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'customer_id') - t.remove_references :customer - end - end - - def test_add_belongs_to_works_like_add_references - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {}) - t.belongs_to :customer - end - end - - def test_remove_belongs_to_works_like_remove_references - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'customer_id') - t.remove_belongs_to :customer - end - end - - def test_references_column_type_with_polymorphic_adds_type - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {}) - @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {}) - t.references :taggable, :polymorphic => true - end - end - - def test_remove_references_column_type_with_polymorphic_removes_type - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'taggable_type') - @connection.expects(:remove_column).with(:delete_me, 'taggable_id') - t.remove_references :taggable, :polymorphic => true - end - end - - def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {:null => false}) - @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {:null => false}) - t.references :taggable, :polymorphic => true, :null => false - end - end - - def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, 'taggable_type') - @connection.expects(:remove_column).with(:delete_me, 'taggable_id') - t.remove_references :taggable, :polymorphic => true, :null => false - end - end - - def test_timestamps_creates_updated_at_and_created_at - with_change_table do |t| - @connection.expects(:add_timestamps).with(:delete_me) - t.timestamps - end - end - - def test_remove_timestamps_creates_updated_at_and_created_at - with_change_table do |t| - @connection.expects(:remove_timestamps).with(:delete_me) - t.remove_timestamps - end - end - - def string_column - if current_adapter?(:PostgreSQLAdapter) - "character varying(255)" - elsif current_adapter?(:OracleAdapter) - 'VARCHAR2(255)' - else - 'varchar(255)' - end - end - - def integer_column - if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) - 'int(11)' - elsif current_adapter?(:OracleAdapter) - 'NUMBER(38)' - else - 'integer' - end - end - - def test_integer_creates_integer_column - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :foo, integer_column, {}) - @connection.expects(:add_column).with(:delete_me, :bar, integer_column, {}) - t.integer :foo, :bar - end - end - - def test_string_creates_string_column - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :foo, string_column, {}) - @connection.expects(:add_column).with(:delete_me, :bar, string_column, {}) - t.string :foo, :bar - end - end - - def test_column_creates_column - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :bar, :integer, {}) - t.column :bar, :integer - end - end - - def test_column_creates_column_with_options - with_change_table do |t| - @connection.expects(:add_column).with(:delete_me, :bar, :integer, {:null => false}) - t.column :bar, :integer, :null => false - end - end - - def test_index_creates_index - with_change_table do |t| - @connection.expects(:add_index).with(:delete_me, :bar, {}) - t.index :bar - end - end - - def test_index_creates_index_with_options - with_change_table do |t| - @connection.expects(:add_index).with(:delete_me, :bar, {:unique => true}) - t.index :bar, :unique => true - end - end - - def test_index_exists - with_change_table do |t| - @connection.expects(:index_exists?).with(:delete_me, :bar, {}) - t.index_exists?(:bar) - end - end - - def test_index_exists_with_options - with_change_table do |t| - @connection.expects(:index_exists?).with(:delete_me, :bar, {:unique => true}) - t.index_exists?(:bar, :unique => true) - end - end - - def test_change_changes_column - with_change_table do |t| - @connection.expects(:change_column).with(:delete_me, :bar, :string, {}) - t.change :bar, :string - end - end - - def test_change_changes_column_with_options - with_change_table do |t| - @connection.expects(:change_column).with(:delete_me, :bar, :string, {:null => true}) - t.change :bar, :string, :null => true - end - end - - def test_change_default_changes_column - with_change_table do |t| - @connection.expects(:change_column_default).with(:delete_me, :bar, :string) - t.change_default :bar, :string - end - end - - def test_remove_drops_single_column - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, :bar) - t.remove :bar - end - end - - def test_remove_drops_multiple_columns - with_change_table do |t| - @connection.expects(:remove_column).with(:delete_me, :bar, :baz) - t.remove :bar, :baz - end - end - - def test_remove_index_removes_index_with_options - with_change_table do |t| - @connection.expects(:remove_index).with(:delete_me, {:unique => true}) - t.remove_index :unique => true - end - end - - def test_rename_renames_column - with_change_table do |t| - @connection.expects(:rename_column).with(:delete_me, :bar, :baz) - t.rename :bar, :baz - end - end - - protected - def with_change_table - Person.connection.change_table :delete_me do |t| - yield t - end - end -end - if ActiveRecord::Base.connection.supports_bulk_alter? class BulkAlterTableMigrationsTest < ActiveRecord::TestCase def setup |