diff options
Diffstat (limited to 'activerecord/lib/active_record')
16 files changed, 129 insertions, 102 deletions
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 1369212837..f6274c027e 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -31,7 +31,7 @@ module ActiveRecord::Associations::Builder def self.define_validations(model, reflection) super if reflection.options[:required] - model.validates_presence_of reflection.name + model.validates_presence_of reflection.name, message: :required end end end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 4b75370171..fcf06323e6 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -232,6 +232,7 @@ module ActiveRecord end def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) + return if ar_parent.nil? primary_id = ar_parent.id parent.children.each do |node| @@ -248,7 +249,11 @@ module ActiveRecord key = aliases.column_alias(node, node.primary_key) id = row[key] - next if id.nil? + if id.nil? + nil_association = ar_parent.association(node.reflection.name) + nil_association.loaded! + next + end model = seen[parent.base_klass][primary_id][node.base_klass][id] diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 48b50d9017..91886f1324 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -66,6 +66,10 @@ module ActiveRecord self.class.with_cast_value(name, value, type) end + def with_type(type) + self.class.new(name, value_before_type_cast, type) + end + def type_cast(*) raise NotImplementedError end @@ -137,6 +141,10 @@ module ActiveRecord nil end + def with_type(type) + self.class.with_cast_value(name, nil, type) + end + def with_value_from_database(value) raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" end diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 5b96623b6e..7d0ae32411 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -15,7 +15,7 @@ module ActiveRecord end def decorate_matching_attribute_types(matcher, decorator_name, &block) - clear_caches_calculated_from_columns + reload_schema_from_cache decorator_name = decorator_name.to_s # Create new hashes so we don't modify parent classes @@ -24,10 +24,11 @@ module ActiveRecord private - def add_user_provided_columns(*) - super.map do |column| - decorated_type = attribute_type_decorations.apply(column.name, column.cast_type) - column.with_type(decorated_type) + def load_schema! + super + attribute_types.each do |name, type| + decorated_type = attribute_type_decorations.apply(name, type) + define_attribute(name, decorated_type) end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 6de71896ee..9d58a19304 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -83,7 +83,7 @@ module ActiveRecord generated_attribute_methods.synchronize do return false if @attribute_methods_generated superclass.define_attribute_methods unless self == base_class - super(column_names) + super(attribute_names) @attribute_methods_generated = true end true @@ -185,7 +185,7 @@ module ActiveRecord # # => ["id", "created_at", "updated_at", "name", "age"] def attribute_names @attribute_names ||= if !abstract_class? && table_exists? - column_names + attribute_types.keys else [] end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 06d87ee01e..7ba907f786 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -131,10 +131,8 @@ module ActiveRecord partial_writes? ? super(keys_for_partial_write) : super end - # Serialized attributes should always be written in case they've been - # changed in place. def keys_for_partial_write - changed & persistable_attribute_names + changed & self.class.column_names end def _field_changed?(attr, old_value) diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index fdce68ce45..9142317646 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -10,6 +10,10 @@ module ActiveRecord attributes[name] || Attribute.null(name) end + def []=(name, value) + attributes[name] = value + end + def values_before_type_cast attributes.transform_values(&:value_before_type_cast) end @@ -24,7 +28,7 @@ module ActiveRecord end def keys - attributes.initialized_keys + attributes.each_key.select { |name| self[name].initialized? } end def fetch_value(name) @@ -49,7 +53,7 @@ module ActiveRecord end def initialize_dup(_) - @attributes = attributes.dup + @attributes = attributes.deep_dup super end diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index 3a76f5262d..0f3c285a80 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -20,7 +20,7 @@ module ActiveRecord end class LazyAttributeHash # :nodoc: - delegate :select, :transform_values, to: :materialize + delegate :select, :transform_values, :each_key, to: :materialize def initialize(types, values, additional_types) @types = types @@ -45,10 +45,6 @@ module ActiveRecord delegate_hash[key] = value end - def initialized_keys - delegate_hash.keys | values.keys - end - def initialize_dup(_) @delegate_hash = delegate_hash.transform_values(&:dup) super diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index faf5d632ec..c1b69092bb 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -5,12 +5,8 @@ module ActiveRecord Type = ActiveRecord::Type included do - class_attribute :user_provided_columns, instance_accessor: false # :internal: - class_attribute :user_provided_defaults, instance_accessor: false # :internal: - self.user_provided_columns = {} - self.user_provided_defaults = {} - - delegate :persistable_attribute_names, to: :class + class_attribute :user_provided_types, instance_accessor: false # :internal: + self.user_provided_types = {} end module ClassMethods # :nodoc: @@ -77,70 +73,44 @@ module ActiveRecord # # store_listing = StoreListing.new(price_in_cents: '$10.00') # store_listing.price_in_cents # => 1000 - def attribute(name, cast_type, options = {}) + def attribute(name, cast_type, **options) name = name.to_s - clear_caches_calculated_from_columns - # Assign a new hash to ensure that subclasses do not share a hash - self.user_provided_columns = user_provided_columns.merge(name => cast_type) - - if options.key?(:default) - self.user_provided_defaults = user_provided_defaults.merge(name => options[:default]) - end - end + reload_schema_from_cache - # Returns an array of column objects for the table associated with this class. - def columns - @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name)) + self.user_provided_types = user_provided_types.merge(name => [cast_type, options]) end - # Returns a hash of column objects for the table associated with this class. - def columns_hash - @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] + def define_attribute( + name, + cast_type, + default: NO_DEFAULT_PROVIDED, + user_provided_default: true + ) + attribute_types[name] = cast_type + define_default_attribute(name, default, cast_type, from_user: user_provided_default) end - def persistable_attribute_names # :nodoc: - @persistable_attribute_names ||= connection.schema_cache.columns_hash(table_name).keys - end - - def reset_column_information # :nodoc: + def load_schema! super - clear_caches_calculated_from_columns + user_provided_types.each do |name, (type, options)| + define_attribute(name, type, **options) + end end private - def add_user_provided_columns(schema_columns) - existing_columns = schema_columns.map do |column| - new_type = user_provided_columns[column.name] - if new_type - column.with_type(new_type) - else - column - end - end + NO_DEFAULT_PROVIDED = Object.new # :nodoc: + private_constant :NO_DEFAULT_PROVIDED - existing_column_names = existing_columns.map(&:name) - new_columns = user_provided_columns.except(*existing_column_names).map do |(name, type)| - connection.new_column(name, nil, type) + def define_default_attribute(name, value, type, from_user:) + if value == NO_DEFAULT_PROVIDED + default_attribute = _default_attributes[name].with_type(type) + elsif from_user + default_attribute = Attribute.from_user(name, value, type) + else + default_attribute = Attribute.from_database(name, value, type) end - - existing_columns + new_columns - end - - def clear_caches_calculated_from_columns - @arel_table = nil - @attributes_builder = nil - @column_names = nil - @column_types = nil - @columns = nil - @columns_hash = nil - @content_columns = nil - @default_attributes = nil - @persistable_attribute_names = nil - end - - def raw_default_values - super.merge(user_provided_defaults) + _default_attributes[name] = default_attribute end end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index e68b2c399c..4416217897 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -210,7 +210,7 @@ module ActiveRecord elsif !connected? "#{super} (call '#{super}.connection' to establish a connection)" elsif table_exists? - attr_list = column_types.map { |name, type| "#{name}: #{type.type}" } * ', ' + attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ', ' "#{super}(#{attr_list})" else "#{super}(Table doesn't exist)" diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 5b1b7fe73d..739be524da 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -1,6 +1,7 @@ require 'erb' require 'yaml' require 'zlib' +require 'set' require 'active_support/dependencies' require 'active_support/core_ext/digest/uuid' require 'active_record/fixture_set/file' @@ -131,7 +132,7 @@ module ActiveRecord # Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path))) # end # end - # ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers + # ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers # # - use the helper method in a fixture # photo: @@ -521,12 +522,16 @@ module ActiveRecord update_all_loaded_fixtures fixtures_map connection.transaction(:requires_new => true) do + deleted_tables = Set.new fixture_sets.each do |fs| conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection table_rows = fs.table_rows table_rows.each_key do |table| - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + unless deleted_tables.include? table + conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + end + deleted_tables << table end table_rows.each do |fixture_set_name, rows| diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index fd1e22349b..24098f72dc 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -192,7 +192,7 @@ module ActiveRecord # If this is a StrongParameters hash, and access to inheritance_column is not permitted, # this will ignore the inheritance column and return nil def subclass_from_attributes?(attrs) - columns_hash.include?(inheritance_column) && attrs.is_a?(Hash) + attribute_names.include?(inheritance_column) && attrs.is_a?(Hash) end def subclass_from_attributes(attrs) diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index b1fbd38622..8a3c27e6da 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -7,6 +7,7 @@ en: # Default error messages errors: messages: + required: "must exist" taken: "has already been taken" # Active Record models configuration diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 6f2b65c137..008cda46cd 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -144,7 +144,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) - clear_caches_calculated_from_columns + reload_schema_from_cache @locking_column = value.to_s end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index af0b667262..293db1c57f 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -217,28 +217,37 @@ module ActiveRecord end def attributes_builder # :nodoc: - @attributes_builder ||= AttributeSet::Builder.new(column_types, primary_key) + @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) end - def column_types # :nodoc: - @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h| - h.default = Type::Value.new - end + def columns_hash # :nodoc: + load_schema + @columns_hash + end + + def columns + load_schema + @columns ||= columns_hash.values + end + + def attribute_types # :nodoc: + load_schema + @attribute_types ||= Hash.new(Type::Value.new) end def type_for_attribute(attr_name) # :nodoc: - column_types[attr_name] + attribute_types[attr_name] end # Returns a hash where the keys are column names and the values are # default values when instantiating the AR object for this table. def column_defaults + load_schema _default_attributes.to_hash end def _default_attributes # :nodoc: - @default_attributes ||= attributes_builder.build_from_database( - raw_default_values) + @default_attributes ||= AttributeSet.new({}) end # Returns an array of column names as strings. @@ -281,19 +290,53 @@ module ActiveRecord def reset_column_information connection.clear_cache! undefine_attribute_methods - connection.schema_cache.clear_table_cache!(table_name) if table_exists? + connection.schema_cache.clear_table_cache!(table_name) - @arel_engine = nil - @arel_table = nil - @column_names = nil - @column_types = nil - @content_columns = nil - @default_attributes = nil - @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column + reload_schema_from_cache end private + def schema_loaded? + defined?(@columns_hash) && @columns_hash + end + + def load_schema + unless schema_loaded? + load_schema! + end + end + + def load_schema! + @columns_hash = connection.schema_cache.columns_hash(table_name) + @columns_hash.each do |name, column| + define_attribute( + name, + column.cast_type, + default: column.default, + user_provided_default: false + ) + end + end + + def reload_schema_from_cache + @arel_engine = nil + @arel_table = nil + @column_names = nil + @attribute_types = nil + @content_columns = nil + @default_attributes = nil + @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column + @attributes_builder = nil + @column_names = nil + @attribute_types = nil + @columns = nil + @columns_hash = nil + @content_columns = nil + @default_attributes = nil + @attribute_names = nil + end + # Guesses the table name, but does not decorate it with prefix and suffix information. def undecorated_table_name(class_name = base_class.name) table_name = class_name.to_s.demodulize.underscore @@ -317,10 +360,6 @@ module ActiveRecord base.table_name end end - - def raw_default_values - columns_hash.transform_values(&:default) - end end end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index c3c4d7f1ce..63e0d2fc21 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -167,7 +167,7 @@ module ActiveRecord columns_hash.key?(cn) ? arel_table[cn] : cn } result = klass.connection.select_all(relation.arel, nil, bound_attributes) - result.cast_values(klass.column_types) + result.cast_values(klass.attribute_types) end end |