diff options
Diffstat (limited to 'activerecord/lib/active_record')
38 files changed, 807 insertions, 363 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index a4db627535..f0b549e5ad 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -71,7 +71,7 @@ module ActiveRecord # Now it's possible to access attributes from the database through the value objects instead. If # you choose to name the composition the same as the attribute's name, it will be the only way to # access that attribute. That's the case with our +balance+ attribute. You interact with the value - # objects just like you would any other attribute, though: + # objects just like you would with any other attribute: # # customer.balance = Money.new(20) # sets the Money value object and the attribute # customer.balance # => Money value object @@ -193,7 +193,8 @@ module ActiveRecord # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt> # or a Proc that is called when a new value is assigned to the value object. The converter is # passed the single value that is used in the assignment and is only called if the new value is - # not an instance of <tt>:class_name</tt>. + # not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter + # can return nil to skip the assignment. # # Option examples: # composed_of :temperature, :mapping => %w(reading celsius) @@ -241,16 +242,15 @@ module ActiveRecord def writer_method(name, class_name, mapping, allow_nil, converter) define_method("#{name}=") do |part| + klass = class_name.constantize + unless part.is_a?(klass) || converter.nil? || part.nil? + part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part) + end + if part.nil? && allow_nil mapping.each { |pair| self[pair.first] = nil } @aggregation_cache[name] = nil else - unless part.is_a?(class_name.constantize) || converter.nil? - part = converter.respond_to?(:call) ? - converter.call(part) : - class_name.constantize.send(converter, part) - end - mapping.each { |pair| self[pair.first] = part.send(pair.last) } @aggregation_cache[name] = part.freeze end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index c30e8e08b8..68f8bbeb1c 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -543,7 +543,7 @@ module ActiveRecord # end # # @group = Group.first - # @group.users.collect { |u| u.avatar }.flatten # select all avatars for all users in the group + # @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group # @group.avatars # selects all avatars by going through the User join model. # # An important caveat with going through +has_one+ or +has_many+ associations on the 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 14aa557b6c..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 @@ -16,12 +25,6 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class CollectionAssociation < Association #:nodoc: - attr_reader :proxy - - def initialize(owner, reflection) - super - @proxy = CollectionProxy.new(self) - end # Implements the reader method, e.g. foo.items for Foo.has_many :items def reader(force_reload = false) @@ -31,7 +34,7 @@ module ActiveRecord reload end - proxy + CollectionProxy.new(self) end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -121,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? @@ -148,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. @@ -175,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) } @@ -224,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 @@ -248,8 +255,12 @@ module ActiveRecord # This method is abstract in the sense that it relies on # +count_records+, which is a method descendants have to provide. def size - if !find_target? || (loaded? && !options[:uniq]) - target.size + if !find_target? || loaded? + if options[:uniq] + target.uniq.size + else + target.size + end elsif !loaded? && options[:group] load_target.size elsif !loaded? && !options[:uniq] && target.is_a?(Array) @@ -269,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) } @@ -284,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) } @@ -300,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 @@ -474,6 +489,8 @@ module ActiveRecord raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \ "new records could not be saved." end + + 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 261a829281..fa316a8c9d 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -33,16 +33,231 @@ module ActiveRecord # # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. - class CollectionProxy # :nodoc: - alias :proxy_extend :extend + class CollectionProxy < Relation + delegate :target, :load_target, :loaded?, :to => :@association - instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } + ## + # :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) # => [] - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, - :lock, :readonly, :having, :pluck, :to => :scoped + ## + # :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) # => [] - delegate :target, :load_target, :loaded?, :to => :@association + ## + # :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, @@ -52,7 +267,8 @@ module ActiveRecord def initialize(association) @association = association - Array(association.options[:extend]).each { |ext| proxy_extend(ext) } + super association.klass, association.klass.arel_table + merge! association.scoped end alias_method :new, :build @@ -61,54 +277,28 @@ module ActiveRecord @association end - def scoped(options = nil) - association = @association - scope = association.scoped - - scope.extending! do - define_method(:proxy_association) { association } - end - scope.merge!(options) if options - scope + # We don't want this object to be put on the scoping stack, because + # that could create an infinite loop where we call an @association + # method, which gets the current scope, which is this object, which + # delegates to @association, and so on. + def scoping + @association.scoped.scoping { yield } end - def respond_to?(name, include_private = false) - super || - (load_target && target.respond_to?(name, include_private)) || - proxy_association.klass.respond_to?(name, include_private) + def spawn + scoped end - def method_missing(method, *args, &block) - match = DynamicMatchers::Method.match(self, method) - if match && match.is_a?(DynamicMatchers::Instantiator) - scoped.send(method, *args) do |r| - proxy_association.send :set_owner_attributes, r - proxy_association.send :add_to_target, r - yield(r) if block_given? - end - - elsif target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method)) - if load_target - if target.respond_to?(method) - target.send(method, *args, &block) - else - begin - super - rescue NoMethodError => e - raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}") - end - end - end - - else - scoped.readonly(nil).public_send(method, *args, &block) + def scoped(options = nil) + association = @association + + super.extending! do + define_method(:proxy_association) { association } end end - # Forwards <tt>===</tt> explicitly to the \target because the instance method - # removal above doesn't catch it. Loads the \target if needed. - def ===(other) - other === load_target + def ==(other) + load_target == other end def to_ary @@ -116,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 @@ -130,19 +366,6 @@ module ActiveRecord proxy_association.reload self end - - # Define array public methods because we know it should be invoked over - # the target, so we can have a performance improvement using those methods - # in association collections - Array.public_instance_methods.each do |m| - unless method_defined?(m) - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{m}(*args, &block) - target.public_send(:#{m}, *args, &block) if load_target - end - RUBY - end - end end end end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 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/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 3005bef092..d545e7799d 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -350,7 +350,7 @@ module ActiveRecord end records_to_destroy.each do |record| - association.proxy.destroy(record) + association.destroy(record) end end 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..be36c9695f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -55,19 +55,27 @@ module ActiveRecord # # == Options # - # There are two connection-pooling-related options that you can add to + # There are several 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 + # * +checkout_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). + # * +reaping_frequency+: frequency in seconds to periodically run the + # Reaper, which attempts to find and close dead connections, which can + # occur if a programmer forgets to close a connection at the end of a + # thread or a thread dies unexpectedly. (Default nil, which means don't + # run the Reaper). + # * +dead_connection_timeout+: number of seconds from last checkout + # after which the Reaper will consider a connection reapable. (default + # 5 seconds). class ConnectionPool # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. # A reaper instantiated with a nil frequency will never reap the # connection pool. # # Configure the frequency by setting "reaping_frequency" in your - # database yaml file. + # database yaml file. class Reaper attr_reader :pool, :frequency @@ -89,7 +97,7 @@ module ActiveRecord include MonitorMixin - attr_accessor :automatic_reconnect, :timeout + attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout attr_reader :spec, :connections, :size, :reaper class Latch # :nodoc: @@ -121,7 +129,8 @@ module ActiveRecord # The cache of reserved connections mapped to threads @reserved_connections = {} - @timeout = spec.config[:wait_timeout] || 5 + @checkout_timeout = spec.config[:checkout_timeout] || 5 + @dead_connection_timeout = spec.config[:dead_connection_timeout] @reaper = Reaper.new self, spec.config[:reaping_frequency] @reaper.run @@ -139,14 +148,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. @@ -237,7 +250,7 @@ module ActiveRecord return checkout_and_verify(conn) if conn end - Timeout.timeout(@timeout, PoolFullError) { @latch.await } + Timeout.timeout(@checkout_timeout, PoolFullError) { @latch.await } end end @@ -275,7 +288,7 @@ module ActiveRecord # or a thread dies unexpectedly. def reap synchronize do - stale = Time.now - @timeout + stale = Time.now - @dead_connection_timeout connections.dup.each do |conn| remove conn if conn.in_use? && stale > conn.last_use && !conn.active? end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 44ac37c498..6f9f0399db 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -34,6 +34,7 @@ module ActiveRecord when Numeric then value.to_s when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" + when Class then "'#{value.to_s}'" else "'#{quote_string(YAML.dump(value))}'" end 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 f0b6ae2b7d..df78ba6c5a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -23,7 +23,7 @@ module ActiveRecord end def sql_type - base.type_to_sql(type.to_sym, limit, precision, scale) rescue type + base.type_to_sql(type.to_sym, limit, precision, scale) end def to_sql @@ -262,7 +262,7 @@ module ActiveRecord # Adds index options to the indexes hash, keyed by column name # This is primarily used to track indexes that need to be created after the table - # === Examples + # # index(:account_id, :name => 'index_projects_on_account_id') def index(column_name, options = {}) indexes[column_name] = options @@ -348,7 +348,7 @@ module ActiveRecord # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. - # ===== Example + # # ====== Creating a simple column # t.column(:name, :string) def column(column_name, type, options = {}) @@ -363,7 +363,6 @@ module ActiveRecord # Adds a new index to the table. +column_name+ can be a single Symbol, or # an Array of Symbols. See SchemaStatements#add_index # - # ===== Examples # ====== Creating a simple index # t.index(:name) # ====== Creating a unique index @@ -380,7 +379,7 @@ module ActiveRecord end # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps - # ===== Example + # # t.timestamps def timestamps @base.add_timestamps(@table_name) @@ -388,7 +387,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. - # ===== Examples + # # t.change(:name, :string, :limit => 80) # t.change(:description, :text) def change(column_name, type, options = {}) @@ -396,7 +395,7 @@ module ActiveRecord end # Sets a new default value for a column. See SchemaStatements#change_column_default - # ===== Examples + # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) def change_default(column_name, default) @@ -404,7 +403,7 @@ module ActiveRecord end # Removes the column(s) from the table definition. - # ===== Examples + # # t.remove(:qualification) # t.remove(:qualification, :experience) def remove(*column_names) @@ -413,7 +412,6 @@ module ActiveRecord # Removes the given index from the table. # - # ===== Examples # ====== Remove the index_table_name_on_column in the table_name table # t.remove_index :column # ====== Remove the index named index_table_name_on_branch_id in the table_name table @@ -427,14 +425,14 @@ module ActiveRecord end # Removes the timestamp columns (+created_at+ and +updated_at+) from the table. - # ===== Example + # # t.remove_timestamps def remove_timestamps @base.remove_timestamps(@table_name) end # Renames a column. - # ===== Example + # # t.rename(:description, :name) def rename(column_name, new_column_name) @base.rename_column(@table_name, column_name, new_column_name) @@ -442,7 +440,7 @@ module ActiveRecord # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. - # ===== Examples + # # t.references(:goat) # t.references(:goat, :polymorphic => true) # t.belongs_to(:goat) @@ -460,7 +458,7 @@ module ActiveRecord # Removes a reference. Optionally removes a +type+ column. # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. - # ===== Examples + # # t.remove_references(:goat) # t.remove_references(:goat, :polymorphic => true) # t.remove_belongs_to(:goat) @@ -475,7 +473,7 @@ module ActiveRecord alias :remove_belongs_to :remove_references # Adds a column or columns of a specified type - # ===== Examples + # # t.string(:goat) # t.string(:goat, :sheep) %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type| diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index e7a4f061fd..62b0f51bb2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -21,7 +21,6 @@ module ActiveRecord # Checks to see if the table +table_name+ exists on the database. # - # === Example # table_exists?(:developers) def table_exists?(table_name) tables.include?(table_name.to_s) @@ -32,7 +31,6 @@ module ActiveRecord # Checks to see if an index exists on a table for a given index definition. # - # === Examples # # Check an index exists # index_exists?(:suppliers, :company_id) # @@ -126,7 +124,6 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # ===== Examples # ====== Add a backend specific option to the generated SQL (MySQL) # create_table(:suppliers, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -193,7 +190,6 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # - # ===== Examples # ====== Add a backend specific option to the generated SQL (MySQL) # create_join_table(:assemblies, :parts, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -215,7 +211,6 @@ module ActiveRecord # A block for changing columns in +table+. # - # === Example # # change_table() yields a Table instance # change_table(:suppliers) do |t| # t.column :name, :string, :limit => 60 @@ -229,7 +224,6 @@ module ActiveRecord # # Defaults to false. # - # ===== Examples # ====== Add a column # change_table(:suppliers) do |t| # t.column :name, :string, :limit => 60 @@ -288,7 +282,7 @@ module ActiveRecord end # Renames a table. - # ===== Example + # # rename_table('octopuses', 'octopi') def rename_table(table_name, new_name) raise NotImplementedError, "rename_table is not implemented" @@ -308,7 +302,7 @@ module ActiveRecord end # Removes the column(s) from the table definition. - # ===== Examples + # # remove_column(:suppliers, :qualification) # remove_columns(:suppliers, :qualification, :experience) def remove_column(table_name, *column_names) @@ -318,7 +312,7 @@ module ActiveRecord # Changes the column's definition according to the new options. # See TableDefinition#column for details of the options you can use. - # ===== Examples + # # change_column(:suppliers, :name, :string, :limit => 80) # change_column(:accounts, :description, :text) def change_column(table_name, column_name, type, options = {}) @@ -326,7 +320,7 @@ module ActiveRecord end # Sets a new default value for a column. - # ===== Examples + # # change_column_default(:suppliers, :qualification, 'new') # change_column_default(:accounts, :authorized, 1) # change_column_default(:users, :email, nil) @@ -335,7 +329,7 @@ module ActiveRecord end # Renames a column. - # ===== Example + # # rename_column(:suppliers, :description, :name) def rename_column(table_name, column_name, new_column_name) raise NotImplementedError, "rename_column is not implemented" @@ -347,8 +341,6 @@ module ActiveRecord # The index will be named after the table and the column name(s), unless # you pass <tt>:name</tt> as an option. # - # ===== Examples - # # ====== Creating a simple index # add_index(:suppliers, :name) # generates @@ -537,7 +529,7 @@ module ActiveRecord end # Adds timestamps (created_at and updated_at) columns to the named table. - # ===== Examples + # # add_timestamps(:suppliers) def add_timestamps(table_name) add_column table_name, :created_at, :datetime @@ -545,7 +537,7 @@ module ActiveRecord end # Removes the timestamp columns (created_at and updated_at) from the table definition. - # ===== Examples + # # remove_timestamps(:suppliers) def remove_timestamps(table_name) remove_column table_name, :updated_at diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index a848838a4e..9794c5663e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -251,7 +251,7 @@ module ActiveRecord end # MysqlAdapter has to free a result after using it, so we use this method to write - # stuff in a abstract way without concerning ourselves about whether it needs to be + # stuff in an abstract way without concerning ourselves about whether it needs to be # explicitly freed or not. def execute_and_free(sql, name = nil) #:nodoc: yield execute(sql, name) diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 1933ce2b46..01bd3ae26c 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -124,6 +124,7 @@ module ActiveRecord when :binary then "#{klass}.binary_to_string(#{var_name})" when :boolean then "#{klass}.value_to_boolean(#{var_name})" when :hstore then "#{klass}.string_to_hstore(#{var_name})" + when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})" else var_name end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index c82afc232c..df3d5e4657 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -137,6 +137,14 @@ module ActiveRecord end end + class Cidr < Type + def type_cast(value) + return if value.nil? + + ConnectionAdapters::PostgreSQLColumn.string_to_cidr value + end + end + class TypeMap def initialize @mapping = {} @@ -212,11 +220,9 @@ module ActiveRecord # FIXME: why are we keeping these types as strings? alias_type 'tsvector', 'text' alias_type 'interval', 'text' - alias_type 'cidr', 'text' - alias_type 'inet', 'text' - alias_type 'macaddr', 'text' alias_type 'bit', 'text' alias_type 'varbit', 'text' + alias_type 'macaddr', 'text' # FIXME: I don't think this is correct. We should probably be returning a parsed date, # but the tests pass with a string returned. @@ -237,6 +243,9 @@ module ActiveRecord register_type 'polygon', OID::Identity.new register_type 'circle', OID::Identity.new register_type 'hstore', OID::Hstore.new + + register_type 'cidr', OID::Cidr.new + alias_type 'inet', 'cidr' end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 4d5459939b..15c3d7be36 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -8,6 +8,8 @@ require 'arel/visitors/bind_visitor' gem 'pg', '~> 0.11' require 'pg' +require 'ipaddr' + module ActiveRecord module ConnectionHandling # Establishes a connection to the database that's used by all Active Record objects @@ -79,6 +81,25 @@ module ActiveRecord end end + def string_to_cidr(string) + if string.nil? + nil + elsif String === string + IPAddr.new(string) + else + string + end + + end + + def cidr_to_string(object) + if IPAddr === object + "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + else + object + end + end + private HstorePair = begin quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ @@ -197,6 +218,13 @@ module ActiveRecord :decimal when 'hstore' :hstore + # Network address types + when 'inet' + :inet + when 'cidr' + :cidr + when 'macaddr' + :macaddr # Character types when /^(?:character varying|bpchar)(?:\(\d+\))?$/ :string @@ -211,9 +239,6 @@ module ActiveRecord # Geometric types when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/ :string - # Network address types - when /^(?:cidr|inet|macaddr)$/ - :string # Bit strings when /^bit(?: varying)?(?:\(\d+\))?$/ :string @@ -282,6 +307,18 @@ module ActiveRecord def hstore(name, options = {}) column(name, 'hstore', options) end + + def inet(name, options = {}) + column(name, 'inet', options) + end + + def cidr(name, options = {}) + column(name, 'cidr', options) + end + + def macaddr(name, options = {}) + column(name, 'macaddr', options) + end end ADAPTER_NAME = 'PostgreSQL' @@ -301,7 +338,10 @@ module ActiveRecord :boolean => { :name => "boolean" }, :xml => { :name => "xml" }, :tsvector => { :name => "tsvector" }, - :hstore => { :name => "hstore" } + :hstore => { :name => "hstore" }, + :inet => { :name => "inet" }, + :cidr => { :name => "cidr" }, + :macaddr => { :name => "macaddr" } } # Returns 'PostgreSQL' as adapter name for identification purposes. @@ -510,6 +550,11 @@ module ActiveRecord when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) else super end + when IPAddr + case column.sql_type + when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) + else super + end when Float if value.infinite? && column.type == :datetime "'#{value.to_s.downcase}'" @@ -549,6 +594,9 @@ module ActiveRecord when Hash return super unless 'hstore' == column.sql_type PostgreSQLColumn.hstore_to_string(value) + when IPAddr + return super unless ['inet','cidr'].includes? column.sql_type + PostgreSQLColumn.cidr_to_string(value) else super end @@ -1216,14 +1264,25 @@ module ActiveRecord # Maps logical Rails types to PostgreSQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) - return super unless type.to_s == 'integer' - return 'integer' unless limit - - case limit - when 1, 2; 'smallint' - when 3, 4; 'integer' - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") + case type.to_s + when 'binary' + # PostgreSQL doesn't support limits on binary (bytea) columns. + # The hard limit is 1Gb, because of a 32-bit size field, and TOAST. + case limit + when nil, 0..0x3fffffff; super(type) + else raise(ActiveRecordError, "No binary type has byte size #{limit}.") + end + when 'integer' + return 'integer' unless limit + + case limit + when 1, 2; 'smallint' + when 3, 4; 'integer' + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") + end + else + super end end @@ -1277,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/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 44e407a561..d4ffa82b17 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -21,10 +21,6 @@ module ActiveRecord config[:database] = File.expand_path(config[:database], Rails.root) end - unless 'sqlite3' == config[:adapter] - raise ArgumentError, 'adapter name should be "sqlite3"' - end - db = SQLite3::Database.new( config[:database], :results_as_hash => true @@ -195,7 +191,7 @@ module ActiveRecord :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, :timestamp => { :name => "datetime" }, - :time => { :name => "time" }, + :time => { :name => "datetime" }, :date => { :name => "date" }, :binary => { :name => "blob" }, :boolean => { :name => "boolean" } @@ -522,7 +518,11 @@ module ActiveRecord end def copy_table(from, to, options = {}) #:nodoc: - options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s)) + from_primary_key = primary_key(from) + options[:primary_key] = from_primary_key if from_primary_key != 'id' + unless options[:primary_key] + options[:id] = columns(from).detect{|c| c.name == 'id'}.present? && from_primary_key == 'id' + end create_table(to, options) do |definition| @definition = definition columns(from).each do |column| @@ -536,7 +536,7 @@ module ActiveRecord :precision => column.precision, :scale => column.scale, :null => column.null) end - @definition.primary_key(primary_key(from)) if primary_key(from) + @definition.primary_key(from_primary_key) if from_primary_key yield @definition if block_given? end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index a869ed8c04..f2833fbf3c 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -77,7 +77,7 @@ module ActiveRecord ## # :singleton-method: - # Specifies wether or not has_many or has_one association option + # Specifies whether or not has_many or has_one association option # :dependent => :restrict raises an exception. If set to true, the # ActiveRecord::DeleteRestrictionError exception will be raised # along with a DEPRECATION WARNING. If set to false, an error would @@ -127,10 +127,16 @@ module ActiveRecord object.is_a?(self) end + # 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)) + # end def arel_table @arel_table ||= Arel::Table.new(table_name, arel_engine) end + # Returns the Arel engine. def arel_engine @arel_engine ||= connection_handler.retrieve_connection_pool(self) ? self : active_record_super.arel_engine end @@ -205,13 +211,34 @@ module ActiveRecord self end + ## + # :method: clone + # Identical to Ruby's clone method. This is a "shallow" copy. Be warned that your attributes are not copied. + # That means that modifying attributes of the clone will modify the original, since they will both point to the + # same attributes hash. If you need a copy of your attributes hash, please use the #dup method. + # + # user = User.first + # new_user = user.clone + # user.name # => "Bob" + # new_user.name = "Joe" + # user.name # => "Joe" + # + # user.object_id == new_user.object_id # => false + # user.name.object_id == new_user.name.object_id # => true + # + # user.name.object_id == user.dup.name.object_id # => false + + ## + # :method: dup # Duped objects have no id assigned and are treated as new records. Note # that this is a "shallow" copy as it copies the object's attributes # only, not its associations. The extent of a "deep" copy is application # specific and is therefore left to the application to implement according # to its need. # The dup method does not preserve the timestamps (created|updated)_(at|on). - def initialize_dup(other) + + ## + def initialize_dup(other) # :nodoc: cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) self.class.initialize_attributes(cloned_attributes) diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index a01e2f74ff..7e6512501c 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -83,7 +83,7 @@ module ActiveRecord # end # # test "find_alt_method_2" do - # assert_equal "Ruby on Rails", @rubyonrails.news + # assert_equal "Ruby on Rails", @rubyonrails.name # end # # In order to use these methods to access fixtured data within your testcases, you must specify one of the @@ -372,19 +372,23 @@ module ActiveRecord # # Any fixture labeled "DEFAULTS" is safely ignored. class Fixtures + #-- + # NOTE: an instance of Fixtures can be called fixture_set, it is normally stored in a single YAML file and possibly in a folder with the same name. + #++ MAX_ID = 2 ** 30 - 1 @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.default_fixture_model_name(fixture_name) # :nodoc: + def self.default_fixture_model_name(fixture_set_name) # :nodoc: ActiveRecord::Base.pluralize_table_names ? - fixture_name.singularize.camelize : - fixture_name.camelize + fixture_set_name.singularize.camelize : + fixture_set_name.camelize end - def self.default_fixture_table_name(fixture_name) # :nodoc: - "#{ActiveRecord::Base.table_name_prefix}"\ - "#{fixture_name.tr('/', '_')}#{ActiveRecord::Base.table_name_suffix}".to_sym + def self.default_fixture_table_name(fixture_set_name) # :nodoc: + "#{ ActiveRecord::Base.table_name_prefix }"\ + "#{ fixture_set_name.tr('/', '_') }"\ + "#{ ActiveRecord::Base.table_name_suffix }".to_sym end def self.reset_cache @@ -411,11 +415,11 @@ module ActiveRecord cache_for_connection(connection).update(fixtures_map) end - def self.instantiate_fixtures(object, fixture_name, fixtures, load_instances = true) + def self.instantiate_fixtures(object, fixture_set, load_instances = true) if load_instances - fixtures.each do |name, fixture| + fixture_set.each do |fixture_name, fixture| begin - object.instance_variable_set "@#{name}", fixture.find + object.instance_variable_set "@#{fixture_name}", fixture.find rescue FixtureClassNotFound nil end @@ -424,61 +428,59 @@ module ActiveRecord end def self.instantiate_all_loaded_fixtures(object, load_instances = true) - all_loaded_fixtures.each do |table_name, fixtures| - ActiveRecord::Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances) + all_loaded_fixtures.each_value do |fixture_set| + instantiate_fixtures(object, fixture_set, load_instances) end end cattr_accessor :all_loaded_fixtures self.all_loaded_fixtures = {} - def self.create_fixtures(fixtures_directory, table_names, class_names = {}) - table_names = Array(table_names).map(&:to_s) + def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}) + fixture_set_names = Array(fixture_set_names).map(&:to_s) class_names = class_names.stringify_keys # FIXME: Apparently JK uses this. connection = block_given? ? yield : ActiveRecord::Base.connection - files_to_read = table_names.reject { |table_name| - fixture_is_cached?(connection, table_name) + files_to_read = fixture_set_names.reject { |fs_name| + fixture_is_cached?(connection, fs_name) } unless files_to_read.empty? connection.disable_referential_integrity do fixtures_map = {} - fixture_files = files_to_read.map do |path| - fixture_name = path - - fixtures_map[fixture_name] = new( # ActiveRecord::Fixtures.new + fixture_sets = files_to_read.map do |fs_name| + fixtures_map[fs_name] = new( # ActiveRecord::Fixtures.new connection, - fixture_name, - class_names[fixture_name.to_s] || default_fixture_model_name(fixture_name), - ::File.join(fixtures_directory, path)) + fs_name, + class_names[fs_name] || default_fixture_model_name(fs_name), + ::File.join(fixtures_directory, fs_name)) end all_loaded_fixtures.update(fixtures_map) connection.transaction(:requires_new => true) do - fixture_files.each do |ff| - conn = ff.model_class.respond_to?(:connection) ? ff.model_class.connection : connection - table_rows = ff.table_rows + fixture_sets.each do |fs| + conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection + table_rows = fs.table_rows table_rows.keys.each do |table| conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' end - table_rows.each do |table_name,rows| + table_rows.each do |fixture_set_name, rows| rows.each do |row| - conn.insert_fixture(row, table_name) + conn.insert_fixture(row, fixture_set_name) end end end # Cap primary key sequences to max(pk). if connection.respond_to?(:reset_pk_sequence!) - fixture_files.each do |ff| - connection.reset_pk_sequence!(ff.table_name) + fixture_sets.each do |fs| + connection.reset_pk_sequence!(fs.table_name) end end end @@ -486,7 +488,7 @@ module ActiveRecord cache_fixtures(connection, fixtures_map) end end - cached_fixtures(connection, table_names) + cached_fixtures(connection, fixture_set_names) end # Returns a consistent, platform-independent identifier for +label+. @@ -497,26 +499,23 @@ module ActiveRecord attr_reader :table_name, :name, :fixtures, :model_class - def initialize(connection, fixture_name, class_name, fixture_path) - @connection = connection - @fixture_path = fixture_path - @name = fixture_name - @class_name = class_name - - @fixtures = {} + def initialize(connection, name, class_name, path) + @fixtures = {} # Ordered hash + @name = name + @path = path - # Should be an AR::Base type class - if class_name.is_a?(Class) + if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @model_class = class_name else @model_class = class_name.constantize rescue nil end - @connection = model_class.connection if model_class && model_class.respond_to?(:connection) + @connection = ( model_class.respond_to?(:connection) ? + model_class.connection : connection ) @table_name = ( model_class.respond_to?(:table_name) ? model_class.table_name : - self.class.default_fixture_table_name(fixture_name) ) + self.class.default_fixture_table_name(name) ) read_fixture_files end @@ -555,8 +554,8 @@ module ActiveRecord if model_class && model_class < ActiveRecord::Model # fill in timestamp columns if they aren't specified and the model is set to record_timestamps if model_class.record_timestamps - timestamp_column_names.each do |name| - row[name] = now unless row.key?(name) + timestamp_column_names.each do |c_name| + row[c_name] = now unless row.key?(c_name) end end @@ -634,26 +633,23 @@ module ActiveRecord end def read_fixture_files - yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f| + yaml_files = Dir["#{@path}/**/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path] yaml_files.each do |file| Fixtures::File.open(file) do |fh| - fh.each do |name, row| - fixtures[name] = ActiveRecord::Fixture.new(row, model_class) + fh.each do |fixture_name, row| + fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) end end end end def yaml_file_path - "#{@fixture_path}.yml" + "#{@path}.yml" end - def yaml_fixtures_key(path) - ::File.basename(@fixture_path).split(".").first - end end class Fixture #:nodoc: @@ -708,7 +704,7 @@ module ActiveRecord class_attribute :fixture_table_names class_attribute :fixture_class_names class_attribute :use_transactional_fixtures - class_attribute :use_instantiated_fixtures # true, false, or :no_instances + class_attribute :use_instantiated_fixtures # true, false, or :no_instances class_attribute :pre_loaded_fixtures self.fixture_table_names = [] @@ -716,9 +712,8 @@ module ActiveRecord self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false - self.fixture_class_names = Hash.new do |h, fixture_name| - fixture_name = fixture_name.to_s - h[fixture_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_name) + self.fixture_class_names = Hash.new do |h, fixture_set_name| + h[fixture_set_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_set_name) end end @@ -742,18 +737,18 @@ module ActiveRecord self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys) end - def fixtures(*fixture_names) - if fixture_names.first == :all - fixture_names = Dir["#{fixture_path}/**/*.yml"].map { |f| + def fixtures(*fixture_set_names) + if fixture_set_names.first == :all + fixture_set_names = Dir["#{fixture_path}/**/*.yml"].map { |f| File.basename f, '.yml' } else - fixture_names = fixture_names.flatten.map { |n| n.to_s } + fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s } end - self.fixture_table_names |= fixture_names - require_fixture_classes(fixture_names) - setup_fixture_accessors(fixture_names) + self.fixture_table_names |= fixture_set_names + require_fixture_classes(fixture_set_names) + setup_fixture_accessors(fixture_set_names) end def try_to_load_dependency(file_name) @@ -768,33 +763,39 @@ module ActiveRecord end end - def require_fixture_classes(fixture_names = nil) - (fixture_names || fixture_table_names).each do |fixture_name| - file_name = fixture_name.to_s + def require_fixture_classes(fixture_set_names = nil) + if fixture_set_names + fixture_set_names = fixture_set_names.map { |n| n.to_s } + else + fixture_set_names = fixture_table_names + end + + fixture_set_names.each do |file_name| file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names try_to_load_dependency(file_name) end end - def setup_fixture_accessors(fixture_names = nil) - fixture_names = Array(fixture_names || fixture_table_names) + def setup_fixture_accessors(fixture_set_names = nil) + fixture_set_names = Array(fixture_set_names || fixture_table_names) methods = Module.new do - fixture_names.each do |fixture_name| - fixture_name = fixture_name.to_s - accessor_name = fixture_name.tr('/', '_').to_sym + fixture_set_names.each do |fs_name| + fs_name = fs_name.to_s + accessor_name = fs_name.tr('/', '_').to_sym - define_method(accessor_name) do |*fixtures| - force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload + define_method(accessor_name) do |*fixture_names| + force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload - @fixture_cache[fixture_name] ||= {} + @fixture_cache[fs_name] ||= {} - instances = fixtures.map do |fixture| - @fixture_cache[fixture_name].delete(fixture) if force_reload + instances = fixture_names.map do |f_name| + f_name = f_name.to_s + @fixture_cache[fs_name].delete(f_name) if force_reload - if @loaded_fixtures[fixture_name][fixture.to_s] - @fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find + if @loaded_fixtures[fs_name][f_name] + @fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find else - raise StandardError, "No entry named '#{fixture}' found for fixture collection '#{fixture_name}'" + raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'" end end @@ -901,8 +902,8 @@ module ActiveRecord ActiveRecord::Fixtures.instantiate_all_loaded_fixtures(self, load_instances?) else raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? - @loaded_fixtures.each do |fixture_name, fixtures| - ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_name, fixtures, load_instances?) + @loaded_fixtures.each_value do |fixture_set| + ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_set, load_instances?) end end end 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/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 32a1dae6bc..95a2ddcc11 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -19,10 +19,10 @@ module ActiveRecord # = Active Record Nested Attributes # # Nested attributes allow you to save attributes on associated records - # through the parent. By default nested attribute updating is turned off, - # you can enable it using the accepts_nested_attributes_for class method. - # When you enable nested attributes an attribute writer is defined on - # the model. + # through the parent. By default nested attribute updating is turned off + # and you can enable it using the accepts_nested_attributes_for class + # method. When you enable nested attributes an attribute writer is + # defined on the model. # # The attribute writer is named after the association, which means that # in the following example, two new methods are added to your model: diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 4a987c2343..a1bc39a32d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -123,6 +123,7 @@ module ActiveRecord # 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 + raise ReadOnlyRecord if readonly? destroy_associations destroy_row if persisted? @destroyed = true 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/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 3ce9995031..31d99f0192 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -108,34 +108,57 @@ module ActiveRecord 0 end - # This method is designed to perform select by a single column as direct SQL query - # Returns <tt>Array</tt> with values of the specified column name - # The values has same data type as column. + # Use <tt>pluck</tt> as a shortcut to select a single attribute without + # loading a bunch of records just to grab one attribute you want. + # + # Person.pluck(:name) + # + # instead of + # + # Person.all.map(&:name) + # + # Pluck returns an <tt>Array</tt> of attribute values type-casted to match + # the plucked column name, if it can be deduced. Plucking a SQL fragment + # returns String values by default. # # Examples: # - # Person.pluck(:id) # SELECT people.id FROM people - # Person.uniq.pluck(:role) # SELECT DISTINCT role FROM people - # Person.where(:age => 21).limit(5).pluck(:id) # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5 + # Person.pluck(:id) + # # SELECT people.id FROM people + # # => [1, 2, 3] + # + # Person.uniq.pluck(:role) + # # SELECT DISTINCT role FROM people + # # => ['admin', 'member', 'guest'] + # + # Person.where(:age => 21).limit(5).pluck(:id) + # # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5 + # # => [2, 3] + # + # Person.pluck('DATEDIFF(updated_at, created_at)') + # # SELECT DATEDIFF(updated_at, created_at) FROM people + # # => ['0', '27761', '173'] # def pluck(column_name) - key = column_name.to_s.split('.', 2).last - if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s) column_name = "#{table_name}.#{column_name}" end result = klass.connection.select_all(select(column_name).arel, nil, bind_values) - types = result.column_types.merge klass.column_types - column = types[key] + + key = result.columns.first + column = klass.column_types.fetch(key) { + result.column_types.fetch(key) { + Class.new { def type_cast(v); v; end }.new + } + } result.map do |attributes| - value = klass.initialize_attributes(attributes)[key] - if column - column.type_cast value - else - value - end + raise ArgumentError, "Pluck expects to select just one attribute: #{attributes.inspect}" unless attributes.one? + + value = klass.initialize_attributes(attributes).values.first + + column.type_cast(value) end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index cc716bbfd1..4fedd33d64 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -170,9 +170,8 @@ module ActiveRecord # Person.exists?(['name LIKE ?', "%#{query}%"]) # Person.exists? def exists?(id = false) - return false if id.nil? - id = id.id if ActiveRecord::Model === id + return false if id.nil? join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 3f880ce5e9..36f98c6480 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -43,7 +43,7 @@ module ActiveRecord def normal_values Relation::SINGLE_VALUE_METHODS + Relation::MULTI_VALUE_METHODS - - [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering] + [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] end def merge @@ -76,6 +76,7 @@ module ActiveRecord end def merge_single_values + relation.from_value = values[:from] unless relation.from_value relation.lock_value = values[:lock] unless relation.lock_value relation.reverse_order_value = values[:reverse_order] diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index b40bf2b3cf..6a0cdd5917 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -34,9 +34,6 @@ module ActiveRecord private def self.build(attribute, value) case value - when ActiveRecord::Relation - value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? - attribute.in(value.arel.ast) when Array, ActiveRecord::Associations::CollectionProxy values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x} ranges, values = values.partition {|v| v.is_a?(Range)} @@ -59,6 +56,9 @@ module ActiveRecord array_predicates = ranges.map { |range| attribute.in(range) } array_predicates << values_predicate array_predicates.inject { |composite, predicate| composite.or(predicate) } + when ActiveRecord::Relation + value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? + attribute.in(value.arel.ast) when Range attribute.in(value) when ActiveRecord::Model diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 855477eaed..19fe8155d9 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -40,7 +40,7 @@ module ActiveRecord alias extensions extending_values def includes(*args) - args.empty? ? self : clone.includes!(*args) + args.empty? ? self : spawn.includes!(*args) end def includes!(*args) @@ -51,7 +51,7 @@ module ActiveRecord end def eager_load(*args) - args.blank? ? self : clone.eager_load!(*args) + args.blank? ? self : spawn.eager_load!(*args) end def eager_load!(*args) @@ -60,7 +60,7 @@ module ActiveRecord end def preload(*args) - args.blank? ? self : clone.preload!(*args) + args.blank? ? self : spawn.preload!(*args) end def preload!(*args) @@ -79,7 +79,7 @@ module ActiveRecord # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # => Query now knows the string references posts, so adds a JOIN def references(*args) - args.blank? ? self : clone.references!(*args) + args.blank? ? self : spawn.references!(*args) end def references!(*args) @@ -120,7 +120,7 @@ module ActiveRecord if block_given? to_a.select { |*block_args| value.call(*block_args) } else - clone.select!(value) + spawn.select!(value) end end @@ -130,7 +130,7 @@ module ActiveRecord end def group(*args) - args.blank? ? self : clone.group!(*args) + args.blank? ? self : spawn.group!(*args) end def group!(*args) @@ -139,7 +139,7 @@ module ActiveRecord end def order(*args) - args.blank? ? self : clone.order!(*args) + args.blank? ? self : spawn.order!(*args) end def order!(*args) @@ -165,7 +165,7 @@ module ActiveRecord # generates a query with 'ORDER BY id ASC, name ASC'. # def reorder(*args) - args.blank? ? self : clone.reorder!(*args) + args.blank? ? self : spawn.reorder!(*args) end def reorder!(*args) @@ -175,7 +175,7 @@ module ActiveRecord end def joins(*args) - args.compact.blank? ? self : clone.joins!(*args) + args.compact.blank? ? self : spawn.joins!(*args) end def joins!(*args) @@ -186,7 +186,7 @@ module ActiveRecord end def bind(value) - clone.bind!(value) + spawn.bind!(value) end def bind!(value) @@ -195,7 +195,7 @@ module ActiveRecord end def where(opts, *rest) - opts.blank? ? self : clone.where!(opts, *rest) + opts.blank? ? self : spawn.where!(opts, *rest) end def where!(opts, *rest) @@ -206,7 +206,7 @@ module ActiveRecord end def having(opts, *rest) - opts.blank? ? self : clone.having!(opts, *rest) + opts.blank? ? self : spawn.having!(opts, *rest) end def having!(opts, *rest) @@ -217,7 +217,7 @@ module ActiveRecord end def limit(value) - clone.limit!(value) + spawn.limit!(value) end def limit!(value) @@ -226,7 +226,7 @@ module ActiveRecord end def offset(value) - clone.offset!(value) + spawn.offset!(value) end def offset!(value) @@ -235,7 +235,7 @@ module ActiveRecord end def lock(locks = true) - clone.lock!(locks) + spawn.lock!(locks) end def lock!(locks = true) @@ -283,7 +283,7 @@ module ActiveRecord end def readonly(value = true) - clone.readonly!(value) + spawn.readonly!(value) end def readonly!(value = true) @@ -292,7 +292,7 @@ module ActiveRecord end def create_with(value) - clone.create_with!(value) + spawn.create_with!(value) end def create_with!(value) @@ -300,12 +300,25 @@ module ActiveRecord self end - def from(value) - clone.from!(value) + # Specifies table from which the records will be fetched. For example: + # + # Topic.select('title').from('posts') + # #=> SELECT title FROM posts + # + # Can accept other relation objects. For example: + # + # Topic.select('title').from(Topics.approved) + # # => SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery + # + # Topics.select('a.title').from(Topics.approved, :a) + # # => SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a + # + def from(value, subquery_name = nil) + spawn.from!(value, subquery_name) end - def from!(value) - self.from_value = value + def from!(value, subquery_name = nil) + self.from_value = [value, subquery_name] self end @@ -320,7 +333,7 @@ module ActiveRecord # User.select(:name).uniq.uniq(false) # # => You can also remove the uniqueness def uniq(value = true) - clone.uniq!(value) + spawn.uniq!(value) end def uniq!(value = true) @@ -366,7 +379,7 @@ module ActiveRecord # end def extending(*modules, &block) if modules.any? || block - clone.extending!(*modules, &block) + spawn.extending!(*modules, &block) else self end @@ -382,7 +395,7 @@ module ActiveRecord end def reverse_order - clone.reverse_order! + spawn.reverse_order! end def reverse_order! @@ -415,7 +428,7 @@ module ActiveRecord build_select(arel, select_values.uniq) arel.distinct(uniq_value) - arel.from(from_value) if from_value + arel.from(build_from) if from_value arel.lock(lock_value) if lock_value arel @@ -464,6 +477,17 @@ module ActiveRecord end end + def build_from + opts, name = from_value + case opts + when Relation + name ||= 'subquery' + opts.arel.as(name.to_s) + else + opts + end + end + def build_joins(manager, joins) buckets = joins.group_by do |join| case join diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 41e55dfd0e..80d087a9ea 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -5,9 +5,14 @@ require 'active_record/relation/merger' module ActiveRecord module SpawnMethods - + + # This is overridden by Associations::CollectionProxy + def spawn #:nodoc: + clone + end + # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>. - # Returns an array representing the union of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. + # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. # # ==== Examples # @@ -16,14 +21,14 @@ module ActiveRecord # # recent_posts = Post.order('created_at DESC').first(5) # Post.where(:published => true).merge(recent_posts) - # # Returns the union of all published posts with the 5 most recently created posts. + # # Returns the intersection of all published posts with the 5 most recently created posts. # # (This is just an example. You'd probably want to do this with a single query!) # def merge(other) if other.is_a?(Array) to_a & other elsif other - clone.merge!(other) + spawn.merge!(other) else self end @@ -42,7 +47,7 @@ module ActiveRecord # Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order # def except(*skips) - result = self.class.new(@klass, table, values.except(*skips)) + result = Relation.new(klass, table, values.except(*skips)) result.default_scoped = default_scoped result.extend(*extending_values) if extending_values.any? result @@ -56,7 +61,7 @@ module ActiveRecord # Post.order('id asc').only(:where, :order) # uses the specified order # def only(*onlies) - result = self.class.new(@klass, table, values.slice(*onlies)) + result = Relation.new(klass, table, values.slice(*onlies)) result.default_scoped = default_scoped result.extend(*extending_values) if extending_values.any? result diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index fb4b89b87b..fd276ccf5d 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -28,6 +28,7 @@ module ActiveRecord alias :map! :map alias :collect! :map + # Returns true if there are no records. def empty? rows.empty? end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index d815ab05ac..599e68379a 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -34,6 +34,15 @@ module ActiveRecord ActiveRecord::Migrator.migrations_paths end + def define(info, &block) + instance_eval(&block) + + unless info[:version].blank? + initialize_schema_migrations_table + assume_migrated_upto_version(info[:version], migrations_paths) + end + end + # Eval the given block. All methods available to the current connection # adapter are available within the block, so you can easily use the # database definition DSL to build up your schema (+create_table+, @@ -46,13 +55,7 @@ module ActiveRecord # ... # end def self.define(info={}, &block) - schema = new - schema.instance_eval(&block) - - unless info[:version].blank? - initialize_schema_migrations_table - assume_migrated_upto_version(info[:version], schema.migrations_paths) - end + new.define(info, &block) end end end diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 2e60521638..b833af64fe 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -18,8 +18,8 @@ module ActiveRecord #:nodoc: # <id type="integer">1</id> # <approved type="boolean">false</approved> # <replies-count type="integer">0</replies-count> - # <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time> - # <written-on type="datetime">2003-07-16T09:28:00+1200</written-on> + # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time> + # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on> # <content>Have a nice day</content> # <author-email-address>david@loudthinking.com</author-email-address> # <parent-id></parent-id> diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index ed47a26749..5a256b040b 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -201,10 +201,10 @@ module ActiveRecord class << self alias :data_column_name :data_column - + # Use the ActiveRecord::Base.connection by default. attr_writer :connection - + # Use the ActiveRecord::Base.connection_pool by default. attr_writer :connection_pool @@ -218,12 +218,12 @@ module ActiveRecord # Look up a session by id and unmarshal its data if found. def find_by_session_id(session_id) - if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id)}") + if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id.to_s)}") new(:session_id => session_id, :marshaled_data => record['data']) end end end - + delegate :connection, :connection=, :connection_pool, :connection_pool=, :to => self attr_reader :session_id, :new_record @@ -241,6 +241,11 @@ module ActiveRecord @new_record = @marshaled_data.nil? end + # Returns true if the record is persisted, i.e. it's not a new record + def persisted? + !@new_record + end + # Lazy-unmarshal session state. def data unless @data @@ -287,7 +292,7 @@ module ActiveRecord connect = connection connect.delete <<-end_sql, 'Destroy session' DELETE FROM #{table_name} - WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)} + WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id.to_s)} end_sql end end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 1c7b839e5e..fdd82b489a 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/indifferent_access' + module ActiveRecord # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column. # It's like a simple key/value store backed into your record when you don't care about being able to @@ -10,16 +12,23 @@ module ActiveRecord # Make sure that you declare the database column used for the serialized store as a text, so there's # plenty of room. # + # You can set custom coder to encode/decode your serialized attributes to/from different formats. + # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. + # # Examples: # # class User < ActiveRecord::Base - # store :settings, accessors: [ :color, :homepage ] + # store :settings, accessors: [ :color, :homepage ], coder: JSON # end # # u = User.new(color: 'black', homepage: '37signals.com') # u.color # Accessor stored attribute # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor # + # # There is no difference between strings and symbols for accessing custom attributes + # u.settings[:country] # => 'Denmark' + # u.settings['country'] # => 'Denmark' + # # # Add additional accessors to an existing store through store_accessor # class SuperUser < User # store_accessor :settings, :privileges, :servants @@ -29,24 +38,38 @@ module ActiveRecord module ClassMethods def store(store_attribute, options = {}) - serialize store_attribute, Hash + serialize store_attribute, options.fetch(:coder, ActiveSupport::HashWithIndifferentAccess) store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors end def store_accessor(store_attribute, *keys) keys.flatten.each do |key| define_method("#{key}=") do |value| - send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) + initialize_store_attribute(store_attribute) send(store_attribute)[key] = value send("#{store_attribute}_will_change!") end define_method(key) do - send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash) + initialize_store_attribute(store_attribute) send(store_attribute)[key] end end end end + + private + def initialize_store_attribute(store_attribute) + case attribute = send(store_attribute) + when ActiveSupport::HashWithIndifferentAccess + # Already initialized. Do nothing. + when Hash + # Initialized as a Hash. Convert to indifferent access. + send :"#{store_attribute}=", attribute.with_indifferent_access + else + # Uninitialized. Set to an indifferent hash. + send :"#{store_attribute}=", ActiveSupport::HashWithIndifferentAccess.new + end + end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 743dfc5a38..9cb9b4627b 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -290,7 +290,15 @@ module ActiveRecord status = nil self.class.transaction do add_to_transaction - status = yield + begin + status = yield + rescue ActiveRecord::Rollback + if defined?(@_start_transaction_state) + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + end + status = nil + end + raise ActiveRecord::Rollback unless status end status @@ -302,12 +310,8 @@ module ActiveRecord def remember_transaction_record_state #:nodoc: @_start_transaction_state ||= {} @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) - unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:new_record] = @new_record - end - unless @_start_transaction_state.include?(:destroyed) - @_start_transaction_state[:destroyed] = @destroyed - end + @_start_transaction_state[:new_record] = @new_record + @_start_transaction_state[:destroyed] = @destroyed @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 end @@ -325,7 +329,8 @@ module ActiveRecord @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 if @_start_transaction_state[:level] < 1 restore_state = remove_instance_variable(:@_start_transaction_state) - @attributes = @attributes.dup if @attributes.frozen? + was_frozen = @attributes.frozen? + @attributes = @attributes.dup if was_frozen @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] if restore_state.has_key?(:id) @@ -334,6 +339,7 @@ module ActiveRecord @attributes.delete(self.class.primary_key) @attributes_cache.delete(self.class.primary_key) end + @attributes.freeze if was_frozen end end end |