diff options
Diffstat (limited to 'activerecord/lib')
32 files changed, 434 insertions, 342 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 3490057298..6fd4f3042c 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -586,9 +586,10 @@ module ActiveRecord # belongs_to :tag, inverse_of: :taggings # end # - # If you do not set the +:inverse_of+ record, the association will do its - # best to match itself up with the correct inverse. Automatic +:inverse_of+ - # detection only works on +has_many+, +has_one+, and +belongs_to+ associations. + # If you do not set the <tt>:inverse_of</tt> record, the association will + # do its best to match itself up with the correct inverse. Automatic + # inverse detection only works on <tt>has_many</tt>, <tt>has_one</tt>, and + # <tt>belongs_to</tt> associations. # # Extra options on the associations, as defined in the # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will @@ -599,10 +600,10 @@ module ActiveRecord # especially the ones with non-standard names. # # You can turn off the automatic detection of inverse associations by setting - # the +:automatic_inverse_of+ option to +false+ like so: + # the <tt>:inverse_of</tt> option to <tt>false</tt> like so: # # class Taggable < ActiveRecord::Base - # belongs_to :tag, automatic_inverse_of: false + # belongs_to :tag, inverse_of: false # end # # == Nested \Associations diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index aa5551fe0c..f1bec5787a 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -96,7 +96,7 @@ module ActiveRecord item = eval_scope(klass, scope_chain_item) if scope_chain_item == self.reflection.scope - scope.merge! item.except(:where, :includes) + scope.merge! item.except(:where, :includes, :bind) end scope.includes! item.includes_values diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 6a3658f328..3254da4677 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -111,13 +111,8 @@ module ActiveRecord::Associations::Builder ) end - mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{macro}_dependent_for_#{name} - association(:#{name}).handle_dependency - end - CODE - - model.before_destroy "#{macro}_dependent_for_#{name}" + n = name + model.before_destroy lambda { |o| o.association(n).handle_dependency } end def valid_dependent_options diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 63e9526436..d4e1a0dda1 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -19,82 +19,116 @@ module ActiveRecord::Associations::Builder reflection end - def add_counter_cache_callbacks(reflection) - cache_column = reflection.counter_cache_column - foreign_key = reflection.foreign_key + def valid_dependent_options + [:destroy, :delete] + end + + private - mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def belongs_to_counter_cache_after_create_for_#{name} - if record = #{name} - record.class.increment_counter(:#{cache_column}, record.id) + def add_counter_cache_methods(mixin) + return if mixin.method_defined? :belongs_to_counter_cache_after_create + + mixin.class_eval do + def belongs_to_counter_cache_after_create(association, reflection) + if record = send(association.name) + cache_column = reflection.counter_cache_column + record.class.increment_counter(cache_column, record.id) @_after_create_counter_called = true end end - def belongs_to_counter_cache_before_destroy_for_#{name} - unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == #{foreign_key.to_sym.inspect} - record = #{name} + def belongs_to_counter_cache_before_destroy(association, reflection) + foreign_key = reflection.foreign_key.to_sym + unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key + record = send association.name if record && !self.destroyed? - record.class.decrement_counter(:#{cache_column}, record.id) + cache_column = reflection.counter_cache_column + record.class.decrement_counter(cache_column, record.id) end end end - def belongs_to_counter_cache_after_update_for_#{name} + def belongs_to_counter_cache_after_update(association, reflection) + foreign_key = reflection.foreign_key + cache_column = reflection.counter_cache_column + if (@_after_create_counter_called ||= false) @_after_create_counter_called = false - elsif self.#{foreign_key}_changed? && !new_record? && defined?(#{name.to_s.camelize}) - model = #{name.to_s.camelize} - foreign_key_was = self.#{foreign_key}_was - foreign_key = self.#{foreign_key} + elsif attribute_changed?(foreign_key) && !new_record? && association.constructable? + model = reflection.klass + foreign_key_was = attribute_was foreign_key + foreign_key = attribute foreign_key if foreign_key && model.respond_to?(:increment_counter) - model.increment_counter(:#{cache_column}, foreign_key) + model.increment_counter(cache_column, foreign_key) end if foreign_key_was && model.respond_to?(:decrement_counter) - model.decrement_counter(:#{cache_column}, foreign_key_was) + model.decrement_counter(cache_column, foreign_key_was) end end end - CODE + end + end + + def add_counter_cache_callbacks(reflection) + cache_column = reflection.counter_cache_column + add_counter_cache_methods mixin + association = self + + model.after_create lambda { |record| + record.belongs_to_counter_cache_after_create(association, reflection) + } - model.after_create "belongs_to_counter_cache_after_create_for_#{name}" - model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}" - model.after_update "belongs_to_counter_cache_after_update_for_#{name}" + model.before_destroy lambda { |record| + record.belongs_to_counter_cache_before_destroy(association, reflection) + } + + model.after_update lambda { |record| + record.belongs_to_counter_cache_after_update(association, reflection) + } klass = reflection.class_name.safe_constantize klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) end - def add_touch_callbacks(reflection) - mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def belongs_to_touch_after_save_or_destroy_for_#{name} - foreign_key_field = #{reflection.foreign_key.inspect} - old_foreign_id = attribute_was(foreign_key_field) + def self.touch_record(o, foreign_key, name, touch) # :nodoc: + old_foreign_id = o.attribute_was(foreign_key) - if old_foreign_id - klass = association(#{name.inspect}).klass - old_record = klass.find_by(klass.primary_key => old_foreign_id) + if old_foreign_id + klass = o.association(name).klass + old_record = klass.find_by(klass.primary_key => old_foreign_id) - if old_record - old_record.touch #{options[:touch].inspect if options[:touch] != true} - end - end - - record = #{name} - unless record.nil? || record.new_record? - record.touch #{options[:touch].inspect if options[:touch] != true} + if old_record + if touch != true + old_record.touch touch + else + old_record.touch end end - CODE - - model.after_save "belongs_to_touch_after_save_or_destroy_for_#{name}" - model.after_touch "belongs_to_touch_after_save_or_destroy_for_#{name}" - model.after_destroy "belongs_to_touch_after_save_or_destroy_for_#{name}" + end + + record = o.send name + unless record.nil? || record.new_record? + if touch != true + record.touch touch + else + record.touch + end + end end - def valid_dependent_options - [:destroy, :delete] + def add_touch_callbacks(reflection) + foreign_key = reflection.foreign_key + n = name + touch = options[:touch] + + callback = lambda { |record| + BelongsTo.touch_record(record, foreign_key, n, touch) + } + + model.after_save callback + model.after_touch callback + model.after_destroy callback end end end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 429def5455..0d1bdd21ee 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def valid_options - super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :automatic_inverse_of, :counter_cache] + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache] end def valid_dependent_options diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 96ccbeb8a3..76e48e66e5 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -3,7 +3,7 @@ module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: def valid_options - super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of, :automatic_inverse_of] + super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of] end def constructable? diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 29fae809da..cf8a589496 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -115,8 +115,7 @@ module ActiveRecord if records == :all scope = self.scope else - keys = records.map { |r| r[reflection.association_primary_key] } - scope = self.scope.where(reflection.association_primary_key => keys) + scope = self.scope.where(reflection.klass.primary_key => records) end if method == :delete_all diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 5b2f2d1902..5aa17e5fbb 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -48,7 +48,7 @@ module ActiveRecord end def join_associations - join_parts.last(join_parts.length - 1) + join_parts.drop 1 end def join_base @@ -56,10 +56,9 @@ module ActiveRecord end def join_relation(relation) - join_associations.each do |association| - relation = association.join_relation(relation) + join_associations.inject(relation) do |rel,association| + association.join_relation(rel) end - relation end def columns @@ -132,8 +131,7 @@ module ActiveRecord ref[association.reflection.name] ||= {} end - def build(associations, parent = nil, join_type = Arel::InnerJoin) - parent ||= join_parts.last + def build(associations, parent = join_parts.last, join_type = Arel::InnerJoin) case associations when Symbol, String reflection = parent.reflections[associations.intern] or diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 7f1ebab4cd..9caf73e627 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -50,20 +50,13 @@ module ActiveRecord end end - # *DEPRECATED*: Use ActiveRecord::AttributeMethods::Serialization::ClassMethods#serialized_attributes class level method instead. - def serialized_attributes - message = "Instance level serialized_attributes method is deprecated, please use class level method." - ActiveSupport::Deprecation.warn message - defined?(@serialized_attributes) ? @serialized_attributes : self.class.serialized_attributes - end - class Type # :nodoc: def initialize(column) @column = column end def type_cast(value) - value.unserialized_value + value.unserialized_value @column.type_cast value.value end def type @@ -72,17 +65,17 @@ module ActiveRecord end class Attribute < Struct.new(:coder, :value, :state) # :nodoc: - def unserialized_value - state == :serialized ? unserialize : value + def unserialized_value(v = value) + state == :serialized ? unserialize(v) : value end def serialized_value state == :unserialized ? serialize : value end - def unserialize + def unserialize(v) self.state = :unserialized - self.value = coder.load(value) + self.value = coder.load(v) end def serialize diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 87d4daa6d9..a8a1847554 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -301,7 +301,7 @@ module ActiveRecord def association_valid?(reflection, record) return true if record.destroyed? || record.marked_for_destruction? - unless valid = record.valid?(validation_context) + unless valid = record.valid? if reflection.options[:autosave] record.errors.each do |attribute, message| attribute = "#{reflection.name}.#{attribute}" 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 aabedf15e9..0be4b5cb19 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -25,6 +25,9 @@ module ActiveRecord end end + class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc: + end + # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # @@ -269,6 +272,7 @@ module ActiveRecord end column.limit = limit + column.array = options[:array] if column.respond_to?(:array) column.precision = options[:precision] column.scale = options[:scale] column.default = options[:default] 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 8ffe150de6..3ac55a0f11 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -606,7 +606,7 @@ module ActiveRecord index_options = options.delete(:index) add_column(table_name, "#{ref_name}_id", :integer, options) add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic - add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options + add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options end alias :add_belongs_to :add_reference @@ -694,17 +694,6 @@ module ActiveRecord end end - def add_column_options!(sql, options) #:nodoc: - sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options) - # must explicitly check for :null to allow change_column to work on migrations - if options[:null] == false - sql << " NOT NULL" - end - if options[:auto_increment] == true - sql << " AUTO_INCREMENT" - end - end - # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. # # distinct("posts.id", ["posts.created_at desc"]) @@ -775,37 +764,23 @@ module ActiveRecord column_names = Array(column_name) index_name = index_name(table_name, column: column_names) - if Hash === options # legacy support, since this param was a string - options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) - - index_type = options[:unique] ? "UNIQUE" : "" - index_type = options[:type].to_s if options.key?(:type) - index_name = options[:name].to_s if options.key?(:name) - max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length + options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) - if options.key?(:algorithm) - algorithm = index_algorithms.fetch(options[:algorithm]) { - raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}") - } - end + index_type = options[:unique] ? "UNIQUE" : "" + index_type = options[:type].to_s if options.key?(:type) + index_name = options[:name].to_s if options.key?(:name) + max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length - using = "USING #{options[:using]}" if options[:using].present? - - if supports_partial_index? - index_options = options[:where] ? " WHERE #{options[:where]}" : "" - end - else - if options - message = "Passing a string as third argument of `add_index` is deprecated and will" + - " be removed in Rails 4.1." + - " Use add_index(#{table_name.inspect}, #{column_name.inspect}, unique: true) instead" + if options.key?(:algorithm) + algorithm = index_algorithms.fetch(options[:algorithm]) { + raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}") + } + end - ActiveSupport::Deprecation.warn message - end + using = "USING #{options[:using]}" if options[:using].present? - index_type = options - max_index_length = allowed_index_name_length - algorithm = using = nil + if supports_partial_index? + index_options = options[:where] ? " WHERE #{options[:where]}" : "" end if index_name.length > max_index_length diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 26586f0974..e232cad982 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -116,6 +116,12 @@ module ActiveRecord send m, o end + def visit_AddColumn(o) + sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + sql = "ADD #{quote_column_name(o.name)} #{sql_type}" + add_column_options!(sql, column_options(o)) + end + private def visit_AlterTable(o) @@ -123,12 +129,6 @@ module ActiveRecord sql << o.adds.map { |col| visit_AddColumn col }.join(' ') end - def visit_AddColumn(o) - sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) - sql = "ADD #{quote_column_name(o.name)} #{sql_type}" - add_column_options!(sql, column_options(o)) - end - def visit_ColumnDefinition(o) sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) column_sql = "#{quote_column_name(o.name)} #{sql_type}" @@ -149,6 +149,8 @@ module ActiveRecord column_options[:null] = o.null unless o.null.nil? column_options[:default] = o.default unless o.default.nil? column_options[:column] = o + column_options[:first] = o.first + column_options[:after] = o.after column_options end @@ -164,9 +166,20 @@ module ActiveRecord @conn.type_to_sql type.to_sym, limit, precision, scale end - def add_column_options!(column_sql, column_options) - @conn.add_column_options! column_sql, column_options - column_sql + def add_column_options!(sql, options) + sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options) + # must explicitly check for :null to allow change_column to work on migrations + if options[:null] == false + sql << " NOT NULL" + end + if options[:auto_increment] == true + sql << " AUTO_INCREMENT" + end + sql + end + + def options_include_default?(options) + options.include?(:default) && !(options[:null] == false && options[:default].nil?) end 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 d098ded77c..5b25b26164 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -4,17 +4,26 @@ module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter class SchemaCreation < AbstractAdapter::SchemaCreation - private def visit_AddColumn(o) - add_column_position!(super, o) + add_column_position!(super, column_options(o)) + end + + private + def visit_ChangeColumnDefinition(o) + column = o.column + options = o.options + sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale]) + change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}" + add_column_options!(change_column_sql, options) + add_column_position!(change_column_sql, options) end - def add_column_position!(sql, column) - if column.first + def add_column_position!(sql, options) + if options[:first] sql << " FIRST" - elsif column.after - sql << " AFTER #{quote_column_name(column.after)}" + elsif options[:after] + sql << " AFTER #{quote_column_name(options[:after])}" end sql end @@ -661,10 +670,9 @@ module ActiveRecord end def add_column_sql(table_name, column_name, type, options = {}) - add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - add_column_position!(add_column_sql, options) - add_column_sql + td = create_table_definition table_name, options[:temporary], options[:options] + cd = td.new_column_definition(column_name, type, options) + schema_creation.visit_AddColumn cd end def change_column_sql(table_name, column_name, type, options = {}) @@ -678,14 +686,12 @@ module ActiveRecord options[:null] = column.null end - change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(change_column_sql, options) - add_column_position!(change_column_sql, options) - change_column_sql + options[:name] = column.name + schema_creation.accept ChangeColumnDefinition.new column, type, options end def rename_column_sql(table_name, column_name, new_column_name) - options = {} + options = { name: new_column_name } if column = columns(table_name).find { |c| c.name == column_name.to_s } options[:default] = column.default @@ -696,9 +702,7 @@ module ActiveRecord end current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"] - rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" - add_column_options!(rename_column_sql, options) - rename_column_sql + schema_creation.accept ChangeColumnDefinition.new column, current_type, options end def remove_column_sql(table_name, column_name, type = nil, options = {}) diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 017b8bace6..7e38719811 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -69,10 +69,6 @@ module ActiveRecord end end - # Raised when SQL statement is invalid and the application gets a blank result. - class ThrowResult < ActiveRecordError - end - # Defunct wrapper class kept for compatibility. # +StatementInvalid+ wraps the original exception now. class WrappedDatabaseException < StatementInvalid diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 45dc26f0ed..33e19313a0 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -567,7 +567,7 @@ module ActiveRecord # interpolate the fixture label row.each do |key, value| - row[key] = label if value == "$LABEL" + row[key] = label if "$LABEL" == value end # generate a primary key if necessary diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 8e4ddcac82..ddf2afca0c 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -64,7 +64,7 @@ module ActiveRecord end # Wraps the passed block in a transaction, locking the object - # before yielding. You pass can the SQL locking clause + # before yielding. You can pass the SQL locking clause # as argument (see <tt>lock!</tt>). def with_lock(lock = true) transaction do diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 511a1585a7..e96c347f6f 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -357,11 +357,14 @@ module ActiveRecord class CheckPending def initialize(app) @app = app + @last_check = 0 end def call(env) - ActiveRecord::Base.logger.silence do + mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + if @last_check < mtime ActiveRecord::Migration.check_pending! + @last_check = mtime end @app.call(env) end @@ -700,6 +703,10 @@ module ActiveRecord File.basename(filename) end + def mtime + File.mtime filename + end + delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration private @@ -715,6 +722,16 @@ module ActiveRecord end + class NullMigration < MigrationProxy #:nodoc: + def initialize + super(nil, 0, nil, nil) + end + + def mtime + 0 + end + end + class Migrator#:nodoc: class << self attr_writer :migrations_paths @@ -785,7 +802,11 @@ module ActiveRecord end def last_version - migrations(migrations_paths).last.try(:version)||0 + last_migration.version + end + + def last_migration #:nodoc: + migrations(migrations_paths).last || NullMigration.new end def proper_table_name(name) diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 8bdaeef924..e53e8553ad 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -230,6 +230,10 @@ module ActiveRecord # validates_presence_of :member # end # + # Note that if you do not specify the <tt>inverse_of</tt> option, then + # Active Record will try to automatically guess the inverse association + # based on heuristics. + # # For one-to-one nested associations, if you build the new (in-memory) # child object yourself before assignment, then this module will not # overwrite it, e.g.: @@ -302,14 +306,9 @@ module ActiveRecord attr_names.each do |association_name| if reflection = reflect_on_association(association_name) - reflection.options[:autosave] = true + reflection.autosave = true add_autosave_association_callbacks(reflection) - # Clear cached values of any inverse associations found in the - # reflection and prevent the reflection from finding inverses - # automatically in the future. - reflection.remove_automatic_inverse_of! - nested_attributes_options = self.nested_attributes_options.dup nested_attributes_options[association_name.to_sym] = options self.nested_attributes_options = nested_attributes_options diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 711fc8b883..d166f0dd66 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -39,7 +39,7 @@ module ActiveRecord end def to_sql - @to_sql ||= "" + "" end def where_values_hash @@ -55,7 +55,11 @@ module ActiveRecord end def calculate(_operation, _column_name, _options = {}) - nil + if _operation == :count + 0 + else + nil + end end def exists?(_id = false) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a8905ed739..582006ea7d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -333,10 +333,44 @@ module ActiveRecord toggle(attribute).update_attribute(attribute, self[attribute]) end - # Reloads the attributes of this object from the database. - # The optional options argument is passed to find when reloading so you - # may do e.g. record.reload(lock: true) to reload the same record with - # an exclusive row lock. + # Reloads the record from the database. + # + # This method modifies the receiver in-place. Attributes are updated, and + # caches busted, in particular the associations cache. + # + # If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt> + # is raised. Otherwise, in addition to the in-place modification the method + # returns +self+ for convenience. + # + # The optional <tt>:lock</tt> flag option allows you to lock the reloaded record: + # + # reload(lock: true) # reload with pessimistic locking + # + # Reloading is commonly used in test suites to test something is actually + # written to the database, or when some action modifies the corresponding + # row in the database but not the object in memory: + # + # assert account.deposit!(25) + # assert_equal 25, account.credit # check it is updated in memory + # assert_equal 25, account.reload.credit # check it is also persisted + # + # Another commom use case is optimistic locking handling: + # + # def with_optimistic_retry + # begin + # yield + # rescue ActiveRecord::StaleObjectError + # begin + # # Reload lock_version in particular. + # reload + # rescue ActiveRecord::RecordNotFound + # # If the record is gone there is nothing to do. + # else + # retry + # end + # end + # end + # def reload(options = nil) clear_aggregation_cache clear_association_cache diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 434af3c5f8..f69e9b2217 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -360,7 +360,7 @@ db_namespace = namespace :db do end # desc 'Check for pending migrations and load the test schema' - task :prepare do + task :prepare => :load_config do unless ActiveRecord::Base.configurations.blank? db_namespace['test:load'].invoke end diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 8499bb16e7..b3ddfd63d4 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -20,11 +20,5 @@ module ActiveRecord self._attr_readonly end end - - def _attr_readonly - message = "Instance level _attr_readonly method is deprecated, please use class level method." - ActiveSupport::Deprecation.warn message - defined?(@_attr_readonly) ? @_attr_readonly : self.class._attr_readonly - end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 1f76adb367..cfae2ffdb5 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -5,7 +5,9 @@ module ActiveRecord included do class_attribute :reflections + class_attribute :aggregate_reflections self.reflections = {} + self.aggregate_reflections = {} end # \Reflection enables to interrogate Active Record classes and objects @@ -21,18 +23,24 @@ module ActiveRecord case macro when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many klass = options[:through] ? ThroughReflection : AssociationReflection - reflection = klass.new(macro, name, scope, options, active_record) when :composed_of - reflection = AggregateReflection.new(macro, name, scope, options, active_record) + 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 - self.reflections = self.reflections.merge(name => reflection) reflection end # Returns an array of AggregateReflection objects for all the aggregations in the class. def reflect_on_all_aggregations - reflections.values.grep(AggregateReflection) + aggregate_reflections.values end # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). @@ -40,8 +48,7 @@ module ActiveRecord # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection # def reflect_on_aggregation(aggregation) - reflection = reflections[aggregation] - reflection if reflection.is_a?(AggregateReflection) + aggregate_reflections[aggregation] end # Returns an array of AssociationReflection objects for all the @@ -55,7 +62,7 @@ module ActiveRecord # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations # def reflect_on_all_associations(macro = nil) - association_reflections = reflections.values.grep(AssociationReflection) + association_reflections = reflections.values macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections end @@ -65,8 +72,7 @@ module ActiveRecord # Invoice.reflect_on_association(:line_items).macro # returns :has_many # def reflect_on_association(association) - reflection = reflections[association] - reflection if reflection.is_a?(AssociationReflection) + reflections[association] end # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. @@ -117,6 +123,11 @@ module ActiveRecord name.to_s.pluralize : name.to_s end + def autosave=(autosave) + @automatic_inverse_of = false + @options[:autosave] = autosave + end + # Returns the class for the macro. # # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class @@ -290,15 +301,13 @@ module ActiveRecord alias :source_macro :macro def has_inverse? - @options[:inverse_of] || find_inverse_of_automatically + inverse_name end def inverse_of - @inverse_of ||= if options[:inverse_of] - klass.reflect_on_association(options[:inverse_of]) - else - find_inverse_of_automatically - end + return unless inverse_name + + @inverse_of ||= klass.reflect_on_association inverse_name end # Clears the cached value of +@inverse_of+ on this object. This will @@ -308,14 +317,6 @@ module ActiveRecord @inverse_of = nil end - # Removes the cached inverse association that was found automatically - # and prevents this object from finding the inverse association - # automatically in the future. - def remove_automatic_inverse_of! - @automatic_inverse_of = nil - options[:automatic_inverse_of] = false - end - def polymorphic_inverse_of(associated_class) if has_inverse? if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of]) @@ -388,26 +389,21 @@ module ActiveRecord INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key] private - # Attempts to find the inverse association automatically. - # If it cannot find a suitable inverse association, it returns + # Attempts to find the inverse association name automatically. + # If it cannot find a suitable inverse association name, it returns # nil. - def find_inverse_of_automatically - if @automatic_inverse_of == false - nil - elsif @automatic_inverse_of.nil? - set_automatic_inverse_of - else - klass.reflect_on_association(@automatic_inverse_of) + def inverse_name + options.fetch(:inverse_of) do + if @automatic_inverse_of == false + nil + else + @automatic_inverse_of ||= automatic_inverse_of + end end end - # Sets the +@automatic_inverse_of+ instance variable, and returns - # either nil or the inverse association that it finds. - # - # This method caches the inverse association that is found so that - # future calls to +find_inverse_of_automatically+ have much less - # overhead. - def set_automatic_inverse_of + # 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 @@ -420,16 +416,11 @@ module ActiveRecord end if valid_inverse_reflection?(reflection) - @automatic_inverse_of = inverse_name - reflection - else - @automatic_inverse_of = false - nil + return inverse_name end - else - @automatic_inverse_of = false - nil end + + false end # Checks if the inverse reflection that is returned from the @@ -441,22 +432,23 @@ module ActiveRecord # from calling +klass+, +reflection+ will already be set to false. def valid_inverse_reflection?(reflection) reflection && - klass.name == reflection.active_record.try(:name) && + klass.name == reflection.active_record.name && klass.primary_key == reflection.active_record_primary_key && can_find_inverse_of_automatically?(reflection) end # Checks to see if the reflection doesn't have any options that prevent # us from being able to guess the inverse automatically. First, the - # +automatic_inverse_of+ option cannot be set to false. Second, we must - # have +has_many+, +has_one+, +belongs_to+ associations. Third, we must - # not have options such as +:polymorphic+ or +:foreign_key+ which prevent us - # from correctly guessing the inverse association. + # <tt>inverse_of</tt> option cannot be set to false. Second, we must + # have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations. + # Third, we must not have options such as <tt>:polymorphic</tt> or + # <tt>:foreign_key</tt> which prevent us from correctly guessing the + # inverse association. # # Anything with a scope can additionally ruin our attempt at finding an # inverse, so we exclude reflections with scopes. def can_find_inverse_of_automatically?(reflection) - reflection.options[:automatic_inverse_of] != false && + reflection.options[:inverse_of] != false && VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro) && !INVALID_AUTOMATIC_INVERSE_OPTIONS.any? { |opt| reflection.options[opt] } && !reflection.scope @@ -493,6 +485,11 @@ module ActiveRecord delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :type, :to => :source_reflection + def initialize(macro, name, scope, options, active_record) + super + @source_reflection_name = options[:source] + end + # Returns the source of the through reflection. It checks both a singularized # and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>. # @@ -511,7 +508,7 @@ module ActiveRecord # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags"> # def source_reflection - @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first + through_reflection.klass.reflect_on_association(source_reflection_name) end # Returns the AssociationReflection object specified in the <tt>:through</tt> option @@ -527,7 +524,7 @@ module ActiveRecord # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings"> # def through_reflection - @through_reflection ||= active_record.reflect_on_association(options[:through]) + active_record.reflect_on_association(options[:through]) end # Returns an array of reflections which are involved in this association. Each item in the @@ -629,7 +626,32 @@ module ActiveRecord # # => [:tag, :tags] # def source_reflection_names - @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym } + (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }.uniq + end + + def source_reflection_name # :nodoc: + return @source_reflection_name if @source_reflection_name + + names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq + names = names.find_all { |n| + through_reflection.klass.reflect_on_association(n) + } + + if names.length > 1 + example_options = options.dup + example_options[:source] = source_reflection_names.first + ActiveSupport::Deprecation.warn <<-eowarn +Ambiguous source reflection for through association. Please specify a :source +directive on your declaration like: + + class #{active_record.name} < ActiveRecord::Base + #{macro} :#{name}, #{example_options} + end + + eowarn + end + + @source_reflection_name = names.first end def source_options diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 94ab465cc1..d37471e9ad 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -26,7 +26,6 @@ module ActiveRecord @klass = klass @table = table @values = values - @implicit_readonly = nil @loaded = false @default_scoped = false end @@ -76,10 +75,10 @@ module ActiveRecord def update_record(values, id, id_was) # :nodoc: substitutes, binds = substitute_values values um = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes) - + @klass.connection.update( - um, - 'SQL', + um, + 'SQL', binds) end @@ -94,7 +93,7 @@ module ActiveRecord end [substitutes, binds] - end + end # Initializes new record from relation while maintaining the current # scope. @@ -155,34 +154,58 @@ module ActiveRecord first || new(attributes, &block) end - # Finds the first record with the given attributes, or creates a record with the attributes - # if one is not found. + # Finds the first record with the given attributes, or creates a record + # with the attributes if one is not found: # - # ==== Examples - # # Find the first user named Penélope or create a new one. + # # Find the first user named "Penélope" or create a new one. # User.find_or_create_by(first_name: 'Penélope') - # # => <User id: 1, first_name: 'Penélope', last_name: nil> + # # => #<User id: 1, first_name: "Penélope", last_name: nil> # - # # Find the first user named Penélope or create a new one. + # # Find the first user named "Penélope" or create a new one. # # We already have one so the existing record will be returned. # User.find_or_create_by(first_name: 'Penélope') - # # => <User id: 1, first_name: 'Penélope', last_name: nil> + # # => #<User id: 1, first_name: "Penélope", last_name: nil> # - # # Find the first user named Scarlett or create a new one with a particular last name. + # # Find the first user named "Scarlett" or create a new one with + # # a particular last name. # User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett') - # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> + # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson"> # - # # Find the first user named Scarlett or create a new one with a different last name. - # # We already have one so the existing record will be returned. + # This method accepts a block, which is passed down to +create+. The last example + # above can be alternatively written this way: + # + # # Find the first user named "Scarlett" or create a new one with a + # # different last name. # User.find_or_create_by(first_name: 'Scarlett') do |user| - # user.last_name = "O'Hara" + # user.last_name = 'Johansson' # end - # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> + # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson"> + # + # This method always returns a record, but if creation was attempted and + # failed due to validation errors it won't be persisted, you get what + # +create+ returns in such situation. + # + # Please note *this method is not atomic*, it runs first a SELECT, and if + # there are no results an INSERT is attempted. If there are other threads + # or processes there is a race condition between both calls and it could + # be the case that you end up with two similar records. + # + # Whether that is a problem or not depends on the logic of the + # application, but in the particular case in which rows have a UNIQUE + # constraint an exception may be raised, just retry: + # + # begin + # CreditAccount.find_or_create_by(user_id: user.id) + # rescue ActiveRecord::RecordNotUnique + # retry + # end + # def find_or_create_by(attributes, &block) find_by(attributes) || create(attributes, &block) end - # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception is raised if the created record is invalid. + # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception + # is raised if the created record is invalid. def find_or_create_by!(attributes, &block) find_by(attributes) || create!(attributes, &block) end @@ -224,7 +247,7 @@ module ActiveRecord def empty? return @records.empty? if loaded? - c = count + c = count(:all) c.respond_to?(:zero?) ? c.zero? : c.empty? end @@ -582,10 +605,7 @@ module ActiveRecord ActiveRecord::Associations::Preloader.new(@records, associations).run end - # @readonly_value is true only if set explicitly. @implicit_readonly is true if there - # are JOINS and no explicit SELECT. - readonly = readonly_value.nil? ? @implicit_readonly : readonly_value - @records.each { |record| record.readonly! } if readonly + @records.each { |record| record.readonly! } if readonly_value else @records = default_scoped.to_a end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 7239270c4d..4becf3980d 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -56,15 +56,7 @@ module ActiveRecord # # Person.sum(:age) # => 4562 def sum(*args) - if block_given? - ActiveSupport::Deprecation.warn( - "Calling #sum with a block is deprecated and will be removed in Rails 4.1. " \ - "If you want to perform sum calculation over the array of elements, use `to_a.sum(&block)`." - ) - self.to_a.sum(*args) {|*block_args| yield(*block_args)} - else - calculate(:sum, *args) - end + calculate(:sum, *args) end # This calculates aggregate values in the given column. Methods for count, sum, average, @@ -114,8 +106,6 @@ module ActiveRecord else relation.calculate(operation, column_name, options) end - rescue ThrowResult - 0 end # Use <tt>pluck</tt> as a shortcut to select one or more attributes without @@ -217,15 +207,18 @@ module ActiveRecord end if operation == "count" - column_name ||= (select_for_count || :all) + if select_values.present? + column_name ||= select_values.join(", ") + else + column_name ||= :all + end unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? distinct = true end column_name = primary_key if column_name == :all && distinct - - distinct = nil if column_name =~ /\s*DISTINCT\s+/i + distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i end if group_values.any? @@ -386,13 +379,6 @@ module ActiveRecord column ? column.type_cast(value) : value end - def select_for_count - if select_values.present? - select = select_values.join(", ") - select if select !~ /[,*]/ - 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/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index f1c4b5392f..1550d702a6 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -203,8 +203,9 @@ module ActiveRecord conditions = conditions.id if Base === conditions return false if !conditions - join_dependency = construct_join_dependency - relation = construct_relation_for_association_find(join_dependency) + 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) case conditions @@ -214,9 +215,8 @@ module ActiveRecord relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none end - connection.select_value(relation, "#{name} Exists", relation.bind_values) - rescue ThrowResult - false + relation = relation.with_default_scope + connection.select_value(relation.arel, "#{name} Exists", relation.bind_values) end # This method is called whenever no records are found with either a single @@ -246,10 +246,12 @@ module ActiveRecord def find_with_associations join_dependency = construct_join_dependency relation = construct_relation_for_association_find(join_dependency) - rows = connection.select_all(relation, 'SQL', relation.bind_values.dup) - join_dependency.instantiate(rows) - rescue ThrowResult - [] + if ActiveRecord::NullRelation === relation + [] + else + rows = connection.select_all(relation, 'SQL', relation.bind_values.dup) + join_dependency.instantiate(rows) + end end def construct_join_dependency(joins = []) @@ -273,21 +275,22 @@ module ActiveRecord if using_limitable_reflections?(join_dependency.reflections) relation else - relation.where!(construct_limited_ids_condition(relation)) if relation.limit_value + if relation.limit_value + limited_ids = limited_ids_for(relation) + limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids)) + end relation.except(:limit, :offset) end end - def construct_limited_ids_condition(relation) + def limited_ids_for(relation) values = @klass.connection.columns_for_distinct( "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values) relation = relation.except(:select).select(values).distinct! id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values) - ids_array = id_rows.map {|row| row[primary_key]} - - ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) + id_rows.map {|row| row[primary_key]} end def find_with_ids(*ids) diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index eea43aff0e..c114ea0c0d 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -99,8 +99,15 @@ module ActiveRecord end def merge_multi_values - relation.where_values = merged_wheres - relation.bind_values = merged_binds + 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 if values[:reordering] # override any order specified in the original relation @@ -123,33 +130,27 @@ module ActiveRecord end end - def merged_binds - if values[:bind] - (relation.bind_values + values[:bind]).uniq(&:first) - else - relation.bind_values - end - end - - def merged_wheres - values[:where] ||= [] + def filter_binds(lhs_binds, removed_wheres) + return lhs_binds if removed_wheres.empty? - if values[:where].empty? || relation.where_values.empty? - relation.where_values + values[:where] - else - sanitized_wheres + values[:where] - end + set = Set.new removed_wheres.map { |x| x.left.name } + lhs_binds.dup.delete_if { |col,_| set.include? col.name } end # Remove equalities from the existing relation with a LHS which is # present in the relation being merged in. - def sanitized_wheres - seen = Set.new - values[:where].each do |w| - seen << w.left if w.respond_to?(:operator) && w.operator == :== + # returns [things_to_remove, things_to_keep] + def partition_overwrites(lhs_wheres, rhs_wheres) + if lhs_wheres.empty? || rhs_wheres.empty? + return [[], lhs_wheres] + end + + nodes = rhs_wheres.find_all do |w| + w.respond_to?(:operator) && w.operator == :== end + seen = Set.new(nodes) { |node| node.left } - relation.where_values.reject do |w| + lhs_wheres.partition do |w| w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left) end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index d020f1ba52..ca1de2d4dc 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -864,8 +864,6 @@ module ActiveRecord return [] if joins.empty? - @implicit_readonly = true - joins.map do |join| case join when Array @@ -947,8 +945,6 @@ module ActiveRecord join_dependency.graft(*stashed_association_joins) - @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty? - # FIXME: refactor this to build an AST join_dependency.join_associations.each do |association| association.join_to(manager) @@ -961,7 +957,6 @@ module ActiveRecord def build_select(arel, selects) unless selects.empty? - @implicit_readonly = false arel.project(*selects) else arel.project(@klass.arel_table[Arel.star]) @@ -1015,7 +1010,7 @@ module ActiveRecord end def validate_order_args(args) - args.select { |a| Hash === a }.each do |h| + args.grep(Hash) do |h| unless (h.values - [:asc, :desc]).empty? raise ArgumentError, 'Direction should be :asc or :desc' end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 6077144265..fee19b1096 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -4,31 +4,37 @@ require 'active_record/base' module ActiveRecord class SchemaMigration < ActiveRecord::Base + class << self - def self.table_name - "#{Base.table_name_prefix}schema_migrations#{Base.table_name_suffix}" - end + def table_name + "#{table_name_prefix}schema_migrations#{table_name_suffix}" + end - def self.index_name - "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}" - end + def index_name + "#{table_name_prefix}unique_schema_migrations#{table_name_suffix}" + end - def self.create_table(limit=nil) - unless connection.table_exists?(table_name) - version_options = {null: false} - version_options[:limit] = limit if limit + def table_exists? + connection.table_exists?(table_name) + end + + def create_table(limit=nil) + unless table_exists? + version_options = {null: false} + version_options[:limit] = limit if limit - connection.create_table(table_name, id: false) do |t| - t.column :version, :string, version_options + connection.create_table(table_name, id: false) do |t| + t.column :version, :string, version_options + end + connection.add_index table_name, :version, unique: true, name: index_name end - connection.add_index table_name, :version, unique: true, name: index_name end - end - def self.drop_table - if connection.table_exists?(table_name) - connection.remove_index table_name, name: index_name - connection.drop_table(table_name) + def drop_table + if table_exists? + connection.remove_index table_name, name: index_name + connection.drop_table(table_name) + end end end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 744780d069..b4785d3ba4 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -2,7 +2,7 @@ module ActiveRecord module Validations class AssociatedValidator < ActiveModel::EachValidator #:nodoc: def validate_each(record, attribute, value) - if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?(record.validation_context) }.any? + if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any? record.errors.add(attribute, :invalid, options.merge(:value => value)) end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index a705d8c2c4..52e46e1ffe 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -7,11 +7,7 @@ module ActiveRecord "Pass a callable instead: `conditions: -> { where(approved: true) }`" end super({ case_sensitive: true }.merge!(options)) - end - - # Unfortunately, we have to tie Uniqueness validators to a class. - def setup(klass) - @klass = klass + @klass = options[:class] end def validate_each(record, attribute, value) @@ -34,7 +30,6 @@ module ActiveRecord end protected - # The check for an existing value should be run from a class that # isn't abstract. This means working down from the current class # (self), to the first non-abstract class. Since classes don't know |