diff options
Diffstat (limited to 'activerecord/lib')
81 files changed, 966 insertions, 1300 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 994cacb4f9..9d418dacaf 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -25,7 +25,6 @@ require 'active_support' require 'active_support/rails' require 'active_model' require 'arel' -require 'active_record/deprecated_finders' require 'active_record/version' @@ -152,7 +151,6 @@ module ActiveRecord autoload :OracleDatabaseTasks, 'active_record/tasks/oracle_database_tasks' end - autoload :TestCase autoload :TestFixtures, 'active_record/fixtures' def self.eager_load! diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 9d1c12ec62..d075edc159 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -223,7 +223,8 @@ module ActiveRecord reader_method(name, class_name, mapping, allow_nil, constructor) writer_method(name, class_name, mapping, allow_nil, converter) - create_reflection(:composed_of, part_id, nil, options, self) + reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self) + Reflection.add_reflection self, part_id, reflection end private diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 6fd4f3042c..5ceda933f2 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1205,7 +1205,8 @@ module ActiveRecord # has_many :reports, -> { readonly } # has_many :subscribers, through: :subscriptions, source: :user def has_many(name, scope = nil, options = {}, &extension) - Builder::HasMany.build(self, name, scope, options, &extension) + reflection = Builder::HasMany.build(self, name, scope, options, &extension) + Reflection.add_reflection self, name, reflection end # Specifies a one-to-one association with another class. This method should only be used @@ -1308,7 +1309,8 @@ module ActiveRecord # has_one :club, through: :membership # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable def has_one(name, scope = nil, options = {}) - Builder::HasOne.build(self, name, scope, options) + reflection = Builder::HasOne.build(self, name, scope, options) + Reflection.add_reflection self, name, reflection end # Specifies a one-to-one association with another class. This method should only be used @@ -1420,7 +1422,8 @@ module ActiveRecord # belongs_to :company, touch: true # belongs_to :company, touch: :employees_last_updated_at def belongs_to(name, scope = nil, options = {}) - Builder::BelongsTo.build(self, name, scope, options) + reflection = Builder::BelongsTo.build(self, name, scope, options) + Reflection.add_reflection self, name, reflection end # Specifies a many-to-many relationship with another class. This associates two classes via an @@ -1557,7 +1560,8 @@ module ActiveRecord # has_and_belongs_to_many :categories, join_table: "prods_cats" # has_and_belongs_to_many :categories, -> { readonly } def has_and_belongs_to_many(name, scope = nil, options = {}, &extension) - Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension) + reflection = Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension) + Reflection.add_reflection self, name, reflection end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index ee62298793..67d24b35d1 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -84,11 +84,6 @@ module ActiveRecord target_scope.merge(association_scope) end - def scoped - ActiveSupport::Deprecation.warn "#scoped is deprecated. use #scope instead." - scope - end - # The scope for this association. # # Note that the association_scope is merged into the target_scope only when the @@ -122,11 +117,7 @@ module ActiveRecord # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the # through association's scope) def target_scope - all = klass.all - scope = AssociationRelation.new(klass, klass.arel_table, self) - scope.merge! all - scope.default_scoped = all.default_scoped? - scope + AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all) end # Loads the \target if needed and returns it. diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index f1bec5787a..8027acfb83 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -82,17 +82,19 @@ module ActiveRecord constraint = table[key].eq(foreign_table[foreign_key]) if reflection.type - type = chain[i + 1].klass.base_class.name - constraint = constraint.and(table[reflection.type].eq(type)) + value = chain[i + 1].klass.base_class.name + bind_val = bind scope, table.table_name, reflection.type.to_s, value + scope = scope.where(table[reflection.type].eq(bind_val)) end scope = scope.joins(join(foreign_table, constraint)) end + klass = i == 0 ? self.klass : reflection.klass + # Exclude the scope of the association itself, because that # was already merged in the #scope method. scope_chain[i].each do |scope_chain_item| - klass = i == 0 ? self.klass : reflection.klass item = eval_scope(klass, scope_chain_item) if scope_chain_item == self.reflection.scope @@ -119,7 +121,7 @@ module ActiveRecord # the owner klass.table_name else - reflection.table_name + super end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 3254da4677..34de1a1f32 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -2,7 +2,7 @@ # used by all associations. # # The hierarchy is defined as follows: -# Association +# Association # - SingularAssociation # - BelongsToAssociation # - HasOneAssociation @@ -13,50 +13,44 @@ module ActiveRecord::Associations::Builder class Association #:nodoc: class << self - attr_accessor :valid_options + attr_accessor :extensions end + self.extensions = [] - self.valid_options = [:class_name, :foreign_key, :validate] + VALID_OPTIONS = [:class_name, :foreign_key, :validate] - attr_reader :model, :name, :scope, :options, :reflection + attr_reader :name, :scope, :options - def self.build(*args, &block) - new(*args, &block).build - end - - def initialize(model, name, scope, options) + def self.build(model, name, scope, options, &block) raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol) - @model = model - @name = name - if scope.is_a?(Hash) - @scope = nil - @options = scope - else - @scope = scope - @options = options + options = scope + scope = nil end - if @scope && @scope.arity == 0 - prev_scope = @scope - @scope = proc { instance_exec(&prev_scope) } - end - end - - def mixin - @model.generated_feature_methods + builder = new(name, scope, options, &block) + reflection = builder.build(model) + builder.define_accessors model + builder.define_callbacks model, reflection + builder.define_extensions model + reflection end - include Module.new { def build; end } + def initialize(name, scope, options) + @name = name + @scope = scope + @options = options - def build validate_options - define_accessors - configure_dependency if options[:dependent] - @reflection = model.create_reflection(macro, name, scope, options, model) - super # provides an extension point - @reflection + + if scope && scope.arity == 0 + @scope = proc { instance_exec(&scope) } + end + end + + def build(model) + ActiveRecord::Reflection.create(macro, name, scope, options, model) end def macro @@ -64,26 +58,37 @@ module ActiveRecord::Associations::Builder end def valid_options - Association.valid_options + VALID_OPTIONS + Association.extensions.flat_map(&:valid_options) end def validate_options options.assert_valid_keys(valid_options) end - + + def define_extensions(model) + end + + def define_callbacks(model, reflection) + add_before_destroy_callbacks(model, name) if options[:dependent] + Association.extensions.each do |extension| + extension.build model, reflection + end + end + # Defines the setter and getter methods for the association # class Post < ActiveRecord::Base # has_many :comments # end - # + # # Post.first.comments and Post.first.comments= methods are defined by this method... - def define_accessors - define_readers - define_writers + def define_accessors(model) + mixin = model.generated_feature_methods + define_readers(mixin) + define_writers(mixin) end - def define_readers + def define_readers(mixin) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}(*args) association(:#{name}).reader(*args) @@ -91,7 +96,7 @@ module ActiveRecord::Associations::Builder CODE end - def define_writers + def define_writers(mixin) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}=(value) association(:#{name}).writer(value) @@ -99,24 +104,18 @@ module ActiveRecord::Associations::Builder CODE end - def configure_dependency + def valid_dependent_options + raise NotImplementedError + end + + private + + def add_before_destroy_callbacks(model, name) unless valid_dependent_options.include? options[:dependent] raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}" end - if options[:dependent] == :restrict - ActiveSupport::Deprecation.warn( - "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \ - "provides the same functionality." - ) - end - - n = name - model.before_destroy lambda { |o| o.association(n).handle_dependency } - end - - def valid_dependent_options - raise NotImplementedError + model.before_destroy lambda { |o| o.association(name).handle_dependency } end end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index d4e1a0dda1..4e88b50ec5 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -12,17 +12,21 @@ module ActiveRecord::Associations::Builder !options[:polymorphic] end - def build - reflection = super - add_counter_cache_callbacks(reflection) if options[:counter_cache] - add_touch_callbacks(reflection) if options[:touch] - reflection - end - def valid_dependent_options [:destroy, :delete] end + def define_callbacks(model, reflection) + super + add_counter_cache_callbacks(model, reflection) if options[:counter_cache] + add_touch_callbacks(model, reflection) if options[:touch] + end + + def define_accessors(mixin) + super + add_counter_cache_methods mixin + end + private def add_counter_cache_methods(mixin) @@ -70,9 +74,8 @@ module ActiveRecord::Associations::Builder end end - def add_counter_cache_callbacks(reflection) + def add_counter_cache_callbacks(model, reflection) cache_column = reflection.counter_cache_column - add_counter_cache_methods mixin association = self model.after_create lambda { |record| @@ -92,7 +95,7 @@ module ActiveRecord::Associations::Builder end def self.touch_record(o, foreign_key, name, touch) # :nodoc: - old_foreign_id = o.attribute_was(foreign_key) + old_foreign_id = o.changed_attributes[foreign_key] if old_foreign_id klass = o.association(name).klass @@ -117,7 +120,7 @@ module ActiveRecord::Associations::Builder end end - def add_touch_callbacks(reflection) + def add_touch_callbacks(model, reflection) foreign_key = reflection.foreign_key n = name touch = options[:touch] diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 9c6690b721..7bd0687c0b 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -8,69 +8,54 @@ module ActiveRecord::Associations::Builder CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] def valid_options - super + [:table_name, :finder_sql, :counter_sql, :before_add, + super + [:table_name, :before_add, :after_add, :before_remove, :after_remove, :extend] end - attr_reader :block_extension, :extension_module + attr_reader :block_extension - def initialize(*args, &extension) - super(*args) - @block_extension = extension - end - - def build - show_deprecation_warnings - wrap_block_extension - reflection = super - CALLBACKS.each { |callback_name| define_callback(callback_name) } - reflection + def initialize(name, scope, options) + super + @mod = nil + if block_given? + @mod = Module.new(&Proc.new) + @scope = wrap_scope @scope, @mod + end end - def writable? - true + def define_callbacks(model, reflection) + super + CALLBACKS.each { |callback_name| define_callback(model, callback_name) } end - def show_deprecation_warnings - [:finder_sql, :counter_sql].each do |name| - if options.include? name - ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using scopes).") - end + def define_extensions(model) + if @mod + extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" + model.parent.const_set(extension_module_name, @mod) end end - def wrap_block_extension - if block_extension - @extension_module = mod = Module.new(&block_extension) - silence_warnings do - model.parent.const_set(extension_module_name, mod) - end - - prev_scope = @scope + def define_callback(model, callback_name) + full_callback_name = "#{callback_name}_for_#{name}" - if prev_scope - @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) } + # TODO : why do i need method_defined? I think its because of the inheritance chain + model.class_attribute full_callback_name unless model.method_defined?(full_callback_name) + callbacks = Array(options[callback_name.to_sym]).map do |callback| + case callback + when Symbol + ->(method, owner, record) { owner.send(callback, record) } + when Proc + ->(method, owner, record) { callback.call(owner, record) } else - @scope = proc { extending(mod) } + ->(method, owner, record) { callback.send(method, owner, record) } end end - end - - def extension_module_name - @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" - end - - def define_callback(callback_name) - full_callback_name = "#{callback_name}_for_#{name}" - - # TODO : why do i need method_defined? I think its because of the inheritance chain - model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name) - model.send("#{full_callback_name}=", Array(options[callback_name.to_sym])) + model.send "#{full_callback_name}=", callbacks end # Defines the setter and getter methods for the collection_singular_ids. - def define_readers + def define_readers(mixin) super mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 @@ -80,7 +65,7 @@ module ActiveRecord::Associations::Builder CODE end - def define_writers + def define_writers(mixin) super mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 @@ -89,5 +74,15 @@ module ActiveRecord::Associations::Builder end CODE end + + private + + def wrap_scope(scope, mod) + if scope + proc { |owner| instance_exec(owner, &scope).extending(mod) } + else + proc { extending(mod) } + end + end end end 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 bdac02b5bf..55ec3bf4f1 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 @@ -5,26 +5,11 @@ module ActiveRecord::Associations::Builder end def valid_options - super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql] + super + [:join_table, :association_foreign_key] end - def build - reflection = super - define_destroy_hook - reflection - end - - def show_deprecation_warnings + def define_callbacks(model, reflection) super - - [:delete_sql, :insert_sql].each do |name| - if options.include? name - ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using has_many :through).") - end - end - end - - def define_destroy_hook name = self.name model.send(:include, Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 0d1bdd21ee..a60cb4769a 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -9,7 +9,7 @@ module ActiveRecord::Associations::Builder end def valid_dependent_options - [:destroy, :delete_all, :nullify, :restrict, :restrict_with_error, :restrict_with_exception] + [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception] end end end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 0da564f402..62d454ce55 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -14,12 +14,14 @@ module ActiveRecord::Associations::Builder !options[:through] end - def configure_dependency - super unless options[:through] + def valid_dependent_options + [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception] end - def valid_dependent_options - [:destroy, :delete, :nullify, :restrict, :restrict_with_error, :restrict_with_exception] + private + + def add_before_destroy_callbacks(model, name) + super unless options[:through] end end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 76e48e66e5..d97c0e9afd 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -10,14 +10,14 @@ module ActiveRecord::Associations::Builder true end - def define_accessors + def define_accessors(model) super - define_constructors if constructable? + define_constructors(model.generated_feature_methods) if constructable? end # Defines the (build|create)_association methods for belongs_to or has_one association - - def define_constructors + + def define_constructors(mixin) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def build_#{name}(*args, &block) association(:#{name}).build(*args, &block) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 9833822f8f..228c500f0a 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -34,7 +34,7 @@ module ActiveRecord reload end - @proxy ||= CollectionProxy.new(klass, self) + @proxy ||= CollectionProxy.create(klass, self) end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -44,7 +44,7 @@ module ActiveRecord # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items def ids_reader - if loaded? || options[:finder_sql] + if loaded? load_target.map do |record| record.send(reflection.association_primary_key) end @@ -79,9 +79,7 @@ module ActiveRecord if block_given? load_target.find(*args) { |*block_args| yield(*block_args) } else - if options[:finder_sql] - find_by_scan(*args) - elsif options[:inverse_of] && loaded? + if options[:inverse_of] && loaded? args = args.flatten raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args.blank? @@ -153,11 +151,35 @@ module ActiveRecord end end - # Remove all records from this association. + # Removes all records from the association without calling callbacks + # on the associated records. It honors the `:dependent` option. However + # if the `:dependent` value is `:destroy` then in that case the default + # deletion strategy for the association is applied. + # + # You can force a particular deletion strategy by passing a parameter. + # + # Example: + # + # @author.books.delete_all(:nullify) + # @author.books.delete_all(:delete_all) # # See delete for more info. - def delete_all - delete(:all).tap do + def delete_all(dependent = nil) + if dependent.present? && ![:nullify, :delete_all].include?(dependent) + raise ArgumentError, "Valid values are :nullify or :delete_all" + end + + dependent = if dependent.present? + dependent + elsif options[:dependent] == :destroy + # since delete_all should not invoke callbacks so use the default deletion strategy + # for :destroy + reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) ? :delete_all : :nullify + else + options[:dependent] + end + + delete(:all, dependent: dependent).tap do reset loaded! end @@ -173,36 +195,27 @@ module ActiveRecord end end - # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the - # association, it will be used for the query. Otherwise, construct options and pass them with + # Count all records using SQL. Construct options and pass them with # scope to the target class's +count+. def count(column_name = nil, count_options = {}) column_name, count_options = nil, column_name if column_name.is_a?(Hash) - if options[:counter_sql] || options[:finder_sql] - unless count_options.blank? - raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed" - end - - reflection.klass.count_by_sql(custom_counter_sql) - else - relation = scope - if association_scope.distinct_value - # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. - column_name ||= reflection.klass.primary_key - relation = relation.distinct - end + relation = scope + if association_scope.distinct_value + # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. + column_name ||= reflection.klass.primary_key + relation = relation.distinct + end - value = relation.count(column_name) + value = relation.count(column_name) - limit = options[:limit] - offset = options[:offset] + limit = options[:limit] + offset = options[:offset] - if limit || offset - [ [value - offset.to_i, 0].max, limit.to_i ].min - else - value - end + if limit || offset + [ [value - offset.to_i, 0].max, limit.to_i ].min + else + value end end @@ -214,18 +227,10 @@ 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) - dependent = options[:dependent] + _options = records.extract_options! + dependent = _options[:dependent] || options[:dependent] if records.first == :all - - if dependent && dependent == :destroy - message = 'In Rails 4.1 delete_all on associations would not fire callbacks. ' \ - 'It means if the :dependent option is :destroy then the associated ' \ - 'records would be deleted without loading and invoking callbacks.' - - ActiveRecord::Base.logger ? ActiveRecord::Base.logger.warn(message) : $stderr.puts(message) - end - if loaded? || dependent == :destroy delete_or_destroy(load_target, dependent) else @@ -285,14 +290,14 @@ module ActiveRecord # Returns true if the collection is empty. # - # If the collection has been loaded or the <tt>:counter_sql</tt> option - # is provided, it is equivalent to <tt>collection.size.zero?</tt>. If the + # If the collection has been loaded + # it is equivalent to <tt>collection.size.zero?</tt>. If the # collection has not been loaded, it is equivalent to # <tt>collection.exists?</tt>. If the collection has not already been # loaded and you are going to fetch the records anyway it is better to # check <tt>collection.length.zero?</tt>. def empty? - if loaded? || options[:counter_sql] + if loaded? size.zero? else @target.blank? && !scope.exists? @@ -345,7 +350,6 @@ module ActiveRecord if record.new_record? include_in_memory?(record) else - load_target if options[:finder_sql] loaded? ? target.include?(record) : scope.exists?(record) end else @@ -362,8 +366,8 @@ module ActiveRecord target end - def add_to_target(record) - callback(:before_add, record) + def add_to_target(record, skip_callbacks = false) + callback(:before_add, record) unless skip_callbacks yield(record) if block_given? if association_scope.distinct_value && index = @target.index(record) @@ -372,7 +376,7 @@ module ActiveRecord @target << record end - callback(:after_add, record) + callback(:after_add, record) unless skip_callbacks set_inverse_instance(record) record @@ -390,31 +394,8 @@ module ActiveRecord private - def custom_counter_sql - if options[:counter_sql] - interpolate(options[:counter_sql]) - else - # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */ - interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do - count_with = $2.to_s - count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/ - "SELECT #{$1}COUNT(#{count_with}) FROM" - end - end - end - - def custom_finder_sql - interpolate(options[:finder_sql]) - end - def find_target - records = - if options[:finder_sql] - reflection.klass.find_by_sql(custom_finder_sql) - else - scope.to_a - end - + records = scope.to_a records.each { |record| set_inverse_instance(record) } records end @@ -529,20 +510,13 @@ module ActiveRecord def callback(method, record) callbacks_for(method).each do |callback| - case callback - when Symbol - owner.send(callback, record) - when Proc - callback.call(owner, record) - else - callback.send(method, owner, record) - end + callback.call(method, owner, record) end end def callbacks_for(callback_name) full_callback_name = "#{callback_name}_for_#{reflection.name}" - owner.class.send(full_callback_name.to_sym) || [] + owner.class.send(full_callback_name) end # Should we deal with assoc.first or assoc.last by issuing an independent query to @@ -553,7 +527,6 @@ module ActiveRecord # Otherwise, go to the database only if none of the following are true: # * target already loaded # * owner is new record - # * custom :finder_sql exists # * target contains new or changed record(s) # * the first arg is an integer (which indicates the number of records to be returned) def fetch_first_or_last_using_find?(args) @@ -562,7 +535,6 @@ module ActiveRecord else !(loaded? || owner.new_record? || - options[:finder_sql] || target.any? { |record| record.new_record? || record.changed? } || args.first.kind_of?(Integer)) end @@ -579,7 +551,7 @@ module ActiveRecord end end - # If using a custom finder_sql or if the :inverse_of option has been + # If the :inverse_of option has been # specified, then #find scans the entire collection. def find_by_scan(*args) expects_array = args.first.kind_of?(Array) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 7cdb5ba5b3..6dc2da56d1 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -33,7 +33,6 @@ module ActiveRecord def initialize(klass, association) #:nodoc: @association = association super klass, klass.arel_table - self.default_scoped = true merge! association.scope(nullify: false) end @@ -418,8 +417,8 @@ module ActiveRecord # # Pet.find(1, 2, 3) # # => ActiveRecord::RecordNotFound - def delete_all - @association.delete_all + def delete_all(dependent = nil) + @association.delete_all(dependent) end # Deletes the records of the collection directly from the database @@ -727,7 +726,7 @@ module ActiveRecord end # Returns +true+ if the collection is empty. If the collection has been - # loaded or the <tt>:counter_sql</tt> option is provided, it is equivalent + # loaded it is equivalent # to <tt>collection.size.zero?</tt>. If the collection has not been loaded, # it is equivalent to <tt>collection.exists?</tt>. If the collection has # not already been loaded and you are going to fetch the records anyway it 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 bb3e3db379..b2e6c708bf 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 @@ -18,16 +18,12 @@ module ActiveRecord end end - if options[:insert_sql] - owner.connection.insert(interpolate(options[:insert_sql], record)) - else - stmt = join_table.compile_insert( - join_table[reflection.foreign_key] => owner.id, - join_table[reflection.association_foreign_key] => record.id - ) + stmt = join_table.compile_insert( + join_table[reflection.foreign_key] => owner.id, + join_table[reflection.association_foreign_key] => record.id + ) - owner.class.connection.insert stmt - end + owner.class.connection.insert stmt record end @@ -39,22 +35,17 @@ module ActiveRecord end def delete_records(records, method) - if sql = options[:delete_sql] - records = load_target if records == :all - records.each { |record| owner.class.connection.delete(interpolate(sql, record)) } - else - 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.class.connection.delete(relation.where(condition).compile_delete) + 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.class.connection.delete(relation.where(condition).compile_delete) end def invertible_for?(record) diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index cf8a589496..607ed0da46 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -9,7 +9,7 @@ module ActiveRecord def handle_dependency case options[:dependent] - when :restrict, :restrict_with_exception + when :restrict_with_exception raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty? when :restrict_with_error @@ -58,8 +58,6 @@ module ActiveRecord def count_records count = if has_cached_counter? owner.send(:read_attribute, cached_counter_attribute_name) - elsif options[:counter_sql] || options[:finder_sql] - reflection.klass.count_by_sql(custom_counter_sql) else scope.count end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 920038a543..3ab1ea1ff4 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -6,7 +6,7 @@ module ActiveRecord def handle_dependency case options[:dependent] - when :restrict, :restrict_with_exception + when :restrict_with_exception raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target when :restrict_with_error 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 b81aecb4e5..58fc00d811 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -25,11 +25,8 @@ module ActiveRecord attr_reader :tables 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! @@ -47,6 +44,9 @@ module ActiveRecord @tables = construct_tables.reverse end + def parent_table_name; parent.table_name; end + alias :alias_suffix :parent_table_name + def ==(other) other.class == self.class && other.reflection == reflection && @@ -64,15 +64,20 @@ module ActiveRecord end end - def join_to(manager) + def join_constraints + joins = [] tables = @tables.dup - foreign_table = parent_table + + foreign_table = parent.table foreign_klass = parent.base_klass + scope_chain_iter = reflection.scope_chain.reverse_each + # 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| + chain.reverse_each do |reflection| table = tables.shift + klass = reflection.klass case reflection.source_macro when :belongs_to @@ -80,11 +85,10 @@ module ActiveRecord foreign_key = reflection.foreign_key when :has_and_belongs_to_many # Join the join table first... - manager.from(join( + joins << join( table, table[reflection.foreign_key]. - eq(foreign_table[reflection.active_record_primary_key]) - )) + eq(foreign_table[reflection.active_record_primary_key])) foreign_table, table = table, tables.shift @@ -95,36 +99,39 @@ module ActiveRecord foreign_key = reflection.active_record_primary_key end - constraint = build_constraint(reflection, table, key, foreign_table, foreign_key) + constraint = build_constraint(klass, table, key, foreign_table, foreign_key) - scope_chain_items = scope_chain[i] + scope_chain_items = scope_chain_iter.next.map do |item| + if item.is_a?(Relation) + item + else + ActiveRecord::Relation.create(klass, table).instance_exec(self, &item) + end + end if reflection.type - scope_chain_items += [ - ActiveRecord::Relation.new(reflection.klass, table) + scope_chain_items << + ActiveRecord::Relation.create(klass, table) .where(reflection.type => foreign_klass.base_class.name) - ] end - constraint = scope_chain_items.inject(constraint) do |chain, item| - unless item.is_a?(Relation) - item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item) - end + scope_chain_items.concat [klass.send(:build_default_scope)].compact - if item.arel.constraints.empty? - chain - else - chain.and(item.arel.constraints) - end + rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| + left.merge right + end + + if rel && !rel.arel.constraints.empty? + constraint = constraint.and rel.arel.constraints end - manager.from(join(table, constraint)) + joins << join(table, constraint) # The current table in this iteration becomes the foreign table in the next - foreign_table, foreign_klass = table, reflection.klass + foreign_table, foreign_klass = table, klass end - manager + joins end # Builds equality condition. @@ -142,13 +149,13 @@ module ActiveRecord # foreign_table #=> #<Arel::Table @name="physicians" ...> # foreign_key #=> id # - def build_constraint(reflection, table, key, foreign_table, foreign_key) + def build_constraint(klass, table, key, foreign_table, foreign_key) constraint = table[key].eq(foreign_table[foreign_key]) - if reflection.klass.finder_needs_type_condition? + if klass.finder_needs_type_condition? constraint = table.create_and([ constraint, - reflection.klass.send(:type_condition, table) + klass.send(:type_condition, table) ]) end @@ -167,11 +174,6 @@ module ActiveRecord def aliased_table_name table.table_alias || table.name end - - def scope_chain - @scope_chain ||= reflection.scope_chain.reverse - end - end end end diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb index 5a41b40c8f..27b70edf1a 100644 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -19,7 +19,7 @@ module ActiveRecord if reflection.source_macro == :has_and_belongs_to_many tables << alias_tracker.aliased_table_for( - (reflection.source_reflection || reflection).join_table, + reflection.source_reflection.join_table, table_alias_for(reflection, true) ) end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 82bf426b22..2317e34bc0 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -85,7 +85,7 @@ module ActiveRecord def initialize(records, associations, preload_scope = nil) @records = Array.wrap(records).compact.uniq @associations = Array.wrap(associations) - @preload_scope = preload_scope || Relation.new(nil, nil) + @preload_scope = preload_scope || Relation.create(nil, nil) end def run diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 82588905c6..0cc836f991 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -77,7 +77,7 @@ module ActiveRecord # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) # Make several smaller queries if necessary or make one query if the adapter supports it sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) - records = sliced.map { |slice| records_for(slice).to_a }.flatten + records = sliced.flat_map { |slice| records_for(slice).to_a } end # Each record may have multiple owners, and vice-versa @@ -98,7 +98,6 @@ module ActiveRecord def build_scope scope = klass.unscoped - scope.default_scoped = true values = reflection_scope.values preload_values = preload_scope.values @@ -113,7 +112,7 @@ module ActiveRecord scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end - scope + klass.default_scoped.merge(scope) 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 c4b50ab306..de06931845 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -41,21 +41,21 @@ module ActiveRecord end def through_scope - through_scope = through_reflection.klass.unscoped + scope = through_reflection.klass.unscoped if options[:source_type] - through_scope.where! reflection.foreign_type => options[:source_type] + scope.where! reflection.foreign_type => options[:source_type] else unless reflection_scope.where_values.empty? - through_scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) - through_scope.where_values = reflection_scope.values[:where] + scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) + scope.where_values = reflection_scope.values[:where] end - through_scope.references! reflection_scope.values[:references] - through_scope.order! reflection_scope.values[:order] if through_scope.eager_loading? + scope.references! reflection_scope.values[:references] + scope.order! reflection_scope.values[:order] if scope.eager_loading? end - through_scope + scope end end end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 35f29b37a2..ba7d2a3782 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -13,9 +13,9 @@ module ActiveRecord # 2. To get the type conditions for any STI models in the chain def target_scope scope = super - chain[1..-1].each do |reflection| + chain.drop(1).each do |reflection| scope.merge!( - reflection.klass.all.with_default_scope. + reflection.klass.all. except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 75377bba57..4f06955406 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -3,7 +3,6 @@ require 'active_model/forbidden_attributes_protection' module ActiveRecord module AttributeAssignment extend ActiveSupport::Concern - include ActiveModel::DeprecatedMassAssignmentSecurity include ActiveModel::ForbiddenAttributesProtection # Allows you to set all the attributes by passing in a hash of attributes with diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 609c6e8cab..208da2cb77 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/enumerable' +require 'mutex_m' module ActiveRecord # = Active Record Attribute Methods @@ -7,6 +8,7 @@ module ActiveRecord include ActiveModel::AttributeMethods included do + initialize_generated_modules include Read include Write include BeforeTypeCast @@ -17,27 +19,71 @@ module ActiveRecord include Serialization end + AttrNames = Module.new { + def self.set_name_cache(name, value) + const_name = "ATTR_#{name}" + unless const_defined? const_name + const_set const_name, value.dup.freeze + end + end + } + + class AttributeMethodCache + include Mutex_m + + def initialize + super + @module = Module.new + @method_cache = {} + end + + def [](name) + synchronize do + @method_cache.fetch(name) { + safe_name = name.unpack('h*').first + temp_method = "__temp__#{safe_name}" + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__ + @method_cache[name] = @module.instance_method temp_method + } + end + end + + private + def method_body; raise NotImplementedError; end + end + module ClassMethods + def inherited(child_class) #:nodoc: + child_class.initialize_generated_modules + super + end + + def initialize_generated_modules # :nodoc: + @generated_attribute_methods = Module.new { extend Mutex_m } + @attribute_methods_generated = false + include @generated_attribute_methods + end + # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods # :nodoc: # Use a mutex; we don't want two thread simultaneously trying to define # attribute methods. - @attribute_methods_mutex.synchronize do - return if attribute_methods_generated? + generated_attribute_methods.synchronize do + return false if @attribute_methods_generated superclass.define_attribute_methods unless self == base_class super(column_names) @attribute_methods_generated = true end - end - - def attribute_methods_generated? # :nodoc: - @attribute_methods_generated ||= false + true end def undefine_attribute_methods # :nodoc: - super if attribute_methods_generated? - @attribute_methods_generated = false + generated_attribute_methods.synchronize do + super if @attribute_methods_generated + @attribute_methods_generated = false + end end # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an @@ -119,9 +165,7 @@ module ActiveRecord # If we haven't generated any methods yet, generate them, then # see if we've created the method we're looking for. def method_missing(method, *args, &block) # :nodoc: - unless self.class.attribute_methods_generated? - self.class.define_attribute_methods - + if self.class.define_attribute_methods if respond_to_without_attributes?(method) send(method, *args, &block) else @@ -132,20 +176,6 @@ module ActiveRecord end end - def attribute_missing(match, *args, &block) # :nodoc: - if self.class.columns_hash[match.attr_name] - ActiveSupport::Deprecation.warn( - "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \ - "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \ - "is a column of the table. If this error has happened through normal usage of Active " \ - "Record (rather than through your own code or external libraries), please report it as " \ - "a bug." - ) - end - - super - end - # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt> # which will all return +true+. It also define the attribute methods if they have @@ -164,7 +194,7 @@ module ActiveRecord # person.respond_to(:nothing) # => false def respond_to?(name, include_private = false) name = name.to_s - self.class.define_attribute_methods unless self.class.attribute_methods_generated? + self.class.define_attribute_methods result = super # If the result is false the answer is false. @@ -174,7 +204,7 @@ module ActiveRecord # For queries selecting a subset of columns, return false for unselected columns. # We check defined?(@attributes) not to issue warnings if called on objects that # have been allocated but not yet initialized. - if defined?(@attributes) && @attributes.present? && self.class.column_names.include?(name) + if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name) return has_attribute?(name) end @@ -229,7 +259,7 @@ module ActiveRecord # person = Person.create!(name: 'David Heinemeier Hansson ' * 3) # # person.attribute_for_inspect(:name) - # # => "\"David Heinemeier Hansson David Heinemeier Hansson D...\"" + # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\"" # # person.attribute_for_inspect(:created_at) # # => "\"2012-10-22 00:15:07\"" @@ -237,7 +267,7 @@ module ActiveRecord value = read_attribute(attr_name) if value.is_a?(String) && value.length > 50 - "#{value[0..50]}...".inspect + "#{value[0, 50]}...".inspect elsif value.is_a?(Date) || value.is_a?(Time) %("#{value.to_s(:db)}") else diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 6315dd9549..dc2399643c 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -14,17 +14,6 @@ module ActiveRecord class_attribute :partial_writes, instance_writer: false self.partial_writes = true - - def self.partial_updates=(v); self.partial_writes = v; end - def self.partial_updates?; partial_writes?; end - def self.partial_updates; partial_writes; end - - ActiveSupport::Deprecation.deprecate_methods( - singleton_class, - :partial_updates= => :partial_writes=, - :partial_updates? => :partial_writes?, - :partial_updates => :partial_writes - ) end # Attempts to +save+ the record and clears changed attributes if successful. diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 506f5d75f9..1cf3aba41c 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,6 +1,38 @@ +require 'active_support/core_ext/module/method_transplanting' + module ActiveRecord module AttributeMethods module Read + ReaderMethodCache = Class.new(AttributeMethodCache) { + private + # We want to generate the methods via module_eval rather than + # define_method, because define_method is slower on dispatch. + # Evaluating many similar methods may use more memory as the instruction + # sequences are duplicated and cached (in MRI). define_method may + # be slower on dispatch, but if you're careful about the closure + # created, then define_method will consume much less memory. + # + # But sometimes the database might return columns with + # characters that are not allowed in normal method names (like + # 'my_column(omg)'. So to work around this we first define with + # the __temp__ identifier, and then use alias method to rename + # it to what we want. + # + # We are also defining a constant to hold the frozen string of + # the attribute name. Using a constant means that we do not have + # to allocate an object on each call to the attribute method. + # Making it frozen means that it doesn't get duped when used to + # key the @attributes_cache in read_attribute. + def method_body(method_name, const_name) + <<-EOMETHOD + def #{method_name} + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} + read_attribute(name) { |n| missing_attribute(n, caller) } + end + EOMETHOD + end + }.new + extend ActiveSupport::Concern ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] @@ -32,30 +64,30 @@ module ActiveRecord protected - # We want to generate the methods via module_eval rather than - # define_method, because define_method is slower on dispatch and - # uses more memory (because it creates a closure). - # - # But sometimes the database might return columns with - # characters that are not allowed in normal method names (like - # 'my_column(omg)'. So to work around this we first define with - # the __temp__ identifier, and then use alias method to rename - # it to what we want. - # - # We are also defining a constant to hold the frozen string of - # the attribute name. Using a constant means that we do not have - # to allocate an object on each call to the attribute method. - # Making it frozen means that it doesn't get duped when used to - # key the @attributes_cache in read_attribute. - def define_method_attribute(name) - safe_name = name.unpack('h*').first - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name} - read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) } + if Module.methods_transplantable? + def define_method_attribute(name) + method = ReaderMethodCache[name] + generated_attribute_methods.module_eval { define_method name, method } + end + else + def define_method_attribute(name) + safe_name = name.unpack('h*').first + temp_method = "__temp__#{safe_name}" + + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def #{temp_method} + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + read_attribute(name) { |n| missing_attribute(n, caller) } + end + STR + + generated_attribute_methods.module_eval do + alias_method name, temp_method + undef_method temp_method end - alias_method #{name.inspect}, :__temp__#{safe_name} - undef_method :__temp__#{safe_name} - STR + end end private diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index cd33494cc3..c853fc0917 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -1,6 +1,21 @@ +require 'active_support/core_ext/module/method_transplanting' + module ActiveRecord module AttributeMethods module Write + WriterMethodCache = Class.new(AttributeMethodCache) { + private + + def method_body(method_name, const_name) + <<-EOMETHOD + def #{method_name}(value) + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} + write_attribute(name, value) + end + EOMETHOD + end + }.new + extend ActiveSupport::Concern included do @@ -10,17 +25,29 @@ module ActiveRecord module ClassMethods protected - # See define_method_attribute in read.rb for an explanation of - # this code. - def define_method_attribute=(name) - safe_name = name.unpack('h*').first - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name}=(value) - write_attribute(AttrNames::ATTR_#{safe_name}, value) - end - alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= - undef_method :__temp__#{safe_name}= - STR + if Module.methods_transplantable? + # See define_method_attribute in read.rb for an explanation of + # this code. + def define_method_attribute=(name) + method = WriterMethodCache[name] + generated_attribute_methods.module_eval { + define_method "#{name}=", method + } + end + else + def define_method_attribute=(name) + safe_name = name.unpack('h*').first + ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name + + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 + def __temp__#{safe_name}=(value) + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + write_attribute(name, value) + end + alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= + undef_method :__temp__#{safe_name}= + STR + end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index a8a1847554..b30d1eb0a6 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -127,17 +127,17 @@ module ActiveRecord extend ActiveSupport::Concern module AssociationBuilderExtension #:nodoc: - def build + def self.build(model, reflection) model.send(:add_autosave_association_callbacks, reflection) - super + end + + def self.valid_options + [ :autosave ] end end included do - Associations::Builder::Association.class_eval do - self.valid_options << :autosave - include AssociationBuilderExtension - end + Associations::Builder::Association.extensions << AssociationBuilderExtension end module ClassMethods @@ -343,6 +343,7 @@ module ActiveRecord end records.each do |record| + next if record.destroyed? saved = true 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 816b397fcf..cfdcae7f63 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -332,11 +332,6 @@ module ActiveRecord end end - def clear_stale_cached_connections! # :nodoc: - reap - end - deprecate :clear_stale_cached_connections! => "Please use #reap instead" - # Check-out a database connection from the pool, indicating that you want # to use it. You should call #checkin when you no longer need this. # @@ -629,7 +624,7 @@ module ActiveRecord end response - rescue + rescue Exception ActiveRecord::Base.clear_active_connections! unless testing raise end 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 c64b542286..32244b1755 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -18,8 +18,7 @@ module ActiveRecord end end - # Returns an array of record hashes with the column names as keys and - # column values as values. + # Returns an ActiveRecord::Result instance. def select_all(arel, name = nil, binds = []) select(to_sql(arel, binds), name, binds) end @@ -27,8 +26,7 @@ module ActiveRecord # Returns a record hash with the column names as keys and column values # as values. def select_one(arel, name = nil, binds = []) - result = select_all(arel, name, binds) - result.first if result + select_all(arel, name, binds).first end # Returns a single value from a record @@ -41,8 +39,8 @@ module ActiveRecord # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] def select_values(arel, name = nil) - result = select_rows(to_sql(arel, []), name) - result.map { |v| v[0] } + select_rows(to_sql(arel, []), name) + .map { |v| v[0] } end # Returns an array of arrays containing the field values. @@ -355,8 +353,7 @@ module ActiveRecord subselect end - # Returns an array of record hashes with the column names as keys and - # column values as values. + # Returns an ActiveRecord::Result instance. def select(sql, name = nil, binds = []) end undef_method :select @@ -377,14 +374,14 @@ module ActiveRecord update_sql(sql, name) end - def sql_for_insert(sql, pk, id_value, sequence_name, binds) - [sql, binds] - end - - def last_inserted_id(result) - row = result.rows.first - row && row.first - end + def sql_for_insert(sql, pk, id_value, sequence_name, binds) + [sql, binds] + end + + def last_inserted_id(result) + row = result.rows.first + row && row.first + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 41e07fbda9..7aae297cdc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -20,6 +20,12 @@ module ActiveRecord attr_reader :query_cache, :query_cache_enabled + def initialize(*) + super + @query_cache = Hash.new { |h,sql| h[sql] = {} } + @query_cache_enabled = false + end + # Enable the query cache within the block. def cache old, @query_cache_enabled = @query_cache_enabled, true @@ -75,14 +81,7 @@ module ActiveRecord else @query_cache[sql][binds] = yield end - - # FIXME: we should guarantee that all cached items are Result - # objects. Then we can avoid this conditional - if ActiveRecord::Result === result - result.dup - else - result.collect { |row| row.dup } - end + result.dup end # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index d18b9c991f..c8fa4ef9af 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -15,7 +15,6 @@ module ActiveRecord return "'#{quote_string(value)}'" unless column case column.type - when :binary then "'#{quote_string(column.string_to_binary(value))}'" when :integer then value.to_i.to_s when :float then value.to_f.to_s else 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 0be4b5cb19..063b19871a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -16,9 +16,6 @@ module ActiveRecord # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key) #:nodoc: - def string_to_binary(value) - value - end def primary_key? primary_key || type.to_sym == :primary_key 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 9a1923dec5..4b425494d0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -694,15 +694,6 @@ module ActiveRecord end end - # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. - # - # distinct("posts.id", ["posts.created_at desc"]) - # - def distinct(columns, order_by) - ActiveSupport::Deprecation.warn("#distinct is deprecated and shall be removed from future releases.") - "DISTINCT #{columns_for_distinct(columns, order_by)}" - end - # Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT. # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they # require the order columns appear in the SELECT. @@ -812,12 +803,6 @@ module ActiveRecord index_name end - def columns_for_remove(table_name, *column_names) - ActiveSupport::Deprecation.warn("columns_for_remove is deprecated and will be removed in the future") - raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.blank? - column_names.map {|column_name| quote_column_name(column_name) } - end - def rename_table_indexes(table_name, new_name) indexes(new_name).each do |index| generated_index_name = index_name(table_name, column: index.columns) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index e232cad982..ba1cb05d2c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -95,8 +95,6 @@ module ActiveRecord @last_use = false @logger = logger @pool = pool - @query_cache = Hash.new { |h,sql| h[sql] = {} } - @query_cache_enabled = false @schema_cache = SchemaCache.new self @visitor = nil end @@ -293,6 +291,14 @@ module ActiveRecord false end + # This is meant to be implemented by the adapters that support extensions + def disable_extension(name) + end + + # This is meant to be implemented by the adapters that support extensions + def enable_extension(name) + end + # A list of extensions, to be filled in by adapters that support them. At # the moment only postgresql does. def extensions @@ -307,8 +313,8 @@ module ActiveRecord # QUOTING ================================================== - # Returns a bind substitution value given a +column+ and list of current - # +binds+. + # Returns a bind substitution value given a bind +index+ and +column+ + # NOTE: The column param is currently being used by the sqlserver-adapter def substitute_at(column, index) Arel::Nodes::BindParam.new '?' end @@ -387,20 +393,6 @@ module ActiveRecord @transaction.number end - def increment_open_transactions - ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect" - end - - def decrement_open_transactions - ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect" - end - - def transaction_joinable=(joinable) - message = "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead." - ActiveSupport::Deprecation.warn message - @transaction.joinable = joinable - end - def create_savepoint end 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 5b25b26164..4b11ea795c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -246,8 +246,8 @@ module ActiveRecord # QUOTING ================================================== 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] + if value.kind_of?(String) && column && column.type == :binary + s = value.unpack("H*")[0] "x'#{s}'" elsif value.kind_of?(BigDecimal) value.to_s("F") @@ -469,7 +469,8 @@ module ActiveRecord sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}" execute_and_free(sql, 'SCHEMA') do |result| each_hash(result).map do |field| - new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra]) + field_name = set_field_encoding(field[:Field]) + new_column(field_name, field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra]) end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 609ccc2ed2..bccfa41ad1 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -107,30 +107,6 @@ module ActiveRecord end end - def type_cast_code(var_name) - message = "Column#type_cast_code is deprecated in favor of using Column#type_cast only, " \ - "and it is going to be removed in future Rails versions." - ActiveSupport::Deprecation.warn message - - klass = self.class.name - - case type - when :string, :text then var_name - when :integer then "#{klass}.value_to_integer(#{var_name})" - when :float then "#{var_name}.to_f" - when :decimal then "#{klass}.value_to_decimal(#{var_name})" - when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})" - when :time then "#{klass}.string_to_dummy_time(#{var_name})" - when :date then "#{klass}.value_to_date(#{var_name})" - 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})" - when :json then "#{klass}.string_to_json(#{var_name})" - else var_name - end - end - # Returns the human name of the column name. # # ===== Examples @@ -143,17 +119,7 @@ module ActiveRecord type_cast(default) end - # Used to convert from Strings to BLOBs - def string_to_binary(value) - self.class.string_to_binary(value) - end - class << self - # Used to convert from Strings to BLOBs - def string_to_binary(value) - value - end - # Used to convert from BLOBs to Strings def binary_to_string(value) value diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index edeb338310..92796c996e 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,6 +1,6 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' -gem 'mysql2', '~> 0.3.10' +gem 'mysql2', '~> 0.3.13' require 'mysql2' module ActiveRecord @@ -229,8 +229,7 @@ module ActiveRecord alias exec_without_stmt exec_query - # Returns an array of record hashes with the column names as keys and - # column values as values. + # Returns an ActiveRecord::Result instance. def select(sql, name = nil, binds = []) exec_query(sql, name) end @@ -270,6 +269,10 @@ module ActiveRecord def version @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } end + + def set_field_encoding field_name + field_name + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 1826d88500..15b5452850 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -507,12 +507,12 @@ module ActiveRecord cols = cache[:cols] ||= metadata.fetch_fields.map { |field| field.name } + metadata.free end result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols affected_rows = stmt.affected_rows - stmt.result_metadata.free if cols stmt.free_result stmt.close if binds.empty? @@ -559,6 +559,14 @@ module ActiveRecord def version @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } end + + def set_field_encoding field_name + field_name.force_encoding(client_encoding) + if internal_enc = Encoding.default_internal + field_name = field_name.encoding(internal_enc) + end + field_name + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb index b7d24f2bb3..20de8d1982 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb @@ -2,6 +2,13 @@ module ActiveRecord module ConnectionAdapters class PostgreSQLColumn < Column module ArrayParser + + DOUBLE_QUOTE = '"' + BACKSLASH = "\\" + COMMA = ',' + BRACKET_OPEN = '{' + BRACKET_CLOSE = '}' + private # Loads pg_array_parser if available. String parsing can be # performed quicker by a native extension, which will not create @@ -12,18 +19,18 @@ module ActiveRecord include PgArrayParser rescue LoadError def parse_pg_array(string) - parse_data(string, 0) + parse_data(string) end end - def parse_data(string, index) - local_index = index + def parse_data(string) + local_index = 0 array = [] while(local_index < string.length) case string[local_index] - when '{' + when BRACKET_OPEN local_index,array = parse_array_contents(array, string, local_index + 1) - when '}' + when BRACKET_CLOSE return array end local_index += 1 @@ -33,9 +40,9 @@ module ActiveRecord end def parse_array_contents(array, string, index) - is_escaping = false - is_quoted = false - was_quoted = false + is_escaping = false + is_quoted = false + was_quoted = false current_item = '' local_index = index @@ -47,29 +54,29 @@ module ActiveRecord else if is_quoted case token - when '"' + when DOUBLE_QUOTE is_quoted = false was_quoted = true - when "\\" + when BACKSLASH is_escaping = true else current_item << token end else case token - when "\\" + when BACKSLASH is_escaping = true - when ',' + when COMMA add_item_to_array(array, current_item, was_quoted) current_item = '' was_quoted = false - when '"' + when DOUBLE_QUOTE is_quoted = true - when '{' + when BRACKET_OPEN internal_items = [] local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1) array.push(internal_items) - when '}' + when BRACKET_CLOSE add_item_to_array(array, current_item, was_quoted) return local_index,array else diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index a73f0ac57f..ef7b976d4f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -100,7 +100,11 @@ module ActiveRecord if string.nil? nil elsif String === string - IPAddr.new(string) + begin + IPAddr.new(string) + rescue ArgumentError + nil + end else string end @@ -115,7 +119,7 @@ module ActiveRecord end def string_to_array(string, oid) - parse_pg_array(string).map{|val| oid.type_cast val} + parse_pg_array(string).map {|val| type_cast_array(oid, val)} end private @@ -146,6 +150,14 @@ module ActiveRecord "\"#{value.gsub(/"/,"\\\"")}\"" end end + + def type_cast_array(oid, value) + if ::Array === value + value.map {|item| type_cast_array(oid, item)} + else + oid.type_cast value + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 9b5170f657..751655e61c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -218,13 +218,6 @@ module ActiveRecord execute "ROLLBACK" end - def outside_transaction? - message = "#outside_transaction? is deprecated. This method was only really used " \ - "internally, but you can use #transaction_open? instead." - ActiveSupport::Deprecation.warn message - @connection.transaction_status == PGconn::PQTRANS_IDLE - end - def create_savepoint execute("SAVEPOINT #{current_savepoint_name}") end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 1be116ce10..58c6235059 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -6,10 +6,6 @@ module ActiveRecord module OID class Type def type; end - - def type_cast_for_write(value) - value - end end class Identity < Type @@ -224,6 +220,10 @@ module ActiveRecord end class Hstore < Type + def type_cast_for_write(value) + ConnectionAdapters::PostgreSQLColumn.hstore_to_string value + end + def type_cast(value) return if value.nil? diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index a651b6c32e..3fce8de1ba 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -384,8 +384,9 @@ module ActiveRecord def change_column(table_name, column_name, type, options = {}) clear_cache! quoted_table_name = quote_table_name(table_name) - - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale]) + sql_type << "[]" if options[:array] + execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}" change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 98126249df..5b9453a579 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -129,6 +129,14 @@ module ActiveRecord end end + def type_cast_for_write(value) + if @oid_type.respond_to?(:type_cast_for_write) + @oid_type.type_cast_for_write(value) + else + super + end + end + def type_cast(value) return if value.nil? return super if encoded? @@ -373,15 +381,11 @@ module ActiveRecord self end - def xml(options = {}) - column(args[0], :text, options) - end - private - def create_column_definition(name, type) - ColumnDefinition.new name, type - end + def create_column_definition(name, type) + ColumnDefinition.new name, type + end end class Table < ActiveRecord::ConnectionAdapters::Table diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 1d7a22e831..e5c9f6f54a 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,4 +1,3 @@ -require 'active_support/deprecation/reporting' module ActiveRecord module ConnectionAdapters @@ -16,13 +15,8 @@ module ActiveRecord prepare_default_proc end - def primary_keys(table_name = nil) - if table_name - @primary_keys[table_name] - else - ActiveSupport::Deprecation.warn('call primary_keys with a table name!') - @primary_keys.dup - end + def primary_keys(table_name) + @primary_keys[table_name] end # A cached lookup for table existence. @@ -41,34 +35,19 @@ module ActiveRecord end end - def tables(name = nil) - if name - @tables[name] - else - ActiveSupport::Deprecation.warn('call tables with a name!') - @tables.dup - end + def tables(name) + @tables[name] end # Get the columns for a table - def columns(table = nil) - if table - @columns[table] - else - ActiveSupport::Deprecation.warn('call columns with a table name!') - @columns.dup - end + def columns(table) + @columns[table] end # Get the columns for a table as a hash, key is the column name # value is the column object. - def columns_hash(table = nil) - if table - @columns_hash[table] - else - ActiveSupport::Deprecation.warn('call columns_hash with a table name!') - @columns_hash.dup - end + def columns_hash(table) + @columns_hash[table] end # Clears out internal caches diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 7d940fe1c9..df489a5b1f 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -17,12 +17,14 @@ module ActiveRecord # Allow database path relative to Rails.root, but only if # the database path is not the special path that tells # Sqlite to build a database only in memory. - if defined?(Rails.root) && ':memory:' != config[:database] - config[:database] = File.expand_path(config[:database], Rails.root) + if ':memory:' != config[:database] + config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root) + dirname = File.dirname(config[:database]) + Dir.mkdir(dirname) unless File.directory?(dirname) end db = SQLite3::Database.new( - config[:database], + config[:database].to_s, :results_as_hash => true ) @@ -224,8 +226,8 @@ module ActiveRecord # QUOTING ================================================== 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] + if value.kind_of?(String) && column && column.type == :binary + s = value.unpack("H*")[0] "x'#{s}'" else super diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index f0141aaaab..2b1e997ef4 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -69,13 +69,10 @@ module ActiveRecord mattr_accessor :timestamped_migrations, instance_writer: false self.timestamped_migrations = true - ## - # :singleton-method: - # Disable implicit join references. This feature was deprecated with Rails 4. - # If you don't make use of implicit references but still see deprecation warnings - # you can disable the feature entirely. This will be the default with Rails 4.1. - mattr_accessor :disable_implicit_join_references, instance_writer: false - self.disable_implicit_join_references = false + def self.disable_implicit_join_references=(value) + ActiveSupport::Deprecation.warn("Implicit join references were removed with Rails 4.1." \ + "Make sure to remove this configuration because it does nothing.") + end class_attribute :default_connection_handler, instance_writer: false @@ -91,20 +88,8 @@ module ActiveRecord end module ClassMethods - def inherited(child_class) #:nodoc: - child_class.initialize_generated_modules - super - end - def initialize_generated_modules - @attribute_methods_mutex = Mutex.new - - # force attribute methods to be higher in inheritance hierarchy than other generated methods - generated_attribute_methods.const_set(:AttrNames, Module.new { - def self.const_missing(name) - const_set(name, [name.to_s.sub(/ATTR_/, '')].pack('h*').freeze) - end - }) + super generated_feature_methods end @@ -149,19 +134,18 @@ module ActiveRecord # Returns the Arel engine. def arel_engine - @arel_engine ||= begin + @arel_engine ||= if Base == self || connection_handler.retrieve_connection_pool(self) self else superclass.arel_engine end - end end private def relation #:nodoc: - relation = Relation.new(self, arel_table) + relation = Relation.create(self, arel_table) if finder_needs_type_condition? relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) @@ -340,14 +324,6 @@ module ActiveRecord @readonly = true end - # Returns the connection currently associated with the class. This can - # also be used to "borrow" the connection to do database work that isn't - # easily done without going straight to SQL. - def connection - ActiveSupport::Deprecation.warn("#connection is deprecated in favour of accessing it via the class") - self.class.connection - end - def connection_handler self.class.connection_handler end @@ -370,7 +346,7 @@ module ActiveRecord # Returns a hash of the given methods with their names as keys and returned values as values. def slice(*methods) - Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access + Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access end def set_transaction_state(state) # :nodoc: diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 70eda332b3..b2a81a184a 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -379,12 +379,6 @@ module ActiveRecord @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } - def self.find_table_name(fixture_set_name) # :nodoc: - ActiveSupport::Deprecation.warn( - "ActiveRecord::Fixtures.find_table_name is deprecated and shall be removed from future releases. Use ActiveRecord::Fixtures.default_fixture_model_name instead.") - default_fixture_model_name(fixture_set_name) - end - def self.default_fixture_model_name(fixture_set_name) # :nodoc: ActiveRecord::Base.pluralize_table_names ? fixture_set_name.singularize.camelize : @@ -598,14 +592,7 @@ module ActiveRecord row[fk_name] = ActiveRecord::FixtureSet.identify(value) end when :has_and_belongs_to_many - if (targets = row.delete(association.name.to_s)) - targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - table_name = association.join_table - rows[table_name].concat targets.map { |target| - { association.foreign_key => row[primary_key_name], - association.association_foreign_key => ActiveRecord::FixtureSet.identify(target) } - } - end + handle_habtm(rows, row, association) end end end @@ -620,6 +607,17 @@ module ActiveRecord @primary_key_name ||= model_class && model_class.primary_key end + def handle_habtm(rows, row, association) + if (targets = row.delete(association.name.to_s)) + targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) + table_name = association.join_table + rows[table_name].concat targets.map { |target| + { association.foreign_key => row[primary_key_name], + association.association_foreign_key => ActiveRecord::FixtureSet.identify(target) } + } + end + end + def has_primary_key_column? @has_primary_key_column ||= primary_key_name && model_class.columns.any? { |c| c.name == primary_key_name } @@ -639,7 +637,7 @@ module ActiveRecord end def read_fixture_files - yaml_files = Dir["#{@path}/**/*.yml"].select { |f| + yaml_files = Dir["#{@path}/{**,*}/*.yml"].select { |f| ::File.file?(f) } + [yaml_file_path] @@ -758,7 +756,7 @@ module ActiveRecord def fixtures(*fixture_set_names) if fixture_set_names.first == :all - fixture_set_names = Dir["#{fixture_path}/**/*.{yml}"] + fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"] fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] } else fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s } diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 209de78898..626fe40103 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -82,7 +82,7 @@ module ActiveRecord stmt = relation.where( relation.table[self.class.primary_key].eq(id).and( - relation.table[lock_col].eq(self.class.quote_value(previous_lock_value)) + relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col))) ) ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names)) @@ -138,6 +138,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) + @column_defaults = nil @locking_column = value.to_s end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index e96c347f6f..19c6f8148b 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -32,7 +32,7 @@ module ActiveRecord class PendingMigrationError < ActiveRecordError#:nodoc: def initialize - super("Migrations are pending; run 'rake db:migrate RAILS_ENV=#{Rails.env}' to resolve this issue.") + super("Migrations are pending; run 'bin/rake db:migrate RAILS_ENV=#{::Rails.env}' to resolve this issue.") end end @@ -866,13 +866,7 @@ module ActiveRecord @direction = direction @target_version = target_version @migrated_versions = nil - - if Array(migrations).grep(String).empty? - @migrations = migrations - else - ActiveSupport::Deprecation.warn "instantiate this class with a list of migrations" - @migrations = self.class.migrations(migrations) - end + @migrations = migrations validate(@migrations) @@ -906,15 +900,7 @@ module ActiveRecord raise UnknownMigrationVersionError.new(@target_version) end - running = runnable - - if block_given? - message = "block argument to migrate is deprecated, please filter migrations before constructing the migrator" - ActiveSupport::Deprecation.warn message - running.select! { |m| yield m } - end - - running.each do |migration| + runnable.each do |migration| Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger begin diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 9782a48055..01c73be849 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -73,7 +73,7 @@ module ActiveRecord [:create_table, :create_join_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column_default, :add_reference, :remove_reference, :transaction, - :drop_join_table, :drop_table, :execute_block, + :drop_join_table, :drop_table, :execute_block, :enable_extension, :change_column, :execute, :remove_columns, # irreversible methods need to be here too ].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 @@ -100,6 +100,7 @@ module ActiveRecord add_column: :remove_column, add_timestamps: :remove_timestamps, add_reference: :remove_reference, + enable_extension: :disable_extension }.each do |cmd, inv| [[inv, cmd], [cmd, inv]].uniq.each do |method, inverse| class_eval <<-EOV, __FILE__, __LINE__ + 1 diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index ac2d2f2712..23541d1d27 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -124,7 +124,7 @@ module ActiveRecord @quoted_table_name = nil @arel_table = nil @sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name - @relation = Relation.new(self, arel_table) + @relation = Relation.create(self, arel_table) end # Returns a quoted version of the table name, used to construct SQL statements. @@ -253,19 +253,6 @@ module ActiveRecord @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } end - # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key - # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute - # is available. - def column_methods_hash #:nodoc: - @dynamic_methods_hash ||= column_names.each_with_object(Hash.new(false)) do |attr, methods| - attr_name = attr.to_s - methods[attr.to_sym] = attr_name - methods["#{attr}=".to_sym] = attr_name - methods["#{attr}?".to_sym] = attr_name - methods["#{attr}_before_type_cast".to_sym] = attr_name - end - end - # Resets all the cached information about columns, which will cause them # to be reloaded on the next request. # diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index e53e8553ad..df28451bb7 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -465,19 +465,17 @@ module ActiveRecord association.build(attributes.except(*UNASSIGNABLE_KEYS)) end elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } - unless association.loaded? || call_reject_if(association_name, attributes) + unless call_reject_if(association_name, attributes) # Make sure we are operating on the actual object which is in the association's # proxy_target array (either by finding it, or adding it if not found) - target_record = association.target.detect { |record| record == existing_record } - + # Take into account that the proxy_target may have changed due to callbacks + target_record = association.target.detect { |record| record.id.to_s == attributes['id'].to_s } if target_record existing_record = target_record else - association.add_to_target(existing_record) + association.add_to_target(existing_record, :skip_callbacks) end - end - if !call_reject_if(association_name, attributes) assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) end else diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 3d85898c41..6bee4f38e7 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,15 +1,16 @@ module ActiveRecord module Querying - delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all - delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :all - delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, :to => :all - delegate :find_by, :find_by!, :to => :all - delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :all - delegate :find_each, :find_in_batches, :to => :all + delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all + delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all + delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all + delegate :find_by, :find_by!, to: :all + delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all + delegate :find_each, :find_in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, - :having, :create_with, :uniq, :distinct, :references, :none, :unscope, :to => :all - delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all + :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all + delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all + delegate :pluck, :ids, to: :all # Executes a custom SQL query against your database and returns all the results. The results will # be returned as an array with columns requested encapsulated as attributes of the model you call diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 31a0ace864..269f9f975b 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -37,16 +37,21 @@ module ActiveRecord rake_tasks do require "active_record/base" - ActiveRecord::Tasks::DatabaseTasks.env = Rails.env - ActiveRecord::Tasks::DatabaseTasks.db_dir = Rails.application.config.paths["db"].first ActiveRecord::Tasks::DatabaseTasks.seed_loader = Rails.application - ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration - ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths['db/migrate'].to_a - ActiveRecord::Tasks::DatabaseTasks.fixtures_path = File.join Rails.root, 'test', 'fixtures' + ActiveRecord::Tasks::DatabaseTasks.env = Rails.env + + namespace :db do + task :load_config do + ActiveRecord::Tasks::DatabaseTasks.db_dir = Rails.application.config.paths["db"].first + ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration + ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths['db/migrate'].to_a + ActiveRecord::Tasks::DatabaseTasks.fixtures_path = File.join Rails.root, 'test', 'fixtures' - if defined?(APP_RAKEFILE) && engine = Rails::Engine.find(find_engine_path(APP_RAKEFILE)) - if engine.paths['db/migrate'].existent - ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a + if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH) + if engine.paths['db/migrate'].existent + ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a + end + end end end @@ -106,56 +111,6 @@ module ActiveRecord initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do - begin - old_behavior, ActiveSupport::Deprecation.behavior = ActiveSupport::Deprecation.behavior, :stderr - whitelist_attributes = app.config.active_record.delete(:whitelist_attributes) - - if respond_to?(:mass_assignment_sanitizer=) - mass_assignment_sanitizer = nil - else - mass_assignment_sanitizer = app.config.active_record.delete(:mass_assignment_sanitizer) - end - - unless whitelist_attributes.nil? && mass_assignment_sanitizer.nil? - ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, [] - Model based mass assignment security has been extracted - out of Rails into a gem. Please use the new recommended protection model for - params or add `protected_attributes` to your Gemfile to use the old one. - - To disable this message remove the `whitelist_attributes` option from your - `config/application.rb` file and any `mass_assignment_sanitizer` options - from your `config/environments/*.rb` files. - - See http://guides.rubyonrails.org/security.html#mass-assignment for more information. - EOF - end - - unless app.config.active_record.delete(:auto_explain_threshold_in_seconds).nil? - ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, [] - The Active Record auto explain feature has been removed. - - To disable this message remove the `active_record.auto_explain_threshold_in_seconds` - option from the `config/environments/*.rb` config file. - - See http://guides.rubyonrails.org/4_0_release_notes.html for more information. - EOF - end - - unless app.config.active_record.delete(:observers).nil? - ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, [] - Active Record Observers has been extracted out of Rails into a gem. - Please use callbacks or add `rails-observers` to your Gemfile to use observers. - - To disable this message remove the `observers` option from your - `config/application.rb` or from your initializers. - - See http://guides.rubyonrails.org/4_0_release_notes.html for more information. - EOF - end - ensure - ActiveSupport::Deprecation.behavior = old_behavior - end - app.config.active_record.each do |k,v| send "#{k}=", v end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 0b74553bf8..daccab762f 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -12,7 +12,7 @@ db_namespace = namespace :db do end end - desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)' + desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all databases in the config)' task :create => [:load_config] do if ENV['DATABASE_URL'] ActiveRecord::Tasks::DatabaseTasks.create_database_url @@ -172,7 +172,7 @@ db_namespace = namespace :db do end end - desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)' + desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)' task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed] desc 'Load the seed data from db/seeds.rb' @@ -236,7 +236,7 @@ db_namespace = namespace :db do end namespace :schema do - desc 'Create a db/schema.rb file that can be portably used against any DB supported by AR' + desc 'Create a db/schema.rb file that is portable against any DB supported by AR' task :dump => [:environment, :load_config] do require 'active_record/schema_dumper' filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb') @@ -320,11 +320,14 @@ db_namespace = namespace :db do # desc "Recreate the test database from an existent schema.rb file" task :load_schema => 'db:test:purge' do begin + should_reconnect = ActiveRecord::Base.connection_pool.active_connection? ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) ActiveRecord::Schema.verbose = false db_namespace["schema:load"].invoke ensure - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) + if should_reconnect + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) + end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 8a9488656b..f428f160cf 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -10,6 +10,27 @@ module ActiveRecord self.aggregate_reflections = {} end + def self.create(macro, name, scope, options, ar) + case macro + when :has_and_belongs_to_many + klass = AssociationReflection + when :has_many, :belongs_to, :has_one + klass = options[:through] ? ThroughReflection : AssociationReflection + when :composed_of + klass = AggregateReflection + end + + klass.new(macro, name, scope, options, ar) + end + + def self.add_reflection(ar, name, reflection) + if reflection.class == AggregateReflection + ar.aggregate_reflections = ar.aggregate_reflections.merge(name => reflection) + else + ar.reflections = ar.reflections.merge(name => reflection) + end + end + # \Reflection enables to interrogate Active Record classes and objects # about their associations and aggregations. This information can, # for example, be used in a form builder that takes an Active Record object @@ -19,25 +40,6 @@ module ActiveRecord # MacroReflection class has info for AggregateReflection and AssociationReflection # classes. module ClassMethods - def create_reflection(macro, name, scope, options, active_record) - case macro - when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many - klass = options[:through] ? ThroughReflection : AssociationReflection - when :composed_of - klass = AggregateReflection - end - - reflection = klass.new(macro, name, scope, options, active_record) - - if klass == AggregateReflection - self.aggregate_reflections = self.aggregate_reflections.merge(name => reflection) - else - self.reflections = self.reflections.merge(name => reflection) - end - - reflection - end - # Returns an array of AggregateReflection objects for all the aggregations in the class. def reflect_on_all_aggregations aggregate_reflections.values @@ -191,7 +193,7 @@ module ActiveRecord attr_reader :type, :foreign_type - def initialize(*args) + def initialize(macro, name, scope, options, active_record) super @collection = [:has_many, :has_and_belongs_to_many].include?(macro) @automatic_inverse_of = nil @@ -267,7 +269,7 @@ module ActiveRecord end def source_reflection - nil + self end # A chain of reflections from this one back to the owner. For more see the explanation in @@ -369,6 +371,12 @@ module ActiveRecord VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key] + protected + + def actual_source_reflection # FIXME: this is a horrible name + self + end + private # Attempts to find the inverse association name automatically. # If it cannot find a suitable inverse association name, it returns @@ -386,7 +394,7 @@ module ActiveRecord # returns either nil or the inverse association name that it finds. def automatic_inverse_of if can_find_inverse_of_automatically?(self) - inverse_name = active_record.name.downcase.to_sym + inverse_name = ActiveSupport::Inflector.underscore(active_record.name).to_sym begin reflection = klass.reflect_on_association(inverse_name) @@ -405,7 +413,7 @@ module ActiveRecord end # Checks if the inverse reflection that is returned from the - # +set_automatic_inverse_of+ method is a valid reflection. We must + # +automatic_inverse_of+ method is a valid reflection. We must # make sure that the reflection's active_record name matches up # with the current reflection's klass name. # @@ -527,7 +535,9 @@ module ActiveRecord # def chain @chain ||= begin - chain = source_reflection.chain + through_reflection.chain + a = source_reflection.chain + b = through_reflection.chain + chain = a + b chain[0] = self # Use self so we don't lose the information from :source_type chain end @@ -578,7 +588,7 @@ module ActiveRecord # A through association is nested if there would be more than one join table def nested? - chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many + chain.length > 2 || through_reflection.has_and_belongs_to_many? end # We want to use the klass from this reflection, rather than just delegate straight to @@ -587,12 +597,7 @@ module ActiveRecord def association_primary_key(klass = nil) # 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] || primary_key(klass || self.klass) + actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass) end # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form. @@ -671,6 +676,12 @@ directive on your declaration like: check_validity_of_inverse! end + protected + + def actual_source_reflection # FIXME: this is a horrible name + source_reflection.actual_source_reflection + 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 d37471e9ad..4e86e905ed 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -17,17 +17,14 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded - attr_accessor :default_scoped alias :model :klass alias :loaded? :loaded - alias :default_scoped? :default_scoped def initialize(klass, table, values = {}) - @klass = klass - @table = table - @values = values - @loaded = false - @default_scoped = false + @klass = klass + @table = table + @values = values + @loaded = false end def initialize_copy(other) @@ -313,7 +310,7 @@ module ActiveRecord stmt.table(table) stmt.key = table[primary_key] - if with_default_scope.joins_values.any? + if joins_values.any? @klass.connection.join_to_update(stmt, arel) else stmt.take(arel.limit) @@ -438,7 +435,7 @@ module ActiveRecord stmt = Arel::DeleteManager.new(arel.engine) stmt.from(table) - if with_default_scope.joins_values.any? + if joins_values.any? @klass.connection.join_to_delete(stmt, arel, table[primary_key]) else stmt.wheres = arel.constraints @@ -504,7 +501,22 @@ module ActiveRecord # User.where(name: 'Oscar').to_sql # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql - @to_sql ||= klass.connection.to_sql(arel, bind_values.dup) + @to_sql ||= begin + relation = self + connection = klass.connection + visitor = connection.visitor + + if eager_loading? + join_dependency = construct_join_dependency + relation = construct_relation_for_association_find(join_dependency) + end + + ast = relation.arel.ast + binds = relation.bind_values.dup + visitor.accept(ast) do + connection.quote(*binds.shift.reverse) + end + end end # Returns a hash of where conditions. @@ -512,12 +524,11 @@ module ActiveRecord # User.where(name: 'Oscar').where_values_hash # # => {name: "Oscar"} def where_values_hash - scope = with_default_scope - equalities = scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| + equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node| node.left.relation.name == table_name } - binds = Hash[scope.bind_values.find_all(&:first).map { |column, v| [column.name, v] }] + binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }] binds.merge!(Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]) Hash[equalities.map { |where| @@ -565,16 +576,6 @@ module ActiveRecord q.pp(self.to_a) end - def with_default_scope #:nodoc: - if default_scoped? && default_scope = klass.send(:build_default_scope) - default_scope = default_scope.merge(self) - default_scope.default_scoped = false - default_scope - else - self - end - end - # Returns true if relation is blank. def blank? to_a.blank? @@ -594,31 +595,23 @@ module ActiveRecord private def exec_queries - default_scoped = with_default_scope - - if default_scoped.equal?(self) - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) - - preload = preload_values - preload += includes_values unless eager_loading? - preload.each do |associations| - ActiveRecord::Associations::Preloader.new(@records, associations).run - end + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) - @records.each { |record| record.readonly! } if readonly_value - else - @records = default_scoped.to_a + preload = preload_values + preload += includes_values unless eager_loading? + preload.each do |associations| + ActiveRecord::Associations::Preloader.new(@records, associations).run end + @records.each { |record| record.readonly! } if readonly_value + @loaded = true @records end def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| - if join.is_a?(Arel::Nodes::StringJoin) - tables_in_string(join.left) - else + unless join.is_a?(Arel::Nodes::StringJoin) [join.left.table_name, join.left.table_alias] end end @@ -627,41 +620,8 @@ module ActiveRecord # always convert table names to downcase as in Oracle quoted table names are in uppercase joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq - string_tables = tables_in_string(to_sql) - - if (references_values - joined_tables).any? - true - elsif !ActiveRecord::Base.disable_implicit_join_references && - (string_tables - joined_tables).any? - ActiveSupport::Deprecation.warn( - "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \ - "that are referenced in a string SQL snippet. For example: \n" \ - "\n" \ - " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \ - "\n" \ - "Currently, Active Record recognizes the table in the string, and knows to JOIN the " \ - "comments table to the query, rather than loading comments in a separate query. " \ - "However, doing this without writing a full-blown SQL parser is inherently flawed. " \ - "Since we don't want to write an SQL parser, we are removing this functionality. " \ - "From now on, you must explicitly tell Active Record when you are referencing a table " \ - "from a string:\n" \ - "\n" \ - " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n" \ - "\n" \ - "If you don't rely on implicit join references you can disable the feature entirely " \ - "by setting `config.active_record.disable_implicit_join_references = true`." - ) - true - else - false - end - end - def tables_in_string(string) - return [] if string.blank? - # always convert table names to downcase as in Oracle quoted table names are in uppercase - # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries - string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] + (references_values - joined_tables).any? end end end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 41291844fc..fd8496442e 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -19,6 +19,13 @@ module ActiveRecord # person.party_all_night! # end # + # If you do not provide a block to #find_each, it will return an Enumerator + # for chaining with other methods: + # + # Person.find_each.with_index do |person, index| + # person.award_trophy(index + 1) + # end + # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. # * <tt>:start</tt> - Specifies the starting point for the batch processing. @@ -40,8 +47,12 @@ module ActiveRecord # NOTE: You can't set the limit either, that's used to control # the batch sizes. def find_each(options = {}) - find_in_batches(options) do |records| - records.each { |record| yield record } + if block_given? + find_in_batches(options) do |records| + records.each { |record| yield record } + end + else + enum_for :find_each, options end end @@ -78,8 +89,8 @@ module ActiveRecord relation = self - unless arel.orders.blank? && arel.taken.blank? - ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") + if logger && (arel.orders.present? || arel.taken.present?) + logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") end start = options.delete(:start) diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 4becf3980d..52a538e5b5 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -91,20 +91,14 @@ module ActiveRecord # # Person.sum("2 * age") def calculate(operation, column_name, options = {}) - relation = with_default_scope - - if column_name.is_a?(Symbol) && attribute_aliases.key?(column_name.to_s) - column_name = attribute_aliases[column_name.to_s].to_sym + if column_name.is_a?(Symbol) && attribute_alias?(column_name) + column_name = attribute_alias(column_name) end - if relation.equal?(self) - if has_include?(column_name) - construct_relation_for_association_calculations.calculate(operation, column_name, options) - else - perform_calculation(operation, column_name, options) - end + if has_include?(column_name) + construct_relation_for_association_calculations.calculate(operation, column_name, options) else - relation.calculate(operation, column_name, options) + perform_calculation(operation, column_name, options) end end @@ -143,39 +137,32 @@ module ActiveRecord # def pluck(*column_names) column_names.map! do |column_name| - if column_name.is_a?(Symbol) - if attribute_aliases.key?(column_name.to_s) - column_name = attribute_aliases[column_name.to_s].to_sym - end - - if self.columns_hash.key?(column_name.to_s) - column_name = "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}" - end + if column_name.is_a?(Symbol) && attribute_alias?(column_name) + attribute_alias(column_name) + else + column_name.to_s end - - column_name end if has_include?(column_names.first) construct_relation_for_association_calculations.pluck(*column_names) else relation = spawn - relation.select_values = column_names + relation.select_values = column_names.map { |cn| + columns_hash.key?(cn) ? arel_table[cn] : cn + } result = klass.connection.select_all(relation.arel, nil, bind_values) columns = result.columns.map do |key| klass.column_types.fetch(key) { - result.column_types.fetch(key) { - Class.new { def type_cast(v); v; end }.new - } + result.column_types.fetch(key) { result.identity_type } } end result = result.map do |attributes| values = klass.initialize_attributes(attributes).values - columns.zip(values).map do |column, value| - column.type_cast(value) - end + iter = columns.each + values.map { |value| iter.next.type_cast value } end columns.one? ? result.map!(&:first) : result end @@ -200,18 +187,9 @@ module ActiveRecord # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) distinct = self.distinct_value - if options.has_key?(:distinct) - ActiveSupport::Deprecation.warn "The :distinct option for `Relation#count` is deprecated. " \ - "Please use `Relation#distinct` instead. (eg. `relation.distinct.count`)" - distinct = options[:distinct] - end if operation == "count" - if select_values.present? - column_name ||= select_values.join(", ") - else - column_name ||= :all - end + column_name ||= select_for_count unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? distinct = true @@ -379,6 +357,15 @@ module ActiveRecord column ? column.type_cast(value) : value end + # TODO: refactor to allow non-string `select_values` (eg. Arel nodes). + def select_for_count + if select_values.present? + select_values.join(", ") + else + :all + end + end + def build_count_subquery(relation, column_name, distinct) column_alias = Arel.sql('count_column') subquery_alias = Arel.sql('subquery_for_count') diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 8d6740246c..b6f80ac5c7 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -73,16 +73,8 @@ module ActiveRecord module ClassMethods # :nodoc: @@subclasses = ThreadSafe::Cache.new(:initial_capacity => 2) - def new(klass, *args) - relation = relation_class_for(klass).allocate - relation.__send__(:initialize, klass, *args) - relation - end - - # This doesn't have to be thread-safe. relation_class_for guarantees that this will only be - # called exactly once for a given const name. - def const_missing(name) - const_set(name, Class.new(self) { include ClassSpecificRelation }) + def create(klass, *args) + relation_class_for(klass).new(klass, *args) end private @@ -94,7 +86,13 @@ module ActiveRecord # This hash is keyed by klass.name to avoid memory leaks in development mode my_cache.compute_if_absent(klass_name) do # Cache#compute_if_absent guarantees that the block will only executed once for the given klass_name - const_get("#{name.gsub('::', '_')}_#{klass_name.gsub('::', '_')}", false) + subclass_name = "#{name.gsub('::', '_')}_#{klass_name.gsub('::', '_')}" + + if const_defined?(subclass_name) + const_get(subclass_name) + else + const_set(subclass_name, Class.new(self) { include ClassSpecificRelation }) + end end else ActiveRecord::Relation diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 3ea3c33fcc..2d3bd563ac 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,5 +1,7 @@ module ActiveRecord module FinderMethods + ONE_AS_ONE = '1 AS one' + # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key # is an integer, find by id coerces its arguments using +to_i+. @@ -32,7 +34,7 @@ module ActiveRecord # end # # ==== Variations of +find+ - # + # # Person.where(name: 'Spartacus', rating: 4) # # returns a chainable list (which can be empty). # @@ -49,7 +51,7 @@ module ActiveRecord # # Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none) # # returns a boolean indicating if any record with the given conditions exist. - # + # # Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3") # # returns a chainable list of instances with only the mentioned fields. # @@ -124,7 +126,7 @@ module ActiveRecord # def first(limit = nil) if limit - find_first_with_limit(order_values, limit) + find_first_with_limit(limit) else find_first end @@ -202,7 +204,7 @@ module ActiveRecord relation = construct_relation_for_association_find(construct_join_dependency) return false if ActiveRecord::NullRelation === relation - relation = relation.except(:select, :order).select("1 AS one").limit(1) + relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1) case conditions when Array, Hash @@ -211,7 +213,6 @@ module ActiveRecord relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none end - relation = relation.with_default_scope connection.select_value(relation.arel, "#{name} Exists", relation.bind_values) end @@ -237,7 +238,7 @@ module ActiveRecord raise RecordNotFound, error end - protected + private def find_with_associations join_dependency = construct_join_dependency @@ -245,7 +246,7 @@ module ActiveRecord if ActiveRecord::NullRelation === relation [] else - rows = connection.select_all(relation, 'SQL', relation.bind_values.dup) + rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup) join_dependency.instantiate(rows) end end @@ -289,6 +290,12 @@ module ActiveRecord id_rows.map {|row| row[primary_key]} end + def using_limitable_reflections?(reflections) + reflections.none? { |r| r.collection? } + end + + protected + def find_with_ids(*ids) expects_array = ids.first.kind_of?(Array) return ids.first if expects_array && ids.first.empty? @@ -354,11 +361,11 @@ module ActiveRecord if loaded? @records.first else - @first ||= find_first_with_limit(with_default_scope.order_values, 1).first + @first ||= find_first_with_limit(1).first end end - def find_first_with_limit(order_values, limit) + def find_first_with_limit(limit) if order_values.empty? && primary_key order(arel_table[primary_key].asc).limit(limit).to_a else @@ -378,9 +385,5 @@ module ActiveRecord end end end - - def using_limitable_reflections?(reflections) - reflections.none? { |r| r.collection? } - end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index c114ea0c0d..c08158d38b 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -22,7 +22,7 @@ module ActiveRecord # build a relation to merge in rather than directly merging # the values. def other - other = Relation.new(relation.klass, relation.table) + other = Relation.create(relation.klass, relation.table) hash.each { |k, v| if k == :joins if Hash === v @@ -42,10 +42,6 @@ module ActiveRecord attr_reader :relation, :values, :other def initialize(relation, other) - if other.default_scoped? && other.klass != relation.klass - other = other.with_default_scope - end - @relation = relation @values = other.values @other = other @@ -101,19 +97,35 @@ module ActiveRecord def merge_multi_values lhs_wheres = relation.where_values rhs_wheres = values[:where] || [] + lhs_binds = relation.bind_values rhs_binds = values[:bind] || [] removed, kept = partition_overwrites(lhs_wheres, rhs_wheres) - relation.where_values = kept + rhs_wheres - relation.bind_values = filter_binds(lhs_binds, removed) + rhs_binds + where_values = kept + rhs_wheres + bind_values = filter_binds(lhs_binds, removed) + rhs_binds + + conn = relation.klass.connection + bviter = bind_values.each.with_index + where_values.map! do |node| + if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right + (column, _), i = bviter.next + substitute = conn.substitute_at column, i + Arel::Nodes::Equality.new(node.left, substitute) + else + node + end + end + + relation.where_values = where_values + relation.bind_values = bind_values if values[:reordering] # override any order specified in the original relation relation.reorder! values[:order] elsif values[:order] - # merge in order_values from r + # merge in order_values from relation relation.order! values[:order] end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index b7609c97b5..8948f2bba5 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,15 +1,26 @@ module ActiveRecord class PredicateBuilder # :nodoc: + @handlers = [] + + autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler' + autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler' + + def self.resolve_column_aliases(klass, hash) + hash = hash.dup + hash.keys.grep(Symbol) do |key| + if klass.attribute_alias? key + hash[klass.attribute_alias(key)] = hash.delete key + end + end + hash + end + def self.build_from_hash(klass, attributes, default_table) queries = [] attributes.each do |column, value| table = default_table - if column.is_a?(Symbol) && klass.attribute_aliases.key?(column.to_s) - column = klass.attribute_aliases[column.to_s] - end - if value.is_a?(Hash) if value.empty? queries << '1=0' @@ -67,44 +78,36 @@ module ActiveRecord end.compact end + # Define how a class is converted to Arel nodes when passed to +where+. + # The handler can be any object that responds to +call+, and will be used + # for any value that +===+ the class given. For example: + # + # MyCustomDateRange = Struct.new(:start, :end) + # handler = proc do |column, range| + # Arel::Nodes::Between.new(column, + # Arel::Nodes::And.new([range.start, range.end]) + # ) + # end + # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler) + def self.register_handler(klass, handler) + @handlers.unshift([klass, handler]) + end + + register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) }) + # FIXME: I think we need to deprecate this behavior + register_handler(Class, ->(attribute, value) { attribute.eq(value.name) }) + register_handler(Base, ->(attribute, value) { attribute.eq(value.id) }) + register_handler(Range, ->(attribute, value) { attribute.in(value) }) + register_handler(Relation, RelationHandler.new) + register_handler(Array, ArrayHandler.new) + private def self.build(attribute, value) - case value - when Array - values = value.to_a.map {|x| x.is_a?(Base) ? x.id : x} - ranges, values = values.partition {|v| v.is_a?(Range)} - - values_predicate = if values.include?(nil) - values = values.compact - - case values.length - when 0 - attribute.eq(nil) - when 1 - attribute.eq(values.first).or(attribute.eq(nil)) - else - attribute.in(values).or(attribute.eq(nil)) - end - else - attribute.in(values) - end + handler_for(value).call(attribute, value) + end - 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::Base - attribute.eq(value.id) - when Class - # FIXME: I think we need to deprecate this behavior - attribute.eq(value.name) - else - attribute.eq(value) - end + def self.handler_for(object) + @handlers.detect { |klass, _| klass === object }.last end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb new file mode 100644 index 0000000000..2f6c34ac08 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -0,0 +1,29 @@ +module ActiveRecord + class PredicateBuilder + class ArrayHandler # :nodoc: + def call(attribute, value) + values = value.map { |x| x.is_a?(Base) ? x.id : x } + ranges, values = values.partition { |v| v.is_a?(Range) } + + values_predicate = if values.include?(nil) + values = values.compact + + case values.length + when 0 + attribute.eq(nil) + when 1 + attribute.eq(values.first).or(attribute.eq(nil)) + else + attribute.in(values).or(attribute.eq(nil)) + end + else + attribute.in(values) + end + + array_predicates = ranges.map { |range| attribute.in(range) } + array_predicates << values_predicate + array_predicates.inject { |composite, predicate| composite.or(predicate) } + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb new file mode 100644 index 0000000000..618fa3cdd9 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb @@ -0,0 +1,13 @@ +module ActiveRecord + class PredicateBuilder + class RelationHandler # :nodoc: + def call(attribute, value) + if value.select_values.empty? + value = value.select(value.klass.arel_table[value.klass.primary_key]) + end + + attribute.in(value.arel.ast) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 0200fcf69b..9f2a039d94 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -100,6 +100,14 @@ module ActiveRecord # firing an additional query. This will often result in a # performance improvement over a simple +join+. # + # You can also specify multiple relationships, like this: + # + # users = User.includes(:address, :friends) + # + # Loading nested relationships is possible using a Hash: + # + # users = User.includes(:address, friends: [:address, :followers]) + # # === conditions # # If you want to add conditions to your included models you'll have @@ -111,14 +119,15 @@ module ActiveRecord # # User.includes(:posts).where('posts.name = ?', 'example').references(:posts) def includes(*args) - check_if_method_has_arguments!("includes", args) + check_if_method_has_arguments!(:includes, args) spawn.includes!(*args) end def includes!(*args) # :nodoc: - args.reject! {|a| a.blank? } + args.reject!(&:blank?) + args.flatten! - self.includes_values = (includes_values + args).flatten.uniq + self.includes_values |= args self end @@ -129,7 +138,7 @@ module ActiveRecord # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = # "users"."id" def eager_load(*args) - check_if_method_has_arguments!("eager_load", args) + check_if_method_has_arguments!(:eager_load, args) spawn.eager_load!(*args) end @@ -143,7 +152,7 @@ module ActiveRecord # User.preload(:posts) # => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) def preload(*args) - check_if_method_has_arguments!("preload", args) + check_if_method_has_arguments!(:preload, args) spawn.preload!(*args) end @@ -161,14 +170,15 @@ 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) - check_if_method_has_arguments!("references", args) + check_if_method_has_arguments!(:references, args) spawn.references!(*args) end def references!(*args) # :nodoc: args.flatten! + args.map!(&:to_s) - self.references_values = (references_values + args.map!(&:to_s)).uniq + self.references_values |= args self end @@ -221,7 +231,9 @@ module ActiveRecord end def select!(*fields) # :nodoc: - self.select_values += fields.flatten + fields.flatten! + + self.select_values += fields self end @@ -241,7 +253,7 @@ module ActiveRecord # User.group('name AS grouped_name, age') # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>] def group(*args) - check_if_method_has_arguments!("group", args) + check_if_method_has_arguments!(:group, args) spawn.group!(*args) end @@ -272,24 +284,24 @@ module ActiveRecord # User.order(:name, email: :desc) # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC def order(*args) - check_if_method_has_arguments!("order", args) + check_if_method_has_arguments!(:order, args) spawn.order!(*args) end def order!(*args) # :nodoc: args.flatten! - validate_order_args args + validate_order_args(args) - references = args.reject { |arg| Arel::Node === arg } + references = args.grep(String) references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! references!(references) if references.any? # if a symbol is given we prepend the quoted table name - args = args.map { |arg| - arg.is_a?(Symbol) ? "#{quoted_table_name}.#{arg} ASC" : arg - } + args.map! do |arg| + arg.is_a?(Symbol) ? Arel::Nodes::Ascending.new(klass.arel_table[arg]) : arg + end - self.order_values = args + self.order_values + self.order_values += args self end @@ -301,15 +313,15 @@ module ActiveRecord # # User.order('email DESC').reorder('id ASC').order('name ASC') # - # generates a query with 'ORDER BY name ASC, id ASC'. + # generates a query with 'ORDER BY id ASC, name ASC'. def reorder(*args) - check_if_method_has_arguments!("reorder", args) + check_if_method_has_arguments!(:reorder, args) spawn.reorder!(*args) end def reorder!(*args) # :nodoc: args.flatten! - validate_order_args args + validate_order_args(args) self.reordering_value = true self.order_values = args @@ -351,7 +363,7 @@ module ActiveRecord # # will still have an order if it comes from the default_scope on Comment. def unscope(*args) - check_if_method_has_arguments!("unscope", args) + check_if_method_has_arguments!(:unscope, args) spawn.unscope!(*args) end @@ -390,8 +402,12 @@ module ActiveRecord # User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id") # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id def joins(*args) - check_if_method_has_arguments!("joins", args) - spawn.joins!(*args.compact.flatten) + check_if_method_has_arguments!(:joins, args) + + args.compact! + args.flatten! + + spawn.joins!(*args) end def joins!(*args) # :nodoc: @@ -773,9 +789,10 @@ module ActiveRecord end def extending!(*modules, &block) # :nodoc: - modules << Module.new(&block) if block_given? + modules << Module.new(&block) if block + modules.flatten! - self.extending_values += modules.flatten + self.extending_values += modules extend(*extending_values) if extending_values.any? self @@ -795,7 +812,7 @@ module ActiveRecord # Returns the Arel object associated with the relation. def arel - @arel ||= with_default_scope.build_arel + @arel ||= build_arel end # Like #arel, but ignores the default scope of the model. @@ -806,12 +823,12 @@ module ActiveRecord collapse_wheres(arel, (where_values - ['']).uniq) - arel.having(*having_values.uniq.reject{|h| h.blank?}) unless having_values.empty? + arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty? arel.take(connection.sanitize_limit(limit_value)) if limit_value arel.skip(offset_value.to_i) if offset_value - arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty? + arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty? build_order(arel) @@ -860,11 +877,11 @@ module ActiveRecord end def custom_join_ast(table, joins) - joins = joins.reject { |join| join.blank? } + joins = joins.reject(&:blank?) return [] if joins.empty? - joins.map do |join| + joins.map! do |join| case join when Array join = Arel.sql(join.join(' ')) if array_of_strings?(join) @@ -891,6 +908,7 @@ module ActiveRecord when String, Array [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash + opts = PredicateBuilder.resolve_column_aliases(klass, opts) attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) attributes.values.grep(ActiveRecord::Relation) do |rel| @@ -933,7 +951,7 @@ module ActiveRecord association_joins = buckets[:association_join] || [] stashed_association_joins = buckets[:stashed_join] || [] join_nodes = (buckets[:join_node] || []).uniq - string_joins = (buckets[:string_join] || []).map { |x| x.strip }.uniq + string_joins = (buckets[:string_join] || []).map(&:strip).uniq join_list = join_nodes + custom_join_ast(manager, string_joins) @@ -945,12 +963,12 @@ module ActiveRecord join_dependency.graft(*stashed_association_joins) - # FIXME: refactor this to build an AST - join_dependency.join_associations.each do |association| - association.join_to(manager) - end + joins = join_dependency.join_associations.map!(&:join_constraints) + joins.flatten! + + joins.each { |join| manager.from(join) } - manager.join_sources.concat join_list + manager.join_sources.concat(join_list) manager end @@ -971,7 +989,7 @@ module ActiveRecord when Arel::Nodes::Ordering o.reverse when String - o.to_s.split(',').collect do |s| + o.to_s.split(',').map! do |s| s.strip! s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') end @@ -988,14 +1006,15 @@ module ActiveRecord end def array_of_strings?(o) - o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)} + o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) } end def build_order(arel) - orders = order_values + orders = order_values.uniq + orders.reject!(&:blank?) orders = reverse_sql_order(orders) if reverse_order_value - orders = orders.uniq.reject(&:blank?).flat_map do |order| + orders = orders.flat_map do |order| case order when Symbol table[order].asc diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index de784f9f57..2552cbd234 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -64,8 +64,7 @@ module ActiveRecord private def relation_with(values) # :nodoc: - result = Relation.new(klass, table, values) - result.default_scoped = default_scoped + result = Relation.create(klass, table, values) result.extend(*extending_values) if extending_values.any? result end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index bea195e9b8..253368ae5b 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -3,11 +3,36 @@ module ActiveRecord # This class encapsulates a Result returned from calling +exec_query+ on any # database connection adapter. For example: # - # x = ActiveRecord::Base.connection.exec_query('SELECT * FROM foo') - # x # => #<ActiveRecord::Result:0xdeadbeef> + # result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts') + # result # => #<ActiveRecord::Result:0xdeadbeef> + # + # # Get the column names of the result: + # result.columns + # # => ["id", "title", "body"] + # + # # Get the record values of the result: + # result.rows + # # => [[1, "title_1", "body_1"], + # [2, "title_2", "body_2"], + # ... + # ] + # + # # Get an array of hashes representing the result (column => value): + # result.to_hash + # # => [{"id" => 1, "title" => "title_1", "body" => "body_1"}, + # {"id" => 2, "title" => "title_2", "body" => "body_2"}, + # ... + # ] + # + # # ActiveRecord::Result also includes Enumerable. + # result.each do |row| + # puts row['title'] + " " + row['body'] + # end class Result include Enumerable + IDENTITY_TYPE = Class.new { def type_cast(v); v; end }.new # :nodoc: + attr_reader :columns, :rows, :column_types def initialize(columns, rows, column_types = {}) @@ -17,8 +42,16 @@ module ActiveRecord @column_types = column_types end + def identity_type # :nodoc: + IDENTITY_TYPE + end + def each - hash_rows.each { |row| yield row } + if block_given? + hash_rows.each { |row| yield row } + else + hash_rows.to_enum + end end def to_hash @@ -52,6 +85,7 @@ module ActiveRecord end private + def hash_rows @hash_rows ||= begin diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 0ed97b66d6..0b87ab9926 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -3,8 +3,8 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - def quote_value(value, column = nil) #:nodoc: - connection.quote(value,column) + def quote_value(value, column) #:nodoc: + connection.quote(value, column) end # Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>. @@ -86,6 +86,7 @@ module ActiveRecord # { address: Address.new("123 abc st.", "chicago") } # # => "address_street='123 abc st.' and address_city='chicago'" def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) + attrs = PredicateBuilder.resolve_column_aliases self, attrs attrs = expand_hash_conditions_for_aggregates(attrs) table = Arel::Table.new(table_name, arel_engine).alias(default_table_name) diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index d37d33d552..a5d6aad3f0 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -8,14 +8,6 @@ module ActiveRecord class_attribute :default_scopes, instance_writer: false, instance_predicate: false self.default_scopes = [] - - def self.default_scopes? - ActiveSupport::Deprecation.warn( - "#default_scopes? is deprecated. Do something like #default_scopes.empty? instead." - ) - - !!self.default_scopes - end end module ClassMethods @@ -91,12 +83,11 @@ module ActiveRecord scope = Proc.new if block_given? if scope.is_a?(Relation) || !scope.respond_to?(:call) - ActiveSupport::Deprecation.warn( - "Calling #default_scope without a block is deprecated. For example instead " \ + raise ArgumentError, + "Support for calling #default_scope without a block is removed. For example instead " \ "of `default_scope where(color: 'red')`, please use " \ "`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \ "self.default_scope.)" - ) end self.default_scopes += [scope] diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index da73bead32..2a5718f388 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -25,22 +25,18 @@ module ActiveRecord if current_scope current_scope.clone else - scope = relation - scope.default_scoped = true - scope + default_scoped end end + def default_scoped # :nodoc: + relation.merge(build_default_scope) + end + # Collects attributes from scopes that should be applied when creating # an AR instance for the particular class this is called on. def scope_attributes # :nodoc: - if current_scope - current_scope.scope_for_create - else - scope = relation - scope.default_scoped = true - scope.scope_for_create - end + all.scope_for_create end # Are there default attributes associated with this scope? @@ -145,26 +141,9 @@ module ActiveRecord def scope(name, body, &block) extension = Module.new(&block) if block - # Check body.is_a?(Relation) to prevent the relation actually being - # loaded by respond_to? - if body.is_a?(Relation) || !body.respond_to?(:call) - ActiveSupport::Deprecation.warn( - "Using #scope without passing a callable object is deprecated. For " \ - "example `scope :red, where(color: 'red')` should be changed to " \ - "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \ - "in the former usage and it makes the implementation more complicated " \ - "and buggy. (If you prefer, you can just define a class method named " \ - "`self.red`.)" - ) - end - singleton_class.send(:define_method, name) do |*args| - if body.respond_to?(:call) - scope = all.scoping { body.call(*args) } - scope = scope.extending(extension) if extension - else - scope = body - end + scope = all.scoping { body.call(*args) } + scope = scope.extending(extension) if extension scope || all end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 3e8b79c7a0..5ff594fdca 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -50,10 +50,6 @@ module ActiveRecord register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) - register_task(/firebird/, ActiveRecord::Tasks::FirebirdDatabaseTasks) - register_task(/sqlserver/, ActiveRecord::Tasks::SqlserverDatabaseTasks) - register_task(/(oci|oracle)/, ActiveRecord::Tasks::OracleDatabaseTasks) - def current_config(options = {}) options.reverse_merge! :env => env if options.has_key?(:config) diff --git a/activerecord/lib/active_record/tasks/firebird_database_tasks.rb b/activerecord/lib/active_record/tasks/firebird_database_tasks.rb deleted file mode 100644 index 98014a38ea..0000000000 --- a/activerecord/lib/active_record/tasks/firebird_database_tasks.rb +++ /dev/null @@ -1,56 +0,0 @@ -module ActiveRecord - module Tasks # :nodoc: - class FirebirdDatabaseTasks # :nodoc: - delegate :connection, :establish_connection, to: ActiveRecord::Base - - def initialize(configuration) - ActiveSupport::Deprecation.warn "This database tasks were deprecated, because this tasks should be served by the 3rd party adapter." - @configuration = configuration - end - - def create - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' - end - - def drop - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' - end - - def purge - establish_connection(:test) - connection.recreate_database! - end - - def charset - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' - end - - def structure_dump(filename) - set_firebird_env(configuration) - db_string = firebird_db_string(configuration) - Kernel.system "isql -a #{db_string} > #{filename}" - end - - def structure_load(filename) - set_firebird_env(configuration) - db_string = firebird_db_string(configuration) - Kernel.system "isql -i #{filename} #{db_string}" - end - - private - - def set_firebird_env(config) - ENV['ISC_USER'] = config['username'].to_s if config['username'] - ENV['ISC_PASSWORD'] = config['password'].to_s if config['password'] - end - - def firebird_db_string(config) - FireRuby::Database.db_string_for(config.symbolize_keys) - end - - def configuration - @configuration - end - end - end -end diff --git a/activerecord/lib/active_record/tasks/oracle_database_tasks.rb b/activerecord/lib/active_record/tasks/oracle_database_tasks.rb deleted file mode 100644 index de3aa50e5e..0000000000 --- a/activerecord/lib/active_record/tasks/oracle_database_tasks.rb +++ /dev/null @@ -1,45 +0,0 @@ -module ActiveRecord - module Tasks # :nodoc: - class OracleDatabaseTasks # :nodoc: - delegate :connection, :establish_connection, to: ActiveRecord::Base - - def initialize(configuration) - ActiveSupport::Deprecation.warn "This database tasks were deprecated, because this tasks should be served by the 3rd party adapter." - @configuration = configuration - end - - def create - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' - end - - def drop - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' - end - - def purge - establish_connection(:test) - connection.structure_drop.split(";\n\n").each { |ddl| connection.execute(ddl) } - end - - def charset - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' - end - - def structure_dump(filename) - establish_connection(configuration) - File.open(filename, "w:utf-8") { |f| f << connection.structure_dump } - end - - def structure_load(filename) - establish_connection(configuration) - IO.read(filename).split(";\n\n").each { |ddl| connection.execute(ddl) } - end - - private - - def configuration - @configuration - end - end - end -end diff --git a/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb deleted file mode 100644 index c718ee03a8..0000000000 --- a/activerecord/lib/active_record/tasks/sqlserver_database_tasks.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'shellwords' - -module ActiveRecord - module Tasks # :nodoc: - class SqlserverDatabaseTasks # :nodoc: - delegate :connection, :establish_connection, to: ActiveRecord::Base - - def initialize(configuration) - ActiveSupport::Deprecation.warn "This database tasks were deprecated, because this tasks should be served by the 3rd party adapter." - @configuration = configuration - end - - def create - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' - end - - def drop - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' - end - - def purge - test = configuration.deep_dup - test_database = test['database'] - test['database'] = 'master' - establish_connection(test) - connection.recreate_database!(test_database) - end - - def charset - $stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch' - end - - def structure_dump(filename) - Kernel.system("smoscript -s #{configuration['host']} -d #{configuration['database']} -u #{configuration['username']} -p #{configuration['password']} -f #{filename} -A -U") - end - - def structure_load(filename) - Kernel.system("sqlcmd -S #{configuration['host']} -d #{configuration['database']} -U #{configuration['username']} -P #{configuration['password']} -i #{filename}") - end - - private - - def configuration - @configuration - end - end - end -end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb deleted file mode 100644 index 1b4c473bfc..0000000000 --- a/activerecord/lib/active_record/test_case.rb +++ /dev/null @@ -1,96 +0,0 @@ -require 'active_support/test_case' - -ActiveSupport::Deprecation.warn('ActiveRecord::TestCase is deprecated, please use ActiveSupport::TestCase') -module ActiveRecord - # = Active Record Test Case - # - # Defines some test assertions to test against SQL queries. - class TestCase < ActiveSupport::TestCase #:nodoc: - def teardown - SQLCounter.clear_log - end - - def assert_date_from_db(expected, actual, message = nil) - # SybaseAdapter doesn't have a separate column type just for dates, - # so the time is in the string and incorrectly formatted - if current_adapter?(:SybaseAdapter) - assert_equal expected.to_s, actual.to_date.to_s, message - else - assert_equal expected.to_s, actual.to_s, message - end - end - - def assert_sql(*patterns_to_match) - SQLCounter.clear_log - yield - SQLCounter.log_all - ensure - failed_patterns = [] - patterns_to_match.each do |pattern| - failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql } - end - assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" - end - - def assert_queries(num = 1, options = {}) - ignore_none = options.fetch(:ignore_none) { num == :any } - SQLCounter.clear_log - x = yield - the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log - if num == :any - assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed." - else - mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}" - assert_equal num, the_log.size, mesg - end - x - end - - def assert_no_queries(&block) - assert_queries(0, :ignore_none => true, &block) - end - - end - - class SQLCounter - class << self - attr_accessor :ignored_sql, :log, :log_all - def clear_log; self.log = []; self.log_all = []; end - end - - self.clear_log - - self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/] - - # FIXME: this needs to be refactored so specific database can add their own - # ignored SQL, or better yet, use a different notification for the queries - # instead examining the SQL content. - oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] - mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/] - postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] - sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im] - - [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql| - ignored_sql.concat db_ignored_sql - end - - attr_reader :ignore - - def initialize(ignore = Regexp.union(self.class.ignored_sql)) - @ignore = ignore - end - - def call(name, start, finish, message_id, values) - sql = values[:sql] - - # FIXME: this seems bad. we should probably have a better way to indicate - # the query was cached - return if 'CACHE' == values[:name] - - self.class.log_all << sql - self.class.log << sql unless ignore =~ sql - end - end - - ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new) -end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 77634b40bb..dcbf38a89f 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -245,7 +245,7 @@ module ActiveRecord if options.is_a?(Hash) && options[:on] assert_valid_transaction_action(options[:on]) options[:if] = Array(options[:if]) - fire_on = Array(options[:on]).map(&:to_sym) + fire_on = Array(options[:on]) options[:if] << "transaction_include_any_action?(#{fire_on})" end end @@ -288,25 +288,26 @@ module ActiveRecord clear_transaction_record_state end - # Call the after_commit callbacks + # Call the +after_commit+ callbacks. # # Ensure that it is not called if the object was never persisted (failed create), - # but call it after the commit of a destroyed object + # but call it after the commit of a destroyed object. def committed! #:nodoc: run_callbacks :commit if destroyed? || persisted? ensure clear_transaction_record_state end - # Call the after rollback callbacks. The restore_state argument indicates if the record + # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state = false) #:nodoc: run_callbacks :rollback ensure restore_transaction_record_state(force_restore_state) + clear_transaction_record_state end - # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks + # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks # can be called. def add_to_transaction if self.class.connection.add_transaction_record(self) @@ -360,8 +361,8 @@ module ActiveRecord # Restore the new record state and id of a record that was previously saved by a call to save_record_state. def restore_transaction_record_state(force = false) #:nodoc: unless @_start_transaction_state.empty? - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - if @_start_transaction_state[:level] < 1 || force + transaction_level = (@_start_transaction_state[:level] || 0) - 1 + if transaction_level < 1 || force restore_state = @_start_transaction_state was_frozen = restore_state[:frozen?] @attributes = @attributes.dup if @attributes.frozen? @@ -374,7 +375,6 @@ module ActiveRecord @attributes_cache.delete(self.class.primary_key) end @attributes.freeze if was_frozen - @_start_transaction_state.clear end end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 52e46e1ffe..b55af692d6 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -197,8 +197,8 @@ module ActiveRecord # will result in the default Rails exception page being shown), or you # can catch it and restart the transaction (e.g. by telling the user # that the title already exists, and asking him to re-enter the title). - # This technique is also known as optimistic concurrency control: - # http://en.wikipedia.org/wiki/Optimistic_concurrency_control. + # This technique is also known as + # {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control]. # # The bundled ActiveRecord::ConnectionAdapters distinguish unique index # constraint errors from other types of database errors by throwing an |