diff options
Diffstat (limited to 'activerecord/lib/active_record')
28 files changed, 606 insertions, 459 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index e91cbd7f33..08fb6bf3c4 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -52,14 +52,6 @@ module ActiveRecord      end    end -  class HasManyThroughSourceAssociationMacroError < ActiveRecordError #:nodoc: -    def initialize(reflection) -      through_reflection = reflection.through_reflection -      source_reflection  = reflection.source_reflection -      super("Invalid source reflection macro :#{source_reflection.macro}#{" :through" if source_reflection.options[:through]} for has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}.  Use :source to specify the source reflection.") -    end -  end -    class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:      def initialize(owner, reflection)        super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.") @@ -78,6 +70,12 @@ module ActiveRecord      end    end +  class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc +    def initialize(owner, reflection) +      super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") +    end +  end +    class HasAndBelongsToManyAssociationWithPrimaryKeyError < ActiveRecordError #:nodoc:      def initialize(reflection)        super("Primary key is not allowed in a has_and_belongs_to_many join table (#{reflection.options[:join_table]}).") @@ -142,8 +140,11 @@ module ActiveRecord        autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'      end -    autoload :Preloader,      'active_record/associations/preloader' -    autoload :JoinDependency, 'active_record/associations/join_dependency' +    autoload :Preloader,        'active_record/associations/preloader' +    autoload :JoinDependency,   'active_record/associations/join_dependency' +    autoload :AssociationScope, 'active_record/associations/association_scope' +    autoload :AliasTracker,     'active_record/associations/alias_tracker' +    autoload :JoinHelper,       'active_record/associations/join_helper'      # Clears out the association cache.      def clear_association_cache #:nodoc: @@ -548,6 +549,49 @@ module ActiveRecord      #     belongs_to :tag, :inverse_of => :taggings      #   end      # +    # === Nested Associations +    # +    # You can actually specify *any* association with the <tt>:through</tt> option, including an +    # association which has a <tt>:through</tt> option itself. For example: +    # +    #   class Author < ActiveRecord::Base +    #     has_many :posts +    #     has_many :comments, :through => :posts +    #     has_many :commenters, :through => :comments +    #   end +    # +    #   class Post < ActiveRecord::Base +    #     has_many :comments +    #   end +    # +    #   class Comment < ActiveRecord::Base +    #     belongs_to :commenter +    #   end +    # +    #   @author = Author.first +    #   @author.commenters # => People who commented on posts written by the author +    # +    # An equivalent way of setting up this association this would be: +    # +    #   class Author < ActiveRecord::Base +    #     has_many :posts +    #     has_many :commenters, :through => :posts +    #   end +    # +    #   class Post < ActiveRecord::Base +    #     has_many :comments +    #     has_many :commenters, :through => :comments +    #   end +    # +    #   class Comment < ActiveRecord::Base +    #     belongs_to :commenter +    #   end +    # +    # When using nested association, you will not be able to modify the association because there +    # is not enough information to know what modification to make. For example, if you tried to +    # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the +    # intermediate <tt>Post</tt> and <tt>Comment</tt> objects. +    #      # === Polymorphic Associations      #      # Polymorphic associations on models are not restricted on what types of models they @@ -1068,10 +1112,10 @@ module ActiveRecord        # [:as]        #   Specifies a polymorphic interface (See <tt>belongs_to</tt>).        # [:through] -      #   Specifies a join model through which to perform the query.  Options for <tt>:class_name</tt>, +      #   Specifies an association through which to perform the query. This can be any other type +      #   of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>,        #   <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the -      #   source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>, -      #   <tt>has_one</tt> or <tt>has_many</tt> association on the join model. +      #   source reflection.        #        #   If the association on the join model is a +belongs_to+, the collection can be modified        #   and the records on the <tt>:through</tt> model will be automatically created and removed @@ -1198,10 +1242,10 @@ module ActiveRecord        #   you want to do a join but not include the joined columns. Do not forget to include the        #   primary and foreign keys, otherwise it will raise an error.        # [:through] -      #   Specifies a Join Model through which to perform the query.  Options for <tt>:class_name</tt> -      #   and <tt>:foreign_key</tt> are ignored, as the association uses the source reflection. You -      #   can only use a <tt>:through</tt> query through a <tt>has_one</tt> or <tt>belongs_to</tt> -      #   association on the join model. +      #   Specifies a Join Model through which to perform the query.  Options for <tt>:class_name</tt>, +      #   <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the +      #   source reflection. You can only use a <tt>:through</tt> query through a <tt>has_one</tt> +      #   or <tt>belongs_to</tt> association on the join model.        # [:source]        #   Specifies the source association name used by <tt>has_one :through</tt> queries.        #   Only use it if the name cannot be inferred from the association. diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb new file mode 100644 index 0000000000..634dee2289 --- /dev/null +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -0,0 +1,85 @@ +require 'active_support/core_ext/string/conversions' + +module ActiveRecord +  module Associations +    # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and +    # ActiveRecord::Associations::ThroughAssociationScope +    class AliasTracker # :nodoc: +      attr_reader :aliases, :table_joins + +      # table_joins is an array of arel joins which might conflict with the aliases we assign here +      def initialize(table_joins = []) +        @aliases     = Hash.new +        @table_joins = table_joins +      end + +      def aliased_table_for(table_name, aliased_name = nil) +        table_alias = aliased_name_for(table_name, aliased_name) + +        if table_alias == table_name +          Arel::Table.new(table_name) +        else +          Arel::Table.new(table_name).alias(table_alias) +        end +      end + +      def aliased_name_for(table_name, aliased_name = nil) +        aliased_name ||= table_name + +        initialize_count_for(table_name) if aliases[table_name].nil? + +        if aliases[table_name].zero? +          # If it's zero, we can have our table_name +          aliases[table_name] = 1 +          table_name +        else +          # Otherwise, we need to use an alias +          aliased_name = connection.table_alias_for(aliased_name) + +          initialize_count_for(aliased_name) if aliases[aliased_name].nil? + +          # Update the count +          aliases[aliased_name] += 1 + +          if aliases[aliased_name] > 1 +            "#{truncate(aliased_name)}_#{aliases[aliased_name]}" +          else +            aliased_name +          end +        end +      end + +      def pluralize(table_name) +        ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name +      end + +      private + +        def initialize_count_for(name) +          aliases[name] = 0 + +          unless Arel::Table === table_joins +            # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase +            quoted_name = connection.quote_table_name(name).downcase + +            aliases[name] += table_joins.map { |join| +              # Table names + table aliases +              join.left.downcase.scan( +                /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ +              ).size +            }.sum +          end + +          aliases[name] +        end + +        def truncate(name) +          name[0..connection.table_alias_length-3] +        end + +        def connection +          ActiveRecord::Base.connection +        end +    end +  end +end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 25b4b9d90d..27c446b12c 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -93,23 +93,9 @@ module ActiveRecord        # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which        # actually gets built.        def construct_scope -        @association_scope = association_scope if klass -      end - -      def association_scope -        scope = klass.unscoped -        scope = scope.create_with(creation_attributes) -        scope = scope.apply_finder_options(options.slice(:readonly, :include)) -        scope = scope.where(interpolate(options[:conditions])) -        if select = select_value -          scope = scope.select(select) +        if klass +          @association_scope = AssociationScope.new(self).scope          end -        scope = scope.extending(*Array.wrap(options[:extend])) -        scope.where(construct_owner_conditions) -      end - -      def aliased_table -        klass.arel_table        end        # Set the inverse association, if possible @@ -174,42 +160,24 @@ module ActiveRecord            end          end -        def select_value -          options[:select] -        end - -        # Implemented by (some) subclasses          def creation_attributes -          { } -        end - -        # Returns a hash linking the owner to the association represented by the reflection -        def construct_owner_attributes(reflection = reflection)            attributes = {} -          if reflection.macro == :belongs_to -            attributes[reflection.association_primary_key] = owner[reflection.foreign_key] -          else + +          if [:has_one, :has_many].include?(reflection.macro) && !options[:through]              attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]              if reflection.options[:as]                attributes[reflection.type] = owner.class.base_class.name              end            end -          attributes -        end -        # Builds an array of arel nodes from the owner attributes hash -        def construct_owner_conditions(table = aliased_table, reflection = reflection) -          conditions = construct_owner_attributes(reflection).map do |attr, value| -            table[attr].eq(value) -          end -          table.create_and(conditions) +          attributes          end          # Sets the owner attributes on the given record          def set_owner_attributes(record)            if owner.persisted? -            construct_owner_attributes.each { |key, value| record[key] = value } +            creation_attributes.each { |key, value| record[key] = value }            end          end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb new file mode 100644 index 0000000000..ab102b2b8f --- /dev/null +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -0,0 +1,120 @@ +module ActiveRecord +  module Associations +    class AssociationScope #:nodoc: +      include JoinHelper + +      attr_reader :association, :alias_tracker + +      delegate :klass, :owner, :reflection, :interpolate, :to => :association +      delegate :chain, :conditions, :options, :source_options, :active_record, :to => :reflection + +      def initialize(association) +        @association   = association +        @alias_tracker = AliasTracker.new +      end + +      def scope +        scope = klass.unscoped +        scope = scope.extending(*Array.wrap(options[:extend])) + +        # It's okay to just apply all these like this. The options will only be present if the +        # association supports that option; this is enforced by the association builder. +        scope = scope.apply_finder_options(options.slice( +          :readonly, :include, :order, :limit, :joins, :group, :having, :offset)) + +        if options[:through] && !options[:include] +          scope = scope.includes(source_options[:include]) +        end + +        if select = select_value +          scope = scope.select(select) +        end + +        add_constraints(scope) +      end + +      private + +      def select_value +        select_value = options[:select] + +        if reflection.collection? +          select_value ||= options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*" +        end + +        if reflection.macro == :has_and_belongs_to_many +          select_value ||= reflection.klass.arel_table[Arel.star] +        end + +        select_value +      end + +      def add_constraints(scope) +        tables = construct_tables + +        chain.each_with_index do |reflection, i| +          table, foreign_table = tables.shift, tables.first + +          if reflection.source_macro == :has_and_belongs_to_many +            join_table = tables.shift + +            scope = scope.joins(join( +              join_table, +              table[reflection.active_record_primary_key]. +                eq(join_table[reflection.association_foreign_key]) +            )) + +            table, foreign_table = join_table, tables.first +          end + +          if reflection.source_macro == :belongs_to +            key         = reflection.association_primary_key +            foreign_key = reflection.foreign_key +          else +            key         = reflection.foreign_key +            foreign_key = reflection.active_record_primary_key +          end + +          if reflection == chain.last +            scope = scope.where(table[key].eq(owner[foreign_key])) + +            conditions[i].each do |condition| +              if options[:through] && condition.is_a?(Hash) +                condition = { table.name => condition } +              end + +              scope = scope.where(interpolate(condition)) +            end +          else +            constraint = table[key].eq(foreign_table[foreign_key]) +            join       = join(foreign_table, constraint) + +            scope = scope.joins(join) + +            unless conditions[i].empty? +              scope = scope.where(sanitize(conditions[i], table)) +            end +          end +        end + +        scope +      end + +      def alias_suffix +        reflection.name +      end + +      def table_name_for(reflection) +        if reflection == self.reflection +          # If this is a polymorphic belongs_to, we want to get the klass from the +          # association because it depends on the polymorphic_type attribute of +          # the owner +          klass.table_name +        else +          reflection.table_name +        end +      end + +    end +  end +end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index f3761bd2c7..9f4fc44cc6 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -331,11 +331,6 @@ module ActiveRecord          @scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args)        end -      def association_scope -        options = reflection.options.slice(:order, :limit, :joins, :group, :having, :offset) -        super.apply_finder_options(options) -      end -        def load_target          if find_target?            targets = [] @@ -373,14 +368,6 @@ module ActiveRecord        private -        def select_value -          super || uniq_select_value -        end - -        def uniq_select_value -          options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*" -        end -          def custom_counter_sql            if options[:counter_sql]              interpolate(options[:counter_sql]) 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 028630977d..217213808b 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 @@ -26,10 +26,6 @@ module ActiveRecord          record        end -      def association_scope -        super.joins(construct_joins) -      end -        private          def count_records @@ -48,24 +44,6 @@ module ActiveRecord            end          end -        def construct_joins -          right = join_table -          left  = reflection.klass.arel_table - -          condition = left[reflection.klass.primary_key].eq( -            right[reflection.association_foreign_key]) - -          right.create_join(right, right.create_on(condition)) -        end - -        def construct_owner_conditions -          super(join_table) -        end - -        def select_value -          super || reflection.klass.arel_table[Arel.star] -        end -          def invertible_for?(record)            false          end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index cebf3e477a..78c5c4b870 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -94,8 +94,6 @@ module ActiveRecord              end            end          end - -        alias creation_attributes construct_owner_attributes      end    end  end 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 acac68fda5..9d2b29685b 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -34,7 +34,9 @@ module ActiveRecord        end        def insert_record(record, validate = true) +        ensure_not_nested          return if record.new_record? && !record.save(:validate => validate) +          through_record(record).save!          update_counter(1)          record @@ -59,6 +61,8 @@ module ActiveRecord          end          def build_record(attributes) +          ensure_not_nested +            record = super(attributes)            inverse = source_reflection.inverse_of @@ -93,6 +97,8 @@ module ActiveRecord          end          def delete_records(records, method) +          ensure_not_nested +            through = owner.association(through_reflection.name)            scope   = through.scoped.where(construct_join_attributes(*records)) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index e13f97125f..1d2e8667e4 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -39,14 +39,8 @@ module ActiveRecord          end        end -      def association_scope -        super.order(options[:order]) -      end -        private -        alias creation_attributes construct_owner_attributes -          # The reason that the save param for replace is false, if for create (not just build),          # is because the setting of the foreign keys is actually handled by the scoping when          # the record is instantiated, and so they are set straight away and do not need to be diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index d76d729303..fdf8ae1453 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -12,6 +12,8 @@ module ActiveRecord        private          def create_through_record(record) +          ensure_not_nested +            through_proxy  = owner.association(through_reflection.name)            through_record = through_proxy.send(:load_target) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index c7c3cf521c..504f25271c 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -5,18 +5,16 @@ module ActiveRecord        autoload :JoinBase,        'active_record/associations/join_dependency/join_base'        autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association' -      attr_reader :join_parts, :reflections, :table_aliases, :active_record +      attr_reader :join_parts, :reflections, :alias_tracker, :active_record        def initialize(base, associations, joins) -        @active_record         = base -        @table_joins           = joins -        @join_parts            = [JoinBase.new(base)] -        @associations          = {} -        @reflections           = [] -        @table_aliases         = Hash.new do |h,name| -          h[name] = count_aliases_from_table_joins(name.downcase) -        end -        @table_aliases[base.table_name] = 1 +        @active_record = base +        @table_joins   = joins +        @join_parts    = [JoinBase.new(base)] +        @associations  = {} +        @reflections   = [] +        @alias_tracker = AliasTracker.new(joins) +        @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1          build(associations)        end @@ -45,20 +43,6 @@ module ActiveRecord          }.flatten        end -      def count_aliases_from_table_joins(name) -        return 0 if Arel::Table === @table_joins - -        # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase -        quoted_name = active_record.connection.quote_table_name(name).downcase - -        @table_joins.map { |join| -          # Table names + table aliases -          join.left.downcase.scan( -            /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ -          ).size -        }.sum -      end -        def instantiate(rows)          primary_key = join_base.aliased_primary_key          parents = {} diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index ebe39c35fe..4121a5b378 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -2,6 +2,8 @@ module ActiveRecord    module Associations      class JoinDependency # :nodoc:        class JoinAssociation < JoinPart # :nodoc: +        include JoinHelper +          # The reflection of the association represented          attr_reader :reflection @@ -18,10 +20,15 @@ module ActiveRecord          attr_accessor :join_type          # These implement abstract methods from the superclass -        attr_reader :aliased_prefix, :aliased_table_name +        attr_reader :aliased_prefix + +        attr_reader :tables -        delegate :options, :through_reflection, :source_reflection, :to => :reflection +        delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection          delegate :table, :table_name, :to => :parent, :prefix => :parent +        delegate :alias_tracker, :to => :join_dependency + +        alias :alias_suffix :parent_table_name          def initialize(reflection, join_dependency, parent = nil)            reflection.check_validity! @@ -37,14 +44,7 @@ module ActiveRecord            @parent          = parent            @join_type       = Arel::InnerJoin            @aliased_prefix  = "t#{ join_dependency.join_parts.size }" - -          # This must be done eagerly upon initialisation because the alias which is produced -          # depends on the state of the join dependency, but we want it to work the same way -          # every time. -          allocate_aliases -          @table = Arel::Table.new( -            table_name, :as => aliased_table_name, :engine => arel_engine -          ) +          @tables          = construct_tables.reverse          end          def ==(other) @@ -60,219 +60,84 @@ module ActiveRecord          end          def join_to(relation) -          send("join_#{reflection.macro}_to", relation) -        end - -        def join_relation(joining_relation) -          self.join_type = Arel::OuterJoin -          joining_relation.joins(self) -        end - -        attr_reader :table -        # More semantic name given we are talking about associations -        alias_method :target_table, :table - -        protected - -        def aliased_table_name_for(name, suffix = nil) -          aliases = @join_dependency.table_aliases - -          if aliases[name] != 0 # We need an alias -            connection = active_record.connection - -            name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" -            aliases[name] += 1 -            name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1 -          else -            aliases[name] += 1 -          end - -          name -        end +          tables        = @tables.dup +          foreign_table = parent_table + +          # The chain starts with the target table, but we want to end with it here (makes +          # more sense in this context), so we reverse +          chain.reverse.each_with_index do |reflection, i| +            table = tables.shift + +            case reflection.source_macro +            when :belongs_to +              key         = reflection.association_primary_key +              foreign_key = reflection.foreign_key +            when :has_and_belongs_to_many +              # Join the join table first... +              relation.from(join( +                table, +                table[reflection.foreign_key]. +                  eq(foreign_table[reflection.active_record_primary_key]) +              )) + +              foreign_table, table = table, tables.shift + +              key         = reflection.association_primary_key +              foreign_key = reflection.association_foreign_key +            else +              key         = reflection.foreign_key +              foreign_key = reflection.active_record_primary_key +            end -        def pluralize(table_name) -          ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name -        end +            constraint = table[key].eq(foreign_table[foreign_key]) -        private +            if reflection.klass.finder_needs_type_condition? +              constraint = table.create_and([ +                constraint, +                reflection.klass.send(:type_condition, table) +              ]) +            end -        def allocate_aliases -          @aliased_table_name = aliased_table_name_for(table_name) +            relation.from(join(table, constraint)) -          if reflection.macro == :has_and_belongs_to_many -            @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") -          elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through] -            @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join") -          end -        end +            unless conditions[i].empty? +              relation.where(sanitize(conditions[i], table)) +            end -        def process_conditions(conditions, table_name) -          if conditions.respond_to?(:to_proc) -            conditions = instance_eval(&conditions) +            # The current table in this iteration becomes the foreign table in the next +            foreign_table = table            end -          Arel.sql(sanitize_sql(conditions, table_name)) +          relation          end -        def sanitize_sql(condition, table_name) -          active_record.send(:sanitize_sql, condition, table_name) +        def join_relation(joining_relation) +          self.join_type = Arel::OuterJoin +          joining_relation.joins(self)          end -        def join_target_table(relation, condition) -          conditions = [condition] - -          # If the target table is an STI model then we must be sure to only include records of -          # its type and its sub-types. -          unless active_record.descends_from_active_record? -            sti_column    = target_table[active_record.inheritance_column] -            subclasses    = active_record.descendants -            sti_condition = sti_column.eq(active_record.sti_name) - -            conditions << subclasses.inject(sti_condition) { |attr,subclass| -              attr.or(sti_column.eq(subclass.sti_name)) -            } -          end - -          # If the reflection has conditions, add them -          if options[:conditions] -            conditions << process_conditions(options[:conditions], aliased_table_name) -          end - -          ands = relation.create_and(conditions) - -          join = relation.create_join( -            target_table, -            relation.create_on(ands), -            join_type) - -          relation.from join +        def table +          tables.last          end -        def join_has_and_belongs_to_many_to(relation) -          join_table = Arel::Table.new( -            options[:join_table] -          ).alias(@aliased_join_table_name) - -          fk       = options[:foreign_key]             || reflection.active_record.to_s.foreign_key -          klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key - -          relation = relation.join(join_table, join_type) -          relation = relation.on( -            join_table[fk]. -            eq(parent_table[reflection.active_record.primary_key]) -          ) - -          join_target_table( -            relation, -            target_table[reflection.klass.primary_key]. -            eq(join_table[klass_fk]) -          ) +        def aliased_table_name +          table.table_alias || table.name          end -        def join_has_many_to(relation) -          if reflection.options[:through] -            join_has_many_through_to(relation) -          elsif reflection.options[:as] -            join_has_many_polymorphic_to(relation) -          else -            foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key -            primary_key = options[:primary_key] || parent.primary_key - -            join_target_table( -              relation, -              target_table[foreign_key]. -              eq(parent_table[primary_key]) -            ) -          end +        def conditions +          @conditions ||= reflection.conditions.reverse          end -        alias :join_has_one_to :join_has_many_to - -        def join_has_many_through_to(relation) -          join_table = Arel::Table.new( -            through_reflection.klass.table_name -          ).alias @aliased_join_table_name -          jt_conditions = [] -          first_key = second_key = nil +        private -          if through_reflection.macro == :belongs_to -            jt_primary_key = through_reflection.foreign_key -            jt_foreign_key = through_reflection.association_primary_key +        def interpolate(conditions) +          if conditions.respond_to?(:to_proc) +            instance_eval(&conditions)            else -            jt_primary_key = through_reflection.active_record_primary_key -            jt_foreign_key = through_reflection.foreign_key - -            if through_reflection.options[:as] # has_many :through against a polymorphic join -              jt_conditions << -              join_table["#{through_reflection.options[:as]}_type"]. -                eq(parent.active_record.base_class.name) -            end +            conditions            end - -          case source_reflection.macro -          when :has_many -            second_key = options[:foreign_key] || primary_key - -            if source_reflection.options[:as] -              first_key = "#{source_reflection.options[:as]}_id" -            else -              first_key = through_reflection.klass.base_class.to_s.foreign_key -            end - -            unless through_reflection.klass.descends_from_active_record? -              jt_conditions << -              join_table[through_reflection.active_record.inheritance_column]. -                eq(through_reflection.klass.sti_name) -            end -          when :belongs_to -            first_key = primary_key - -            if reflection.options[:source_type] -              second_key = source_reflection.association_foreign_key - -              jt_conditions << -              join_table[reflection.source_reflection.foreign_type]. -                eq(reflection.options[:source_type]) -            else -              second_key = source_reflection.foreign_key -            end -          end - -          jt_conditions << -          parent_table[jt_primary_key]. -            eq(join_table[jt_foreign_key]) - -          if through_reflection.options[:conditions] -            jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name) -          end - -          relation = relation.join(join_table, join_type).on(*jt_conditions) - -          join_target_table( -            relation, -            target_table[first_key].eq(join_table[second_key]) -          )          end -        def join_has_many_polymorphic_to(relation) -          join_target_table( -            relation, -            target_table["#{reflection.options[:as]}_id"]. -            eq(parent_table[parent.primary_key]).and( -            target_table["#{reflection.options[:as]}_type"]. -            eq(parent.active_record.base_class.name)) -          ) -        end - -        def join_belongs_to_to(relation) -          foreign_key = options[:foreign_key] || reflection.foreign_key -          primary_key = options[:primary_key] || reflection.klass.primary_key - -          join_target_table( -            relation, -            target_table[primary_key].eq(parent_table[foreign_key]) -          ) -        end        end      end    end diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb new file mode 100644 index 0000000000..eae546e76e --- /dev/null +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -0,0 +1,56 @@ +module ActiveRecord +  module Associations +    # Helper class module which gets mixed into JoinDependency::JoinAssociation and AssociationScope +    module JoinHelper #:nodoc: + +      def join_type +        Arel::InnerJoin +      end + +      private + +      def construct_tables +        tables = [] +        chain.each do |reflection| +          tables << alias_tracker.aliased_table_for( +            table_name_for(reflection), +            table_alias_for(reflection, reflection != self.reflection) +          ) + +          if reflection.source_macro == :has_and_belongs_to_many +            tables << alias_tracker.aliased_table_for( +              (reflection.source_reflection || reflection).options[:join_table], +              table_alias_for(reflection, true) +            ) +          end +        end +        tables +      end + +      def table_name_for(reflection) +        reflection.table_name +      end + +      def table_alias_for(reflection, join = false) +        name = alias_tracker.pluralize(reflection.name) +        name << "_#{alias_suffix}" +        name << "_join" if join +        name +      end + +      def join(table, constraint) +        table.create_join(table, table.create_on(constraint), join_type) +      end + +      def sanitize(conditions, table) +        conditions = conditions.map do |condition| +          condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) +          condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) +          condition +        end + +        conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions) +      end +    end +  end +end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index d630fc4c63..ad6374d09a 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -19,8 +19,9 @@ module ActiveRecord              source_reflection.name, options            ).run -          through_records.each do |owner, owner_through_records| -            owner_through_records.map! { |r| r.send(source_reflection.name) }.flatten! +          through_records.each do |owner, records| +            records.map! { |r| r.send(source_reflection.name) }.flatten! +            records.compact!            end          end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index e1d60ccb17..e6ab628719 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -3,79 +3,24 @@ module ActiveRecord    module Associations      module ThroughAssociation #:nodoc: -      delegate :source_options, :through_options, :source_reflection, :through_reflection, :to => :reflection +      delegate :source_reflection, :through_reflection, :chain, :to => :reflection        protected +        # We merge in these scopes for two reasons: +        # +        #   1. To get the default_scope conditions for any of the other reflections in the chain +        #   2. To get the type conditions for any STI models in the chain          def target_scope -          super.merge(through_reflection.klass.scoped) -        end - -        def association_scope -          scope = super.joins(construct_joins) -          scope = add_conditions(scope) -          unless options[:include] -            scope = scope.includes(source_options[:include]) +          scope = super +          chain[1..-1].each do |reflection| +            scope = scope.merge(reflection.klass.scoped)            end            scope          end        private -        # This scope affects the creation of the associated records (not the join records). At the -        # moment we only support creating on a :through association when the source reflection is a -        # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so -        # this scope has can legitimately be empty. -        def creation_attributes -          { } -        end - -        def aliased_through_table -          name = through_reflection.table_name - -          reflection.table_name == name ? -            through_reflection.klass.arel_table.alias(name + "_join") : -            through_reflection.klass.arel_table -        end - -        def construct_owner_conditions -          super(aliased_through_table, through_reflection) -        end - -        def construct_joins -          right = aliased_through_table -          left  = reflection.klass.arel_table - -          conditions = [] - -          if source_reflection.macro == :belongs_to -            reflection_primary_key = source_reflection.association_primary_key -            source_primary_key     = source_reflection.foreign_key - -            if options[:source_type] -              column = source_reflection.foreign_type -              conditions << -                right[column].eq(options[:source_type]) -            end -          else -            reflection_primary_key = source_reflection.foreign_key -            source_primary_key     = source_reflection.active_record_primary_key - -            if source_options[:as] -              column = "#{source_options[:as]}_type" -              conditions << -                left[column].eq(through_reflection.klass.name) -            end -          end - -          conditions << -            left[reflection_primary_key].eq(right[source_primary_key]) - -          right.create_join( -            right, -            right.create_on(right.create_and(conditions))) -        end -          # Construct attributes for :through pointing to owner and associate. This is used by the          # methods which create and delete records on the association.          # @@ -112,37 +57,8 @@ module ActiveRecord            end          end -        # The reason that we are operating directly on the scope here (rather than passing -        # back some arel conditions to be added to the scope) is because scope.where([x, y]) -        # has a different meaning to scope.where(x).where(y) - the first version might -        # perform some substitution if x is a string. -        def add_conditions(scope) -          unless through_reflection.klass.descends_from_active_record? -            scope = scope.where(through_reflection.klass.send(:type_condition)) -          end - -          scope = scope.where(interpolate(source_options[:conditions])) -          scope.where(through_conditions) -        end - -        # If there is a hash of conditions then we make sure the keys are scoped to the -        # through table name if left ambiguous. -        def through_conditions -          conditions = interpolate(through_options[:conditions]) - -          if conditions.is_a?(Hash) -            Hash[conditions.map { |key, value| -              unless value.is_a?(Hash) || key.to_s.include?('.') -                key = aliased_through_table.name + '.' + key.to_s -              end - -              [key, value] -            }] -          else -            conditions -          end -        end - +        # Note: this does not capture all cases, for example it would be crazy to try to +        # properly support stale-checking for nested associations.          def stale_state            if through_reflection.macro == :belongs_to              owner[through_reflection.foreign_key].to_s @@ -153,6 +69,12 @@ module ActiveRecord            through_reflection.macro == :belongs_to &&            !owner[through_reflection.foreign_key].nil?          end + +        def ensure_not_nested +          if reflection.nested? +            raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) +          end +        end      end    end  end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index ab86d8bad1..69d5cd83f1 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -70,7 +70,14 @@ module ActiveRecord              if cache_attribute?(attr_name)                access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"              end -            generated_attribute_methods.module_eval("def _#{symbol}; #{access_code}; end; alias #{symbol} _#{symbol}", __FILE__, __LINE__) +            if symbol =~ /^[a-zA-Z_]\w*[!?=]?$/ +              generated_attribute_methods.module_eval("def _#{symbol}; #{access_code}; end; alias #{symbol} _#{symbol}", __FILE__, __LINE__) +            else +              generated_attribute_methods.module_eval do +                define_method("_#{symbol}") { eval(access_code) } +                alias_method(symbol, "_#{symbol}") +              end +            end            end        end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 6a593a7e0e..3c4dab304e 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -10,7 +10,13 @@ module ActiveRecord        module ClassMethods          protected            def define_method_attribute=(attr_name) -            generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__) +            if attr_name =~ /^[a-zA-Z_]\w*[!?=]?$/ +              generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__) +            else +              generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value| +                write_attribute(attr_name, new_value) +              end +            end            end        end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index a5e1c91f47..b778b0c0f0 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -973,8 +973,8 @@ module ActiveRecord #:nodoc:            relation          end -        def type_condition -          sti_column = arel_table[inheritance_column.to_sym] +        def type_condition(table = arel_table) +          sti_column = table[inheritance_column.to_sym]            sti_names  = ([self] + descendants).map { |model| model.sti_name }            sti_column.in(sti_names) @@ -995,7 +995,7 @@ module ActiveRecord #:nodoc:              if parent < ActiveRecord::Base && !parent.abstract_class?                contained = parent.table_name                contained = contained.singularize if parent.pluralize_table_names -              contained << '_' +              contained += '_'              end              "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}"            else diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index d54a643d7b..a3082b8f01 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -271,6 +271,10 @@ module ActiveRecord          execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert'        end +      def null_insert_value +        Arel.sql 'DEFAULT' +      end +        def empty_insert_statement_value          "VALUES(DEFAULT)"        end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 368c5b2023..e1186209d3 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -504,14 +504,28 @@ module ActiveRecord          show_variable 'collation_database'        end -      def tables(name = nil) #:nodoc: +      def tables(name = nil, database = nil) #:nodoc:          tables = [] -        result = execute("SHOW TABLES", name) +        result = execute(["SHOW TABLES", database].compact.join(' IN '), name)          result.each { |field| tables << field[0] }          result.free          tables        end +      def table_exists?(name) +        return true if super + +        name          = name.to_s +        schema, table = name.split('.', 2) + +        unless table # A table was provided without a schema +          table  = schema +          schema = nil +        end + +        tables(nil, schema).include? table +      end +        def drop_table(table_name, options = {})          super(table_name, options)        end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 576450bc3a..5a830a50fb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -453,7 +453,7 @@ module ActiveRecord            # If a pk is given, fallback to default sequence name.            # Don't fetch last insert id for a table without a pk.            if pk && sequence_name ||= default_sequence_name(table, pk) -            last_insert_id(table, sequence_name) +            last_insert_id(sequence_name)            end          end        end @@ -1038,8 +1038,9 @@ module ActiveRecord          end          # Returns the current ID of a table's sequence. -        def last_insert_id(table, sequence_name) #:nodoc: -          Integer(select_value("SELECT currval('#{sequence_name}')")) +        def last_insert_id(sequence_name) #:nodoc: +          r = exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]]) +          Integer(r.rows.first.first)          end          # Executes a SELECT query and returns the results, performing any data type diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index c2cd9e8d5e..c3a7b039ff 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -34,6 +34,14 @@ module ActiveRecord    module ConnectionAdapters #:nodoc:      class SQLite3Adapter < SQLiteAdapter # :nodoc: +      def quote(value, column = nil) +        if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) +          s = column.class.string_to_binary(value).unpack("H*")[0] +          "x'#{s}'" +        else +          super +        end +      end        # Returns the current database encoding format as a string, eg: 'UTF-8'        def encoding diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 9ee6b88ab6..ae61d6ce94 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -336,6 +336,10 @@ module ActiveRecord          alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})        end +      def null_insert_value +        Arel.sql 'NULL' +      end +        def empty_insert_statement_value          "VALUES(NULL)"        end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index df7b22080c..17a64b6e86 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -270,17 +270,9 @@ module ActiveRecord      # Creates a record with values matching those of the instance attributes      # and returns its id.      def create -      if id.nil? && connection.prefetch_primary_key?(self.class.table_name) -        self.id = connection.next_sequence_value(self.class.sequence_name) -      end -        attributes_values = arel_attributes_values(!id.nil?) -      new_id = if attributes_values.empty? -        self.class.unscoped.insert connection.empty_insert_statement_value -      else -        self.class.unscoped.insert attributes_values -      end +      new_id = self.class.unscoped.insert attributes_values        self.id ||= new_id diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 5de08953f9..e801bc4afa 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -262,16 +262,30 @@ module ActiveRecord        end        def through_reflection -        false -      end - -      def through_reflection_foreign_key +        nil        end        def source_reflection          nil        end +      # A chain of reflections from this one back to the owner. For more see the explanation in +      # ThroughReflection. +      def chain +        [self] +      end + +      # An array of arrays of conditions. Each item in the outside array corresponds to a reflection +      # in the #chain. The inside arrays are simply conditions (and each condition may itself be +      # a hash, array, arel predicate, etc...) +      def conditions +        conditions = [options[:conditions]].compact +        conditions << { type => active_record.base_class.name } if options[:as] +        [conditions] +      end + +      alias :source_macro :macro +        def has_inverse?          @options[:inverse_of]        end @@ -363,7 +377,7 @@ module ActiveRecord      # Holds all the meta-data about a :through association as it was specified      # in the Active Record class.      class ThroughReflection < AssociationReflection #:nodoc: -      delegate :association_primary_key, :foreign_type, :to => :source_reflection +      delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :to => :source_reflection        # Gets the source of the through reflection.  It checks both a singularized        # and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. @@ -392,6 +406,88 @@ module ActiveRecord          @through_reflection ||= active_record.reflect_on_association(options[:through])        end +      # Returns an array of reflections which are involved in this association. Each item in the +      # array corresponds to a table which will be part of the query for this association. +      # +      # The chain is built by recursively calling #chain on the source reflection and the through +      # reflection. The base case for the recursion is a normal association, which just returns +      # [self] as its #chain. +      def chain +        @chain ||= begin +          chain = source_reflection.chain + through_reflection.chain +          chain[0] = self # Use self so we don't lose the information from :source_type +          chain +        end +      end + +      # Consider the following example: +      # +      #   class Person +      #     has_many :articles +      #     has_many :comment_tags, :through => :articles +      #   end +      # +      #   class Article +      #     has_many :comments +      #     has_many :comment_tags, :through => :comments, :source => :tags +      #   end +      # +      #   class Comment +      #     has_many :tags +      #   end +      # +      # There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags, +      # but only Comment.tags will be represented in the #chain. So this method creates an array +      # of conditions corresponding to the chain. Each item in the #conditions array corresponds +      # to an item in the #chain, and is itself an array of conditions from an arbitrary number +      # of relevant reflections, plus any :source_type or polymorphic :as constraints. +      def conditions +        @conditions ||= begin +          conditions = source_reflection.conditions + +          # Add to it the conditions from this reflection if necessary. +          conditions.first << options[:conditions] if options[:conditions] + +          through_conditions = through_reflection.conditions + +          if options[:source_type] +            through_conditions.first << { foreign_type => options[:source_type] } +          end + +          # Recursively fill out the rest of the array from the through reflection +          conditions += through_conditions + +          # And return +          conditions +        end +      end + +      # The macro used by the source association +      def source_macro +        source_reflection.source_macro +      end + +      # A through association is nested iff there would be more than one join table +      def nested? +        chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many +      end + +      # We want to use the klass from this reflection, rather than just delegate straight to +      # the source_reflection, because the source_reflection may be polymorphic. We still +      # need to respect the source_reflection's :primary_key option, though. +      def association_primary_key +        @association_primary_key ||= begin +          # Get the "actual" source reflection if the immediate source reflection has a +          # source reflection itself +          source_reflection = self.source_reflection +          while source_reflection.source_reflection +            source_reflection = source_reflection.source_reflection +          end + +          source_reflection.options[:primary_key] || klass.primary_key +        end +      end +        # Gets an array of possible <tt>:through</tt> source reflection names:        #        #   [:singularized, :pluralized] @@ -429,10 +525,6 @@ module ActiveRecord            raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)          end -        unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil? -          raise HasManyThroughSourceAssociationMacroError.new(self) -        end -          if macro == :has_one && through_reflection.collection?            raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)          end @@ -440,14 +532,6 @@ module ActiveRecord          check_validity_of_inverse!        end -      def through_reflection_primary_key -        through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.foreign_key -      end - -      def through_reflection_foreign_key -        through_reflection.foreign_key if through_reflection.belongs_to? -      end -        private          def derive_class_name            # get the class_name of the belongs_to association of the through reflection diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 371403f510..8e545f9cad 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -30,15 +30,26 @@ module ActiveRecord      end      def insert(values) -      im = arel.compile_insert values -      im.into @table -        primary_key_value = nil        if primary_key && Hash === values          primary_key_value = values[values.keys.find { |k|            k.name == primary_key          }] + +        if !primary_key_value && connection.prefetch_primary_key?(klass.table_name) +          primary_key_value = connection.next_sequence_value(klass.sequence_name) +          values[klass.arel_table[klass.primary_key]] = primary_key_value +        end +      end + +      im = arel.create_insert +      im.into @table + +      if values.empty? # empty insert +        im.values = im.create_values [connection.null_insert_value], [] +      else +        im.insert values        end        @klass.connection.insert( diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index cd1d7108b3..9470e7c6c5 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -261,7 +261,7 @@ module ActiveRecord        )        join_nodes.each do |join| -        join_dependency.table_aliases[join.left.name.downcase] = 1 +        join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase)        end        join_dependency.graft(*stashed_association_joins) diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 4150e36a9a..128e0fbd86 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -79,6 +79,9 @@ module ActiveRecord          result.send(:"#{method}_value=", send(:"#{method}_value"))        end +      # Apply scope extension modules +      result.send(:apply_modules, extensions) +        result      end @@ -100,6 +103,9 @@ module ActiveRecord          result.send(:"#{method}_value=", send(:"#{method}_value"))        end +      # Apply scope extension modules +      result.send(:apply_modules, extensions) +        result      end  | 
