diff options
Diffstat (limited to 'activerecord/lib')
18 files changed, 170 insertions, 102 deletions
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 38bda0d2a5..7da20d8eea 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -103,7 +103,7 @@ module ActiveRecord counter = reflection.counter_cache_column owner[counter] ||= 0 owner[counter] += difference - owner.send(:clear_attribute_changes, counter) # eww + owner.send(:clear_attribute_change, counter) # eww end end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 1dc8bff193..92792a7a15 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -154,7 +154,7 @@ module ActiveRecord scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end - scope.unscope_values = Array(values[:unscope]) + scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope]) klass.default_scoped.merge(scope) end end diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 73dd3fa041..21fe032a9c 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -55,6 +55,7 @@ module ActiveRecord end def with_value_from_user(value) + type.assert_valid_value(value) self.class.from_user(name, value, type) end diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb index e0bee8c17e..501590cf0e 100644 --- a/activerecord/lib/active_record/attribute/user_provided_default.rb +++ b/activerecord/lib/active_record/attribute/user_provided_default.rb @@ -16,7 +16,7 @@ module ActiveRecord end end - def changed_in_place_from?(old_value) + def changed_from?(old_value) super || changed_from?(database_default.value) end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 82b07de482..99b500526d 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -96,7 +96,7 @@ module ActiveRecord end end - # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an + # Raises an <tt>ActiveRecord::DangerousAttributeError</tt> exception when an # \Active \Record method is defined in the model, otherwise +false+. # # class Person < ActiveRecord::Base diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 0171ef3bdf..84b942d559 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/module/attribute_accessors' +require 'active_record/attribute_mutation_tracker' module ActiveRecord module AttributeMethods @@ -38,19 +39,40 @@ module ActiveRecord end end + def init_internals + super + @mutation_tracker = AttributeMutationTracker.new(@attributes, @attributes.dup) + end + def initialize_dup(other) # :nodoc: super - calculate_changes_from_defaults + @mutation_tracker = AttributeMutationTracker.new(@attributes, + self.class._default_attributes.dup) end def changes_applied - super - store_original_raw_attributes + @previous_mutation_tracker = @mutation_tracker + @changed_attributes = HashWithIndifferentAccess.new + store_original_attributes end def clear_changes_information + @previous_mutation_tracker = nil + @changed_attributes = HashWithIndifferentAccess.new + store_original_attributes + end + + def raw_write_attribute(attr_name, *) + result = super + clear_attribute_change(attr_name) + result + end + + def clear_attribute_changes(attr_names) super - original_raw_attributes.clear + attr_names.each do |attr_name| + clear_attribute_change(attr_name) + end end def changed_attributes @@ -59,7 +81,7 @@ module ActiveRecord if defined?(@cached_changed_attributes) @cached_changed_attributes else - super.reverse_merge(attributes_changed_in_place).freeze + super.reverse_merge(@mutation_tracker.changed_values).freeze end end @@ -69,59 +91,22 @@ module ActiveRecord end end + def previous_changes + previous_mutation_tracker.changes + end + def attribute_changed_in_place?(attr_name) - old_value = original_raw_attribute(attr_name) - @attributes[attr_name].changed_in_place_from?(old_value) + @mutation_tracker.changed_in_place?(attr_name) end private def changes_include?(attr_name) - super || attribute_changed_in_place?(attr_name) - end - - def calculate_changes_from_defaults - @changed_attributes = nil - self.class.column_defaults.each do |attr, orig_value| - set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value) - end - end - - # Wrap write_attribute to remember original attribute value. - def write_attribute(attr, value) - attr = attr.to_s - - old_value = old_attribute_value(attr) - - result = super - store_original_raw_attribute(attr) - save_changed_attribute(attr, old_value) - result - end - - def raw_write_attribute(attr, value) - attr = attr.to_s - - result = super - original_raw_attributes[attr] = value - result - end - - def save_changed_attribute(attr, old_value) - clear_changed_attributes_cache - if attribute_changed_by_setter?(attr) - clear_attribute_changes(attr) unless _field_changed?(attr, old_value) - else - set_attribute_was(attr, old_value) if _field_changed?(attr, old_value) - end + super || @mutation_tracker.changed?(attr_name) end - def old_attribute_value(attr) - if attribute_changed?(attr) - changed_attributes[attr] - else - clone_attribute_value(:_read_attribute, attr) - end + def clear_attribute_change(attr_name) + @mutation_tracker.forget_change(attr_name) end def _update_record(*) @@ -136,41 +121,12 @@ module ActiveRecord changed & self.class.column_names end - def _field_changed?(attr, old_value) - @attributes[attr].changed_from?(old_value) - end - - def attributes_changed_in_place - changed_in_place.each_with_object({}) do |attr_name, h| - orig = @attributes[attr_name].original_value - h[attr_name] = orig - end - end - - def changed_in_place - self.class.attribute_names.select do |attr_name| - attribute_changed_in_place?(attr_name) - end - end - - def original_raw_attribute(attr_name) - original_raw_attributes.fetch(attr_name) do - read_attribute_before_type_cast(attr_name) - end - end - - def original_raw_attributes - @original_raw_attributes ||= {} + def store_original_attributes + @mutation_tracker = @mutation_tracker.now_tracking(@attributes) end - def store_original_raw_attribute(attr_name) - original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database rescue nil - end - - def store_original_raw_attributes - attribute_names.each do |attr| - store_original_raw_attribute(attr) - end + def previous_mutation_tracker + @previous_mutation_tracker ||= NullMutationTracker.new end def cache_changed_attributes diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index f9beb43e4b..9e693b6aee 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -13,7 +13,7 @@ module ActiveRecord set_time_zone_without_conversion(super) elsif value.respond_to?(:in_time_zone) begin - user_input_in_time_zone(value) || super + super(user_input_in_time_zone(value)) || super rescue ArgumentError nil end diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb new file mode 100644 index 0000000000..168794fcb4 --- /dev/null +++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb @@ -0,0 +1,84 @@ +module ActiveRecord + class AttributeMutationTracker + def initialize(attributes, original_attributes) + @attributes = attributes + @original_attributes = original_attributes + end + + def changed_values + attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| + if changed?(attr_name) + result[attr_name] = original_attributes.fetch_value(attr_name) + end + end + end + + def changes + attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| + if changed?(attr_name) + result[attr_name] = [original_attributes.fetch_value(attr_name), attributes.fetch_value(attr_name)] + end + end + end + + def changed?(attr_name) + attr_name = attr_name.to_s + modified?(attr_name) || changed_in_place?(attr_name) + end + + def changed_in_place?(attr_name) + original_database_value = original_attributes[attr_name].value_before_type_cast + attributes[attr_name].changed_in_place_from?(original_database_value) + end + + def forget_change(attr_name) + attr_name = attr_name.to_s + original_attributes[attr_name] = attributes[attr_name].dup + end + + def now_tracking(new_attributes) + AttributeMutationTracker.new(new_attributes, clean_copy_of(new_attributes)) + end + + protected + + attr_reader :attributes, :original_attributes + + private + + def attr_names + attributes.keys + end + + def modified?(attr_name) + attributes[attr_name].changed_from?(original_attributes.fetch_value(attr_name)) + end + + def clean_copy_of(attributes) + attributes.map do |attr| + attr.with_value_from_database(attr.value_for_database) + end + end + end + + class NullMutationTracker + def changed_values + {} + end + + def changes + {} + end + + def changed?(*) + false + end + + def changed_in_place?(*) + false + end + + def forget_change(*) + end + end +end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 013a7d0e01..ee278388a4 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -80,6 +80,11 @@ module ActiveRecord attributes.select { |_, attr| attr.has_been_read? }.keys end + def map(&block) + new_attributes = attributes.transform_values(&block) + AttributeSet.new(new_attributes) + end + protected attr_reader :attributes diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 9ea22ed798..2456b8ad8c 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -14,10 +14,7 @@ module ActiveRecord def dump(obj) return if obj.nil? - unless obj.is_a?(object_class) - raise SerializationTypeMismatch, - "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" - end + assert_valid_value(obj) YAML.dump obj end @@ -26,15 +23,19 @@ module ActiveRecord return yaml unless yaml.is_a?(String) && yaml =~ /^---/ obj = YAML.load(yaml) - unless obj.is_a?(object_class) || obj.nil? - raise SerializationTypeMismatch, - "Attribute was supposed to be a #{object_class}, but was a #{obj.class}" - end + assert_valid_value(obj) obj ||= object_class.new if object_class != Object obj end + def assert_valid_value(obj) + unless obj.nil? || obj.is_a?(object_class) + raise SerializationTypeMismatch, + "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" + end + end + private def check_arity_of_constructor diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 9de79a614c..25dfda9ef8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -761,7 +761,7 @@ module ActiveRecord end def extract_table_ref_from_insert_sql(sql) # :nodoc: - sql[/into\s+([^\(]*).*values\s*\(/im] + sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im] $1.strip if $1 end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index a79bf0366b..10b5fcab24 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -118,7 +118,7 @@ module ActiveRecord elsif mapping.has_value?(value) mapping.key(value) else - raise ArgumentError, "'#{value}' is not a valid #{name}" + assert_valid_value(value) end end @@ -131,6 +131,12 @@ module ActiveRecord mapping.fetch(value, value) end + def assert_valid_value(value) + unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value) + raise ArgumentError, "'#{value}' is not a valid #{name}" + end + end + protected attr_reader :name, :mapping diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index a09437b4b0..2336d23a1c 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -22,7 +22,7 @@ module ActiveRecord # p1.save # # p2.first_name = "should fail" - # p2.save # Raises a ActiveRecord::StaleObjectError + # p2.save # Raises an ActiveRecord::StaleObjectError # # Optimistic locking will also check for stale data when objects are destroyed. Example: # @@ -32,7 +32,7 @@ module ActiveRecord # p1.first_name = "Michael" # p1.save # - # p2.destroy # Raises a ActiveRecord::StaleObjectError + # p2.destroy # Raises an ActiveRecord::StaleObjectError # # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, # or otherwise apply the business logic needed to resolve the conflict. diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index c461c04a26..a9bd094a66 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -50,6 +50,13 @@ module ActiveRecord class_attribute :pluralize_table_names, instance_writer: false self.pluralize_table_names = true + ## + # :singleton-method: + # Accessor for the list of columns names the model should ignore. Ignored columns won't have attribute + # accessors defined, and won't be referenced in SQL queries. + class_attribute :ignored_columns, instance_accessor: false + self.ignored_columns = [].freeze + self.inheritance_column = 'type' delegate :type_for_attribute, to: :class @@ -308,7 +315,7 @@ module ActiveRecord end def load_schema! - @columns_hash = connection.schema_cache.columns_hash(table_name) + @columns_hash = connection.schema_cache.columns_hash(table_name).except(*ignored_columns) @columns_hash.each do |name, column| warn_if_deprecated_type(column) define_attribute( diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 09c36d7b4d..7b53f6e5a0 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -211,6 +211,7 @@ module ActiveRecord def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) + became.instance_variable_set("@mutation_tracker", @mutation_tracker) became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index b113baec33..d940ac244a 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -353,7 +353,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test'] end - # desc 'Check for pending migrations and load the test schema' + # desc 'Load the test schema' task :prepare => %w(environment load_config) do unless ActiveRecord::Base.configurations.blank? db_namespace['test:load'].invoke diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 4a569fc242..1a2988ea77 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -11,6 +11,7 @@ module ActiveRecord :before_commit_without_transaction_enrollment, :commit_without_transaction_enrollment, :rollback_without_transaction_enrollment, + terminator: deprecated_false_terminator, scope: [:kind, :name] end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 203a395415..4ff0740cfb 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -41,6 +41,12 @@ module ActiveRecord ActiveRecord::Store::IndifferentHashAccessor end + def assert_valid_value(value) + if coder.respond_to?(:assert_valid_value) + coder.assert_valid_value(value) + end + end + private def default_value?(value) |