diff options
Diffstat (limited to 'activerecord')
197 files changed, 3773 insertions, 1362 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index f6f7de7fbc..1ba01d3618 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,199 @@ +* Fix the schema dump generated for tables without constraints and with + primary key with default value of custom PostgreSQL function result. + + Fixes #16111 + + *Andrey Novikov* + +* Fix the SQL generated when a `delete_all` is run on an association to not + produce an `IN` statements. + + Before: + + UPDATE "categorizations" SET "category_id" = NULL WHERE + "categorizations"."category_id" = 1 AND "categorizations"."id" IN (1, 2) + + After: + + UPDATE "categorizations" SET "category_id" = NULL WHERE + "categorizations"."category_id" = 1 + + *Eileen M. Uchitelle, Aaron Patterson* + +* Avoid type casting boolean and ActiveSupport::Duration values to numeric + values for string columns. Otherwise, in some database, the string column + values will be coerced to a numeric allowing false or 0.seconds match any + string starting with a non-digit. + + Example: + + App.where(apikey: false) # => SELECT * FROM users WHERE apikey = '0' + + *Dylan Thacker-Smith* + +* Add a `:required` option to singular associations, providing a nicer + API for presence validations on associations. + + *Sean Griffin* + +* Fixed error in `reset_counters` when associations have `select` scope. + (Call to `count` generates invalid SQL.) + + *Cade Truitt* + +* After a successful `reload`, `new_record?` is always false. + + Fixes #12101. + + *Matthew Draper* + +* PostgreSQL renaming table doesn't attempt to rename non existent sequences. + + *Abdelkader Boudih* + +* Move 'dependent: :destroy' handling for 'belongs_to' + from 'before_destroy' to 'after_destroy' callback chain + + Fix #12380. + + *Ivan Antropov* + +* Detect in-place modifications on String attributes. + + Before this change user have to mark the attribute as changed to it be persisted + in the database. Now it is not required anymore. + + Before: + + user = User.first + user.name << ' Griffin' + user.name_will_change! + user.save + user.reload.name # => "Sean Griffin" + + After: + + user = User.first + user.name << ' Griffin' + user.save + user.reload.name # => "Sean Griffin" + + *Sean Griffin* + +* Add `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the record + is invalid. + + *Bogdan Gusiev*, *Marc Schütz* + +* Support for adding and removing foreign keys. Foreign keys are now + a part of `schema.rb`. This is supported by Mysql2Adapter, MysqlAdapter + and PostgreSQLAdapter. + + Many thanks to *Matthew Higgins* for laying the foundation with his work on + [foreigner](https://github.com/matthuhiggins/foreigner). + + Example: + + # within your migrations: + add_foreign_key :articles, :authors + remove_foreign_key :articles, :authors + + *Yves Senn* + +* Fix subtle bugs regarding attribute assignment on models with no primary + key. `'id'` will no longer be part of the attributes hash. + + *Sean Griffin* + +* Deprecate automatic counter caches on `has_many :through`. The behavior was + broken and inconsistent. + + *Sean Griffin* + +* `preload` preserves readonly flag for associations. + + See #15853. + + *Yves Senn* + +* Assume numeric types have changed if they were assigned to a value that + would fail numericality validation, regardless of the old value. Previously + this would only occur if the old value was 0. + + Example: + + model = Model.create!(number: 5) + model.number = '5wibble' + model.number_changed? # => true + + Fixes #14731. + + *Sean Griffin* + +* `reload` no longer merges with the existing attributes. + The attribute hash is fully replaced. The record is put into the same state + as it would be with `Model.find(model.id)`. + + *Sean Griffin* + +* The object returned from `select_all` must respond to `column_types`. + If this is not the case a `NoMethodError` is raised. + + *Sean Griffin* + +* `has_many :through` associations will no longer save the through record + twice when added in an `after_create` callback defined before the + associations. + + Fixes #3798. + + *Sean Griffin* + +* Detect in-place modifications of PG array types + + *Sean Griffin* + +* Add `bin/rake db:purge` task to empty the current database. + + *Yves Senn* + +* Deprecate `serialized_attributes` without replacement. + + *Sean Griffin* + +* Correctly extract IPv6 addresses from `DATABASE_URI`: the square brackets + are part of the URI structure, not the actual host. + + Fixes #15705. + + *Andy Bakun*, *Aaron Stone* + +* Ensure both parent IDs are set on join records when both sides of a + through association are new. + + *Sean Griffin* + +* `ActiveRecord::Dirty` now detects in-place changes to mutable values. + Serialized attributes on ActiveRecord models will no longer save when + unchanged. Fixes #8328. + + *Sean Griffin* + +* Pluck now works when selecting columns from different tables with the same + name. + + Fixes #15649 + + *Sean Griffin* + +* Remove `cache_attributes` and friends. All attributes are cached. + + *Sean Griffin* + +* Remove deprecated method `ActiveRecord::Base.quoted_locking_column`. + + *Akshay Vishnoi* + * `ActiveRecord::FinderMethods.find` with block can handle proc parameter as `Enumerable#find` does. @@ -45,7 +241,8 @@ *Lauro Caetano*, *Carlos Antonio da Silva* -* Return a null column from `column_for_attribute` when no column exists. +* Deprecate returning `nil` from `column_for_attribute` when no column exists. + It will return a null object in Rails 5.0 *Sean Griffin* @@ -378,7 +575,7 @@ *Eric Chahin* -* `sanitize_sql_like` helper method to escape a string for safe use in a SQL +* `sanitize_sql_like` helper method to escape a string for safe use in an SQL LIKE statement. Example: @@ -414,7 +611,7 @@ *Lauro Caetano* * Calling `delete_all` on an unloaded `CollectionProxy` no longer - generates a SQL statement containing each id of the collection: + generates an SQL statement containing each id of the collection: Before: @@ -681,7 +878,7 @@ *Vilius Luneckas* *Ahmed AbouElhamayed* * `before_add` callbacks are fired before the record is saved on - `has_and_belongs_to_many` assocations *and* on `has_many :through` + `has_and_belongs_to_many` associations *and* on `has_many :through` associations. Before this change, `before_add` callbacks would be fired before the record was saved on `has_and_belongs_to_many` associations, but *not* on `has_many :through` associations. diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index f856c482d6..17b00bbaea 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -27,10 +27,12 @@ require 'active_model' require 'arel' require 'active_record/version' +require 'active_record/attribute_set' module ActiveRecord extend ActiveSupport::Autoload + autoload :Attribute autoload :Base autoload :Callbacks autoload :Core diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 6222bfe903..7ec7eb1b24 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -717,9 +717,9 @@ module ActiveRecord # == Eager loading of associations # # Eager loading is a way to find objects of a certain class and a number of named associations. - # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 + # This is one of the easiest ways of to prevent the dreaded N+1 problem in which fetching 100 # posts that each need to display their author triggers 101 database queries. Through the - # use of eager loading, the 101 queries can be reduced to 2. + # use of eager loading, the number of queries will be reduced from 101 to 2. # # class Post < ActiveRecord::Base # belongs_to :author @@ -1309,6 +1309,10 @@ module ActiveRecord # that is the inverse of this <tt>has_one</tt> association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. # # Option examples: # has_one :credit_card, dependent: :destroy # destroys the associated credit card @@ -1320,6 +1324,7 @@ module ActiveRecord # has_one :boss, readonly: :true # has_one :club, through: :membership # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable + # has_one :credit_card, required: true def has_one(name, scope = nil, options = {}) reflection = Builder::HasOne.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1421,6 +1426,10 @@ module ActiveRecord # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in # combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. # # Option examples: # belongs_to :firm, foreign_key: "client_of" @@ -1433,6 +1442,7 @@ module ActiveRecord # belongs_to :post, counter_cache: true # belongs_to :company, touch: true # belongs_to :company, touch: :employees_last_updated_at + # belongs_to :company, required: true def belongs_to(name, scope = nil, options = {}) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1577,7 +1587,7 @@ module ActiveRecord scope = nil end - habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(:has_and_belongs_to_many, name, scope, options, self) + habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self) builder = Builder::HasAndBelongsToMany.new name, self, options diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 31108cc1aa..519d4d8651 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -105,18 +105,9 @@ module ActiveRecord chain.each_with_index do |reflection, i| table, foreign_table = tables.shift, tables.first - if reflection.source_macro == :belongs_to - if reflection.polymorphic? - key = reflection.association_primary_key(assoc_klass) - else - key = reflection.association_primary_key - end - - foreign_key = reflection.foreign_key - else - key = reflection.foreign_key - foreign_key = reflection.active_record_primary_key - end + join_keys = reflection.join_keys(assoc_klass) + key = join_keys.key + foreign_key = join_keys.foreign_key if reflection == chain.last bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index f085fd1cfd..947d61ee7b 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -36,6 +36,7 @@ module ActiveRecord::Associations::Builder reflection = builder.build(model) define_accessors model, reflection define_callbacks model, reflection + define_validations model, reflection builder.define_extensions model reflection end @@ -85,7 +86,11 @@ module ActiveRecord::Associations::Builder end def self.define_callbacks(model, reflection) - add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent] + if dependent = reflection.options[:dependent] + check_dependent_options(dependent) + add_destroy_callbacks(model, reflection) + end + Association.extensions.each do |extension| extension.build model, reflection end @@ -120,17 +125,23 @@ module ActiveRecord::Associations::Builder CODE end + def self.define_validations(model, reflection) + # noop + end + def self.valid_dependent_options raise NotImplementedError end private - def self.add_before_destroy_callbacks(model, reflection) - unless valid_dependent_options.include? reflection.options[:dependent] - raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}" + def self.check_dependent_options(dependent) + unless valid_dependent_options.include? dependent + raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}" end + end + def self.add_destroy_callbacks(model, reflection) name = reflection.name model.before_destroy lambda { |o| o.association(name).handle_dependency } end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 3998aca23e..954ea3878a 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -107,5 +107,10 @@ module ActiveRecord::Associations::Builder model.after_touch callback model.after_destroy callback end + + def self.add_destroy_callbacks(model, reflection) + name = reflection.name + model.after_destroy lambda { |o| o.association(name).handle_dependency } + 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 0ad5206980..34a555dfd4 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 @@ -15,7 +15,10 @@ module ActiveRecord::Associations::Builder end private - def klass; @rhs_class_name.constantize; end + + def klass + @lhs_class.send(:compute_type, @rhs_class_name) + end end def self.build(lhs_class, name, options) @@ -23,13 +26,7 @@ module ActiveRecord::Associations::Builder KnownTable.new options[:join_table].to_s else class_name = options.fetch(:class_name) { - model_name = name.to_s.camelize.singularize - - if lhs_class.parent_name - model_name.prepend("#{lhs_class.parent_name}::") - end - - model_name + name.to_s.camelize.singularize } KnownClass.new lhs_class, class_name end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index a1f4f51664..c194c8ae9a 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -16,7 +16,7 @@ module ActiveRecord::Associations::Builder private - def self.add_before_destroy_callbacks(model, reflection) + def self.add_destroy_callbacks(model, reflection) super unless reflection.options[:through] 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 e655c389a6..17f36acf40 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, :primary_key, :inverse_of] + super + [:remote, :dependent, :primary_key, :inverse_of, :required] end def self.define_accessors(model, reflection) @@ -27,5 +27,12 @@ module ActiveRecord::Associations::Builder end CODE end + + def self.define_validations(model, reflection) + super + if reflection.options[:required] + model.validates_presence_of reflection.name + end + end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 306588ac66..065a2cff01 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -55,9 +55,9 @@ module ActiveRecord # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) - pk_column = reflection.primary_key_column + pk_type = reflection.primary_key_type ids = Array(ids).reject { |id| id.blank? } - ids.map! { |i| pk_column.type_cast_from_user(i) } + ids.map! { |i| pk_type.type_cast_from_user(i) } replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids)) end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 3ffc015975..2a97d0ed31 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -88,9 +88,20 @@ module ActiveRecord end def update_counter(difference, reflection = reflection()) + update_counter_in_database(difference, reflection) + update_counter_in_memory(difference, reflection) + end + + def update_counter_in_database(difference, reflection = reflection()) if has_cached_counter?(reflection) counter = cached_counter_attribute_name(reflection) owner.class.update_counters(owner.id, counter => difference) + end + end + + def update_counter_in_memory(difference, reflection = reflection()) + if has_cached_counter?(reflection) + counter = cached_counter_attribute_name(reflection) owner[counter] += difference owner.changed_attributes.delete(counter) # eww end @@ -108,6 +119,10 @@ module ActiveRecord # Hence this method. def inverse_updates_counter_cache?(reflection = reflection()) counter_name = cached_counter_attribute_name(reflection) + inverse_updates_counter_named?(counter_name, reflection) + end + + def inverse_updates_counter_named?(counter_name, reflection = reflection()) reflection.klass._reflections.values.any? { |inverse_reflection| :belongs_to == inverse_reflection.macro && inverse_reflection.counter_cache_column == counter_name @@ -145,6 +160,25 @@ module ActiveRecord false end end + + def concat_records(records, *) + update_counter_if_success(super, records.length) + end + + def _create_record(attributes, *) + if attributes.is_a?(Array) + super + else + update_counter_if_success(super, 1) + end + end + + def update_counter_if_success(saved_successfully, difference) + if saved_successfully + update_counter_in_memory(difference) + end + saved_successfully + end end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index da9b125fd6..007e3bc555 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -63,7 +63,15 @@ module ActiveRecord end save_through_record(record) - update_counter(1) + if has_cached_counter? && !through_reflection_updates_counter_cache? + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + Automatic updating of counter caches on through associations has been + deprecated, and will be removed in Rails 5.0. Instead, please set the + appropriate counter_cache options on the has_many and belongs_to for + your associations to #{through_reflection.name}. + MESSAGE + update_counter_in_database(1) + end record end @@ -82,14 +90,20 @@ module ActiveRecord @through_records[record.object_id] ||= begin ensure_mutable - through_record = through_association.build through_scope_attributes + through_record = through_association.build(*options_for_through_record) through_record.send("#{source_reflection.name}=", record) through_record end end + def options_for_through_record + [through_scope_attributes] + end + def through_scope_attributes - scope.where_values_hash(through_association.reflection.name.to_s).except!(through_association.reflection.foreign_key) + scope.where_values_hash(through_association.reflection.name.to_s). + except!(through_association.reflection.foreign_key, + through_association.reflection.klass.inheritance_column) end def save_through_record(record) @@ -180,7 +194,11 @@ module ActiveRecord def through_records_for(record) attributes = construct_join_attributes(record) candidates = Array.wrap(through_association.target) - candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes } + candidates.find_all do |c| + attributes.all? do |key, value| + c.public_send(key) == value + end + end end def delete_through_records(records) @@ -208,6 +226,11 @@ module ActiveRecord def invertible_for?(record) false end + + def through_reflection_updates_counter_cache? + counter_name = cached_counter_attribute_name + inverse_updates_counter_named?(counter_name, through_reflection) + end end end end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index fbb4551b22..ec5c189cd3 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -131,7 +131,6 @@ module ActiveRecord def instantiate(result_set, aliases) primary_key = aliases.column_alias(join_root, join_root.primary_key) - type_caster = result_set.column_type primary_key seen = Hash.new { |h,parent_klass| h[parent_klass] = Hash.new { |i,parent_id| @@ -144,8 +143,7 @@ module ActiveRecord column_aliases = aliases.column_aliases join_root result_set.each { |row_hash| - primary_id = type_caster.type_cast_from_database row_hash[primary_key] - parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases) + parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases) construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) } diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 33c8619359..c0639742be 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -104,11 +104,11 @@ module ActiveRecord end def association_key_type - @klass.column_for_attribute(association_key_name).type + @klass.type_for_attribute(association_key_name.to_s).type end def owner_key_type - @model.column_for_attribute(owner_key_name).type + @model.type_for_attribute(owner_key_name.to_s).type end def load_slices(slices) @@ -151,6 +151,10 @@ module ActiveRecord end end + if preload_values[:readonly] || values[:readonly] + scope.readonly! + end + if options[:as] scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index fcf3b219d4..f00fef8b9e 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -41,12 +41,16 @@ module ActiveRecord def construct_join_attributes(*records) ensure_mutable - join_attributes = { - source_reflection.foreign_key => - records.map { |record| - record.send(source_reflection.association_primary_key(reflection.klass)) - } - } + if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key + join_attributes = { source_reflection.name => records } + else + join_attributes = { + source_reflection.foreign_key => + records.map { |record| + record.send(source_reflection.association_primary_key(reflection.klass)) + } + } + end if options[:source_type] join_attributes[source_reflection.foreign_type] = diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb new file mode 100644 index 0000000000..6d38224830 --- /dev/null +++ b/activerecord/lib/active_record/attribute.rb @@ -0,0 +1,120 @@ +module ActiveRecord + class Attribute # :nodoc: + class << self + def from_database(name, value, type) + FromDatabase.new(name, value, type) + end + + def from_user(name, value, type) + FromUser.new(name, value, type) + end + + def null(name) + Null.new(name) + end + + def uninitialized(name, type) + Uninitialized.new(name, type) + end + end + + attr_reader :name, :value_before_type_cast, :type + + # This method should not be called directly. + # Use #from_database or #from_user + def initialize(name, value_before_type_cast, type) + @name = name + @value_before_type_cast = value_before_type_cast + @type = type + end + + def value + # `defined?` is cheaper than `||=` when we get back falsy values + @value = type_cast(value_before_type_cast) unless defined?(@value) + @value + end + + def value_for_database + type.type_cast_for_database(value) + end + + def changed_from?(old_value) + type.changed?(old_value, value, value_before_type_cast) + end + + def changed_in_place_from?(old_value) + type.changed_in_place?(old_value, value) + end + + def with_value_from_user(value) + self.class.from_user(name, value, type) + end + + def with_value_from_database(value) + self.class.from_database(name, value, type) + end + + def type_cast + raise NotImplementedError + end + + def initialized? + true + end + + protected + + def initialize_dup(other) + if defined?(@value) && @value.duplicable? + @value = @value.dup + end + end + + class FromDatabase < Attribute # :nodoc: + def type_cast(value) + type.type_cast_from_database(value) + end + end + + class FromUser < Attribute # :nodoc: + def type_cast(value) + type.type_cast_from_user(value) + end + end + + class Null < Attribute # :nodoc: + def initialize(name) + super(name, nil, Type::Value.new) + end + + def value + nil + end + + def with_value_from_database(value) + raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" + end + alias_method :with_value_from_user, :with_value_from_database + end + + class Uninitialized < Attribute # :nodoc: + def initialize(name, type) + super(name, nil, type) + end + + def value + if block_given? + yield name + end + end + + def value_for_database + end + + def initialized? + false + end + end + private_constant :FromDatabase, :FromUser, :Null, :Uninitialized + end +end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 40e2918777..2887db3bf7 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -115,7 +115,7 @@ module ActiveRecord end class MultiparameterAttribute #:nodoc: - attr_reader :object, :name, :values, :column + attr_reader :object, :name, :values, :cast_type def initialize(object, name, values) @object = object @@ -126,22 +126,22 @@ module ActiveRecord def read_value return if values.values.compact.empty? - @column = object.column_for_attribute(name) - klass = column ? column.klass : nil + @cast_type = object.type_for_attribute(name) + klass = cast_type.klass if klass == Time read_time elsif klass == Date read_date else - read_other(klass) + read_other end end private def instantiate_time_object(set_values) - if object.class.send(:create_time_zone_conversion_attribute?, name, column) + if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type) Time.zone.local(*set_values) else Time.send(object.class.default_timezone, *set_values) @@ -151,7 +151,7 @@ module ActiveRecord def read_time # If column is a :time (and not :date or :datetime) there is no need to validate if # there are year/month/day fields - if column.type == :time + if cast_type.type == :time # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value| values[key] ||= value @@ -181,7 +181,7 @@ module ActiveRecord end end - def read_other(klass) + def read_other max_position = extract_max_param positions = (1..max_position) validate_required_parameters!(positions) diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 596161f81d..5b96623b6e 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -4,31 +4,63 @@ module ActiveRecord included do class_attribute :attribute_type_decorations, instance_accessor: false # :internal: - self.attribute_type_decorations = Hash.new({}) + self.attribute_type_decorations = TypeDecorator.new end - module ClassMethods + module ClassMethods # :nodoc: def decorate_attribute_type(column_name, decorator_name, &block) + matcher = ->(name, _) { name == column_name.to_s } + key = "_#{column_name}_#{decorator_name}" + decorate_matching_attribute_types(matcher, key, &block) + end + + def decorate_matching_attribute_types(matcher, decorator_name, &block) clear_caches_calculated_from_columns - column_name = column_name.to_s + decorator_name = decorator_name.to_s # Create new hashes so we don't modify parent classes - decorations_for_column = attribute_type_decorations[column_name] - new_decorations = decorations_for_column.merge(decorator_name.to_s => block) - self.attribute_type_decorations = attribute_type_decorations.merge(column_name => new_decorations) + self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block]) end private def add_user_provided_columns(*) super.map do |column| - decorations = attribute_type_decorations[column.name].values - decorated_type = decorations.inject(column.cast_type) do |type, block| - block.call(type) - end + decorated_type = attribute_type_decorations.apply(column.name, column.cast_type) column.with_type(decorated_type) end end end + + class TypeDecorator # :nodoc: + delegate :clear, to: :@decorations + + def initialize(decorations = {}) + @decorations = decorations + end + + def merge(*args) + TypeDecorator.new(@decorations.merge(*args)) + end + + def apply(name, type) + decorations = decorators_for(name, type) + decorations.inject(type) do |new_type, block| + block.call(new_type) + end + end + + private + + def decorators_for(name, type) + matching(name, type).map(&:last) + end + + def matching(name, type) + @decorations.values.select do |(matcher, _)| + matcher.call(name, type) + end + end + end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index e626227e7e..cbe5cf202a 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -73,7 +73,7 @@ module ActiveRecord # accessors, mutators and query methods. def define_attribute_methods # :nodoc: return false if @attribute_methods_generated - # Use a mutex; we don't want two thread simultaneously trying to define + # Use a mutex; we don't want two threads simultaneously trying to define # attribute methods. generated_attribute_methods.synchronize do return false if @attribute_methods_generated @@ -156,16 +156,6 @@ module ActiveRecord end end - def find_generated_attribute_method(method_name) # :nodoc: - klass = self - until klass == Base - gen_methods = klass.generated_attribute_methods - return gen_methods.instance_method(method_name) if method_defined_within?(method_name, gen_methods, Object) - klass = klass.superclass - end - nil - end - # Returns +true+ if +attribute+ is an attribute method and table exists, # +false+ otherwise. # @@ -196,8 +186,7 @@ module ActiveRecord end # Returns the column object for the named attribute. - # Returns a +ActiveRecord::ConnectionAdapters::NullColumn+ if the - # named attribute does not exist. + # Returns nil if the named attribute does not exist. # # class Person < ActiveRecord::Base # end @@ -207,31 +196,17 @@ module ActiveRecord # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> # # person.column_for_attribute(:nothing) - # # => #<ActiveRecord::ConnectionAdapters::NullColumn:0xXXX @name=nil, @sql_type=nil, @cast_type=#<Type::Value>, ...> + # # => nil def column_for_attribute(name) - name = name.to_s - columns_hash.fetch(name) do - ConnectionAdapters::NullColumn.new(name) + column = columns_hash[name.to_s] + if column.nil? + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `column_for_attribute` will return a null object for non-existent columns + in Rails 5.0. Use `has_attribute?` if you need to check for an + attribute's existence. + MESSAGE end - end - end - - # 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: - self.class.define_attribute_methods - if respond_to_without_attributes?(method) - # make sure to invoke the correct attribute method, as we might have gotten here via a `super` - # call in a overwritten attribute method - if attribute_method = self.class.find_generated_attribute_method(method) - # this is probably horribly slow, but should only happen at most once for a given AR class - attribute_method.bind(self).call(*args, &block) - else - return super unless respond_to_missing?(method, true) - send(method, *args, &block) - end - else - super + column end end @@ -252,18 +227,14 @@ module ActiveRecord # person.respond_to('age?') # => true # person.respond_to(:nothing) # => false def respond_to?(name, include_private = false) + return false unless super name = name.to_s - self.class.define_attribute_methods - result = super - - # If the result is false the answer is false. - return false unless result # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. - # We check defined?(@raw_attributes) not to issue warnings if called on objects that + # We check defined?(@attributes) not to issue warnings if called on objects that # have been allocated but not yet initialized. - if defined?(@raw_attributes) && @raw_attributes.any? && self.class.column_names.include?(name) + if defined?(@attributes) && self.class.column_names.include?(name) return has_attribute?(name) end @@ -280,7 +251,7 @@ module ActiveRecord # person.has_attribute?('age') # => true # person.has_attribute?(:nothing) # => false def has_attribute?(attr_name) - @raw_attributes.has_key?(attr_name.to_s) + @attributes.include?(attr_name.to_s) end # Returns an array of names for the attributes available on this object. @@ -292,7 +263,7 @@ module ActiveRecord # person.attribute_names # # => ["id", "created_at", "updated_at", "name", "age"] def attribute_names - @raw_attributes.keys + @attributes.keys end # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. @@ -304,9 +275,7 @@ module ActiveRecord # person.attributes # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22} def attributes - attribute_names.each_with_object({}) { |name, attrs| - attrs[name] = read_attribute(name) - } + @attributes.to_hash end # Returns an <tt>#inspect</tt>-like string for the value of the @@ -400,13 +369,6 @@ module ActiveRecord protected - def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc: - attribute_names.each do |name| - attributes[name] = clone_attribute_value(reader_method, name) - end - attributes - end - def clone_attribute_value(reader_method, attribute_name) # :nodoc: value = send(reader_method, attribute_name) value.duplicable? ? value.clone : value @@ -424,7 +386,7 @@ module ActiveRecord def attribute_method?(attr_name) # :nodoc: # We check defined? because Syck calls respond_to? before actually calling initialize. - defined?(@raw_attributes) && @raw_attributes.include?(attr_name) + defined?(@attributes) && @attributes.include?(attr_name) end private @@ -465,9 +427,6 @@ module ActiveRecord end def typecasted_attribute_value(name) - # FIXME: we need @attributes to be used consistently. - # If the values stored in @attributes were already typecasted, this code - # could be simplified read_attribute(name) end end diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index 4365f5a1a1..fd61febd57 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -43,7 +43,7 @@ module ActiveRecord # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21" def read_attribute_before_type_cast(attr_name) - @raw_attributes[attr_name.to_s] + @attributes[attr_name.to_s].value_before_type_cast end # Returns a hash of attributes before typecasting and deserialization. @@ -57,7 +57,7 @@ module ActiveRecord # task.attributes_before_type_cast # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast - @raw_attributes + @attributes.values_before_type_cast end private diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index be438aba95..e1a86fd3aa 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -38,23 +38,42 @@ module ActiveRecord end end - def initialize_dup(other) # :nodoc: - super - init_changed_attributes - end + def initialize_dup(other) # :nodoc: + super + calculate_changes_from_defaults + end + + def changed? + super || changed_in_place.any? + end - private - def initialize_internals_callback + def changed + super | changed_in_place + end + + def attribute_changed?(attr_name, options = {}) + result = super + # We can't change "from" something in place. Only setters can define + # "from" and "to" + result ||= changed_in_place?(attr_name) unless options.key?(:from) + result + end + + def changes_applied + super + store_original_raw_attributes + end + + def reset_changes super - init_changed_attributes + original_raw_attributes.clear end - def init_changed_attributes + private + + def calculate_changes_from_defaults @changed_attributes = nil - # Intentionally avoid using #column_defaults since overridden defaults (as is done in - # optimistic locking) won't get written unless they get marked as changed - self.class.columns.each do |c| - attr, orig_value = c.name, c.default + self.class.column_defaults.each do |attr, orig_value| changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value) end end @@ -65,11 +84,20 @@ module ActiveRecord old_value = old_attribute_value(attr) - result = super(attr, value) + 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) if attribute_changed?(attr) changed_attributes.delete(attr) unless _field_changed?(attr, old_value) @@ -101,9 +129,38 @@ module ActiveRecord end def _field_changed?(attr, old_value) - new_value = read_attribute(attr) - raw_value = read_attribute_before_type_cast(attr) - column_for_attribute(attr).changed?(old_value, new_value, raw_value) + @attributes[attr].changed_from?(old_value) + end + + def changed_in_place + self.class.attribute_names.select do |attr_name| + changed_in_place?(attr_name) + end + end + + def changed_in_place?(attr_name) + old_value = original_raw_attribute(attr_name) + @attributes[attr_name].changed_in_place_from?(old_value) + 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 ||= {} + end + + def store_original_raw_attribute(attr_name) + original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database + end + + def store_original_raw_attributes + attribute_names.each do |attr| + store_original_raw_attribute(attr) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 931209b07b..cadad60ddd 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -15,8 +15,10 @@ module ActiveRecord # Returns the primary key value. def id - sync_with_transaction_state - read_attribute(self.class.primary_key) + if pk = self.class.primary_key + sync_with_transaction_state + read_attribute(pk) + end end # Sets the primary key value. @@ -81,12 +83,9 @@ module ActiveRecord end def get_primary_key(base_name) #:nodoc: - return 'id' if base_name.blank? - - case primary_key_prefix_type - when :table_name + if base_name && primary_key_prefix_type == :table_name base_name.foreign_key(false) - when :table_name_with_underscore + elsif base_name && primary_key_prefix_type == :table_name_with_underscore base_name.foreign_key else if ActiveRecord::Base != self && table_exists? diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index a354cd7503..10869dfc1e 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -35,35 +35,22 @@ module ActiveRecord extend ActiveSupport::Concern - ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :time, :date] - - included do - class_attribute :attribute_types_cached_by_default, instance_writer: false - self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - end - module ClassMethods - # +cache_attributes+ allows you to declare which converted attribute - # values should be cached. Usually caching only pays off for attributes - # with expensive conversion methods, like time related columns (e.g. - # +created_at+, +updated_at+). - def cache_attributes(*attribute_names) - cached_attributes.merge attribute_names.map { |attr| attr.to_s } + [:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name| + define_method method_name do |*| + cached_attributes_deprecation_warning(method_name) + true + end end - # Returns the attributes which are cached. By default time related columns - # with datatype <tt>:datetime, :time, :date</tt> are cached. - def cached_attributes - @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set - end + protected - # Returns +true+ if the provided attribute is being cached. - def cache_attribute?(attr_name) - cached_attributes.include?(attr_name) + def cached_attributes_deprecation_warning(method_name) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + Calling `#{method_name}` is no longer necessary. All attributes are cached. + MESSAGE end - protected - if Module.methods_transplantable? def define_method_attribute(name) method = ReaderMethodCache[name] @@ -89,45 +76,15 @@ module ActiveRecord end end end - - private - - def cacheable_column?(column) - if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - true - else - attribute_types_cached_by_default.include?(column.type) - end - end end # Returns the value of the attribute identified by <tt>attr_name</tt> after # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). - def read_attribute(attr_name) - # If it's cached, just return it - # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829. + def read_attribute(attr_name, &block) name = attr_name.to_s - @attributes[name] || @attributes.fetch(name) { - column = @column_types_override[name] if @column_types_override - column ||= @column_types[name] - - return @raw_attributes.fetch(name) { - if name == 'id' && self.class.primary_key != name - read_attribute(self.class.primary_key) - end - } unless column - - value = @raw_attributes.fetch(name) { - return block_given? ? yield(name) : nil - } - - if self.class.cache_attribute?(name) - @attributes[name] = column.type_cast_from_database(value) - else - column.type_cast_from_database value - end - } + name = self.class.primary_key if name == 'id' + @attributes.fetch_value(name, &block) end private diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 60debb7d18..734d94865a 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -3,20 +3,7 @@ module ActiveRecord module Serialization extend ActiveSupport::Concern - included do - # Returns a hash of all the attributes that have been specified for - # serialization as keys and their class restriction as values. - class_attribute :serialized_attributes, instance_accessor: false - self.serialized_attributes = {} - end - module ClassMethods - ## - # :method: serialized_attributes - # - # Returns a hash of all the attributes that have been specified for - # serialization as keys and their class restriction as values. - # If you have an attribute that needs to be saved to the database as an # object, and retrieved as the same object, then specify the name of that # attribute using this method and it will be handled automatically. The @@ -50,8 +37,6 @@ module ActiveRecord # serialize :preferences, Hash # end def serialize(attr_name, class_name_or_coder = Object) - include Behavior - coder = if [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } class_name_or_coder else @@ -61,25 +46,18 @@ module ActiveRecord decorate_attribute_type(attr_name, :serialize) do |type| Type::Serialized.new(type, coder) end - - # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy - # has its own hash of own serialized attributes - self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) - end - end - - - # This is only added to the model when serialize is called, which - # ensures we do not make things slower when serialization is not used. - module Behavior # :nodoc: - extend ActiveSupport::Concern - - def should_record_timestamps? - super || (self.record_timestamps && (attributes.keys & self.class.serialized_attributes.keys).present?) end - def keys_for_partial_write - super | (attributes.keys & self.class.serialized_attributes.keys) + def serialized_attributes + ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc) + `serialized_attributes` is deprecated without replacement, and will + be removed in Rails 5.0. + WARNING + @serialized_attributes ||= Hash[ + columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c| + [c.name, c.cast_type.coder] + } + ] end end end 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 abad949ef4..f439bd1ffe 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,7 +1,7 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion - class Type < SimpleDelegator # :nodoc: + class TimeZoneConverter < SimpleDelegator # :nodoc: def type_cast_from_database(value) convert_time_to_time_zone(super) end @@ -37,10 +37,25 @@ module ActiveRecord module ClassMethods private - def create_time_zone_conversion_attribute?(name, column) + + def inherited(subclass) + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or + # `skip_time_zone_conversion_for_attributes` would not be picked up. + subclass.class_eval do + matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) } + decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type| + TimeZoneConverter.new(type) + end + end + super + end + + def create_time_zone_conversion_attribute?(name, cast_type) time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && - (:datetime == column.type) + (:datetime == cast_type.type) end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index b72a6219b0..b3c8209a74 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -69,22 +69,14 @@ module ActiveRecord def write_attribute_with_type_cast(attr_name, value, should_type_cast) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - @attributes.delete(attr_name) - column = type_for_attribute(attr_name) - unless has_attribute?(attr_name) || self.class.columns_hash.key?(attr_name) - raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" - end - - # If we're dealing with a binary column, write the data to the cache - # so we don't attempt to typecast multiple times. - if column.binary? - @attributes[attr_name] = value - elsif should_type_cast - @attributes[attr_name] = column.type_cast_from_user(value) + if should_type_cast + @attributes.write_from_user(attr_name, value) + else + @attributes.write_from_database(attr_name, value) end - @raw_attributes[attr_name] = value + value end end end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb new file mode 100644 index 0000000000..64df6f6358 --- /dev/null +++ b/activerecord/lib/active_record/attribute_set.rb @@ -0,0 +1,77 @@ +require 'active_record/attribute_set/builder' + +module ActiveRecord + class AttributeSet # :nodoc: + delegate :keys, to: :initialized_attributes + + def initialize(attributes) + @attributes = attributes + end + + def [](name) + attributes[name] || Attribute.null(name) + end + + def values_before_type_cast + attributes.transform_values(&:value_before_type_cast) + end + + def to_hash + initialized_attributes.transform_values(&:value) + end + alias_method :to_h, :to_hash + + def include?(name) + attributes.include?(name) && self[name].initialized? + end + + def fetch_value(name, &block) + self[name].value(&block) + end + + def write_from_database(name, value) + attributes[name] = self[name].with_value_from_database(value) + end + + def write_from_user(name, value) + attributes[name] = self[name].with_value_from_user(value) + end + + def freeze + @attributes.freeze + super + end + + def initialize_dup(_) + @attributes = attributes.transform_values(&:dup) + super + end + + def initialize_clone(_) + @attributes = attributes.clone + super + end + + def reset(key) + if include?(key) + write_from_database(key, nil) + end + end + + def ensure_initialized(key) + unless self[key].initialized? + write_from_database(key, nil) + end + end + + protected + + attr_reader :attributes + + private + + def initialized_attributes + attributes.select { |_, attr| attr.initialized? } + end + end +end diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb new file mode 100644 index 0000000000..1e146a07da --- /dev/null +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -0,0 +1,32 @@ +module ActiveRecord + class AttributeSet # :nodoc: + class Builder # :nodoc: + attr_reader :types + + def initialize(types) + @types = types + end + + def build_from_database(values = {}, additional_types = {}) + attributes = build_attributes_from_values(values, additional_types) + add_uninitialized_attributes(attributes) + AttributeSet.new(attributes) + end + + private + + def build_attributes_from_values(values, additional_types) + values.each_with_object({}) do |(name, value), hash| + type = additional_types.fetch(name, types[name]) + hash[name] = Attribute.from_database(name, value, type) + end + end + + def add_uninitialized_attributes(attributes) + types.except(*attributes.keys).each do |name, type| + attributes[name] = Attribute.uninitialized(name, type) + end + end + end + end +end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 9c80121d70..890a1314d9 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -9,7 +9,7 @@ module ActiveRecord self.user_provided_columns = {} end - module ClassMethods + module ClassMethods # :nodoc: # Defines or overrides a attribute on this model. This allows customization of # Active Record's type casting behavior, as well as adding support for user defined # types. @@ -27,7 +27,7 @@ module ActiveRecord # # ==== Examples # - # The type detected by Active Record can be overriden. + # The type detected by Active Record can be overridden. # # # db/schema.rb # create_table :store_listings, force: true do |t| @@ -109,13 +109,13 @@ module ActiveRecord end def clear_caches_calculated_from_columns + @attributes_builder = nil + @column_names = nil + @column_types = nil @columns = nil @columns_hash = nil - @column_types = nil - @column_defaults = nil - @raw_column_defaults = nil - @column_names = nil @content_columns = nil + @default_attributes = nil end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index b3c3e26c9f..dd92e29199 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -184,9 +184,7 @@ module ActiveRecord before_save :before_save_collection_association define_non_cyclic_method(save_method) { save_collection_association(reflection) } - # Doesn't use after_save as that would save associations added in after_create/after_update twice - after_create save_method - after_update save_method + after_save save_method elsif reflection.has_one? define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method) # Configures two callbacks instead of a single after_save so that @@ -364,6 +362,7 @@ module ActiveRecord raise ActiveRecord::Rollback unless saved end + @new_record_before_save = false end # reconstruct the scope now that we know the owner's id diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index e4d0abb8ef..64cc5b68cc 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -9,6 +9,7 @@ require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/transform_values' require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/introspection' @@ -219,25 +220,9 @@ module ActiveRecord #:nodoc: # # == Single table inheritance # - # Active Record allows inheritance by storing the name of the class in a column that by - # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). - # This means that an inheritance looking like this: - # - # class Company < ActiveRecord::Base; end - # class Firm < Company; end - # class Client < Company; end - # class PriorityClient < Client; end - # - # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in - # the companies table with type = "Firm". You can then fetch this row again using - # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object. - # - # If you don't have a type column defined in your table, single-table inheritance won't - # be triggered. In that case, it'll work just like normal subclasses with no special magic - # for differentiating between them or reloading the right type with find. - # - # Note, all the attributes for all the cases are kept in the same table. Read more: - # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # Active Record allows inheritance by storing the name of the class in a + # column that is named "type" by default. See ActiveRecord::Inheritance for + # more details. # # == Connection to multiple databases in different models # @@ -308,6 +293,8 @@ module ActiveRecord #:nodoc: include Integration include Validations include CounterCache + include Attributes + include AttributeDecorators include Locking::Optimistic include Locking::Pessimistic include AttributeMethods @@ -323,8 +310,6 @@ module ActiveRecord #:nodoc: include Reflection include Serialization include Store - include Attributes - include AttributeDecorators end ActiveSupport.run_load_hooks(:active_record, Base) 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 db80c0faee..cb75070e3a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -364,7 +364,7 @@ module ActiveRecord conn.expire end - release conn, owner + release owner @available.add conn end @@ -377,7 +377,7 @@ module ActiveRecord @connections.delete conn @available.delete conn - release conn, conn.owner + release conn.owner @available.add checkout_new_connection if @available.any_waiting? end @@ -425,7 +425,7 @@ module ActiveRecord end end - def release(conn, owner) + def release(owner) thread_id = owner.object_id @reserved_connections.delete thread_id 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 7ff5001796..e8ce00d92b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -193,7 +193,7 @@ module ActiveRecord # * You are creating a nested (savepoint) transaction # # The mysql, mysql2 and postgresql adapters support setting the transaction - # isolation level. However, support is disabled for mysql versions below 5, + # isolation level. However, support is disabled for MySQL versions below 5, # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] # which means the isolation level gets persisted outside the transaction. def transaction(options = {}) @@ -338,8 +338,8 @@ module ActiveRecord end # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work - # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in - # an UPDATE statement, so in the mysql adapters we redefine this to do that. + # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in + # an UPDATE statement, so in the MySQL adapters we redefine this to do that. def join_to_update(update, select) #:nodoc: key = update.key subselect = subquery_for(key, select) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 04ae67234f..eb88845913 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -9,13 +9,8 @@ module ActiveRecord # records are quoted as their primary key return value.quoted_id if value.respond_to?(:quoted_id) - # FIXME: The only case we get an object other than nil or a real column - # is `SchemaStatements#add_column` with a PG array that has a non-empty default - # value. Is this really the only case? Are we missing tests for other types? - # We should have a real column object passed (or nil) here, and check for that - # instead - if column.respond_to?(:type_cast_for_database) - value = column.type_cast_for_database(value) + if column + value = column.cast_type.type_cast_for_database(value) end _quote(value) @@ -29,13 +24,8 @@ module ActiveRecord return value.id end - # FIXME: The only case we get an object other than nil or a real column - # is `SchemaStatements#add_column` with a PG array that has a non-empty default - # value. Is this really the only case? Are we missing tests for other types? - # We should have a real column object passed (or nil) here, and check for that - # instead - if column.respond_to?(:type_cast_for_database) - value = column.type_cast_for_database(value) + if column + value = column.cast_type.type_cast_for_database(value) end _type_cast(value) @@ -66,7 +56,7 @@ module ActiveRecord # This works for mysql and mysql2 where table.column can be used to # resolve ambiguity. # - # We override this in the sqlite and postgresql adapters to use only + # We override this in the sqlite3 and postgresql adapters to use only # the column name (as per syntax requirements). def quote_table_name_for_assignment(table, attr) quote_table_name("#{table}.#{attr}") diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 47fe501752..adad6cd542 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -23,6 +23,8 @@ module ActiveRecord def visit_AlterTable(o) sql = "ALTER TABLE #{quote_table_name(o.name)} " sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ') + sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ') end def visit_ColumnDefinition(o) @@ -41,6 +43,21 @@ module ActiveRecord create_sql end + def visit_AddForeignKey(o) + sql = <<-SQL.strip_heredoc + ADD CONSTRAINT #{quote_column_name(o.name)} + FOREIGN KEY (#{quote_column_name(o.column)}) + REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) + SQL + sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete + sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update + sql + end + + def visit_DropForeignKey(name) + "DROP CONSTRAINT #{quote_column_name(name)}" + end + def column_options(o) column_options = {} column_options[:null] = o.null unless o.null.nil? @@ -77,6 +94,7 @@ module ActiveRecord def quote_value(value, column) column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale) + column.cast_type ||= type_for_column(column) @conn.quote(value, column) end @@ -84,6 +102,23 @@ module ActiveRecord def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end + + def action_sql(action, dependency) + case dependency + when :nullify then "ON #{action} SET NULL" + when :cascade then "ON #{action} CASCADE" + when :restrict then "ON #{action} RESTRICT" + else + raise ArgumentError, <<-MSG.strip_heredoc + '#{dependency}' is not supported for :on_update or :on_delete. + Supported values are: :nullify, :cascade, :restrict + MSG + end + end + + def type_for_column(column) + @conn.lookup_cast_type(column.sql_type) + end end end end 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 a9b3e9cfb9..98e6795f10 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,7 +15,7 @@ module ActiveRecord # are typically created by methods in TableDefinition, and added to the # +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, :sql_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type, :cast_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key @@ -25,6 +25,37 @@ module ActiveRecord class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc: end + class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: + def name + options[:name] + end + + def column + options[:column] + end + + def primary_key + options[:primary_key] || default_primary_key + end + + def on_delete + options[:on_delete] + end + + def on_update + options[:on_update] + end + + def custom_primary_key? + options[:primary_key] != default_primary_key + end + + private + def default_primary_key + "id" + end + end + # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # @@ -303,14 +334,26 @@ module ActiveRecord class AlterTable # :nodoc: attr_reader :adds + attr_reader :foreign_key_adds + attr_reader :foreign_key_drops def initialize(td) @td = td @adds = [] + @foreign_key_adds = [] + @foreign_key_drops = [] end def name; @td.name; end + def add_foreign_key(to_table, options) + @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options) + end + + def drop_foreign_key(name) + @foreign_key_drops << name + end + def add_column(name, type, options) name = name.to_s type = type.to_sym diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index d3e172927d..9bd0401e40 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -1,5 +1,3 @@ -require 'ipaddr' - module ActiveRecord module ConnectionAdapters # :nodoc: # The goal of this module is to move Adapter specific column @@ -25,7 +23,8 @@ module ActiveRecord spec[:precision] = column.precision.inspect if column.precision spec[:scale] = column.scale.inspect if column.scale spec[:null] = 'false' unless column.null - spec[:default] = column.type_cast_for_schema(column.default) if column.has_default? + spec[:default] = schema_default(column) if column.has_default? + spec.delete(:default) if spec[:default].nil? spec end @@ -33,6 +32,15 @@ module ActiveRecord def migration_keys [:name, :limit, :precision, :scale, :default, :null] end + + private + + def schema_default(column) + default = column.type_cast_from_database(column.default) + unless default.nil? + column.type_cast_for_schema(default) + end + end end end end 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 ffa6af6d99..5814c2b711 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -642,6 +642,115 @@ module ActiveRecord end alias :remove_belongs_to :remove_reference + # Returns an array of foreign keys for the given table. + # The foreign keys are represented as +ForeignKeyDefinition+ objects. + def foreign_keys(table_name) + raise NotImplementedError, "foreign_keys is not implemented" + end + + # Adds a new foreign key. +from_table+ is the table with the key column, + # +to_table+ contains the referenced primary key. + # + # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>. + # +identifier+ is a 10 character long random string. A custom name can be specified with + # the <tt>:name</tt> option. + # + # ====== Creating a simple foreign key + # + # add_foreign_key :articles, :authors + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") + # + # ====== Creating a foreign key on a specific column + # + # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id" + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id") + # + # ====== Creating a cascading foreign key + # + # add_foreign_key :articles, :authors, on_delete: :cascade + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE + # + # The +options+ hash can include the following keys: + # [<tt>:column</tt>] + # The foreign key column name on +from_table+. Defaults to <tt>to_table.singularize + "_id"</tt> + # [<tt>:primary_key</tt>] + # The primary key column name on +to_table+. Defaults to +id+. + # [<tt>:name</tt>] + # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>. + # [<tt>:on_delete</tt>] + # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # [<tt>:on_update</tt>] + # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + def add_foreign_key(from_table, to_table, options = {}) + return unless supports_foreign_keys? + + options[:column] ||= foreign_key_column_for(to_table) + + options = { + column: options[:column], + primary_key: options[:primary_key], + name: foreign_key_name(from_table, options), + on_delete: options[:on_delete], + on_update: options[:on_update] + } + at = create_alter_table from_table + at.add_foreign_key to_table, options + + execute schema_creation.accept(at) + end + + # Removes the given foreign key from the table. + # + # Removes the foreign key on +accounts.branch_id+. + # + # remove_foreign_key :accounts, :branches + # + # Removes the foreign key on +accounts.owner_id+. + # + # remove_foreign_key :accounts, column: :owner_id + # + # Removes the foreign key named +special_fk_name+ on the +accounts+ table. + # + # remove_foreign_key :accounts, name: :special_fk_name + # + def remove_foreign_key(from_table, options_or_to_table = {}) + return unless supports_foreign_keys? + + if options_or_to_table.is_a?(Hash) + options = options_or_to_table + else + options = { column: foreign_key_column_for(options_or_to_table) } + end + + fk_name_to_delete = options.fetch(:name) do + fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column] } + + if fk_to_delete + fk_to_delete.name + else + raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'" + end + end + + at = create_alter_table from_table + at.drop_foreign_key fk_name_to_delete + + execute schema_creation.accept(at) + end + + def foreign_key_column_for(table_name) # :nodoc: + "#{table_name.to_s.singularize}_id" + end + def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name @@ -788,7 +897,7 @@ module ActiveRecord return option_strings end - # Overridden by the mysql adapter for supporting index lengths + # Overridden by the MySQL adapter for supporting index lengths def quoted_columns_for_index(column_names, options = {}) option_strings = Hash[column_names.map {|name| [name, '']}] @@ -852,6 +961,12 @@ module ActiveRecord def create_alter_table(name) AlterTable.new create_table_definition(name, false, {}) end + + def foreign_key_name(table_name, options) # :nodoc: + options.fetch(:name) do + "fk_rails_#{SecureRandom.hex(5)}" + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index cc494a7f40..f8c054eb69 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -233,6 +233,11 @@ module ActiveRecord false end + # Does this adapter support creating foreign key constraints? + def supports_foreign_keys? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -370,12 +375,12 @@ module ActiveRecord Column.new(name, default, cast_type, sql_type, null) end - protected - def lookup_cast_type(sql_type) # :nodoc: type_map.lookup(sql_type) end + protected + def initialize_type_map(m) # :nodoc: register_class_with_limit m, %r(boolean)i, Type::Boolean register_class_with_limit m, %r(char)i, Type::String 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 200b773172..ccb957d2c8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -12,6 +12,10 @@ module ActiveRecord private + def visit_DropForeignKey(name) + "DROP FOREIGN KEY #{name}" + end + def visit_TableDefinition(o) name = o.name create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} " @@ -62,20 +66,19 @@ module ActiveRecord @extra = extra super(name, default, cast_type, sql_type, null) assert_valid_default(default) + extract_default end - def default - @default ||= if blob_or_text_column? - null || strict ? nil : '' - elsif missing_default_forged_as_empty_string?(@original_default) - nil - else - super + def extract_default + if blob_or_text_column? + @default = null || strict ? nil : '' + elsif missing_default_forged_as_empty_string?(@default) + @default = nil end end def has_default? - return false if blob_or_text_column? #mysql forbids defaults on blob and text columns + return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns super end @@ -193,6 +196,10 @@ module ActiveRecord true end + def supports_foreign_keys? + true + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -213,7 +220,7 @@ module ActiveRecord Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) end - # Must return the Mysql error number from the exception, if the exception has an + # Must return the MySQL error number from the exception, if the exception has an # error number. def error_number(exception) # :nodoc: raise NotImplementedError @@ -382,7 +389,7 @@ module ActiveRecord end def table_exists?(name) - return false unless name + return false unless name.present? return true if tables(nil, nil, name).any? name = name.to_s @@ -502,6 +509,34 @@ module ActiveRecord execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}" end + def foreign_keys(table_name) + fk_info = select_all <<-SQL.strip_heredoc + SELECT fk.referenced_table_name as 'to_table' + ,fk.referenced_column_name as 'primary_key' + ,fk.column_name as 'column' + ,fk.constraint_name as 'name' + FROM information_schema.key_column_usage fk + WHERE fk.referenced_column_name is not null + AND fk.table_schema = '#{@config[:database]}' + AND fk.table_name = '#{table_name}' + SQL + + create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE") + options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE") + + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s @@ -758,8 +793,8 @@ module ActiveRecord # Make MySQL reject illegal values rather than truncating or blanking them, see # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. - if strict_mode? && !variables.has_key?('sql_mode') - variables['sql_mode'] = 'STRICT_ALL_TABLES' + unless variables.has_key?('sql_mode') + variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' end # NAMES does not have an equals sign, see @@ -780,6 +815,15 @@ module ActiveRecord # ...and send them all in one query @connection.query "SET #{encoding} #{variable_assignments}" end + + def extract_foreign_key_action(structure, name, action) # :nodoc: + if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/ + case $1 + when 'CASCADE'; :cascade + when 'SET NULL'; :nullify + end + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 72c6990ba5..1f1e2c46f4 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -13,10 +13,10 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :cast_type, :null, :sql_type, :default_function + attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function delegate :type, :precision, :scale, :limit, :klass, :accessor, - :text?, :number?, :binary?, :serialized?, :changed?, + :text?, :number?, :binary?, :changed?, :type_cast_from_user, :type_cast_from_database, :type_cast_for_database, :type_cast_for_schema, to: :cast_type @@ -35,7 +35,7 @@ module ActiveRecord @cast_type = cast_type @sql_type = sql_type @null = null - @original_default = default + @default = default @default_function = nil end @@ -51,23 +51,12 @@ module ActiveRecord Base.human_attribute_name(@name) end - def default - @default ||= type_cast_from_database(@original_default) - end - def with_type(type) dup.tap do |clone| - clone.instance_variable_set('@default', nil) clone.instance_variable_set('@cast_type', type) end end end - - class NullColumn < Column - def initialize(name) - super name, nil, Type::Value.new - end - end end # :startdoc: end diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index b79d1a4458..2fcb085ab2 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -85,7 +85,7 @@ module ActiveRecord "password" => uri.password, "port" => uri.port, "database" => database_from_path, - "host" => uri.host }) + "host" => uri.hostname }) 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 909bba8c7d..ad07a46e51 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -263,7 +263,7 @@ module ActiveRecord end module Fields # :nodoc: - class DateTime < Type::DateTime + class DateTime < Type::DateTime # :nodoc: def cast_value(value) if Mysql::Time === value new_time( @@ -280,7 +280,7 @@ module ActiveRecord end end - class Time < Type::Time + class Time < Type::Time # :nodoc: def cast_value(value) if Mysql::Time === value new_time( @@ -409,7 +409,7 @@ module ActiveRecord stmt.execute(*type_casted_binds.map { |_, val| val }) rescue Mysql::Error => e # Older versions of MySQL leave the prepared statement in a bad - # place when an error occurs. To support older mysql versions, we + # place when an error occurs. To support older MySQL versions, we # need to close the statement and delete the statement from the # cache. stmt.close diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb deleted file mode 100644 index 666d1cf6e3..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ /dev/null @@ -1,117 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module Cast # :nodoc: - def point_to_string(point) # :nodoc: - "(#{point[0]},#{point[1]})" - end - - def hstore_to_string(object, array_member = false) # :nodoc: - if Hash === object - string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(',') - string = escape_hstore(string) if array_member - string - else - object - end - end - - def string_to_hstore(string) # :nodoc: - if string.nil? - nil - elsif String === string - Hash[string.scan(HstorePair).map { |k, v| - v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - [k, v] - }] - else - string - end - end - - def json_to_string(object) # :nodoc: - if Hash === object || Array === object - ActiveSupport::JSON.encode(object) - else - object - end - end - - def array_to_string(value, column, adapter) # :nodoc: - casted_values = value.map do |val| - if String === val - if val == "NULL" - "\"#{val}\"" - else - quote_and_escape(adapter.type_cast(val, column, true)) - end - else - adapter.type_cast(val, column, true) - end - end - "{#{casted_values.join(',')}}" - end - - def range_to_string(object) # :nodoc: - from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin - to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end - "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}" - end - - def string_to_json(string) # :nodoc: - if String === string - ActiveSupport::JSON.decode(string) - else - string - end - end - - def string_to_array(string, oid) # :nodoc: - parse_pg_array(string).map {|val| type_cast_array(oid, val)} - end - - private - - HstorePair = begin - quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ - unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ - /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ - end - - def escape_hstore(value) - if value.nil? - 'NULL' - else - if value == "" - '""' - else - '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') - end - end - end - - ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays - - def quote_and_escape(value) - case value - when "NULL", Numeric - value - else - value = value.gsub(/\\/, ARRAY_ESCAPE) - value.gsub!(/"/,"\\\"") - "\"#{value}\"" - end - end - - def type_cast_array(oid, value) - if ::Array === value - value.map {|item| type_cast_array(oid, item)} - else - oid.type_cast_from_database value - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index a579746815..37e5c3859c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -1,5 +1,3 @@ -require 'active_record/connection_adapters/postgresql/cast' - module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. @@ -17,24 +15,6 @@ module ActiveRecord @default_function = default_function end - - # :stopdoc: - class << self - include PostgreSQL::Cast - - # Loads pg_array_parser if available. String parsing can be - # performed quicker by a native extension, which will not create - # a large amount of Ruby objects that will need to be garbage - # collected. pg_array_parser has a C and Java extension - begin - require 'pg_array_parser' - include PgArrayParser - rescue LoadError - require 'active_record/connection_adapters/postgresql/array_parser' - include PostgreSQL::ArrayParser - end - end - # :startdoc: end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 33a98b4fcb..d05ce61330 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -21,6 +21,7 @@ require 'active_record/connection_adapters/postgresql/oid/specialized_string' require 'active_record/connection_adapters/postgresql/oid/time' require 'active_record/connection_adapters/postgresql/oid/uuid' require 'active_record/connection_adapters/postgresql/oid/vector' +require 'active_record/connection_adapters/postgresql/oid/xml' require 'active_record/connection_adapters/postgresql/oid/type_map_initializer' diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index 0e9dcd8c0c..cd5efe2bb8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -2,21 +2,93 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Array < Type::Value - attr_reader :subtype + class Array < Type::Value # :nodoc: + include Type::Mutable + + # Loads pg_array_parser if available. String parsing can be + # performed quicker by a native extension, which will not create + # a large amount of Ruby objects that will need to be garbage + # collected. pg_array_parser has a C and Java extension + begin + require 'pg_array_parser' + include PgArrayParser + rescue LoadError + require 'active_record/connection_adapters/postgresql/array_parser' + include PostgreSQL::ArrayParser + end + + attr_reader :subtype, :delimiter delegate :type, to: :subtype - def initialize(subtype) + def initialize(subtype, delimiter = ',') @subtype = subtype + @delimiter = delimiter + end + + def type_cast_from_database(value) + if value.is_a?(::String) + type_cast_array(parse_pg_array(value), :type_cast_from_database) + else + super + end + end + + def type_cast_from_user(value) + type_cast_array(value, :type_cast_from_user) + end + + def type_cast_for_database(value) + if value.is_a?(::Array) + cast_value_for_database(value) + else + super + end + end + + private + + def type_cast_array(value, method) + if value.is_a?(::Array) + value.map { |item| type_cast_array(item, method) } + else + @subtype.public_send(method, value) + end end - def type_cast(value) - if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype + def cast_value_for_database(value) + if value.is_a?(::Array) + casted_values = value.map { |item| cast_value_for_database(item) } + "{#{casted_values.join(delimiter)}}" else - value + quote_and_escape(subtype.type_cast_for_database(value)) end end + + ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays + + def quote_and_escape(value) + case value + when ::String + if string_requires_quoting?(value) + value = value.gsub(/\\/, ARRAY_ESCAPE) + value.gsub!(/"/,"\\\"") + %("#{value}") + else + value + end + when nil then "NULL" + else value + end + end + + # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO + # for a list of all cases in which strings will be quoted. + def string_requires_quoting?(string) + string.empty? || + string == "NULL" || + string =~ /[\{\}"\\\s]/ || + string.include?(delimiter) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index 3073f8ff30..1dbb40ca1d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Bit < Type::Value + class Bit < Type::Value # :nodoc: def type :bit end @@ -19,6 +19,32 @@ module ActiveRecord value end end + + def type_cast_for_database(value) + Data.new(super) if value + end + + class Data + def initialize(value) + @value = value + end + + def to_s + value + end + + def binary? + /\A[01]*\Z/ === value + end + + def hex? + /\A[0-9A-F]*\Z/i === value + end + + protected + + attr_reader :value + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb index 054af285bb..4c21097d48 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class BitVarying < OID::Bit + class BitVarying < OID::Bit # :nodoc: def type :bit_varying end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb index 36c53d8732..997613d7be 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -2,9 +2,10 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Bytea < Type::Binary - def cast_value(value) - PGconn.unescape_bytea value + class Bytea < Type::Binary # :nodoc: + def type_cast_from_database(value) + return if value.nil? + PGconn.unescape_bytea(super) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb index 534961a414..a53b4ee8e2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Cidr < Type::Value + class Cidr < Type::Value # :nodoc: def type :cidr end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb index 3c30ad5fec..1d8d264530 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Date < Type::Date + class Date < Type::Date # :nodoc: include Infinity end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb index 34e2276dd1..b9e7894e5c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class DateTime < Type::DateTime + class DateTime < Type::DateTime # :nodoc: include Infinity def cast_value(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb index ed4b8911d9..43d22c8daf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Decimal < Type::Decimal + class Decimal < Type::Decimal # :nodoc: def infinity(options = {}) BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb index 5fed8b0f89..77d5038efd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Enum < Type::Value + class Enum < Type::Value # :nodoc: def type :enum end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb index 77dd97e140..78ef94b912 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb @@ -2,11 +2,12 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Float < Type::Float + class Float < Type::Float # :nodoc: include Infinity def cast_value(value) case value + when ::Float then value when 'Infinity' then ::Float::INFINITY when '-Infinity' then -::Float::INFINITY when 'NaN' then ::Float::NAN diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index 88de816d4f..be4525c94f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -2,26 +2,56 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Hstore < Type::Value + class Hstore < Type::Value # :nodoc: + include Type::Mutable + def type :hstore end - def type_cast_from_user(value) - type_cast_from_database(type_cast_for_database(value)) - end - def type_cast_from_database(value) - ConnectionAdapters::PostgreSQLColumn.string_to_hstore(value) + if value.is_a?(::String) + ::Hash[value.scan(HstorePair).map { |k, v| + v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + [k, v] + }] + else + value + end end def type_cast_for_database(value) - ConnectionAdapters::PostgreSQLColumn.hstore_to_string(value) + if value.is_a?(::Hash) + value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ') + else + value + end end def accessor ActiveRecord::Store::StringKeyedHashAccessor end + + private + + HstorePair = begin + quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ + unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ + /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ + end + + def escape_hstore(value) + if value.nil? + 'NULL' + else + if value == "" + '""' + else + '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') + end + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb index 7ed8f5f031..96486fa65b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Inet < Cidr + class Inet < Cidr # :nodoc: def type :inet end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb index d438ffa140..e47780399a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - module Infinity + module Infinity # :nodoc: def infinity(options = {}) options[:negative] ? -::Float::INFINITY : ::Float::INFINITY end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb index 388d3dd9ed..59abdc0009 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Integer < Type::Integer + class Integer < Type::Integer # :nodoc: include Infinity end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index b4fed1bcab..e12ddd9901 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -2,21 +2,27 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Json < Type::Value + class Json < Type::Value # :nodoc: + include Type::Mutable + def type :json end - def type_cast_from_user(value) - type_cast_from_database(type_cast_for_database(value)) - end - def type_cast_from_database(value) - ConnectionAdapters::PostgreSQLColumn.string_to_json(value) + if value.is_a?(::String) + ::ActiveSupport::JSON.decode(value) + else + super + end end def type_cast_for_database(value) - ConnectionAdapters::PostgreSQLColumn.json_to_string(value) + if value.is_a?(::Array) || value.is_a?(::Hash) + ::ActiveSupport::JSON.encode(value) + else + super + end end def accessor diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb index d25eb256c2..df890c2ed6 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Money < Type::Decimal + class Money < Type::Decimal # :nodoc: include Infinity class_attribute :precision diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index 9007bfb178..bac8b01d6b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -2,21 +2,40 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Point < Type::Value + class Point < Type::Value # :nodoc: + include Type::Mutable + def type :point end def type_cast(value) - if ::String === value + case value + when ::String if value[0] == '(' && value[-1] == ')' value = value[1...-1] end - value.split(',').map{ |v| Float(v) } + type_cast(value.split(',')) + when ::Array + value.map { |v| Float(v) } else value end end + + def type_cast_for_database(value) + if value.is_a?(::Array) + "(#{number_for_point(value[0])},#{number_for_point(value[1])})" + else + super + end + end + + private + + def number_for_point(number) + number.to_s.gsub(/\.0$/, '') + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index c289ba8980..ae967d5167 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Range < Type::Value + class Range < Type::Value # :nodoc: attr_reader :subtype, :type def initialize(subtype, type) @@ -10,28 +10,10 @@ module ActiveRecord @type = type end - def extract_bounds(value) - from, to = value[1..-2].split(',') - { - from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, - to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, - exclude_start: (value[0] == '('), - exclude_end: (value[-1] == ')') - } - end - - def infinity?(value) - value.respond_to?(:infinite?) && value.infinite? - end - def type_cast_for_schema(value) value.inspect.gsub('Infinity', '::Float::INFINITY') end - def type_cast_single(value) - infinity?(value) ? value : @subtype.type_cast_from_database(value) - end - def cast_value(value) return if value == 'empty' return value if value.is_a?(::Range) @@ -53,6 +35,40 @@ This is not reliable and will be removed in the future. end ::Range.new(from, to, extracted[:exclude_end]) end + + def type_cast_for_database(value) + if value.is_a?(::Range) + from = type_cast_single_for_database(value.begin) + to = type_cast_single_for_database(value.end) + "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}" + else + super + end + end + + private + + def type_cast_single(value) + infinity?(value) ? value : @subtype.type_cast_from_database(value) + end + + def type_cast_single_for_database(value) + infinity?(value) ? '' : @subtype.type_cast_for_database(value) + end + + def extract_bounds(value) + from, to = value[1..-2].split(',') + { + from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, + to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, + exclude_start: (value[0] == '('), + exclude_end: (value[-1] == ')') + } + end + + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb index 7b1ca16bc4..2d2fede4e8 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -2,16 +2,12 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class SpecializedString < Type::String + class SpecializedString < Type::String # :nodoc: attr_reader :type def initialize(type) @type = type end - - def text? - false - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb index ea1f599b0f..8f0246eddb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Time < Type::Time + class Time < Type::Time # :nodoc: include Infinity end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 28f7a4eafb..e396ff4a1e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -40,7 +40,7 @@ module ActiveRecord def register_array_type(row) if subtype = @store.lookup(row['typelem'].to_i) - register row['oid'], OID::Array.new(subtype) + register row['oid'], OID::Array.new(subtype, row['typdelim']) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb index 0ed5491887..89728b0fe2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Uuid < Type::Value + class Uuid < Type::Value # :nodoc: def type :uuid end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb index 2f7d1be197..de4187b028 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Vector < Type::Value + class Vector < Type::Value # :nodoc: attr_reader :delim, :subtype # +delim+ corresponds to the `typdelim` column in the pg_types diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb new file mode 100644 index 0000000000..7323f12763 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb @@ -0,0 +1,32 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Xml < Type::String # :nodoc: + def type + :xml + end + + def text? + false + end + + def type_cast_for_database(value) + return unless value + Data.new(super) + end + + class Data # :nodoc: + def initialize(value) + @value = value + end + + def to_s + @value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 3cf40e6cd4..cf5c8d288e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -18,103 +18,18 @@ module ActiveRecord def quote(value, column = nil) #:nodoc: return super unless column - sql_type = type_to_sql(column.type, column.limit, column.precision, column.scale) - case value - when Range - if /range$/ =~ sql_type - "'#{PostgreSQLColumn.range_to_string(value)}'::#{sql_type}" - else - super - end - when Array - case sql_type - when 'point' then super(PostgreSQLColumn.point_to_string(value)) - when 'json' then super(PostgreSQLColumn.json_to_string(value)) - else - if column.array - "'#{PostgreSQLColumn.array_to_string(value, column, self).gsub(/'/, "''")}'" - else - super - end - end - when Hash - case sql_type - when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) - when 'json' then super(PostgreSQLColumn.json_to_string(value), column) - else super - end when Float - if value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" - elsif value.infinite? || value.nan? + if value.infinite? || value.nan? "'#{value.to_s}'" else super end - when Numeric - if sql_type == 'money' || [:string, :text].include?(column.type) - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" - else - super - end - when String - case sql_type - when 'xml' then "xml '#{quote_string(value)}'" - when /^bit/ - case value - when /^[01]*$/ then "B'#{value}'" # Bit-string notation - when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation - end - else - super - end else super end end - def type_cast(value, column, array_member = false) - return super(value, column) unless column - - case value - when Range - if /range$/ =~ column.sql_type - PostgreSQLColumn.range_to_string(value) - else - super(value, column) - end - when NilClass - if column.array && array_member - 'NULL' - elsif column.array - value - else - super(value, column) - end - when Array - case column.sql_type - when 'point' then PostgreSQLColumn.point_to_string(value) - when 'json' then PostgreSQLColumn.json_to_string(value) - else - if column.array - PostgreSQLColumn.array_to_string(value, column, self) - else - super(value, column) - end - end - when Hash - case column.sql_type - when 'hstore' then PostgreSQLColumn.hstore_to_string(value, array_member) - when 'json' then PostgreSQLColumn.json_to_string(value) - else super(value, column) - end - else - super(value, column) - end - end - # Quotes strings for use in SQL input. def quote_string(s) #:nodoc: @connection.escape(s) @@ -168,19 +83,31 @@ module ActiveRecord private def _quote(value) - if value.is_a?(Type::Binary::Data) + case value + when Type::Binary::Data "'#{escape_bytea(value.to_s)}'" + when OID::Xml::Data + "xml '#{quote_string(value.to_s)}'" + when OID::Bit::Data + if value.binary? + "B'#{value}'" + elsif value.hex? + "X'#{value}'" + end else super end end def _type_cast(value) - if value.is_a?(Type::Binary::Data) + case value + when Type::Binary::Data # Return a bind param hash with format as binary. # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc # for more information { value: value.to_s, format: 1 } + when OID::Xml::Data, OID::Bit::Data + value.to_s else super end 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 b2aeb3a058..e09672d239 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -31,6 +31,14 @@ module ActiveRecord super end end + + def type_for_column(column) + if column.array + @conn.lookup_cast_type("#{column.sql_type}[]") + else + super + end + end end module SchemaStatements @@ -375,8 +383,8 @@ module ActiveRecord end # Renames a table. - # Also renames a table's primary key sequence if the sequence name matches the - # Active Record default. + # Also renames a table's primary key sequence if the sequence name exists and + # matches the Active Record default. # # Example: # rename_table('octopuses', 'octopi') @@ -384,7 +392,7 @@ module ActiveRecord clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq.identifier == "#{table_name}_#{pk}_seq" + if seq && seq.identifier == "#{table_name}_#{pk}_seq" new_seq = "#{new_name}_#{pk}_seq" execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" end @@ -448,6 +456,42 @@ module ActiveRecord execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" end + def foreign_keys(table_name) + fk_info = select_all <<-SQL.strip_heredoc + SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete + FROM pg_constraint c + JOIN pg_class t1 ON c.conrelid = t1.oid + JOIN pg_class t2 ON c.confrelid = t2.oid + JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid + JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid + JOIN pg_namespace t3 ON c.connamespace = t3.oid + WHERE c.contype = 'f' + AND t1.relname = #{quote(table_name)} + AND t3.nspname = ANY (current_schemas(false)) + ORDER BY c.conname + SQL + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + options[:on_delete] = extract_foreign_key_action(row['on_delete']) + options[:on_update] = extract_foreign_key_action(row['on_update']) + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + + def extract_foreign_key_action(specifier) # :nodoc: + case specifier + when 'c'; :cascade + when 'n'; :nullify + when 'r'; :restrict + end + end + def index_name_length 63 end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 71b05cdbae..f660fc41cf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -159,6 +159,10 @@ module ActiveRecord true end + def supports_foreign_keys? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end @@ -306,10 +310,6 @@ module ActiveRecord self.client_min_messages = old end - def supports_insert_with_returning? - true - end - def supports_ddl_transactions? true end @@ -348,14 +348,13 @@ module ActiveRecord if supports_extensions? res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", 'SCHEMA' - res.column_types['enabled'].type_cast_from_database res.rows.first.first + res.cast_values.first end end def extensions if supports_extensions? - res = exec_query "SELECT extname from pg_extension", "SCHEMA" - res.rows.map { |r| res.column_types['extname'].type_cast_from_database r.first } + exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values else super end @@ -384,6 +383,11 @@ module ActiveRecord PostgreSQL::Table.new(table_name, base) end + def lookup_cast_type(sql_type) # :nodoc: + oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i + super(oid) + end + protected # Returns the version of the connected PostgreSQL server. @@ -450,7 +454,7 @@ module ActiveRecord m.register_type 'cidr', OID::Cidr.new m.register_type 'inet', OID::Inet.new m.register_type 'uuid', OID::Uuid.new - m.register_type 'xml', OID::SpecializedString.new(:xml) + m.register_type 'xml', OID::Xml.new m.register_type 'tsvector', OID::SpecializedString.new(:tsvector) m.register_type 'macaddr', OID::SpecializedString.new(:macaddr) m.register_type 'citext', OID::SpecializedString.new(:citext) @@ -565,7 +569,7 @@ module ActiveRecord end def exec_no_cache(sql, name, binds) - log(sql, name, binds) { @connection.async_exec(sql) } + log(sql, name, binds) { @connection.async_exec(sql, []) } end def exec_cache(sql, name, binds) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index e6163771e8..bf96acad4a 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -14,9 +14,9 @@ module ActiveRecord raise ArgumentError, "No database file specified. Missing argument: database" end - # 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. + # 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 ':memory:' != config[:database] config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root) dirname = File.dirname(config[:database]) @@ -50,6 +50,16 @@ module ActiveRecord end end + class SQLite3String < Type::String # :nodoc: + def type_cast_for_database(value) + if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT + value.encode(Encoding::UTF_8) + else + super + end + end + end + # The SQLite3 adapter works SQLite 3.6.16 or newer # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3). # @@ -220,13 +230,23 @@ module ActiveRecord # QUOTING ================================================== def _quote(value) # :nodoc: - if value.is_a?(Type::Binary::Data) + case value + when Type::Binary::Data "x'#{value.hex}'" else super end end + def _type_cast(value) # :nodoc: + case value + when BigDecimal + value.to_f + else + super + end + end + def quote_string(s) #:nodoc: @connection.class.quote(s) end @@ -249,19 +269,6 @@ module ActiveRecord end end - def type_cast(value, column) # :nodoc: - return value.to_f if BigDecimal === value - return super unless String === value - return super unless column && value - - value = super - if column.type == :string && value.encoding == Encoding::ASCII_8BIT - logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger - value = value.encode Encoding::UTF_8 - end - value - end - # DATABASE STATEMENTS ====================================== def explain(arel, binds = []) @@ -503,6 +510,7 @@ module ActiveRecord def initialize_type_map(m) super m.register_type(/binary/i, SQLite3Binary.new) + register_class_with_limit m, %r(char)i, SQLite3String end def select(sql, name = nil, binds = []) #:nodoc: diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 79388f53b5..b11c4f804f 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -16,7 +16,6 @@ module ActiveRecord mattr_accessor :logger, instance_writer: false ## - # :singleton-method: # Contains the database configuration - as is typically stored in config/database.yml - # as a Hash. # @@ -249,16 +248,12 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil, options = {}) - defaults = self.class.raw_column_defaults.dup - defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } - - @raw_attributes = defaults - @column_types_override = nil - @column_types = self.class.column_types + @attributes = self.class.default_attributes.dup init_internals initialize_internals_callback + self.class.define_attribute_methods # +options+ argument is only needed to make protected_attributes gem easier to hook. # Remove it when we drop support to this gem. init_attributes(attributes, options) if attributes @@ -278,13 +273,10 @@ module ActiveRecord # post.init_with('attributes' => { 'title' => 'hello world' }) # post.title # => 'hello world' def init_with(coder) - @raw_attributes = coder['raw_attributes'] - @column_types_override = coder['column_types'] - @column_types = self.class.column_types + @attributes = coder['attributes'] init_internals - @attributes = coder['attributes'] if coder['attributes'] @new_record = coder['new_record'] self.class.define_attribute_methods @@ -323,12 +315,8 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: - cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - - @raw_attributes = cloned_attributes - @raw_attributes[self.class.primary_key] = nil - @attributes = other.clone_attributes(:read_attribute) - @attributes[self.class.primary_key] = nil + @attributes = @attributes.dup + @attributes.reset(self.class.primary_key) run_callbacks(:initialize) unless _initialize_callbacks.empty? @@ -354,7 +342,8 @@ module ActiveRecord # Post.new.encode_with(coder) # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) - coder['raw_attributes'] = @raw_attributes + # FIXME: Remove this when we better serialize attributes + coder['raw_attributes'] = attributes_before_type_cast coder['attributes'] = @attributes coder['new_record'] = new_record? end @@ -379,20 +368,24 @@ module ActiveRecord # Delegates to id in order to allow two records of the same type and id to work with something like: # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] def hash - id.hash + if id + id.hash + else + super + end end # Clone and freeze the attributes hash such that associations are still # accessible, even on destroyed records, but cloned models will not be # frozen. def freeze - @raw_attributes = @raw_attributes.clone.freeze + @attributes = @attributes.clone.freeze self end # Returns +true+ if the attributes hash has been frozen. def frozen? - @raw_attributes.frozen? + @attributes.frozen? end # Allows sort on objects @@ -421,9 +414,9 @@ module ActiveRecord # Returns the contents of the record as a nicely formatted string. def inspect - # We check defined?(@raw_attributes) not to issue warnings if the object is + # We check defined?(@attributes) not to issue warnings if the object is # allocated but not initialized. - inspection = if defined?(@raw_attributes) && @raw_attributes + inspection = if defined?(@attributes) && @attributes self.class.column_names.collect { |name| if has_attribute?(name) "#{name}: #{attribute_for_inspect(name)}" @@ -521,12 +514,10 @@ module ActiveRecord end def init_internals - pk = self.class.primary_key - @raw_attributes[pk] = nil unless @raw_attributes.key?(pk) + @attributes.ensure_initialized(self.class.primary_key) @aggregation_cache = {} @association_cache = {} - @attributes = {} @readonly = false @destroyed = false @marked_for_destruction = false @@ -549,7 +540,7 @@ module ActiveRecord def thaw if frozen? - @raw_attributes = @raw_attributes.dup + @attributes = @attributes.dup end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 05c4b13016..a33c7c64a7 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -38,7 +38,7 @@ module ActiveRecord counter_name = reflection.counter_cache_column stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(counter_association).count + arel_table[counter_name] => object.send(counter_association).count(:all) }, primary_key) connection.update stmt end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 2ccb1b0702..52c70977ef 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -30,17 +30,18 @@ module ActiveRecord class SerializationTypeMismatch < ActiveRecordError end - # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt> - # misses adapter field). + # Raised when adapter not specified on connection (or configuration file + # +config/database.yml+ misses adapter field). class AdapterNotSpecified < ActiveRecordError end - # Raised when Active Record cannot find database adapter specified in <tt>config/database.yml</tt> or programmatically. + # Raised when Active Record cannot find database adapter specified in + # +config/database.yml+ or programmatically. class AdapterNotFound < ActiveRecordError end - # Raised when connection to the database could not been established (for example when <tt>connection=</tt> - # is given a nil object). + # Raised when connection to the database could not been established (for + # example when +connection=+ is given a nil object). class ConnectionNotEstablished < ActiveRecordError end @@ -82,19 +83,17 @@ module ActiveRecord class InvalidForeignKey < WrappedDatabaseException end - # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example, - # when using +find+ method) - # does not match number of expected variables. + # Raised when number of bind variables in statement given to +:condition+ key + # (for example, when using +find+ method) does not match number of expected + # values supplied. # - # For example, in + # For example, when there are two placeholders with only one value supplied: # # Location.where("lat = ? AND lng = ?", 53.7362) - # - # two placeholders are given but only one variable to fill them. class PreparedStatementInvalid < ActiveRecordError end - # Raised when a given database does not exist + # Raised when a given database does not exist. class NoDatabaseError < StatementInvalid end @@ -102,7 +101,8 @@ module ActiveRecord # instantiation, for example, when two users edit the same wiki page and one starts editing and saves # the page before the other. # - # Read more about optimistic locking in ActiveRecord::Locking module RDoc. + # Read more about optimistic locking in ActiveRecord::Locking module + # documentation. class StaleObjectError < ActiveRecordError attr_reader :record, :attempted_action @@ -114,8 +114,9 @@ module ActiveRecord end - # Raised when association is being configured improperly or - # user tries to use offset and limit together with has_many or has_and_belongs_to_many associations. + # Raised when association is being configured improperly or user tries to use + # offset and limit together with +has_many+ or +has_and_belongs_to_many+ + # associations. class ConfigurationError < ActiveRecordError end @@ -153,7 +154,8 @@ module ActiveRecord class Rollback < ActiveRecordError end - # Raised when attribute has a name reserved by Active Record (when attribute has name of one of Active Record instance methods). + # Raised when attribute has a name reserved by Active Record (when attribute + # has name of one of Active Record instance methods). class DangerousAttributeError < ActiveRecordError end @@ -171,7 +173,7 @@ module ActiveRecord end # Raised when an error occurred while doing a mass assignment to an attribute through the - # <tt>attributes=</tt> method. The exception has an +attribute+ property that is the name of the + # +attributes=+ method. The exception has an +attribute+ property that is the name of the # offending attribute. class AttributeAssignmentError < ActiveRecordError attr_reader :exception, :attribute diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 6ba667b996..4cd5f92207 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -15,9 +15,10 @@ module ActiveRecord # They are stored in YAML files, one file per model, which are placed in the directory # appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically # configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>). - # The fixture file ends with the <tt>.yml</tt> file extension (Rails example: - # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a fixture file looks - # like this: + # The fixture file ends with the +.yml+ file extension, for example: + # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). + # + # The format of a fixture file looks like this: # # rubyonrails: # id: 1 @@ -61,8 +62,8 @@ module ActiveRecord # end # end # - # By default, <tt>test_helper.rb</tt> will load all of your fixtures into your test database, - # so this test will succeed. + # By default, +test_helper.rb+ will load all of your fixtures into your test + # database, so this test will succeed. # # The testing environment will automatically load the all fixtures into the database before each # test. To ensure consistent data, the environment deletes the fixtures before running the load. @@ -375,8 +376,8 @@ module ActiveRecord # == Support for YAML defaults # # You can set and reuse defaults in your fixtures YAML file. - # This is the same technique used in the <tt>database.yml</tt> file - # to specify defaults: + # This is the same technique used in the +database.yml+ file to specify + # defaults: # # DEFAULTS: &DEFAULTS # created_on: <%= 3.weeks.ago.to_s(:db) %> @@ -392,7 +393,8 @@ module ActiveRecord # Any fixture labeled "DEFAULTS" is safely ignored. class FixtureSet #-- - # An instance of FixtureSet is normally stored in a single YAML file and possibly in a folder with the same name. + # An instance of FixtureSet is normally stored in a single YAML file and + # possibly in a folder with the same name. #++ MAX_ID = 2 ** 30 - 1 @@ -462,13 +464,7 @@ module ActiveRecord @config = config # Remove string values that aren't constants or subclasses of AR - @class_names.delete_if { |k,klass| - unless klass.is_a? Class - klass = klass.safe_constantize - ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `set_fixture_class` will be removed in Rails 4.2. Use the class itself instead.") - end - !insert_class(@class_names, k, klass) - } + @class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) } end def [](fs_name) @@ -574,10 +570,6 @@ module ActiveRecord @config = config @model_class = nil - if class_name.is_a?(String) - ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `FixtureSet.new` will be removed in Rails 4.2. Use the class itself instead.") - end - if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @model_class = class_name else diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 08fc91c9df..f6c265a6d6 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,6 +1,37 @@ require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord + # == Single table inheritance + # + # Active Record allows inheritance by storing the name of the class in a column that by + # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). + # This means that an inheritance looking like this: + # + # class Company < ActiveRecord::Base; end + # class Firm < Company; end + # class Client < Company; end + # class PriorityClient < Client; end + # + # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in + # the companies table with type = "Firm". You can then fetch this row again using + # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object. + # + # Be aware that because the type column is an attribute on the record every new + # subclass will instantly be marked as dirty and the type column will be included + # in the list of changed attributes on the record. This is different from non + # STI classes: + # + # Company.new.changed? # => false + # Firm.new.changed? # => true + # Firm.new.changes # => {"type"=>["","Firm"]} + # + # If you don't have a type column defined in your table, single-table inheritance won't + # be triggered. In that case, it'll work just like normal subclasses with no special magic + # for differentiating between them or reloading the right type with find. + # + # Note, all the attributes for all the cases are kept in the same table. Read more: + # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # module Inheritance extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index f7ceff7469..52eeb8ae1f 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -141,7 +141,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) - @column_defaults = nil + clear_caches_calculated_from_columns @locking_column = value.to_s end @@ -151,12 +151,6 @@ module ActiveRecord @locking_column end - # Quote the column name used for optimistic locking. - def quoted_locking_column - ActiveSupport::Deprecation.warn "ActiveRecord::Base.quoted_locking_column is deprecated and will be removed in Rails 4.2 or later." - connection.quote_column_name(locking_column) - end - # Reset the column used for optimistic locking back to the +lock_version+ default. def reset_locking_column self.locking_column = DEFAULT_LOCKING_COLUMN @@ -169,18 +163,42 @@ module ActiveRecord super end - def column_defaults - @column_defaults ||= begin - defaults = super - - if defaults.key?(locking_column) && lock_optimistically - defaults[locking_column] ||= 0 + private + + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `lock_optimistically`, or + # `locking_column` would not be picked up. + def inherited(subclass) + subclass.class_eval do + is_lock_column = ->(name, _) { lock_optimistically && name == locking_column } + decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type| + LockingType.new(type) end - - defaults end + super end end end + + class LockingType < SimpleDelegator # :nodoc: + def type_cast_from_database(value) + # `nil` *should* be changed to 0 + super.to_i + end + + def changed?(old_value, *) + # Ensure we save if the default was `nil` + super || old_value == 0 + end + + def init_with(coder) + __setobj__(coder['subtype']) + end + + def encode_with(coder) + coder['subtype'] = __getobj__ + end + end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 481e5c17e4..e94b6ae9eb 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -372,13 +372,21 @@ module ActiveRecord end def call(env) - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i - if @last_check < mtime - ActiveRecord::Migration.check_pending! - @last_check = mtime + if connection.supports_migrations? + mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + if @last_check < mtime + ActiveRecord::Migration.check_pending!(connection) + @last_check = mtime + end end @app.call(env) end + + private + + def connection + ActiveRecord::Base.connection + end end class << self @@ -642,7 +650,9 @@ module ActiveRecord unless @connection.respond_to? :revert unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) - arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table + if [:rename_table, :add_foreign_key].include?(method) + arguments[1] = proper_table_name(arguments.second, table_name_options) + end end end return super unless connection.respond_to?(method) diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index c44d8c1665..f833caaab6 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -74,7 +74,9 @@ module ActiveRecord :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, :enable_extension, - :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too + :change_column, :execute, :remove_columns, :change_column_null, + :add_foreign_key, :remove_foreign_key + # irreversible methods need to be here too ].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) @@ -167,6 +169,21 @@ module ActiveRecord [:change_column_null, args] end + def invert_add_foreign_key(args) + from_table, to_table, add_options = args + add_options ||= {} + + if add_options[:name] + options = { name: add_options[:name] } + elsif add_options[:column] + options = { column: add_options[:column] } + else + options = to_table + end + + [:remove_foreign_key, [from_table, options]] + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) if @delegate.respond_to?(method) diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index f96f77f696..092c3b4fb7 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -219,40 +219,29 @@ module ActiveRecord connection.schema_cache.table_exists?(table_name) end - def column_types # :nodoc: - @column_types ||= decorate_columns(columns_hash.dup) + def attributes_builder # :nodoc: + @attributes_builder ||= AttributeSet::Builder.new(column_types) end - def type_for_attribute(attr_name) # :nodoc: - column_types.fetch(attr_name) { column_for_attribute(attr_name) } - end - - def decorate_columns(columns_hash) # :nodoc: - return if columns_hash.empty? - - @time_zone_column_names ||= self.columns_hash.find_all do |name, col| - create_time_zone_conversion_attribute?(name, col) - end.map!(&:first) - - @time_zone_column_names.each do |name| - columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(columns_hash[name]) + def column_types # :nodoc: + @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h| + h.default = Type::Value.new end + end - columns_hash + def type_for_attribute(attr_name) # :nodoc: + column_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 - @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }] + default_attributes.to_hash end - # Returns a hash where the keys are the column names and the values - # are the default values suitable for use in `@raw_attriubtes` - def raw_column_defaults # :nodoc: - @raw_column_defauts ||= Hash[column_defaults.map { |name, default| - [name, columns_hash[name].type_cast_for_database(default)] - }] + def default_attributes # :nodoc: + @default_attributes ||= attributes_builder.build_from_database( + columns_hash.transform_values(&:default)) end # Returns an array of column names as strings. @@ -298,11 +287,10 @@ module ActiveRecord connection.schema_cache.clear_table_cache!(table_name) if table_exists? @arel_engine = nil - @column_defaults = nil - @raw_column_defauts = nil @column_names = nil @column_types = nil @content_columns = nil + @default_attributes = nil @dynamic_methods_hash = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @relation = nil diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 525289c270..96e44c2f59 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -48,12 +48,8 @@ module ActiveRecord # how this "single-table" inheritance mapping is implemented. def instantiate(attributes, column_types = {}) klass = discriminate_class_for_record(attributes) - column_types = klass.decorate_columns(column_types.dup) - klass.allocate.init_with( - 'raw_attributes' => attributes, - 'column_types' => column_types, - 'new_record' => false, - ) + attributes = klass.attributes_builder.build_from_database(attributes, column_types) + klass.allocate.init_with('attributes' => attributes, 'new_record' => false) end private @@ -183,7 +179,6 @@ module ActiveRecord # So any change to the attributes in either instance will affect the other. def becomes(klass) became = klass.new - became.instance_variable_set("@raw_attributes", @raw_attributes) became.instance_variable_set("@attributes", @attributes) became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes) became.instance_variable_set("@new_record", new_record?) @@ -400,11 +395,8 @@ module ActiveRecord self.class.unscoped { self.class.find(id) } end - @raw_attributes.update(fresh_object.instance_variable_get('@raw_attributes')) - - @column_types = self.class.column_types - @column_types_override = fresh_object.instance_variable_get('@column_types_override') - @attributes = {} + @attributes = fresh_object.instance_variable_get('@attributes') + @new_record = false self end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index df8654e5c1..16ad942912 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -29,9 +29,10 @@ module ActiveRecord end def call(env) - enabled = ActiveRecord::Base.connection.query_cache_enabled + connection = ActiveRecord::Base.connection + enabled = connection.query_cache_enabled connection_id = ActiveRecord::Base.connection_id - ActiveRecord::Base.connection.enable_query_cache! + connection.enable_query_cache! response = @app.call(env) response[2] = Rack::BodyProxy.new(response[2]) do diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 1fe54cea3f..a9ddd9141f 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -37,14 +37,7 @@ module ActiveRecord # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }] def find_by_sql(sql, binds = []) result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) - column_types = {} - - if result_set.respond_to? :column_types - column_types = result_set.column_types.merge(columns_hash) - else - ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" - end - + column_types = result_set.column_types.except(*columns_hash.keys) result_set.map { |record| instantiate(record, column_types) } end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 6dca206f2a..fa94df7a52 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -28,6 +28,17 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.drop_current end + namespace :purge do + task :all => :load_config do + ActiveRecord::Tasks::DatabaseTasks.purge_all + end + end + + # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." + task :purge => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.purge_current + end + desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task :migrate => [:environment, :load_config] do ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true @@ -82,22 +93,21 @@ db_namespace = namespace :db do desc 'Display status of migrations' task :status => [:environment, :load_config] do - unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) + unless ActiveRecord::SchemaMigration.table_exists? abort 'Schema migrations table does not exist yet.' end - db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}") - db_list.map! { |version| ActiveRecord::SchemaMigration.normalize_migration_number(version) } - file_list = [] - ActiveRecord::Migrator.migrations_paths.each do |path| - Dir.foreach(path) do |file| - # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern - if match_data = /^(\d{3,})_(.+)\.rb$/.match(file) - version = ActiveRecord::SchemaMigration.normalize_migration_number(match_data[1]) - status = db_list.delete(version) ? 'up' : 'down' - file_list << [status, version, match_data[2].humanize] + db_list = ActiveRecord::SchemaMigration.normalized_versions + + file_list = + ActiveRecord::Migrator.migrations_paths.flat_map do |path| + # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern + Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do + version = ActiveRecord::SchemaMigration.normalize_migration_number($1) + status = db_list.delete(version) ? 'up' : 'down' + [status, version, $2.humanize] + end end - end - end + db_list.map! do |version| ['up', version, '********** NO FILE **********'] end @@ -105,8 +115,8 @@ db_namespace = namespace :db do puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 - (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration| - puts "#{migration[0].center(8)} #{migration[1].ljust(14)} #{migration[2]}" + (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name| + puts "#{status.center(8)} #{version.ljust(14)} #{name}" end puts end @@ -178,17 +188,21 @@ db_namespace = namespace :db do task :load => [:environment, :load_config] do require 'active_record/fixtures' - base_dir = if ENV['FIXTURES_PATH'] - File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten - else - ActiveRecord::Tasks::DatabaseTasks.fixtures_path - end + base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path - fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact + fixtures_dir = if ENV['FIXTURES_DIR'] + File.join base_dir, ENV['FIXTURES_DIR'] + else + base_dir + end - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(',') : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| - ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file) - end + fixture_files = if ENV['FIXTURES'] + ENV['FIXTURES'].split(',') + else + Pathname.glob("#{fixtures_dir}/**/*.yml").map {|f| f.basename.sub_ext('').to_s } + end + + ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files) end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." @@ -200,12 +214,7 @@ db_namespace = namespace :db do puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label - base_dir = if ENV['FIXTURES_PATH'] - File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten - else - ActiveRecord::Tasks::DatabaseTasks.fixtures_path - end - + base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path Dir["#{base_dir}/**/*.yml"].each do |file| if data = YAML::load(ERB.new(IO.read(file)).result) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 24f3de019d..12208aecf7 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -13,14 +13,21 @@ module ActiveRecord end def self.create(macro, name, scope, options, ar) - case macro - 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) + klass = case macro + when :composed_of + AggregateReflection + when :has_many + HasManyReflection + when :has_one + HasOneReflection + when :belongs_to + BelongsToReflection + else + raise "Unsupported Macro: #{macro}" + end + + reflection = klass.new(name, scope, options, ar) + options[:through] ? ThroughReflection.new(reflection) : reflection end def self.add_reflection(ar, name, reflection) @@ -110,6 +117,52 @@ module ActiveRecord end end + # Holds all the methods that are shared between MacroReflection, AssociationReflection + # and ThroughReflection + class AbstractReflection # :nodoc: + def table_name + klass.table_name + end + + # Returns a new, unsaved instance of the associated class. +attributes+ will + # be passed to the class's constructor. + def build_association(attributes, &block) + klass.new(attributes, &block) + end + + def quoted_table_name + klass.quoted_table_name + end + + def primary_key_type + klass.type_for_attribute(klass.primary_key) + end + + # Returns the class name for the macro. + # + # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> + # <tt>has_many :clients</tt> returns <tt>'Client'</tt> + def class_name + @class_name ||= (options[:class_name] || derive_class_name).to_s + end + + JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: + + def join_keys(assoc_klass) + if source_macro == :belongs_to + if polymorphic? + reflection_key = association_primary_key(assoc_klass) + else + reflection_key = association_primary_key + end + reflection_foreign_key = foreign_key + else + reflection_foreign_key = active_record_primary_key + reflection_key = foreign_key + end + JoinKeys.new(reflection_key, reflection_foreign_key) + end + end # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. # @@ -117,7 +170,7 @@ module ActiveRecord # AggregateReflection # AssociationReflection # ThroughReflection - class MacroReflection + class MacroReflection < AbstractReflection # Returns the name of the macro. # # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt> @@ -142,8 +195,7 @@ module ActiveRecord attr_reader :plural_name # :nodoc: - def initialize(macro, name, scope, options, active_record) - @macro = macro + def initialize(name, scope, options, active_record) @name = name @scope = scope @options = options @@ -167,15 +219,11 @@ module ActiveRecord # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class # <tt>has_many :clients</tt> returns the Client class def klass - @klass ||= class_name.constantize + @klass ||= compute_class(class_name) end - # Returns the class name for the macro. - # - # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> - # <tt>has_many :clients</tt> returns <tt>'Client'</tt> - def class_name - @class_name ||= (options[:class_name] || derive_class_name).to_s + def compute_class(name) + name.constantize end # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute, @@ -220,15 +268,18 @@ module ActiveRecord # a new association object. Use +build_association+ or +create_association+ # instead. This allows plugins to hook into association object creation. def klass - @klass ||= active_record.send(:compute_type, class_name) + @klass ||= compute_class(class_name) + end + + def compute_class(name) + active_record.send(:compute_type, name) end attr_reader :type, :foreign_type attr_accessor :parent_reflection # [:name, Reflection] - def initialize(macro, name, scope, options, active_record) + def initialize(name, scope, options, active_record) super - @collection = macro == :has_many @automatic_inverse_of = nil @type = options[:as] && "#{options[:as]}_type" @foreign_type = options[:foreign_type] || "#{name}_type" @@ -247,24 +298,10 @@ module ActiveRecord } end - # Returns a new, unsaved instance of the associated class. +attributes+ will - # be passed to the class's constructor. - def build_association(attributes, &block) - klass.new(attributes, &block) - end - def constructable? # :nodoc: @constructable end - def table_name - klass.table_name - end - - def quoted_table_name - klass.quoted_table_name - end - def join_table @join_table ||= options[:join_table] || derive_join_table end @@ -273,10 +310,6 @@ module ActiveRecord @foreign_key ||= options[:foreign_key] || derive_foreign_key end - def primary_key_column - klass.columns_hash[klass.primary_key] - end - def association_foreign_key @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key end @@ -379,7 +412,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # association. Returns +true+ if the +macro+ is either +has_many+ or # +has_and_belongs_to_many+, +false+ otherwise. def collection? - @collection + false end # Returns whether or not the association should be validated as part of @@ -400,6 +433,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi macro == :belongs_to end + # Returns +true+ if +self+ is a +has_one+ reflection. def has_one? macro == :has_one end @@ -469,7 +503,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # returns either nil or the inverse association name that it finds. def automatic_inverse_of if can_find_inverse_of_automatically?(self) - inverse_name = ActiveSupport::Inflector.underscore(active_record.name).to_sym + inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name).to_sym begin reflection = klass._reflect_on_association(inverse_name) @@ -542,22 +576,57 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end end + class HasManyReflection < AssociationReflection #:nodoc: + def initialize(name, scope, options, active_record) + @macro = :has_many + super(name, scope, options, active_record) + end + + def collection? + true + end + end + + class HasOneReflection < AssociationReflection #:nodoc: + def initialize(name, scope, options, active_record) + @macro = :has_one + super(name, scope, options, active_record) + end + end + + class BelongsToReflection < AssociationReflection #:nodoc: + def initialize(name, scope, options, active_record) + @macro = :belongs_to + super(name, scope, options, active_record) + end + end + class HasAndBelongsToManyReflection < AssociationReflection #:nodoc: - def initialize(macro, name, scope, options, active_record) + def initialize(name, scope, options, active_record) + @macro = :has_and_belongs_to_many super - @collection = true + end + + def collection? + true end end # Holds all the meta-data about a :through association as it was specified # in the Active Record class. - class ThroughReflection < AssociationReflection #:nodoc: + class ThroughReflection < AbstractReflection #:nodoc: + attr_reader :delegate_reflection 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] + def initialize(delegate_reflection) + @delegate_reflection = delegate_reflection + @klass = delegate_reflection.options[:class] + @source_reflection_name = delegate_reflection.options[:source] + end + + def klass + @klass ||= delegate_reflection.compute_class(class_name) end # Returns the source of the through reflection. It checks both a singularized @@ -759,15 +828,25 @@ directive on your declaration like: protected - def actual_source_reflection # FIXME: this is a horrible name - source_reflection.actual_source_reflection - end + def actual_source_reflection # FIXME: this is a horrible name + source_reflection.send(:actual_source_reflection) + end + + def primary_key(klass) + klass.primary_key || raise(UnknownPrimaryKey.new(klass)) + end private def derive_class_name # get the class_name of the belongs_to association of the through reflection options[:source_type] || source_reflection.class_name end + + delegate_methods = AssociationReflection.public_instance_methods - + public_instance_methods + + delegate(*delegate_methods, to: :delegate_reflection) + end end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 38970a66ae..90e99957f6 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -19,14 +19,22 @@ module ActiveRecord # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } - # + # + # If +count+ is used with +group+ for multiple columns, it returns a Hash whose + # keys are an array containing the individual values of each column and the value + # of each key would be the +count+. + # + # Article.group(:status, :category).count + # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, + # ["published", "business"]=>0, ["published", "technology"]=>2} + # # If +count+ is used with +select+, it will count the selected columns: # # Person.select(:age).count # # => counts the number of different age values # # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ - # between databases. In invalid cases, an error from the databsae is thrown. + # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil, options = {}) # TODO: Remove options argument as soon we remove support to # activerecord-deprecated_finders. @@ -170,18 +178,7 @@ module ActiveRecord 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) { result.identity_type } - } - end - - result = result.map do |attributes| - values = attributes.values - - columns.zip(values).map { |column, value| column.type_cast_from_database value } - end - columns.one? ? result.map!(&:first) : result + result.cast_values(klass.column_types) end end @@ -267,7 +264,7 @@ module ActiveRecord row = result.first value = row && row.values.first column = result.column_types.fetch(column_alias) do - column_for(column_name) + type_for(column_name) end type_cast_calculated_value(value, column, operation) @@ -330,14 +327,14 @@ module ActiveRecord Hash[calculated_data.map do |row| key = group_columns.map { |aliaz, col_name| column = calculated_data.column_types.fetch(aliaz) do - column_for(col_name) + type_for(col_name) end type_cast_calculated_value(row[aliaz], column) } key = key.first if key.size == 1 key = key_records[key] if associated - column_type = calculated_data.column_types.fetch(aggregate_alias) { column_for(column_name) } + column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) } [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)] end] end @@ -364,24 +361,20 @@ module ActiveRecord @klass.connection.table_alias_for(table_name) end - def column_for(field) + def type_for(field) field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last - @klass.columns_hash[field_name] + @klass.type_for_attribute(field_name) end - def type_cast_calculated_value(value, column, operation = nil) + def type_cast_calculated_value(value, type, operation = nil) case operation when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || 0, column) + when 'sum' then type.type_cast_from_database(value || 0) when 'average' then value.respond_to?(:to_d) ? value.to_d : value - else type_cast_using_column(value, column) + else type.type_cast_from_database(value) end end - def type_cast_using_column(value, column) - column ? column.type_cast_from_database(value) : value - end - # TODO: refactor to allow non-string `select_values` (eg. Arel nodes). def select_for_count if select_values.present? diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index ae53f66d7a..8405fdaeb9 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -42,14 +42,6 @@ module ActiveRecord @column_types = column_types end - def identity_type # :nodoc: - IDENTITY_TYPE - end - - def column_type(name) - @column_types[name] || identity_type - end - def each if block_given? hash_rows.each { |row| yield row } @@ -82,6 +74,15 @@ module ActiveRecord hash_rows.last end + def cast_values(type_overrides = {}) # :nodoc: + types = columns.map { |name| column_type(name, type_overrides) } + result = rows.map do |values| + types.zip(values).map { |type, value| type.type_cast_from_database(value) } + end + + columns.one? ? result.map!(&:first) : result + end + def initialize_copy(other) @columns = columns.dup @rows = rows.dup @@ -91,11 +92,17 @@ module ActiveRecord private + def column_type(name, type_overrides = {}) + type_overrides.fetch(name) do + column_types.fetch(name, IDENTITY_TYPE) + end + end + def hash_rows @hash_rows ||= begin # We freeze the strings to prevent them getting duped when - # used as keys in ActiveRecord::Base's @raw_attributes hash + # used as keys in ActiveRecord::Base's @attributes hash columns = @columns.map { |c| c.dup.freeze } @rows.map { |row| # In the past we used Hash[columns.zip(row)] diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 1aa93ffbb3..ff70cbed0f 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -107,7 +107,7 @@ module ActiveRecord end.join(', ') end - # Sanitizes a +string+ so that it is safe to use within a sql + # Sanitizes a +string+ so that it is safe to use within an SQL # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%" def sanitize_sql_like(string, escape_character = "\\") pattern = Regexp.union(escape_character, "%", "_") diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index e055d571ab..a94364bde1 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -91,7 +91,8 @@ HEADER end def tables(stream) - @connection.tables.sort.each do |tbl| + sorted_tables = @connection.tables.sort + sorted_tables.each do |tbl| next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| case ignored when String; remove_prefix_and_suffix(tbl) == ignored @@ -102,6 +103,13 @@ HEADER end table(tbl, stream) end + + # dump foreign keys at the end to make sure all dependent tables exist. + if @connection.supports_foreign_keys? + sorted_tables.each do |tbl| + foreign_keys(tbl, stream) + end + end end def table(table, stream) @@ -112,7 +120,8 @@ HEADER # first dump primary key column if @connection.respond_to?(:pk_and_sequence_for) pk, _ = @connection.pk_and_sequence_for(table) - elsif @connection.respond_to?(:primary_key) + end + if !pk && @connection.respond_to?(:primary_key) pk = @connection.primary_key(table) end @@ -212,6 +221,36 @@ HEADER end end + def foreign_keys(table, stream) + if (foreign_keys = @connection.foreign_keys(table)).any? + add_foreign_key_statements = foreign_keys.map do |foreign_key| + parts = [ + 'add_foreign_key ' + remove_prefix_and_suffix(foreign_key.from_table).inspect, + remove_prefix_and_suffix(foreign_key.to_table).inspect, + ] + + if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table) + parts << ('column: ' + foreign_key.column.inspect) + end + + if foreign_key.custom_primary_key? + parts << ('primary_key: ' + foreign_key.primary_key.inspect) + end + + if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/ + parts << ('name: ' + foreign_key.name.inspect) + end + + parts << ('on_update: ' + foreign_key.on_update.inspect) if foreign_key.on_update + parts << ('on_delete: ' + foreign_key.on_delete.inspect) if foreign_key.on_delete + + ' ' + parts.join(', ') + end + + stream.puts add_foreign_key_statements.sort.join("\n") + end + end + def remove_prefix_and_suffix(table) table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2") end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 3a004d58c9..b5038104ac 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -43,6 +43,10 @@ module ActiveRecord def normalize_migration_number(number) "%.3d" % number.to_i end + + def normalized_versions + pluck(:version).map { |v| normalize_migration_number v } + end end def version diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 019fe2218e..c2484d02ed 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -182,11 +182,7 @@ module ActiveRecord #:nodoc: klass = @serializable.class column = klass.columns_hash[name] || Type::Value.new - type = if column.serialized? - super - else - column.type - end + type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type { :text => :string, :time => :datetime }[type] || type diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 7014bc6d45..3c291f28e3 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -99,7 +99,7 @@ module ActiveRecord self.local_stored_attributes[store_attribute] |= keys end - def _store_accessors_module + def _store_accessors_module # :nodoc: @_store_accessors_module ||= begin mod = Module.new include mod @@ -129,10 +129,10 @@ module ActiveRecord private def store_accessor_for(store_attribute) - @column_types[store_attribute.to_s].accessor + type_for_attribute(store_attribute.to_s).accessor end - class HashAccessor + class HashAccessor # :nodoc: def self.read(object, attribute, key) prepare(object, attribute) object.public_send(attribute)[key] @@ -151,7 +151,7 @@ module ActiveRecord end end - class StringKeyedHashAccessor < HashAccessor + class StringKeyedHashAccessor < HashAccessor # :nodoc: def self.read(object, attribute, key) super object, attribute, key.to_s end @@ -161,7 +161,7 @@ module ActiveRecord end end - class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor + class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc: def self.prepare(object, store_attribute) attribute = object.send(store_attribute) unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 168b338b97..9bc23b5ec7 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -6,7 +6,7 @@ module ActiveRecord # <tt>ActiveRecord::Tasks::DatabaseTasks</tt> is a utility class, which encapsulates # logic behind common tasks used to manage database and migrations. # - # The tasks defined here are used in rake tasks provided by Active Record. + # The tasks defined here are used with Rake tasks provided by Active Record. # # In order to use DatabaseTasks, a few config values need to be set. All the needed # config values are set by Rails already, so it's necessary to do it only if you @@ -14,7 +14,6 @@ module ActiveRecord # (in such case after configuring the database tasks, you can also use the rake tasks # defined in Active Record). # - # # The possible config values are: # # * +env+: current environment (like Rails.env). @@ -59,7 +58,11 @@ module ActiveRecord end def fixtures_path - @fixtures_path ||= File.join(root, 'test', 'fixtures') + @fixtures_path ||= if ENV['FIXTURES_PATH'] + File.join(root, ENV['FIXTURES_PATH']) + else + File.join(root, 'test', 'fixtures') + end end def root @@ -144,6 +147,18 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(configuration).purge end + def purge_all + each_local_configuration { |configuration| + purge configuration + } + end + + def purge_current(environment = env) + each_current_configuration(environment) { |configuration| + purge configuration + } + end + def structure_dump(*arguments) configuration = arguments.first filename = arguments.delete_at 1 diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index c755831e6d..644c4852b9 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -124,7 +124,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; end def root_password - $stdout.print "Please provide the root password for your mysql installation\n>" + $stdout.print "Please provide the root password for your MySQL installation\n>" $stdin.gets.strip end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 6c30ccab72..e2e37e7c00 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -99,9 +99,11 @@ module ActiveRecord end def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update) - if (timestamps = timestamp_names.map { |attr| self[attr] }.compact).present? - timestamps.map { |ts| ts.to_time }.max - end + timestamp_names + .map { |attr| self[attr] } + .compact + .map(&:to_time) + .max end def current_time_from_proper_timezone diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index e9b827886a..f1384e0bb2 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,3 +1,4 @@ +require 'active_record/type/mutable' require 'active_record/type/numeric' require 'active_record/type/time_value' require 'active_record/type/value' diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb index 3bf29b5026..d29ff4e494 100644 --- a/activerecord/lib/active_record/type/binary.rb +++ b/activerecord/lib/active_record/type/binary.rb @@ -22,9 +22,9 @@ module ActiveRecord Data.new(super) end - class Data + class Data # :nodoc: def initialize(value) - @value = value + @value = value.to_s end def to_s diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index 560d63c101..5f19608a33 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -7,6 +7,16 @@ module ActiveRecord :datetime end + def type_cast_for_database(value) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + + if value.acts_like?(:time) + value.send(zone_conversion_method) + else + super + end + end + private def cast_value(string) diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb new file mode 100644 index 0000000000..066617ea59 --- /dev/null +++ b/activerecord/lib/active_record/type/mutable.rb @@ -0,0 +1,16 @@ +module ActiveRecord + module Type + module Mutable # :nodoc: + def type_cast_from_user(value) + type_cast_from_database(type_cast_for_database(value)) + end + + # +raw_old_value+ will be the `_before_type_cast` version of the + # value (likely a string). +new_value+ will be the current, type + # cast value. + def changed_in_place?(raw_old_value, new_value) + raw_old_value != type_cast_for_database(new_value) + end + end + end +end diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb index 137c9e4c99..fa43266504 100644 --- a/activerecord/lib/active_record/type/numeric.rb +++ b/activerecord/lib/active_record/type/numeric.rb @@ -16,26 +16,20 @@ module ActiveRecord end def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: - # 0 => 'wibble' should mark as changed so numericality validations run - if nil_or_zero?(old_value) && non_numeric_string?(new_value_before_type_cast) - # nil => '' should not mark as changed - old_value != new_value_before_type_cast.presence - else - super - end + super || number_to_non_number?(old_value, new_value_before_type_cast) end private + def number_to_non_number?(old_value, new_value_before_type_cast) + old_value != nil && non_numeric_string?(new_value_before_type_cast) + end + def non_numeric_string?(value) # 'wibble'.to_i will give zero, we want to make sure # that we aren't marking int zero to string zero as # changed. - value !~ /\A\d+\.?\d*\z/ - end - - def nil_or_zero?(value) - value.nil? || value == 0 + value.to_s !~ /\A\d+\.?\d*\z/ end end end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 9144028576..42bbed7103 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,6 +1,8 @@ module ActiveRecord module Type class Serialized < SimpleDelegator # :nodoc: + include Mutable + attr_reader :subtype, :coder def initialize(subtype, coder) @@ -17,10 +19,6 @@ module ActiveRecord end end - def type_cast_from_user(value) - type_cast_from_database(type_cast_for_database(value)) - end - def type_cast_for_database(value) return if value.nil? unless is_default_value?(value) @@ -28,14 +26,21 @@ module ActiveRecord end end - def serialized? - true - end - def accessor ActiveRecord::Store::IndifferentHashAccessor end + def init_with(coder) + @subtype = coder['subtype'] + @coder = coder['coder'] + __setobj__(@subtype) + end + + def encode_with(coder) + coder['subtype'] = @subtype + coder['coder'] = @coder + end + private def is_default_value?(value) diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb index 3b1554bd5a..150defb106 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activerecord/lib/active_record/type/string.rb @@ -5,8 +5,20 @@ module ActiveRecord :string end - def text? - true + def changed_in_place?(raw_old_value, new_value) + if new_value.is_a?(::String) + raw_old_value != new_value + end + end + + def type_cast_for_database(value) + case value + when ::Numeric, ActiveSupport::Duration then value.to_s + when ::String then ::String.new(value) + when true then "1" + when false then "0" + else super + end end private @@ -15,7 +27,8 @@ module ActiveRecord case value when true then "1" when false then "0" - else value.to_s + # String.new is slightly faster than dup + else ::String.new(value.to_s) end end end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index efcdab1c0e..e0a783fb45 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -3,8 +3,8 @@ module ActiveRecord class Value # :nodoc: attr_reader :precision, :scale, :limit - # Valid options are +precision+, +scale+, and +limit+. - # They are only used when dumping schema. + # Valid options are +precision+, +scale+, and +limit+. They are only + # used when dumping schema. def initialize(options = {}) options.assert_valid_keys(:precision, :scale, :limit) @precision = options[:precision] @@ -12,65 +12,81 @@ module ActiveRecord @limit = options[:limit] end - # The simplified type that this object represents. Subclasses - # must override this method. + # The simplified type that this object represents. Returns a symbol such + # as +:string+ or +:integer+ def type; end + # Type casts a string from the database into the appropriate ruby type. + # Classes which do not need separate type casting behavior for database + # and user provided values should override +cast_value+ instead. def type_cast_from_database(value) type_cast(value) end + # Type casts a value from user input (e.g. from a setter). This value may + # be a string from the form builder, or an already type cast value + # provided manually to a setter. + # + # Classes which do not need separate type casting behavior for database + # and user provided values should override +type_cast+ or +cast_value+ + # instead. def type_cast_from_user(value) type_cast(value) end + # Cast a value from the ruby type to a type that the database knows how + # to understand. The returned value from this method should be a + # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or + # +nil+ def type_cast_for_database(value) value end - def type_cast_for_schema(value) + # Type cast a value for schema dumping. This method is private, as we are + # hoping to remove it entirely. + def type_cast_for_schema(value) # :nodoc: value.inspect end - def text? + # These predicates are not documented, as I need to look further into + # their use, and see if they can be removed entirely. + def number? # :nodoc: false end - def number? - false - end - - def binary? - false - end - - def serialized? + def binary? # :nodoc: false end def klass # :nodoc: end - # +old_value+ will always be type-cast. - # +new_value+ will come straight from the database - # or from assignment, so it could be anything. Types - # which cannot typecast arbitrary values should override - # this method. - def changed?(old_value, new_value, _new_value_before_type_cast) # :nodoc: + # Determines whether a value has changed for dirty checking. +old_value+ + # and +new_value+ will always be type-cast. Types should not need to + # override this method. + def changed?(old_value, new_value, _new_value_before_type_cast) old_value != new_value end + # Determines whether the mutable value has been modified since it was + # read. Returns +false+ by default. This method should not need to be + # overriden directly. Types which return a mutable value should include + # +Type::Mutable+, which will define this method. + def changed_in_place?(*) + false + end + private - # Takes an input from the database, or from attribute setters, - # and casts it to a type appropriate for this object. This method - # should not be overriden by subclasses. Instead, override `cast_value`. - def type_cast(value) # :api: public + + def type_cast(value) cast_value(value) unless value.nil? end - # Responsible for casting values from external sources to the appropriate - # type. Called by `type_cast` for all values except `nil`. - def cast_value(value) # :api: public + # Convenience method for types which do not need separate type casting + # behavior for user and database inputs. Called by + # `type_cast_from_database` and `type_cast_from_user` for all values + # except `nil`. + def cast_value(value) # :doc: value end end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 9999624fcf..b4b33804de 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -54,7 +54,7 @@ module ActiveRecord # Attempts to save the record just like Base#save but will raise a +RecordInvalid+ # exception instead of returning +false+ if the record is not valid. def save!(options={}) - perform_validations(options) ? super : raise(RecordInvalid.new(self)) + perform_validations(options) ? super : raise_record_invalid end # Runs all the validations within the specified context. Returns +true+ if @@ -75,8 +75,24 @@ module ActiveRecord alias_method :validate, :valid? + # Runs all the validations within the specified context. Returns +true+ if + # no errors are found, raises +RecordInvalid+ otherwise. + # + # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if + # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not. + # + # Validations with no <tt>:on</tt> option will run no matter the context. Validations with + # some <tt>:on</tt> option will only run in the specified context. + def validate!(context = nil) + valid?(context) || raise_record_invalid + end + protected + def raise_record_invalid + raise(RecordInvalid.new(self)) + end + def perform_validations(options={}) # :nodoc: options[:validate] == false || valid?(options[:context]) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 2e7b1d7206..2a34969a8c 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -48,7 +48,7 @@ module ActiveRecord def build_relation(klass, table, attribute, value) #:nodoc: if reflection = klass._reflect_on_association(attribute) attribute = reflection.foreign_key - value = value.attributes[reflection.primary_key_column.name] unless value.nil? + value = value.attributes[reflection.klass.primary_key] unless value.nil? end attribute_name = attribute.to_s @@ -61,9 +61,11 @@ module ActiveRecord column = klass.columns_hash[attribute_name] value = klass.connection.type_cast(value, column) - value = value.to_s[0, column.limit] if value && column.limit && column.text? + if value.is_a?(String) && column.limit + value = value.to_s[0, column.limit] + end - if !options[:case_sensitive] && value && column.text? + if !options[:case_sensitive] && value.is_a?(String) # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 267aa232d9..f3c711a64b 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -47,7 +47,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal "1", attributes["published"] assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal 1, @connection.type_cast(true, string_column) + assert_equal "1", @connection.type_cast(true, string_column) end test "test type casting without emulated booleans" do @@ -60,7 +60,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal "1", attributes["published"] assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal 1, @connection.type_cast(true, string_column) + assert_equal "1", @connection.type_cast(true, string_column) end test "with booleans stored as 1 and 0" do diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 66750deb68..8df1b7d18c 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -3,6 +3,7 @@ require "cases/helper" class PostgresqlArrayTest < ActiveRecord::TestCase include InTimeZone + OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID class PgArray < ActiveRecord::Base self.table_name = 'pg_arrays' @@ -10,11 +11,20 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection + + unless @connection.extension_enabled?('hstore') + @connection.enable_extension 'hstore' + @connection.commit_db_transaction + end + + @connection.reconnect! + @connection.transaction do @connection.create_table('pg_arrays') do |t| t.string 'tags', array: true t.integer 'ratings', array: true t.datetime :datetimes, array: true + t.hstore :hstores, array: true end end @column = PgArray.columns_hash['tags'] @@ -28,7 +38,6 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal :string, @column.type assert_equal "character varying", @column.sql_type assert @column.array - assert_not @column.text? assert_not @column.number? assert_not @column.binary? @@ -41,9 +50,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_default @connection.add_column 'pg_arrays', 'score', :integer, array: true, default: [4, 4, 2] PgArray.reset_column_information - column = PgArray.columns_hash["score"] - assert_equal([4, 4, 2], column.default) + assert_equal([4, 4, 2], PgArray.column_defaults['score']) assert_equal([4, 4, 2], PgArray.new.score) ensure PgArray.reset_column_information @@ -52,9 +60,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_default_strings @connection.add_column 'pg_arrays', 'names', :string, array: true, default: ["foo", "bar"] PgArray.reset_column_information - column = PgArray.columns_hash["names"] - assert_equal(["foo", "bar"], column.default) + assert_equal(["foo", "bar"], PgArray.column_defaults['names']) assert_equal(["foo", "bar"], PgArray.new.names) ensure PgArray.reset_column_information @@ -68,7 +75,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase column = PgArray.columns_hash['snippets'] assert_equal :text, column.type - assert_equal [], column.default + assert_equal [], PgArray.column_defaults['snippets'] assert column.array end @@ -85,8 +92,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase @connection.change_column_default :pg_arrays, :tags, [] PgArray.reset_column_information - column = PgArray.columns_hash['tags'] - assert_equal [], column.default + assert_equal [], PgArray.column_defaults['tags'] end def test_type_cast_array @@ -97,8 +103,13 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_type_cast_integers x = PgArray.new(ratings: ['1', '2']) - assert x.save! - assert_equal(['1', '2'], x.ratings) + + assert_equal([1, 2], x.ratings) + + x.save! + x.reload + + assert_equal([1, 2], x.ratings) end def test_select_with_strings @@ -180,16 +191,6 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings)) end - def test_update_all - pg_array = PgArray.create! tags: ["one", "two", "three"] - - PgArray.update_all tags: ["four", "five"] - assert_equal ["four", "five"], pg_array.reload.tags - - PgArray.update_all tags: [] - assert_equal [], pg_array.reload.tags - end - def test_escaping unknown = 'foo\\",bar,baz,\\' tags = ["hello_#{unknown}"] @@ -198,6 +199,45 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal tags, ar.tags end + def test_string_quoting_rules_match_pg_behavior + tags = ["", "one{", "two}", %(three"), "four\\", "five ", "six\t", "seven\n", "eight,", "nine", "ten\r", "NULL"] + x = PgArray.create!(tags: tags) + x.reload + + assert_equal x.tags_before_type_cast, PgArray.columns_hash['tags'].type_cast_for_database(tags) + end + + def test_quoting_non_standard_delimiters + strings = ["hello,", "world;"] + comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',') + semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';') + + assert_equal %({"hello,",world;}), comma_delim.type_cast_for_database(strings) + assert_equal %({hello,;"world;"}), semicolon_delim.type_cast_for_database(strings) + end + + def test_mutate_array + x = PgArray.create!(tags: %w(one two)) + + x.tags << "three" + x.save! + x.reload + + assert_equal %w(one two three), x.tags + assert_not x.changed? + end + + def test_mutate_value_in_array + x = PgArray.create!(hstores: [{ a: 'a' }, { b: 'b' }]) + + x.hstores.first['a'] = 'c' + x.save! + x.reload + + assert_equal [{ 'a' => 'c' }, { 'b' => 'b' }], x.hstores + assert_not x.changed? + end + def test_datetime_with_timezone_awareness tz = "Pacific Time (US & Canada)" diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index 3a9397bc26..72222c01fd 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -26,7 +26,6 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase column = PostgresqlBitString.columns_hash["a_bit"] assert_equal :bit, column.type assert_equal "bit(8)", column.sql_type - assert_not column.text? assert_not column.number? assert_not column.binary? assert_not column.array @@ -36,19 +35,16 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase column = PostgresqlBitString.columns_hash["a_bit_varying"] assert_equal :bit_varying, column.type assert_equal "bit varying(4)", column.sql_type - assert_not column.text? assert_not column.number? assert_not column.binary? assert_not column.array end def test_default - column = PostgresqlBitString.columns_hash["a_bit"] - assert_equal "00000011", column.default + assert_equal "00000011", PostgresqlBitString.column_defaults['a_bit'] assert_equal "00000011", PostgresqlBitString.new.a_bit - column = PostgresqlBitString.columns_hash["a_bit_varying"] - assert_equal "0011", column.default + assert_equal "0011", PostgresqlBitString.column_defaults['a_bit_varying'] assert_equal "0011", PostgresqlBitString.new.a_bit_varying end diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index 90e837d426..2acb64f81c 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -35,7 +35,6 @@ if ActiveRecord::Base.connection.supports_extensions? column = Citext.columns_hash['cival'] assert_equal :citext, column.type assert_equal 'citext', column.sql_type - assert_not column.text? assert_not column.number? assert_not column.binary? assert_not column.array diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 0b48fe9af8..cfab5ca902 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -51,7 +51,6 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase assert_nil column.type assert_equal "full_address", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -84,7 +83,7 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase class FullAddressType < ActiveRecord::Type::Value def type; :full_address end - def type_cast(value) + def type_cast_from_database(value) if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/ FullAddress.new($1, $2) end @@ -113,7 +112,6 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase assert_equal :full_address, column.type assert_equal "full_address", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index fd7fdecff1..1500adb42d 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -30,7 +30,6 @@ class PostgresqlDomainTest < ActiveRecord::TestCase assert_equal :decimal, column.type assert_equal "custom_money", column.sql_type assert column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index b809f1a79c..d99c4a292e 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -34,7 +34,6 @@ class PostgresqlEnumTest < ActiveRecord::TestCase assert_equal :enum, column.type assert_equal "mood", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -42,9 +41,8 @@ class PostgresqlEnumTest < ActiveRecord::TestCase def test_enum_defaults @connection.add_column 'postgresql_enums', 'good_mood', :mood, default: 'happy' PostgresqlEnum.reset_column_information - column = PostgresqlEnum.columns_hash["good_mood"] - assert_equal "happy", column.default + assert_equal "happy", PostgresqlEnum.column_defaults['good_mood'] assert_equal "happy", PostgresqlEnum.new.good_mood ensure PostgresqlEnum.reset_column_information diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index ec646de5e9..9dadb177ca 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -9,7 +9,6 @@ class PostgresqlFullTextTest < ActiveRecord::TestCase assert_equal :tsvector, column.type assert_equal "tsvector", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index 2f106ee664..6c0adbbeaa 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -28,19 +28,16 @@ class PostgresqlPointTest < ActiveRecord::TestCase column = PostgresqlPoint.columns_hash["x"] assert_equal :point, column.type assert_equal "point", column.sql_type - assert_not column.text? assert_not column.number? assert_not column.binary? assert_not column.array end def test_default - column = PostgresqlPoint.columns_hash["y"] - assert_equal [12.2, 13.3], column.default + assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y'] assert_equal [12.2, 13.3], PostgresqlPoint.new.y - column = PostgresqlPoint.columns_hash["z"] - assert_equal [14.4, 15.5], column.default + assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z'] assert_equal [14.4, 15.5], PostgresqlPoint.new.z end @@ -61,4 +58,15 @@ class PostgresqlPointTest < ActiveRecord::TestCase assert record.reload assert_equal [1.1, 2.2], record.x end + + def test_mutation + p = PostgresqlPoint.create! x: [10, 20] + + p.x[1] = 25 + p.save! + p.reload + + assert_equal [10.0, 25.0], p.x + assert_not p.changed? + end end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index a25c9cb5e4..1296eb72c0 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -56,7 +56,6 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_equal :hstore, @column.type assert_equal "hstore", @column.sql_type assert_not @column.number? - assert_not @column.text? assert_not @column.binary? assert_not @column.array end @@ -64,9 +63,8 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def test_default @connection.add_column 'hstores', 'permissions', :hstore, default: '"users"=>"read", "articles"=>"write"' Hstore.reset_column_information - column = Hstore.columns_hash["permissions"] - assert_equal({"users"=>"read", "articles"=>"write"}, column.default) + assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.column_defaults['permissions']) assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.new.permissions) ensure Hstore.reset_column_information @@ -113,13 +111,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase end def test_type_cast_hstore - assert @column - - data = "\"1\"=>\"2\"" - hash = @column.class.string_to_hstore data - assert_equal({'1' => '2'}, hash) - assert_equal({'1' => '2'}, @column.type_cast_from_database(data)) - + assert_equal({'1' => '2'}, @column.type_cast_from_database("\"1\"=>\"2\"")) assert_equal({}, @column.type_cast_from_database("")) assert_equal({'key'=>nil}, @column.type_cast_from_database('key => NULL')) assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b"))) @@ -163,20 +155,30 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_equal "GMT", y.timezone end + def test_changes_in_place + hstore = Hstore.create!(settings: { 'one' => 'two' }) + hstore.settings['three'] = 'four' + hstore.save! + hstore.reload + + assert_equal 'four', hstore.settings['three'] + assert_not hstore.changed? + end + def test_gen1 - assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) + assert_equal(%q(" "=>""), @column.cast_type.type_cast_for_database({' '=>''})) end def test_gen2 - assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''})) + assert_equal(%q(","=>""), @column.cast_type.type_cast_for_database({','=>''})) end def test_gen3 - assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''})) + assert_equal(%q("="=>""), @column.cast_type.type_cast_for_database({'='=>''})) end def test_gen4 - assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''})) + assert_equal(%q(">"=>""), @column.cast_type.type_cast_for_database({'>'=>''})) end def test_parse1 @@ -286,16 +288,6 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_cycle("a\nb" => "c\nd") end - def test_update_all - hstore = Hstore.create! tags: { "one" => "two" } - - Hstore.update_all tags: { "three" => "four" } - assert_equal({ "three" => "four" }, hstore.reload.tags) - - Hstore.update_all tags: { } - assert_equal({ }, hstore.reload.tags) - end - class TagCollection def initialize(hash); @hash = hash end def to_hash; @hash end diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb new file mode 100644 index 0000000000..22e8873333 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -0,0 +1,44 @@ +require "cases/helper" + +class PostgresqlInfinityTest < ActiveRecord::TestCase + class PostgresqlInfinity < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:postgresql_infinities) do |t| + t.float :float + t.datetime :datetime + end + end + + teardown do + @connection.execute("DROP TABLE IF EXISTS postgresql_infinities") + end + + test "type casting infinity on a float column" do + record = PostgresqlInfinity.create!(float: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.float + end + + test "update_all with infinity on a float column" do + record = PostgresqlInfinity.create! + PostgresqlInfinity.update_all(float: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.float + end + + test "type casting infinity on a datetime column" do + record = PostgresqlInfinity.create!(datetime: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.datetime + end + + test "update_all with infinity on a datetime column" do + record = PostgresqlInfinity.create! + PostgresqlInfinity.update_all(datetime: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.datetime + end +end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 3ee8839823..cb3c02fa3a 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -35,7 +35,6 @@ class PostgresqlJSONTest < ActiveRecord::TestCase assert_equal :json, column.type assert_equal "json", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -43,9 +42,8 @@ class PostgresqlJSONTest < ActiveRecord::TestCase def test_default @connection.add_column 'json_data_type', 'permissions', :json, default: '{"users": "read", "posts": ["read", "write"]}' JsonDataType.reset_column_information - column = JsonDataType.columns_hash["permissions"] - assert_equal({"users"=>"read", "posts"=>["read", "write"]}, column.default) + assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions']) assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.new.permissions) ensure JsonDataType.reset_column_information @@ -78,7 +76,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase column = JsonDataType.columns_hash["payload"] data = "{\"a_key\":\"a_value\"}" - hash = column.class.string_to_json data + hash = column.type_cast_from_database(data) assert_equal({'a_key' => 'a_value'}, hash) assert_equal({'a_key' => 'a_value'}, column.type_cast_from_database(data)) @@ -156,13 +154,24 @@ class PostgresqlJSONTest < ActiveRecord::TestCase assert_equal "320×480", y.resolution end - def test_update_all - json = JsonDataType.create! payload: { "one" => "two" } + def test_changes_in_place + json = JsonDataType.new + assert_not json.changed? - JsonDataType.update_all payload: { "three" => "four" } - assert_equal({ "three" => "four" }, json.reload.payload) + json.payload = { 'one' => 'two' } + assert json.changed? + assert json.payload_changed? - JsonDataType.update_all payload: { } - assert_equal({ }, json.reload.payload) + json.save! + assert_not json.changed? + + json.payload['three'] = 'four' + assert json.payload_changed? + + json.save! + json.reload + + assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload) + assert_not json.changed? end end diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index ddb7cd658c..889e369bd6 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -31,7 +31,6 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase assert_equal :ltree, column.type assert_equal "ltree", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index bdfeedafab..87183174f2 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -26,14 +26,12 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase assert_equal "money", column.sql_type assert_equal 2, column.scale assert column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end def test_default - column = PostgresqlMoney.columns_hash["depth"] - assert_equal BigDecimal.new("150.55"), column.default + assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults['depth'] assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth end @@ -71,4 +69,28 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase money.reload assert_equal new_value, money.wealth end + + def test_update_all_with_money_string + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: "987.65") + money.reload + + assert_equal 987.65, money.wealth + end + + def test_update_all_with_money_big_decimal + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: '123.45'.to_d) + money.reload + + assert_equal 123.45, money.wealth + end + + def test_update_all_with_money_numeric + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: 123.45) + money.reload + + assert_equal 123.45, money.wealth + end end diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb index 32085cbb17..4f4c1103fa 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -10,7 +10,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_equal :cidr, column.type assert_equal "cidr", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -20,7 +19,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_equal :inet, column.type assert_equal "inet", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -30,7 +28,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_equal :macaddr, column.type assert_equal "macaddr", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 218c59247e..11d5173d37 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -57,6 +57,17 @@ module ActiveRecord assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0)) assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime) end + + def test_quote_range + range = "1,2]'; SELECT * FROM users; --".."a" + c = PostgreSQLColumn.new(nil, nil, OID::Range.new(Type::Integer.new, :int8range)) + assert_equal "'[1,0]'", @conn.quote(range, c) + end + + def test_quote_bit_string + c = PostgreSQLColumn.new(nil, 1, OID::Bit.new) + assert_equal nil, @conn.quote("'); SELECT * FROM users; /*\n01\n*/--", c) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 4d9cfe55f5..d812cd01c4 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -156,7 +156,7 @@ _SQL assert_equal 0.5..0.7, @first_range.float_range assert_equal 0.5...0.7, @second_range.float_range assert_equal 0.5...Float::INFINITY, @third_range.float_range - assert_equal (-Float::INFINITY...Float::INFINITY), @fourth_range.float_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range) assert_nil @empty_range.float_range end @@ -262,6 +262,23 @@ _SQL assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") } end + def test_update_all_with_ranges + PostgresqlRange.create! + + PostgresqlRange.update_all(int8_range: 1..100) + + assert_equal 1...101, PostgresqlRange.first.int8_range + end + + def test_ranges_correctly_escape_input + range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a" + PostgresqlRange.update_all(int8_range: range) + + assert_nothing_raised do + PostgresqlRange.first + end + end + private def assert_equal_round_trip(range, attribute, value) round_trip(range, attribute, value) diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb new file mode 100644 index 0000000000..23817198b1 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -0,0 +1,15 @@ +require 'cases/helper' + +class PostgresqlTypeLookupTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + test "array delimiters are looked up correctly" do + box_array = @connection.type_map.lookup(1020) + int_array = @connection.type_map.lookup(1007) + + assert_equal ';', box_array.delimiter + assert_equal ',', int_array.delimiter + end +end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 40ed0f64a4..df8faff546 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -9,15 +9,6 @@ module PostgresqlUUIDHelper @connection ||= ActiveRecord::Base.connection end - def enable_uuid_ossp - unless connection.extension_enabled?('uuid-ossp') - connection.enable_extension 'uuid-ossp' - connection.commit_db_transaction - end - - connection.reconnect! - end - def drop_table(name) connection.execute "drop table if exists #{name}" end @@ -60,7 +51,6 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase assert_equal :uuid, column.type assert_equal "uuid", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -91,16 +81,32 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase end setup do - enable_uuid_ossp + enable_uuid_ossp!(connection) connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t| t.string 'name' t.uuid 'other_uuid', default: 'uuid_generate_v4()' end + + # Create custom PostgreSQL function to generate UUIDs + # to test dumping tables which columns have defaults with custom functions + connection.execute <<-SQL + CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid + AS $$ SELECT * FROM uuid_generate_v4() $$ + LANGUAGE SQL VOLATILE; + SQL + + # Create such a table with custom function as default value generator + connection.create_table('pg_uuids_2', id: :uuid, default: 'my_uuid_generator()') do |t| + t.string 'name' + t.uuid 'other_uuid_2', default: 'my_uuid_generator()' + end end teardown do drop_table "pg_uuids" + drop_table 'pg_uuids_2' + connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();' end if ActiveRecord::Base.connection.supports_extensions? @@ -132,6 +138,13 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string) assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string) end + + def test_schema_dumper_for_uuid_primary_key_with_custom_default + schema = StringIO.new + ActiveRecord::SchemaDumper.dump(connection, schema) + assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema.string) + assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema.string) + end end end @@ -139,7 +152,7 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase include PostgresqlUUIDHelper setup do - enable_uuid_ossp + enable_uuid_ossp!(connection) connection.create_table('pg_uuids', id: false) do |t| t.primary_key :id, :uuid, default: nil @@ -176,7 +189,7 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase end setup do - enable_uuid_ossp + enable_uuid_ossp!(connection) connection.transaction do connection.create_table('pg_uuid_posts', id: :uuid) do |t| diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index 48c6eeb62c..4165dd5ac9 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -11,7 +11,7 @@ class PostgresqlXMLTest < ActiveRecord::TestCase begin @connection.transaction do @connection.create_table('xml_data_type') do |t| - t.xml 'payload', default: {} + t.xml 'payload' end end rescue ActiveRecord::StatementInvalid @@ -32,4 +32,17 @@ class PostgresqlXMLTest < ActiveRecord::TestCase @connection.execute %q|insert into xml_data_type (payload) VALUES(null)| assert_nil XmlDataType.first.payload end + + def test_round_trip + data = XmlDataType.new(payload: "<foo>bar</foo>") + assert_equal "<foo>bar</foo>", data.payload + data.save! + assert_equal "<foo>bar</foo>", data.reload.payload + end + + def test_update_all + data = XmlDataType.create! + XmlDataType.update_all(payload: "<bar>baz</bar>") + assert_equal "<bar>baz</bar>", data.reload.payload + end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 8c9a051eea..ac8332e2fa 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -15,10 +15,10 @@ module ActiveRecord def test_type_cast_binary_encoding_without_logger @conn.extend(Module.new { def logger; end }) - cast_type = Type::String.new + column = Column.new(nil, nil, Type::String.new) binary = SecureRandom.hex expected = binary.dup.encode!(Encoding::UTF_8) - assert_equal expected, @conn.type_cast(binary, cast_type) + assert_equal expected, @conn.type_cast(binary, column) end def test_type_cast_symbol @@ -103,6 +103,13 @@ module ActiveRecord }.new assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) } end + + def test_quoting_binary_strings + value = "hello".encode('ascii-8bit') + column = Column.new(nil, 1, SQLite3String.new) + + assert_equal "'hello'", @conn.quote(value, column) + end end end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index e55525177f..b2bf9480dd 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -57,10 +57,11 @@ module ActiveRecord end end - # sqlite databases should be able to support any type and not - # just the ones mentioned in the native_database_types. - # Therefore test_invalid column should always return true - # even if the type is not valid. + # sqlite3 databases should be able to support any type and not just the + # ones mentioned in the native_database_types. + # + # Therefore test_invalid column should always return true even if the + # type is not valid. def test_invalid_column assert @conn.valid_type?(:foobar) end @@ -339,7 +340,7 @@ module ActiveRecord column = @conn.columns('ex').find { |x| x.name == 'number' } - assert_equal 10, column.default + assert_equal '10', column.default end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 9c92dc1141..25555bd75c 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -787,8 +787,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) comment = comments(:greetings) - assert_difference lambda { post.reload.taggings_count }, -1 do - assert_difference 'comment.reload.taggings_count', +1 do + assert_difference lambda { post.reload.tags_count }, -1 do + assert_difference 'comment.reload.tags_count', +1 do tagging.taggable = comment end end @@ -935,3 +935,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Column.count end end + +class BelongsToWithForeignKeyTest < ActiveRecord::TestCase + fixtures :authors, :author_addresses + + def test_destroy_linked_models + address = AuthorAddress.create! + author = Author.create! name: "Author", author_address_id: address.id + + author.destroy! + end +end diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 71c0609df5..51d8e0523e 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -35,9 +35,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations assert_nothing_raised do - Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a + Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a end - authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a + authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a assert_equal 1, assert_no_queries { authors.size } assert_equal 10, assert_no_queries { authors[0].comments.size } end diff --git a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb new file mode 100644 index 0000000000..48f7ddbe83 --- /dev/null +++ b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb @@ -0,0 +1,26 @@ +require "cases/helper" + +class DeprecatedCounterCacheOnHasManyThroughTest < ActiveRecord::TestCase + class Post < ActiveRecord::Base + has_many :taggings, as: :taggable + has_many :tags, through: :taggings + end + + class Tagging < ActiveRecord::Base + belongs_to :taggable, polymorphic: true + belongs_to :tag + end + + class Tag < ActiveRecord::Base + end + + test "counter caches are updated in the database if the belongs_to association doesn't specify a counter cache" do + post = Post.create!(title: 'Hello', body: 'World!') + assert_deprecated { post.tags << Tag.create!(name: 'whatever') } + + assert_equal 1, post.tags.size + assert_equal 1, post.tags_count + assert_equal 1, post.reload.tags.size + assert_equal 1, post.reload.tags_count + end +end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 910067666a..21912fdf0f 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1261,4 +1261,33 @@ class EagerAssociationTest < ActiveRecord::TestCase Author.eager_load(:posts_with_signature).to_a end end + + test "preloading readonly association" do + # has-one + firm = Firm.where(id: "1").preload(:readonly_account).first! + assert firm.readonly_account.readonly? + + # has_and_belongs_to_many + project = Project.where(id: "2").preload(:readonly_developers).first! + assert project.readonly_developers.first.readonly? + + # has-many :through + david = Author.where(id: "1").preload(:readonly_comments).first! + assert david.readonly_comments.first.readonly? + end + + test "eager-loading readonly association" do + skip "eager_load does not yet preserve readonly associations" + # has-one + firm = Firm.where(id: "1").eager_load(:readonly_account).first! + assert firm.readonly_account.readonly? + + # has_and_belongs_to_many + project = Project.where(id: "2").eager_load(:readonly_developers).first! + assert project.readonly_developers.first.readonly? + + # has-many :through + david = Author.where(id: "1").eager_load(:readonly_comments).first! + assert david.readonly_comments.first.readonly? + end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 080c499444..cc58a4a1a2 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -860,7 +860,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 'edges', Vertex.reflect_on_association(:sources).join_table end - def test_namespaced_habtm + def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_namespaced_model magazine = Publisher::Magazine.create article = Publisher::Article.create magazine.articles << article @@ -869,6 +869,15 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_includes magazine.articles, article end + def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_non_namespaced_model + article = Publisher::Article.create + tag = Tag.create + article.tags << tag + article.save + + assert_includes article.tags, tag + end + def test_redefine_habtm child = SubDeveloper.new("name" => "Aredridel") child.special_projects << SpecialProject.new("name" => "Special Project") diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index a80be99720..fe961e871c 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -36,7 +36,7 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa author = authors(:david) # this can fail on adapters which require ORDER BY expressions to be included in the SELECT expression # if the reorder clauses are not correctly handled - assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.taggings_count DESC').last + assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.tags_count DESC').last end end @@ -772,6 +772,36 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal topic.replies.to_a.size, topic.replies_count end + def test_counter_cache_updates_in_memory_after_concat + topic = Topic.create title: "Zoom-zoom-zoom" + + topic.replies << Reply.create(title: "re: zoom", content: "speedy quick!") + assert_equal 1, topic.replies_count + assert_equal 1, topic.replies.size + assert_equal 1, topic.reload.replies.size + end + + def test_counter_cache_updates_in_memory_after_create + topic = Topic.create title: "Zoom-zoom-zoom" + + topic.replies.create!(title: "re: zoom", content: "speedy quick!") + assert_equal 1, topic.replies_count + assert_equal 1, topic.replies.size + assert_equal 1, topic.reload.replies.size + end + + def test_counter_cache_updates_in_memory_after_create_with_array + topic = Topic.create title: "Zoom-zoom-zoom" + + topic.replies.create!([ + { title: "re: zoom", content: "speedy quick!" }, + { title: "re: zoom 2", content: "OMG lol!" }, + ]) + assert_equal 2, topic.replies_count + assert_equal 2, topic.replies.size + assert_equal 2, topic.reload.replies.size + end + def test_pushing_association_updates_counter_cache topic = Topic.order("id ASC").first reply = Reply.create! @@ -784,14 +814,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_without_dependent_option post = posts(:welcome) - assert_difference "post.reload.taggings_count", -1 do + assert_difference "post.reload.tags_count", -1 do post.taggings.delete(post.taggings.first) end end def test_deleting_updates_counter_cache_with_dependent_delete_all post = posts(:welcome) - post.update_columns(taggings_with_delete_all_count: post.taggings_count) + post.update_columns(taggings_with_delete_all_count: post.tags_count) assert_difference "post.reload.taggings_with_delete_all_count", -1 do post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first) @@ -800,7 +830,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_with_dependent_destroy post = posts(:welcome) - post.update_columns(taggings_with_destroy_count: post.taggings_count) + post.update_columns(taggings_with_destroy_count: post.tags_count) assert_difference "post.reload.taggings_with_destroy_count", -1 do post.taggings_with_destroy.delete(post.taggings_with_destroy.first) diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 2e62189e7a..a85e020f0c 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -330,6 +330,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert post.single_people.include?(person) end + def test_both_parent_ids_set_when_saving_new + post = Post.new(title: 'Hello', body: 'world') + person = Person.new(first_name: 'Sean') + + post.people = [person] + post.save + + assert post.id + assert person.id + assert_equal post.id, post.readers.first.post_id + assert_equal person.id, post.readers.first.person_id + end + def test_delete_association assert_queries(2){posts(:welcome);people(:michael); } @@ -476,7 +489,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) tag = post.tags.create!(:name => 'doomed') - assert_difference ['post.reload.taggings_count', 'post.reload.tags_count'], -1 do + assert_difference ['post.reload.tags_count'], -1 do posts(:welcome).tags.delete(tag) end end @@ -486,7 +499,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag = post.tags.create!(:name => 'doomed') post.update_columns(tags_with_destroy_count: post.tags.count) - assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do + assert_difference ['post.reload.tags_with_destroy_count'], -1 do posts(:welcome).tags_with_destroy.delete(tag) end end @@ -496,7 +509,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag = post.tags.create!(:name => 'doomed') post.update_columns(tags_with_nullify_count: post.tags.count) - assert_no_difference 'post.reload.taggings_count' do + assert_no_difference 'post.reload.tags_count' do assert_difference 'post.reload.tags_with_nullify_count', -1 do posts(:welcome).tags_with_nullify.delete(tag) end @@ -511,14 +524,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag.tagged_posts = [] post.reload - assert_equal(post.taggings.count, post.taggings_count) + assert_equal(post.taggings.count, post.tags_count) end def test_update_counter_caches_on_destroy post = posts(:welcome) tag = post.tags.create!(name: 'doomed') - assert_difference 'post.reload.taggings_count', -1 do + assert_difference 'post.reload.tags_count', -1 do tag.tagged_posts.destroy(post) end end @@ -1126,4 +1139,31 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal 2, post.lazy_readers_unscope_skimmers.to_a.size assert_equal 2, post.lazy_people_unscope_skimmers.to_a.size end + + def test_has_many_through_add_with_sti_middle_relation + club = SuperClub.create!(name: 'Fight Club') + member = Member.create!(name: 'Tyler Durden') + + club.members << member + assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count + end + + class ClubWithCallbacks < ActiveRecord::Base + self.table_name = 'clubs' + after_create :add_a_member + + has_many :memberships, inverse_of: :club, foreign_key: :club_id + has_many :members, through: :memberships + + def add_a_member + members << Member.last + end + end + + def test_has_many_with_callback_before_association + Member.create! + club = ClubWithCallbacks.create! + + assert_equal 1, club.reload.memberships.count + end end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index a2725441b3..089cb0a3a2 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -45,6 +45,20 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_equal clubs(:moustache_club), new_member.club end + def test_creating_association_sets_both_parent_ids_for_new + member = Member.new(name: 'Sean Griffin') + club = Club.new(name: 'Da Club') + + member.club = club + + member.save! + + assert member.id + assert club.id + assert_equal member.id, member.current_membership.member_id + assert_equal club.id, member.current_membership.club_id + end + def test_replace_target_record new_club = Club.create(:name => "Marx Bros") @member.club = new_club diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index a674a39d65..60df4e14dd 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -100,6 +100,17 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase assert_respond_to club_reflection, :has_inverse? assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically" end + + def test_polymorphic_relationships_should_still_not_have_inverses_when_non_polymorphic_relationship_has_the_same_name + man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse) + face_reflection = Face.reflect_on_association(:man) + + assert_respond_to face_reflection, :has_inverse? + assert face_reflection.has_inverse?, "For this test, the non-polymorphic association must have an inverse" + + assert_respond_to man_reflection, :has_inverse? + assert !man_reflection.has_inverse?, "The target of a polymorphic association should not find an inverse automatically" + end end class InverseAssociationTests < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index aabeea025f..cace7ba142 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -326,11 +326,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_belongs_to_polymorphic_with_counter_cache - assert_equal 1, posts(:welcome)[:taggings_count] + assert_equal 1, posts(:welcome)[:tags_count] tagging = posts(:welcome).taggings.create(:tag => tags(:general)) - assert_equal 2, posts(:welcome, :reload)[:taggings_count] + assert_equal 2, posts(:welcome, :reload)[:tags_count] tagging.destroy - assert_equal 1, posts(:welcome, :reload)[:taggings_count] + assert_equal 1, posts(:welcome, :reload)[:tags_count] end def test_unavailable_through_reflection @@ -489,7 +489,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 1, post_thinking.tags.size) + assert_equal(count + 1, post_thinking.reload.tags.size) assert_equal(count + 1, post_thinking.tags(true).size) assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo') @@ -497,7 +497,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 2, post_thinking.tags.size) + assert_equal(count + 2, post_thinking.reload.tags.size) assert_equal(count + 2, post_thinking.tags(true).size) assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) } @@ -505,7 +505,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 4, post_thinking.tags.size) + assert_equal(count + 4, post_thinking.reload.tags.size) assert_equal(count + 4, post_thinking.tags(true).size) # Raises if the wrong reflection name is used to set the Edge belongs_to @@ -554,34 +554,35 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_associate_when_deleting_from_has_many_through count = posts(:thinking).tags.count - tags_before = posts(:thinking).tags + tags_before = posts(:thinking).tags.sort tag = Tag.create!(:name => 'doomed') post_thinking = posts(:thinking) post_thinking.tags << tag assert_equal(count + 1, post_thinking.taggings(true).size) - assert_equal(count + 1, post_thinking.tags(true).size) + assert_equal(count + 1, post_thinking.reload.tags(true).size) + assert_not_equal(tags_before, post_thinking.tags.sort) assert_nothing_raised { post_thinking.tags.delete(tag) } assert_equal(count, post_thinking.tags.size) assert_equal(count, post_thinking.tags(true).size) assert_equal(count, post_thinking.taggings(true).size) - assert_equal(tags_before.sort, post_thinking.tags.sort) + assert_equal(tags_before, post_thinking.tags.sort) end def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags count = posts(:thinking).tags.count - tags_before = posts(:thinking).tags + tags_before = posts(:thinking).tags.sort doomed = Tag.create!(:name => 'doomed') doomed2 = Tag.create!(:name => 'doomed2') quaked = Tag.create!(:name => 'quaked') post_thinking = posts(:thinking) post_thinking.tags << doomed << doomed2 - assert_equal(count + 2, post_thinking.tags(true).size) + assert_equal(count + 2, post_thinking.reload.tags(true).size) assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) } assert_equal(count, post_thinking.tags.size) assert_equal(count, post_thinking.tags(true).size) - assert_equal(tags_before.sort, post_thinking.tags.sort) + assert_equal(tags_before, post_thinking.tags.sort) end def test_deleting_junk_from_has_many_through_should_raise_type_mismatch diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 8ef351cda8..3720d6d251 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -153,6 +153,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_one_with_has_many_through_source_reflection_preload + ActiveRecord::Base.connection.table_alias_length # preheat cache members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) } groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy) diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb new file mode 100644 index 0000000000..a6934a056e --- /dev/null +++ b/activerecord/test/cases/associations/required_test.rb @@ -0,0 +1,82 @@ +require "cases/helper" + +class RequiredAssociationsTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Parent < ActiveRecord::Base + end + + class Child < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :parents, force: true + @connection.create_table :children, force: true do |t| + t.belongs_to :parent + end + end + + teardown do + @connection.execute("DROP TABLE IF EXISTS parents") + @connection.execute("DROP TABLE IF EXISTS children") + end + + test "belongs_to associations are not required by default" do + model = subclass_of(Child) do + belongs_to :parent, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end + + assert model.new.save + assert model.new(parent: Parent.new).save + end + + test "required belongs_to associations have presence validated" do + model = subclass_of(Child) do + belongs_to :parent, required: true, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end + + record = model.new + assert_not record.save + assert_equal ["Parent can't be blank"], record.errors.full_messages + + record.parent = Parent.new + assert record.save + end + + test "has_one associations are not required by default" do + model = subclass_of(Parent) do + has_one :child, inverse_of: false, + class_name: "RequiredAssociationsTest::Child" + end + + assert model.new.save + assert model.new(child: Child.new).save + end + + test "required has_one associations have presence validated" do + model = subclass_of(Parent) do + has_one :child, required: true, inverse_of: false, + class_name: "RequiredAssociationsTest::Child" + end + + record = model.new + assert_not record.save + assert_equal ["Child can't be blank"], record.errors.full_messages + + record.child = Child.new + assert record.save + end + + private + + def subclass_of(klass, &block) + subclass = Class.new(klass, &block) + def subclass.name + superclass.name + end + subclass + end +end diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb index 35393753a2..b352d1a6c2 100644 --- a/activerecord/test/cases/attribute_decorators_test.rb +++ b/activerecord/test/cases/attribute_decorators_test.rb @@ -98,17 +98,28 @@ module ActiveRecord assert_equal 'Hello! decorated!', model.a_string assert_equal 'whatever', model.another_string assert_equal 'Hello! decorated! decorated!', child.a_string - # We are round tripping the default, and we don't undo our decoration - assert_equal 'whatever decorated! decorated!', child.another_string + assert_equal 'whatever decorated!', child.another_string end - test "defaults are decorated on the column" do - Model.attribute :a_string, Type::String.new, default: 'whatever' - Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + class Multiplier < SimpleDelegator + def type_cast_from_user(value) + return if value.nil? + value * 2 + end + alias type_cast_from_database type_cast_from_user + end + + test "decorating with a proc" do + Model.attribute :an_int, Type::Integer.new + type_is_integer = proc { |_, type| type.type == :integer } + Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type| + Multiplier.new(type) + end - column = Model.columns_hash['a_string'] + model = Model.new(a_string: 'whatever', an_int: 1) - assert_equal 'whatever decorated!', column.default + assert_equal 'whatever', model.a_string + assert_equal 2, model.an_int end end end diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index c0659fddef..4741ee8799 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -12,6 +12,7 @@ module ActiveRecord @klass = Class.new do def self.superclass; Base; end def self.base_class; self; end + def self.decorate_matching_attribute_types(*); end include ActiveRecord::AttributeMethods diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 139fe9c04b..2048e0d5ad 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -253,6 +253,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes end + def test_attributes_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers_projects' + end + + assert_equal klass.column_names, klass.new.attributes.keys + assert_not klass.new.attributes.key?('id') + end + def test_hashes_not_mangled new_topic = { :title => "New Topic" } new_topic_values = { :title => "AnotherTopic" } @@ -451,10 +460,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_declared_suffixed_attribute_method_affects_respond_to_and_method_missing - topic = @target.new(:title => 'Budget') %w(_default _title_default _it! _candidate= able?).each do |suffix| @target.class_eval "def attribute#{suffix}(*args) args end" @target.attribute_method_suffix suffix + topic = @target.new(:title => 'Budget') meth = "title#{suffix}" assert topic.respond_to?(meth) @@ -465,10 +474,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def test_declared_affixed_attribute_method_affects_respond_to_and_method_missing - topic = @target.new(:title => 'Budget') [['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix| @target.class_eval "def #{prefix}attribute#{suffix}(*args) args end" @target.attribute_method_affix({ :prefix => prefix, :suffix => suffix }) + topic = @target.new(:title => 'Budget') meth = "#{prefix}title#{suffix}" assert topic.respond_to?(meth) @@ -517,43 +526,17 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_only_time_related_columns_are_meant_to_be_cached_by_default - expected = %w(datetime time date).sort - assert_equal expected, ActiveRecord::Base.attribute_types_cached_by_default.map(&:to_s).sort - end - - def test_declaring_attributes_as_cached_adds_them_to_the_attributes_cached_by_default - default_attributes = Topic.cached_attributes - Topic.cache_attributes :replies_count - expected = default_attributes + ["replies_count"] - assert_equal expected.sort, Topic.cached_attributes.sort - Topic.instance_variable_set "@cached_attributes", nil - end - - def test_cacheable_columns_are_actually_cached - assert_equal cached_columns.sort, Topic.cached_attributes.sort - end - - def test_accessing_cached_attributes_caches_the_converted_values_and_nothing_else - t = topics(:first) - cache = t.instance_variable_get "@attributes" - - assert_not_nil cache - assert cache.empty? + def test_deprecated_cache_attributes + assert_deprecated do + Topic.cache_attributes :replies_count + end - all_columns = Topic.columns.map(&:name) - uncached_columns = all_columns - cached_columns + assert_deprecated do + Topic.cached_attributes + end - all_columns.each do |attr_name| - attribute_gets_cached = Topic.cache_attribute?(attr_name) - val = t.send attr_name unless attr_name == "type" - if attribute_gets_cached - assert cached_columns.include?(attr_name) - assert_equal val, cache[attr_name] - else - assert uncached_columns.include?(attr_name) - assert !cache.include?(attr_name) - end + assert_deprecated do + Topic.cache_attribute? :replies_count end end @@ -857,6 +840,37 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_attribute_method? + assert @target.attribute_method?(:title) + assert @target.attribute_method?(:title=) + assert_not @target.attribute_method?(:wibble) + end + + def test_attribute_method_returns_false_if_table_does_not_exist + @target.table_name = 'wibble' + assert_not @target.attribute_method?(:title) + end + + def test_attribute_names_on_new_record + model = @target.new + + assert_equal @target.column_names, model.attribute_names + end + + def test_attribute_names_on_queried_record + model = @target.last! + + assert_equal @target.column_names, model.attribute_names + end + + def test_attribute_names_with_custom_select + model = @target.select('id').last! + + assert_equal ['id'], model.attribute_names + # Sanity check, make sure other columns exist + assert_not_equal ['id'], @target.column_names + end + private def new_topic_like_ar_class(&block) diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb new file mode 100644 index 0000000000..cdbb11fa32 --- /dev/null +++ b/activerecord/test/cases/attribute_set_test.rb @@ -0,0 +1,165 @@ +require 'cases/helper' + +module ActiveRecord + class AttributeSetTest < ActiveRecord::TestCase + test "building a new set from raw attributes" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal 1, attributes[:foo].value + assert_equal 2.2, attributes[:bar].value + assert_equal :foo, attributes[:foo].name + assert_equal :bar, attributes[:bar].name + end + + test "building with custom types" do + builder = AttributeSet::Builder.new(foo: Type::Float.new) + attributes = builder.build_from_database({ foo: '3.3', bar: '4.4' }, { bar: Type::Integer.new }) + + assert_equal 3.3, attributes[:foo].value + assert_equal 4, attributes[:bar].value + end + + test "[] returns a null object" do + builder = AttributeSet::Builder.new(foo: Type::Float.new) + attributes = builder.build_from_database(foo: '3.3') + + assert_equal '3.3', attributes[:foo].value_before_type_cast + assert_equal nil, attributes[:bar].value_before_type_cast + assert_equal :bar, attributes[:bar].name + end + + test "duping creates a new hash and dups each attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) + attributes = builder.build_from_database(foo: 1, bar: 'foo') + + # Ensure the type cast value is cached + attributes[:foo].value + attributes[:bar].value + + duped = attributes.dup + duped.write_from_database(:foo, 2) + duped[:bar].value << 'bar' + + assert_equal 1, attributes[:foo].value + assert_equal 2, duped[:foo].value + assert_equal 'foo', attributes[:bar].value + assert_equal 'foobar', duped[:bar].value + end + + test "freezing cloned set does not freeze original" do + attributes = AttributeSet.new({}) + clone = attributes.clone + + clone.freeze + + assert clone.frozen? + assert_not attributes.frozen? + end + + test "to_hash returns a hash of the type cast values" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash) + assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h) + end + + test "values_before_type_cast" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal({ foo: '1.1', bar: '2.2' }, attributes.values_before_type_cast) + end + + test "known columns are built with uninitialized attributes" do + attributes = attributes_with_uninitialized_key + assert attributes[:foo].initialized? + assert_not attributes[:bar].initialized? + end + + test "uninitialized attributes are not included in the attributes hash" do + attributes = attributes_with_uninitialized_key + assert_equal({ foo: 1 }, attributes.to_hash) + end + + test "uninitialized attributes are not included in keys" do + attributes = attributes_with_uninitialized_key + assert_equal [:foo], attributes.keys + end + + test "uninitialized attributes return false for include?" do + attributes = attributes_with_uninitialized_key + assert attributes.include?(:foo) + assert_not attributes.include?(:bar) + end + + test "unknown attributes return false for include?" do + attributes = attributes_with_uninitialized_key + assert_not attributes.include?(:wibble) + end + + test "fetch_value returns the value for the given initialized attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal 1, attributes.fetch_value(:foo) + assert_equal 2.2, attributes.fetch_value(:bar) + end + + test "fetch_value returns nil for unknown attributes" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:wibble) + end + + test "fetch_value uses the given block for uninitialized attributes" do + attributes = attributes_with_uninitialized_key + value = attributes.fetch_value(:bar) { |n| n.to_s + '!' } + assert_equal 'bar!', value + end + + test "fetch_value returns nil for uninitialized attributes if no block is given" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:bar) + end + + class MyType + def type_cast_from_user(value) + return if value.nil? + value + " from user" + end + + def type_cast_from_database(value) + return if value.nil? + value + " from database" + end + end + + test "write_from_database sets the attribute with database typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_database(:foo, "value") + + assert_equal "value from database", attributes.fetch_value(:foo) + end + + test "write_from_user sets the attribute with user typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_user(:foo, "value") + + assert_equal "value from user", attributes.fetch_value(:foo) + end + + def attributes_with_uninitialized_key + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + builder.build_from_database(foo: '1.1') + end + end +end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb new file mode 100644 index 0000000000..24452fdec2 --- /dev/null +++ b/activerecord/test/cases/attribute_test.rb @@ -0,0 +1,142 @@ +require 'cases/helper' +require 'minitest/mock' + +module ActiveRecord + class AttributeTest < ActiveRecord::TestCase + setup do + @type = MiniTest::Mock.new + end + + teardown do + assert @type.verify + end + + test "from_database + read type casts from database" do + @type.expect(:type_cast_from_database, 'type cast from database', ['a value']) + attribute = Attribute.from_database(nil, 'a value', @type) + + type_cast_value = attribute.value + + assert_equal 'type cast from database', type_cast_value + end + + test "from_user + read type casts from user" do + @type.expect(:type_cast_from_user, 'type cast from user', ['a value']) + attribute = Attribute.from_user(nil, 'a value', @type) + + type_cast_value = attribute.value + + assert_equal 'type cast from user', type_cast_value + end + + test "reading memoizes the value" do + @type.expect(:type_cast_from_database, 'from the database', ['whatever']) + attribute = Attribute.from_database(nil, 'whatever', @type) + + type_cast_value = attribute.value + second_read = attribute.value + + assert_equal 'from the database', type_cast_value + assert_same type_cast_value, second_read + end + + test "reading memoizes falsy values" do + @type.expect(:type_cast_from_database, false, ['whatever']) + attribute = Attribute.from_database(nil, 'whatever', @type) + + attribute.value + attribute.value + end + + test "read_before_typecast returns the given value" do + attribute = Attribute.from_database(nil, 'raw value', @type) + + raw_value = attribute.value_before_type_cast + + assert_equal 'raw value', raw_value + end + + test "from_database + read_for_database type casts to and from database" do + @type.expect(:type_cast_from_database, 'read from database', ['whatever']) + @type.expect(:type_cast_for_database, 'ready for database', ['read from database']) + attribute = Attribute.from_database(nil, 'whatever', @type) + + type_cast_for_database = attribute.value_for_database + + assert_equal 'ready for database', type_cast_for_database + end + + test "from_user + read_for_database type casts from the user to the database" do + @type.expect(:type_cast_from_user, 'read from user', ['whatever']) + @type.expect(:type_cast_for_database, 'ready for database', ['read from user']) + attribute = Attribute.from_user(nil, 'whatever', @type) + + type_cast_for_database = attribute.value_for_database + + assert_equal 'ready for database', type_cast_for_database + end + + test "duping dups the value" do + @type.expect(:type_cast_from_database, 'type cast', ['a value']) + attribute = Attribute.from_database(nil, 'a value', @type) + + value_from_orig = attribute.value + value_from_clone = attribute.dup.value + value_from_orig << ' foo' + + assert_equal 'type cast foo', value_from_orig + assert_equal 'type cast', value_from_clone + end + + test "duping does not dup the value if it is not dupable" do + @type.expect(:type_cast_from_database, false, ['a value']) + attribute = Attribute.from_database(nil, 'a value', @type) + + assert_same attribute.value, attribute.dup.value + end + + test "duping does not eagerly type cast if we have not yet type cast" do + attribute = Attribute.from_database(nil, 'a value', @type) + attribute.dup + end + + class MyType + def type_cast_from_user(value) + value + " from user" + end + + def type_cast_from_database(value) + value + " from database" + end + end + + test "with_value_from_user returns a new attribute with the value from the user" do + old = Attribute.from_database(nil, "old", MyType.new) + new = old.with_value_from_user("new") + + assert_equal "old from database", old.value + assert_equal "new from user", new.value + end + + test "with_value_from_database returns a new attribute with the value from the database" do + old = Attribute.from_user(nil, "old", MyType.new) + new = old.with_value_from_database("new") + + assert_equal "old from user", old.value + assert_equal "new from database", new.value + end + + test "uninitialized attributes yield their name if a block is given to value" do + block = proc { |name| name.to_s + "!" } + foo = Attribute.uninitialized(:foo, nil) + bar = Attribute.uninitialized(:bar, nil) + + assert_equal "foo!", foo.value(&block) + assert_equal "bar!", bar.value(&block) + end + + test "uninitialized attributes have no value" do + assert_nil Attribute.uninitialized(:foo, nil).value + end + end +end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index dc8f25b0e7..8f83cf7cb4 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1488,15 +1488,14 @@ class BasicsTest < ActiveRecord::TestCase attrs = topic.attributes.dup attrs.delete 'id' - typecast = Class.new { - def type_cast_from_database value + typecast = Class.new(ActiveRecord::Type::Value) { + def type_cast value "t.lo" end } types = { 'author_name' => typecast.new } - topic = Topic.allocate.init_with 'raw_attributes' => attrs, - 'column_types' => types + topic = Topic.instantiate(attrs, types) assert_equal 't.lo', topic.author_name end @@ -1597,4 +1596,11 @@ class BasicsTest < ActiveRecord::TestCase assert_equal after_handler, new_handler assert_equal orig_handler, klass.connection_handler end + + # Note: This is a performance optimization for Array#uniq and Hash#[] with + # AR::Base objects. If the future has made this irrelevant, feel free to + # delete this. + test "records without an id have unique hashes" do + assert_not_equal Post.new.hash, Post.new.hash + end end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index b41b95309b..ccf2be369d 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -21,7 +21,7 @@ unless current_adapter?(:DB2Adapter) name = binary.name - # Mysql adapter doesn't properly encode things, so we have to do it + # MySQL adapter doesn't properly encode things, so we have to do it if current_adapter?(:MysqlAdapter) name.force_encoding(Encoding::UTF_8) end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index dfc6a7ec67..319ea9260a 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -53,11 +53,6 @@ class CalculationsTest < ActiveRecord::TestCase assert_nil NumericData.average(:bank_balance) end - def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal - assert_equal 0, NumericData.all.send(:type_cast_calculated_value, 0, nil, 'avg') - assert_equal 53.0, NumericData.all.send(:type_cast_calculated_value, 53, nil, 'avg') - end - def test_should_get_maximum_of_field assert_equal 60, Account.maximum(:credit_limit) end @@ -610,4 +605,11 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [1,2,3,4,5], taks_relation.pluck(:id) assert_equal [false, true, true, true, true], taks_relation.pluck(:approved) end + + def test_pluck_columns_with_same_name + expected = [["The First Topic", "The Second Topic of the day"], ["The Third Topic of the day", "The Fourth Topic of the day"]] + actual = Topic.joins(:replies) + .pluck('topics.title', 'replies_topics.title') + assert_equal expected, actual + end end diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index da852aaa02..e1b2804a18 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -144,6 +144,18 @@ module ActiveRecord assert_equal nil, actual[:test] end + def test_database_url_with_ipv6_host_and_port + ENV['DATABASE_URL'] = "postgres://[::1]:5454/foo" + + config = {} + actual = resolve_config(config) + expected = { "adapter" => "postgresql", + "database" => "foo", + "host" => "::1", + "port" => 5454 } + assert_equal expected, actual["default_env"] + end + def test_url_sub_key_with_database_url ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO" diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index ecad7c942f..c7531f5418 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -45,8 +45,8 @@ module ActiveRecord @cache = Marshal.load(Marshal.dump(@cache)) - assert_equal 12, @cache.columns('posts').size - assert_equal 12, @cache.columns_hash('posts').size + assert_equal 11, @cache.columns('posts').size + assert_equal 11, @cache.columns_hash('posts').size assert @cache.tables('posts') assert_equal 'id', @cache.primary_keys('posts') end diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index ab2a749ba8..07a182070b 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -19,6 +19,7 @@ class CounterCacheTest < ActiveRecord::TestCase class ::SpecialTopic < ::Topic has_many :special_replies, :foreign_key => 'parent_id' + has_many :lightweight_special_replies, -> { select('topics.id, topics.title') }, :foreign_key => 'parent_id', :class_name => 'SpecialReply' end class ::SpecialReply < ::Reply @@ -170,4 +171,13 @@ class CounterCacheTest < ActiveRecord::TestCase end assert_equal "'Topic' has no association called 'undefined_count'", e.message end + + test "reset counter works with select declared on association" do + special = SpecialTopic.create!(:title => 'Special') + SpecialTopic.increment_counter(:replies_count, special.id) + + assert_difference 'special.reload.replies_count', -1 do + SpecialTopic.reset_counters(special.id, :lightweight_special_replies) + end + end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 92144bc802..c089e63128 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -154,7 +154,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) t.column :omit, :integer, :null => false end - assert_equal 0, klass.columns_hash['zero'].default + assert_equal '0', klass.columns_hash['zero'].default assert !klass.columns_hash['zero'].null # 0 in MySQL 4, nil in 5. assert [0, nil].include?(klass.columns_hash['omit'].default) diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 87f24e32b2..ea73c561e9 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -309,16 +309,14 @@ class DirtyTest < ActiveRecord::TestCase def test_attribute_will_change! pirate = Pirate.create!(:catchphrase => 'arr') - pirate.catchphrase << ' matey' assert !pirate.catchphrase_changed? - assert pirate.catchphrase_will_change! assert pirate.catchphrase_changed? - assert_equal ['arr matey', 'arr matey'], pirate.catchphrase_change + assert_equal ['arr', 'arr'], pirate.catchphrase_change - pirate.catchphrase << '!' + pirate.catchphrase << ' matey!' assert pirate.catchphrase_changed? - assert_equal ['arr matey', 'arr matey!'], pirate.catchphrase_change + assert_equal ['arr', 'arr matey!'], pirate.catchphrase_change end def test_association_assignment_changes_foreign_key @@ -445,11 +443,20 @@ class DirtyTest < ActiveRecord::TestCase def test_save_should_store_serialized_attributes_even_with_partial_writes with_partial_writes(Topic) do topic = Topic.create!(:content => {:a => "a"}) + + assert_not topic.changed? + topic.content[:b] = "b" - #assert topic.changed? # Known bug, will fail + + assert topic.changed? + topic.save! + + assert_not topic.changed? assert_equal "b", topic.content[:b] + topic.reload + assert_equal "b", topic.content[:b] end end diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 409d9a82e2..638cffe0e6 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -141,5 +141,17 @@ module ActiveRecord ensure Topic.default_scopes = prev_default_scopes end + + def test_dup_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'parrots_pirates' + end + + record = klass.create! + + assert_nothing_raised do + record.dup + end + end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 0f52f2c0fc..40e51a0cdc 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -34,10 +34,10 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_proc_parameter_and_block - e = assert_raises(RuntimeError) do + exception = assert_raises(RuntimeError) do Topic.all.find(-> { raise "should happen" }) { |e| e.title == "non-existing-title" } end - assert_equal "should happen", e.message + assert_equal "should happen", exception.message assert_nothing_raised(RuntimeError) do Topic.all.find(-> { raise "should not happen" }) { |e| e.title == topics(:first).title } @@ -144,8 +144,8 @@ class FinderTest < ActiveRecord::TestCase def test_exists_with_distinct_association_includes_limit_and_order author = Author.first - assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists? - assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists? + assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(0).exists? + assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(1).exists? end def test_exists_with_empty_table_and_no_args_given diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 8bbc0af758..042fdaf0bb 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -84,12 +84,6 @@ class FixturesTest < ActiveRecord::TestCase assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}" end - def test_create_symbol_fixtures_is_deprecated - assert_deprecated do - ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, :collections, :collections => 'Course') { Course.connection } - end - end - def test_attributes topics = create_fixtures("topics").first assert_equal("The First Topic", topics["first"]["title"]) diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 93fd3b9605..0c9dff2c25 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -273,8 +273,11 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty? end - def test_quoted_locking_column_is_deprecated - assert_deprecated { ActiveRecord::Base.quoted_locking_column } + def test_yaml_dumping_with_lock_column + t1 = LockWithoutDefault.new + t2 = YAML.load(YAML.dump(t1)) + + assert_equal t1.attributes, t2.attributes end end diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 9b26c30d14..c66eaf1ee1 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -68,9 +68,9 @@ module ActiveRecord five = columns.detect { |c| c.name == "five" } unless mysql assert_equal "hello", one.default - assert_equal true, two.default - assert_equal false, three.default - assert_equal 1, four.default + assert_equal true, two.type_cast_from_database(two.default) + assert_equal false, three.type_cast_from_database(three.default) + assert_equal '1', four.default assert_equal "hello", five.default unless mysql end @@ -275,7 +275,7 @@ module ActiveRecord person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99 person_klass.reset_column_information - assert_equal 99, person_klass.columns_hash["wealth"].default + assert_equal 99, person_klass.column_defaults["wealth"] assert_equal false, person_klass.columns_hash["wealth"].null # Oracle needs primary key value from sequence if current_adapter?(:OracleAdapter) @@ -287,20 +287,20 @@ module ActiveRecord # change column default to see that column doesn't lose its not null definition person_klass.connection.change_column_default "testings", "wealth", 100 person_klass.reset_column_information - assert_equal 100, person_klass.columns_hash["wealth"].default + assert_equal 100, person_klass.column_defaults["wealth"] assert_equal false, person_klass.columns_hash["wealth"].null # rename column to see that column doesn't lose its not null and/or default definition person_klass.connection.rename_column "testings", "wealth", "money" person_klass.reset_column_information assert_nil person_klass.columns_hash["wealth"] - assert_equal 100, person_klass.columns_hash["money"].default + assert_equal 100, person_klass.column_defaults["money"] assert_equal false, person_klass.columns_hash["money"].null # change column person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000 person_klass.reset_column_information - assert_equal 1000, person_klass.columns_hash["money"].default + assert_equal 1000, person_klass.column_defaults["money"] assert_equal false, person_klass.columns_hash["money"].null # change column, make it nullable and clear default diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 984d1c2597..93adbdd05b 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -62,7 +62,7 @@ module ActiveRecord # Do a manual insertion if current_adapter?(:OracleAdapter) connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)" - elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before mysql 5.0.3 decimals stored as strings + elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')" elsif current_adapter?(:PostgreSQLAdapter) connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index a7c287515d..4e6d7963aa 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -53,13 +53,13 @@ module ActiveRecord add_column 'test_models', 'salary', :integer, :default => 70000 default_before = connection.columns("test_models").find { |c| c.name == "salary" }.default - assert_equal 70000, default_before + assert_equal '70000', default_before rename_column "test_models", "salary", "annual_salary" assert TestModel.column_names.include?("annual_salary") default_after = connection.columns("test_models").find { |c| c.name == "annual_salary" }.default - assert_equal 70000, default_after + assert_equal '70000', default_after end if current_adapter?(:MysqlAdapter, :Mysql2Adapter) @@ -193,14 +193,21 @@ module ActiveRecord old_columns = connection.columns(TestModel.table_name) assert old_columns.find { |c| - c.name == 'approved' && c.type == :boolean && c.default == true + default = c.type_cast_from_database(c.default) + c.name == 'approved' && c.type == :boolean && default == true } change_column :test_models, :approved, :boolean, :default => false new_columns = connection.columns(TestModel.table_name) - assert_not new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true } - assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false } + assert_not new_columns.find { |c| + default = c.type_cast_from_database(c.default) + c.name == 'approved' and c.type == :boolean and default == true + } + assert new_columns.find { |c| + default = c.type_cast_from_database(c.default) + c.name == 'approved' and c.type == :boolean and default == false + } change_column :test_models, :approved, :boolean, :default => true end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index a925cf4c05..e955beae1a 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -157,6 +157,23 @@ module ActiveRecord assert_equal [:remove_column, [:table, :column, :type, {}], nil], remove end + def test_invert_change_column + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :change_column, [:table, :column, :type, {}] + end + end + + def test_invert_change_column_default + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :change_column_default, [:table, :column, 'default_value'] + end + end + + def test_invert_change_column_null + add = @recorder.inverse_of :change_column_null, [:table, :column, true] + assert_equal [:change_column_null, [:table, :column, false]], add + end + def test_invert_remove_column add = @recorder.inverse_of :remove_column, [:table, :column, :type, {}] assert_equal [:add_column, [:table, :column, :type, {}], nil], add @@ -253,6 +270,31 @@ module ActiveRecord enable = @recorder.inverse_of :disable_extension, ['uuid-ossp'] assert_equal [:enable_extension, ['uuid-ossp'], nil], enable end + + def test_invert_add_foreign_key + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people] + assert_equal [:remove_foreign_key, [:dogs, :people]], enable + end + + def test_invert_add_foreign_key_with_column + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"] + assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable + end + + def test_invert_add_foreign_key_with_column_and_name + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] + assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable + end + + def test_remove_foreign_key_is_irreversible + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"] + end + + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"] + end + end end end end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb new file mode 100644 index 0000000000..c985092b4c --- /dev/null +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -0,0 +1,242 @@ +require 'cases/helper' +require 'support/ddl_helper' +require 'support/schema_dumping_helper' + +if ActiveRecord::Base.connection.supports_foreign_keys? +module ActiveRecord + class Migration + class ForeignKeyTest < ActiveRecord::TestCase + include DdlHelper + include SchemaDumpingHelper + + class Rocket < ActiveRecord::Base + end + + class Astronaut < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "rockets" do |t| + t.string :name + end + + @connection.create_table "astronauts" do |t| + t.string :name + t.references :rocket + end + end + + teardown do + if defined?(@connection) + @connection.execute "DROP TABLE IF EXISTS astronauts" + @connection.execute "DROP TABLE IF EXISTS rockets" + end + end + + def test_foreign_keys + foreign_keys = @connection.foreign_keys("fk_test_has_fk") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "fk_test_has_fk", fk.from_table + assert_equal "fk_test_has_pk", fk.to_table + assert_equal "fk_id", fk.column + assert_equal "pk_id", fk.primary_key + assert_equal "fk_name", fk.name + end + + def test_add_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_match(/^fk_rails_.{10}$/, fk.name) + end + + def test_add_foreign_key_with_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_match(/^fk_rails_.{10}$/, fk.name) + end + + def test_add_foreign_key_with_non_standard_primary_key + with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do + @connection.add_foreign_key(:astronauts, :space_shuttles, + column: "rocket_id", primary_key: "pk", name: "custom_pk") + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "space_shuttles", fk.to_table + assert_equal "pk", fk.primary_key + + @connection.remove_foreign_key :astronauts, name: "custom_pk" + end + end + + def test_add_on_delete_restrict_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + # ON DELETE RESTRICT is the default on MySQL + assert_equal nil, fk.on_delete + else + assert_equal :restrict, fk.on_delete + end + end + + def test_add_on_delete_cascade_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :cascade, fk.on_delete + end + + def test_add_on_delete_nullify_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.on_delete + end + + def test_on_update_and_on_delete_raises_with_invalid_values + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid + end + + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid + end + end + + def test_add_foreign_key_with_on_update + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.on_update + end + + def test_remove_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, :rockets + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, column: "rocket_id" + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, name: "fancy_named_fk" + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_non_existing_foreign_key_raises + assert_raises ArgumentError do + @connection.remove_foreign_key :astronauts, :rockets + end + end + + def test_schema_dumping + @connection.add_foreign_key :astronauts, :rockets + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output + end + + def test_schema_dumping_with_options + output = dump_table_schema "fk_test_has_fk" + assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output + end + + def test_schema_dumping_on_delete_and_on_update_options + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade + + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output + end + + class CreateCitiesAndHousesMigration < ActiveRecord::Migration + def change + create_table("cities") { |t| } + + create_table("houses") do |t| + t.column :city_id, :integer + end + add_foreign_key :houses, :cities, column: "city_id" + end + end + + def test_add_foreign_key_is_reversible + migration = CreateCitiesAndHousesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("houses").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + end + end + end +end +else +module ActiveRecord + class Migration + class NoForeignKeySupportTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + def test_add_foreign_key_should_be_noop + @connection.add_foreign_key :clubs, :categories + end + + def test_remove_foreign_key_should_be_noop + @connection.remove_foreign_key :clubs, :categories + end + + def test_foreign_keys_should_raise_not_implemented + assert_raises NotImplementedError do + @connection.foreign_keys("clubs") + end + end + end + end +end +end diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb index 84224e6e4c..319d3e1af3 100644 --- a/activerecord/test/cases/migration/logger_test.rb +++ b/activerecord/test/cases/migration/logger_test.rb @@ -3,7 +3,7 @@ require "cases/helper" module ActiveRecord class Migration class LoggerTest < ActiveRecord::TestCase - # mysql can't roll back ddl changes + # MySQL can't roll back ddl changes self.use_transactional_fixtures = false Migration = Struct.new(:name, :version) do diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb new file mode 100644 index 0000000000..517ee695ce --- /dev/null +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -0,0 +1,53 @@ +require 'cases/helper' +require "minitest/mock" + +module ActiveRecord + class Migration + class PendingMigrationsTest < ActiveRecord::TestCase + def setup + super + @connection = MiniTest::Mock.new + @app = MiniTest::Mock.new + conn = @connection + @pending = Class.new(CheckPending) { + define_method(:connection) { conn } + }.new(@app) + @pending.instance_variable_set :@last_check, -1 # Force checking + end + + def teardown + assert @connection.verify + assert @app.verify + super + end + + def test_errors_if_pending + @connection.expect :supports_migrations?, true + + ActiveRecord::Migrator.stub :needs_migration?, true do + assert_raise ActiveRecord::PendingMigrationError do + @pending.call(nil) + end + end + end + + def test_checks_if_supported + @connection.expect :supports_migrations?, true + @app.expect :call, nil, [:foo] + + ActiveRecord::Migrator.stub :needs_migration?, false do + @pending.call(:foo) + end + end + + def test_doesnt_check_if_unsupported + @connection.expect :supports_migrations?, false + @app.expect :call, nil, [:foo] + + ActiveRecord::Migrator.stub :needs_migration?, true do + @pending.call(:foo) + end + end + end + end +end diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index a52b58c4ac..ba39fb1dec 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -76,6 +76,16 @@ module ActiveRecord assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq end + + def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences + enable_uuid_ossp!(connection) + connection.create_table :cats, id: :uuid + assert_nothing_raised { rename_table :cats, :felines } + assert connection.table_exists? :felines + ensure + connection.drop_table :cats if connection.table_exists? :cats + connection.drop_table :felines if connection.table_exists? :felines + end end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 9855835e27..6b840e16bb 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -567,7 +567,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert_equal 8, columns.size [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type } - assert_equal 0, column(:age).default + assert_equal '0', column(:age).default end def test_removing_columns diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 7f5d0d66d3..2170fe6118 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -20,7 +20,7 @@ require 'models/toy' require 'rexml/document' class PersistenceTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :author_addresses, :categorizations, :categories, :posts, :minivans, :pets, :toys # Oracle UPDATE does not support ORDER BY unless current_adapter?(:OracleAdapter) @@ -250,11 +250,9 @@ class PersistenceTest < ActiveRecord::TestCase end def test_create_columns_not_equal_attributes - topic = Topic.allocate.init_with( - 'raw_attributes' => { - 'title' => 'Another New Topic', - 'does_not_exist' => 'test' - } + topic = Topic.instantiate( + 'title' => 'Another New Topic', + 'does_not_exist' => 'test' ) assert_nothing_raised { topic.save } end @@ -300,10 +298,7 @@ class PersistenceTest < ActiveRecord::TestCase topic.title = "Still another topic" topic.save - topic_reloaded = Topic.allocate - topic_reloaded.init_with( - 'raw_attributes' => topic.attributes.merge('does_not_exist' => 'test') - ) + topic_reloaded = Topic.instantiate(topic.attributes.merge('does_not_exist' => 'test')) topic_reloaded.title = 'A New Topic' assert_nothing_raised { topic_reloaded.save } end @@ -514,14 +509,14 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_column_should_not_leave_the_object_dirty topic = Topic.find(1) - topic.update_column("content", "Have a nice day") + topic.update_column("content", "--- Have a nice day\n...\n") topic.reload - topic.update_column(:content, "You too") + topic.update_column(:content, "--- You too\n...\n") assert_equal [], topic.changed topic.reload - topic.update_column("content", "Have a nice day") + topic.update_column("content", "--- Have a nice day\n...\n") assert_equal [], topic.changed end @@ -605,14 +600,14 @@ class PersistenceTest < ActiveRecord::TestCase def test_update_columns_should_not_leave_the_object_dirty topic = Topic.find(1) - topic.update({ "content" => "Have a nice day", :author_name => "Jose" }) + topic.update({ "content" => "--- Have a nice day\n...\n", :author_name => "Jose" }) topic.reload - topic.update_columns({ content: "You too", "author_name" => "Sebastian" }) + topic.update_columns({ content: "--- You too\n...\n", "author_name" => "Sebastian" }) assert_equal [], topic.changed topic.reload - topic.update_columns({ content: "Have a nice day", author_name: "Jose" }) + topic.update_columns({ content: "--- Have a nice day\n...\n", author_name: "Jose" }) assert_equal [], topic.changed end @@ -863,4 +858,23 @@ class PersistenceTest < ActiveRecord::TestCase post.body end end + + def test_reload_removes_custom_selects + post = Post.select('posts.*, 1 as wibble').last! + + assert_equal 1, post[:wibble] + assert_nil post.reload[:wibble] + end + + def test_find_via_reload + post = Post.new + + assert post.new_record? + + post.id = 1 + post.reload + + assert_equal "Welcome to the weblog", post.title + assert_not post.new_record? + end end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index f43483d291..b04df7ce43 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -134,14 +134,22 @@ class PrimaryKeysTest < ActiveRecord::TestCase end def test_primary_key_returns_value_if_it_exists + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers' + end + if ActiveRecord::Base.connection.supports_primary_key? - assert_equal 'id', ActiveRecord::Base.connection.primary_key('developers') + assert_equal 'id', klass.primary_key end end def test_primary_key_returns_nil_if_it_does_not_exist + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers_projects' + end + if ActiveRecord::Base.connection.supports_primary_key? - assert_nil ActiveRecord::Base.connection.primary_key('developers_projects') + assert_nil klass.primary_key end end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index bbd5298da1..1d6ae2f67f 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -83,69 +83,56 @@ module ActiveRecord def test_quote_with_quoted_id assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil) - assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), 'foo') end def test_quote_nil assert_equal 'NULL', @quoter.quote(nil, nil) - assert_equal 'NULL', @quoter.quote(nil, 'foo') end def test_quote_true assert_equal @quoter.quoted_true, @quoter.quote(true, nil) - assert_equal '1', @quoter.quote(true, Type::Integer.new) end def test_quote_false assert_equal @quoter.quoted_false, @quoter.quote(false, nil) - assert_equal '0', @quoter.quote(false, Type::Integer.new) end def test_quote_float float = 1.2 assert_equal float.to_s, @quoter.quote(float, nil) - assert_equal float.to_s, @quoter.quote(float, Object.new) end def test_quote_fixnum fixnum = 1 assert_equal fixnum.to_s, @quoter.quote(fixnum, nil) - assert_equal fixnum.to_s, @quoter.quote(fixnum, Object.new) end def test_quote_bignum bignum = 1 << 100 assert_equal bignum.to_s, @quoter.quote(bignum, nil) - assert_equal bignum.to_s, @quoter.quote(bignum, Object.new) end def test_quote_bigdecimal bigdec = BigDecimal.new((1 << 100).to_s) assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, nil) - assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, Object.new) end def test_dates_and_times @quoter.extend(Module.new { def quoted_date(value) 'lol' end }) assert_equal "'lol'", @quoter.quote(Date.today, nil) - assert_equal "'lol'", @quoter.quote(Date.today, Object.new) assert_equal "'lol'", @quoter.quote(Time.now, nil) - assert_equal "'lol'", @quoter.quote(Time.now, Object.new) assert_equal "'lol'", @quoter.quote(DateTime.now, nil) - assert_equal "'lol'", @quoter.quote(DateTime.now, Object.new) end def test_crazy_object crazy = Class.new.new expected = "'#{YAML.dump(crazy)}'" assert_equal expected, @quoter.quote(crazy, nil) - assert_equal expected, @quoter.quote(crazy, Object.new) end def test_crazy_object_calls_quote_string crazy = Class.new { def initialize; @lol = 'lo\l' end }.new assert_match "lo\\\\l", @quoter.quote(crazy, nil) - assert_match "lo\\\\l", @quoter.quote(crazy, Object.new) end def test_quote_string_no_column @@ -157,25 +144,6 @@ module ActiveRecord assert_equal "'lo\\\\l'", @quoter.quote(string, nil) end - def test_quote_string_int_column - assert_equal "1", @quoter.quote('1', Type::Integer.new) - assert_equal "1", @quoter.quote('1.2', Type::Integer.new) - end - - def test_quote_string_float_column - assert_equal "1.0", @quoter.quote('1', Type::Float.new) - assert_equal "1.2", @quoter.quote('1.2', Type::Float.new) - end - - def test_quote_as_mb_chars_binary_column - string = ActiveSupport::Multibyte::Chars.new('lo\l') - assert_equal "'lo\\\\l'", @quoter.quote(string, Type::Binary.new) - end - - def test_quote_binary_without_string_to_binary - assert_equal "'lo\\\\l'", @quoter.quote('lo\l', Type::Binary.new) - end - def test_string_with_crazy_column assert_equal "'lo\\\\l'", @quoter.quote('lo\l') end @@ -183,10 +151,6 @@ module ActiveRecord def test_quote_duration assert_equal "1800", @quoter.quote(30.minutes) end - - def test_quote_duration_int_column - assert_equal "7200", @quoter.quote(2.hours, Type::Integer.new) - end end end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index c6e73f78cc..84abaf0291 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -80,28 +80,14 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal :integer, @first.column_for_attribute("id").type end - def test_non_existent_columns_return_null_object - column = @first.column_for_attribute("attribute_that_doesnt_exist") - assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column - assert_equal "attribute_that_doesnt_exist", column.name - assert_equal nil, column.sql_type - assert_equal nil, column.type - assert_not column.number? - assert_not column.text? - assert_not column.binary? - end - - def test_non_existent_columns_are_identity_types - column = @first.column_for_attribute("attribute_that_doesnt_exist") - object = Object.new - - assert_equal object, column.type_cast_from_database(object) - assert_equal object, column.type_cast_from_user(object) - assert_equal object, column.type_cast_for_database(object) + def test_non_existent_columns_return_nil + assert_deprecated do + assert_nil @first.column_for_attribute("attribute_that_doesnt_exist") + end end def test_reflection_klass_for_nested_class_name - reflection = MacroReflection.new(:company, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) + reflection = ActiveRecord::Reflection.create(:has_many, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) assert_nothing_raised do assert_equal MyApplication::Business::Company, reflection.klass end @@ -111,21 +97,21 @@ class ReflectionTest < ActiveRecord::TestCase ActiveSupport::Inflector.inflections do |inflect| inflect.irregular 'plural_irregular', 'plurales_irregulares' end - reflection = AssociationReflection.new(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base) + reflection = ActiveRecord::Reflection.create(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base) assert_equal 'PluralIrregular', reflection.class_name end def test_aggregation_reflection reflection_for_address = AggregateReflection.new( - :composed_of, :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer + :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer ) reflection_for_balance = AggregateReflection.new( - :composed_of, :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer + :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer ) reflection_for_gps_location = AggregateReflection.new( - :composed_of, :gps_location, nil, { }, Customer + :gps_location, nil, { }, Customer ) assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location) @@ -149,7 +135,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_many_reflection - reflection_for_clients = AssociationReflection.new(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm) + reflection_for_clients = ActiveRecord::Reflection.create(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm) assert_equal reflection_for_clients, Firm.reflect_on_association(:clients) @@ -161,7 +147,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_one_reflection - reflection_for_account = AssociationReflection.new(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) + reflection_for_account = ActiveRecord::Reflection.create(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) assert_equal reflection_for_account, Firm.reflect_on_association(:account) assert_equal Account, Firm.reflect_on_association(:account).klass @@ -298,12 +284,12 @@ class ReflectionTest < ActiveRecord::TestCase end def test_association_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author) + reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } through = Class.new(ActiveRecord::Reflection::ThroughReflection) { define_method(:source_reflection) { reflection } - }.new(:fuu, :edge, nil, {}, Author) + }.new(reflection) assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key } end @@ -313,7 +299,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_active_record_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, nil, {}, Edge) + reflection = ActiveRecord::Reflection.create(:has_many, :author, nil, {}, Edge) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key } end @@ -331,32 +317,28 @@ class ReflectionTest < ActiveRecord::TestCase end def test_default_association_validation - assert AssociationReflection.new(:has_many, :clients, nil, {}, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate? - assert !AssociationReflection.new(:has_one, :client, nil, {}, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, nil, {}, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, {}, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm).validate? + assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm).validate? end def test_always_validate_association_if_explicit - assert AssociationReflection.new(:has_one, :client, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :validate => true }, Firm).validate? end def test_validate_association_if_autosave - assert AssociationReflection.new(:has_one, :client, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true }, Firm).validate? end def test_never_validate_association_if_explicit - assert !AssociationReflection.new(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? end def test_foreign_key @@ -378,11 +360,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) reflection.stubs(:klass).returns(category) assert_equal 'categories_products', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) reflection.stubs(:klass).returns(product) assert_equal 'categories_products', reflection.join_table end @@ -391,11 +373,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) reflection.stubs(:klass).returns(category) assert_equal 'catalog_categories_products', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) reflection.stubs(:klass).returns(product) assert_equal 'catalog_categories_products', reflection.join_table end @@ -404,11 +386,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, page) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page) reflection.stubs(:klass).returns(category) assert_equal 'catalog_categories_content_pages', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :pages, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category) reflection.stubs(:klass).returns(page) assert_equal 'catalog_categories_content_pages', reflection.join_table end @@ -417,15 +399,47 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, { :join_table => 'product_categories' }, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product) reflection.stubs(:klass).returns(category) assert_equal 'product_categories', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, { :join_table => 'product_categories' }, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category) reflection.stubs(:klass).returns(product) assert_equal 'product_categories', reflection.join_table end + def test_includes_accepts_symbols + hotel = Hotel.create! + department = hotel.departments.create! + department.chefs.create! + + assert_nothing_raised do + assert_equal department.chefs, Hotel.includes([departments: :chefs]).first.chefs + end + end + + def test_includes_accepts_strings + hotel = Hotel.create! + department = hotel.departments.create! + department.chefs.create! + + assert_nothing_raised do + assert_equal department.chefs, Hotel.includes(['departments' => 'chefs']).first.chefs + end + end + + def test_reflect_on_association_accepts_symbols + assert_nothing_raised do + assert_equal Hotel.reflect_on_association(:departments).name, :departments + end + end + + def test_reflect_on_association_accepts_strings + assert_nothing_raised do + assert_equal Hotel.reflect_on_association("departments").name, :departments + end + end + private def assert_reflection(klass, association, options) assert reflection = klass.reflect_on_association(association) diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 937f226b1d..a6a36a6fd9 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -6,10 +6,11 @@ require 'models/post' require 'models/comment' require 'models/edge' require 'models/topic' +require 'models/binary' module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors + fixtures :posts, :edges, :authors, :binaries def test_where_copies_bind_params author = authors(:david) @@ -179,5 +180,35 @@ module ActiveRecord assert_equal 4, Edge.where(blank).order("sink_id").to_a.size end end + + def test_where_with_integer_for_string_column + count = Post.where(:title => 0).count + assert_equal 0, count + end + + def test_where_with_float_for_string_column + count = Post.where(:title => 0.0).count + assert_equal 0, count + end + + def test_where_with_boolean_for_string_column + count = Post.where(:title => false).count + assert_equal 0, count + end + + def test_where_with_decimal_for_string_column + count = Post.where(:title => BigDecimal.new(0)).count + assert_equal 0, count + end + + def test_where_with_duration_for_string_column + count = Post.where(:title => 0.seconds).count + assert_equal 0, count + end + + def test_where_with_integer_for_binary_column + count = Binary.where(:data => 0).count + assert_equal 0, count + end end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index fb0b906c07..3280945d09 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -235,5 +235,33 @@ module ActiveRecord posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length end + + class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value + def type + :string + end + + def type_cast_from_database(value) + raise value unless value == "type cast for database" + "type cast from database" + end + + def type_cast_for_database(value) + raise value unless value == "value from user" + "type cast for database" + end + end + + class UpdateAllTestModel < ActiveRecord::Base + self.table_name = 'posts' + + attribute :body, EnsureRoundTripTypeCasting.new + end + + def test_update_all_goes_through_normal_type_casting + UpdateAllTestModel.update_all(body: "value from user", type: nil) # No STI + + assert_equal "type cast from database", UpdateAllTestModel.first.body + end end end diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index 2131b32a0c..d6decafad9 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -10,7 +10,7 @@ module ActiveRecord ]) end - def test_to_hash_returns_row_hashes + test "to_hash returns row_hashes" do assert_equal [ {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'}, {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'}, @@ -18,13 +18,13 @@ module ActiveRecord ], result.to_hash end - def test_each_with_block_returns_row_hashes + test "each with block returns row hashes" do result.each do |row| assert_equal ['col_1', 'col_2'], row.keys end end - def test_each_without_block_returns_an_enumerator + test "each without block returns an enumerator" do result.each.with_index do |row, index| assert_equal ['col_1', 'col_2'], row.keys assert_kind_of Integer, index @@ -32,9 +32,45 @@ module ActiveRecord end if Enumerator.method_defined? :size - def test_each_without_block_returns_a_sized_enumerator + test "each without block returns a sized enumerator" do assert_equal 3, result.each.size end end + + test "cast_values returns rows after type casting" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new, "col2" => Type::Float.new } + result = Result.new(columns, values, types) + + assert_equal [[1, 2.2], [3, 4.4]], result.cast_values + end + + test "cast_values uses identity type for unknown types" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new } + result = Result.new(columns, values, types) + + assert_equal [[1, "2.2"], [3, "4.4"]], result.cast_values + end + + test "cast_values returns single dimensional array if single column" do + values = [["1.1"], ["3.3"]] + columns = ["col1"] + types = { "col1" => Type::Integer.new } + result = Result.new(columns, values, types) + + assert_equal [1, 3], result.cast_values + end + + test "cast_values can receive types to use instead" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new, "col2" => Type::Float.new } + result = Result.new(columns, values, types) + + assert_equal [[1.1, 2.2], [3.3, 4.4]], result.cast_values("col1" => Type::Float.new) + end end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index e2a18d138d..4e71d04bc0 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -372,15 +372,28 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{create_table "subscribers", id: false}, output end + if ActiveRecord::Base.connection.supports_foreign_keys? + def test_foreign_keys_are_dumped_at_the_bottom_to_circumvent_dependency_issues + output = standard_dump + assert_match(/^\s+add_foreign_key "fk_test_has_fk"[^\n]+\n\s+add_foreign_key "lessons_students"/, output) + end + end + class CreateDogMigration < ActiveRecord::Migration def up + create_table("dog_owners") do |t| + end + create_table("dogs") do |t| t.column :name, :string + t.column :owner_id, :integer end add_index "dogs", [:name] + add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys? end def down drop_table("dogs") + drop_table("dog_owners") end end @@ -394,15 +407,19 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump assert_no_match %r{create_table "foo_.+_bar"}, output - assert_no_match %r{create_index "foo_.+_bar"}, output + assert_no_match %r{add_index "foo_.+_bar"}, output assert_no_match %r{create_table "schema_migrations"}, output + + if ActiveRecord::Base.connection.supports_foreign_keys? + assert_no_match %r{add_foreign_key "foo_.+_bar"}, output + assert_no_match %r{add_foreign_key "[^"]+", "foo_.+_bar"}, output + end ensure migration.migrate(:down) ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' $stdout = original end - end class SchemaDumperDefaultsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 7dd1f10ce9..3f52e80e11 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -69,6 +69,14 @@ class SerializationTest < ActiveRecord::TestCase ActiveRecord::Base.include_root_in_json = original_root_in_json end + def test_read_attribute_for_serialization_with_format_without_method_missing + klazz = Class.new(ActiveRecord::Base) + klazz.table_name = 'books' + + book = klazz.new + assert_nil book.read_attribute_for_serialization(:format) + end + def test_read_attribute_for_serialization_with_format_after_init klazz = Class.new(ActiveRecord::Base) klazz.table_name = 'books' diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 7d1c240638..186a1a2ade 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -22,7 +22,9 @@ class SerializedAttributeTest < ActiveRecord::TestCase end def test_list_of_serialized_attributes - assert_equal %w(content), Topic.serialized_attributes.keys + assert_deprecated do + assert_equal %w(content), Topic.serialized_attributes.keys + end end def test_serialized_attribute @@ -36,12 +38,6 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal(myobj, topic.content) end - def test_serialized_attribute_init_with - topic = Topic.allocate - topic.init_with('raw_attributes' => { 'content' => '--- foo' }) - assert_equal 'foo', topic.content - end - def test_serialized_attribute_in_base_class Topic.serialize("content", Hash) @@ -213,7 +209,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase t = Topic.create(content: "first") assert_equal("first", t.content) - t.update_column(:content, Topic.serialized_attributes["content"].dump("second")) + t.update_column(:content, Topic.type_for_attribute('content').type_cast_for_database("second")) assert_equal("second", t.content) end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index f841b1c983..e9cdf94c99 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -189,7 +189,6 @@ class StoreTest < ActiveRecord::TestCase assert_equal @john, loaded second_dump = YAML.dump(loaded) - assert_equal dumped, second_dump assert_equal @john, YAML.load(second_dump) end end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index bf9e14fa4f..0f48c8d5fc 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -205,7 +205,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop_all end - def test_creates_configurations_with_local_ip + def test_drops_configurations_with_local_ip @configurations[:development].merge!('host' => '127.0.0.1') ActiveRecord::Tasks::DatabaseTasks.expects(:drop) @@ -213,7 +213,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop_all end - def test_creates_configurations_with_local_host + def test_drops_configurations_with_local_host @configurations[:development].merge!('host' => 'localhost') ActiveRecord::Tasks::DatabaseTasks.expects(:drop) @@ -221,7 +221,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop_all end - def test_creates_configurations_with_blank_hosts + def test_drops_configurations_with_blank_hosts @configurations[:development].merge!('host' => nil) ActiveRecord::Tasks::DatabaseTasks.expects(:drop) @@ -241,7 +241,7 @@ module ActiveRecord ActiveRecord::Base.stubs(:configurations).returns(@configurations) end - def test_creates_current_environment_database + def test_drops_current_environment_database ActiveRecord::Tasks::DatabaseTasks.expects(:drop). with('database' => 'prod-db') @@ -285,6 +285,34 @@ module ActiveRecord end end + class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase + def test_purges_current_environment_database + configurations = { + 'development' => {'database' => 'dev-db'}, + 'test' => {'database' => 'test-db'}, + 'production' => {'database' => 'prod-db'} + } + ActiveRecord::Base.stubs(:configurations).returns(configurations) + + ActiveRecord::Tasks::DatabaseTasks.expects(:purge). + with('database' => 'prod-db') + + ActiveRecord::Tasks::DatabaseTasks.purge_current('production') + end + end + + class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase + def test_purge_all_local_configurations + configurations = {:development => {'database' => 'my-db'}} + ActiveRecord::Base.stubs(:configurations).returns(configurations) + + ActiveRecord::Tasks::DatabaseTasks.expects(:purge). + with('database' => 'my-db') + + ActiveRecord::Tasks::DatabaseTasks.purge_all + end + end + class DatabaseTasksCharsetTest < ActiveRecord::TestCase include DatabaseTasksSetupper diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb new file mode 100644 index 0000000000..420177ed49 --- /dev/null +++ b/activerecord/test/cases/type/string_test.rb @@ -0,0 +1,36 @@ +require 'cases/helper' + +module ActiveRecord + class StringTypeTest < ActiveRecord::TestCase + test "type casting" do + type = Type::String.new + assert_equal "1", type.type_cast_from_user(true) + assert_equal "0", type.type_cast_from_user(false) + assert_equal "123", type.type_cast_from_user(123) + end + + test "values are duped coming out" do + s = "foo" + type = Type::String.new + assert_not_same s, type.type_cast_from_user(s) + assert_not_same s, type.type_cast_from_database(s) + end + + test "string mutations are detected" do + klass = Class.new(Base) + klass.table_name = 'authors' + + author = klass.create!(name: 'Sean') + assert_not author.changed? + + author.name << ' Griffin' + assert author.name_changed? + + author.save! + author.reload + + assert_equal 'Sean Griffin', author.name + assert_not author.changed? + end + end +end diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb index 731f8cfba3..47cf775cb6 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -35,13 +35,6 @@ module ActiveRecord assert_equal false, type.type_cast_from_user('SOMETHING RANDOM') end - def test_type_cast_string - type = Type::String.new - assert_equal "1", type.type_cast_from_user(true) - assert_equal "0", type.type_cast_from_user(false) - assert_equal "123", type.type_cast_from_user(123) - end - def test_type_cast_integer type = Type::Integer.new assert_equal 1, type.type_cast_from_user(1) @@ -79,11 +72,29 @@ module ActiveRecord assert_nil type.type_cast_from_user(1.0/0.0) end + def test_changing_integers + type = Type::Integer.new + + assert type.changed?(5, 5, '5wibble') + assert_not type.changed?(5, 5, '5') + assert_not type.changed?(5, 5, '5.0') + assert_not type.changed?(nil, nil, nil) + end + def test_type_cast_float type = Type::Float.new assert_equal 1.0, type.type_cast_from_user("1") end + def test_changing_float + type = Type::Float.new + + assert type.changed?(5.0, 5.0, '5wibble') + assert_not type.changed?(5.0, 5.0, '5') + assert_not type.changed?(5.0, 5.0, '5.0') + assert_not type.changed?(nil, nil, nil) + end + def test_type_cast_decimal type = Type::Decimal.new assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0")) diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index a6e1dc72e5..55804f9576 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -78,6 +78,20 @@ class ValidationsTest < ActiveRecord::TestCase assert_equal r, invalid.record end + def test_validate_with_bang + assert_raise(ActiveRecord::RecordInvalid) do + WrongReply.new.validate! + end + end + + def test_validate_with_bang_and_context + assert_raise(ActiveRecord::RecordInvalid) do + WrongReply.new.validate!(:special_case) + end + r = WrongReply.new(:title => "Valid title", :author_name => "secret", :content => "Good") + assert r.validate!(:special_case) + end + def test_exception_on_create_bang_many assert_raise(ActiveRecord::RecordInvalid) do WrongReply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }]) diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index d4f8ef5b4d..9f1d110ddb 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -1,8 +1,10 @@ require 'cases/helper' require 'models/topic' +require 'models/post' +require 'models/author' class YamlSerializationTest < ActiveRecord::TestCase - fixtures :topics + fixtures :topics, :authors, :posts def test_to_yaml_with_time_with_zone_should_not_raise_exception with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do @@ -69,4 +71,15 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_not topic.new_record?, "Loaded records without ID are not new" assert_not YAML.load(YAML.dump(topic)).new_record?, "Record should not be new after deserialization" end + + def test_types_of_virtual_columns_are_not_changed_on_round_trip + author = Author.select('authors.*, count(posts.id) as posts_count') + .joins(:posts) + .group('authors.id') + .first + dumped = YAML.load(YAML.dump(author)) + + assert_equal 5, author.posts_count + assert_equal 5, dumped.posts_count + end end diff --git a/activerecord/test/fixtures/fk_test_has_pk.yml b/activerecord/test/fixtures/fk_test_has_pk.yml index c93952180b..73882bac41 100644 --- a/activerecord/test/fixtures/fk_test_has_pk.yml +++ b/activerecord/test/fixtures/fk_test_has_pk.yml @@ -1,2 +1,2 @@ first: - id: 1
\ No newline at end of file + pk_id: 1
\ No newline at end of file diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml index 7298096025..86d46f753a 100644 --- a/activerecord/test/fixtures/posts.yml +++ b/activerecord/test/fixtures/posts.yml @@ -4,7 +4,6 @@ welcome: title: Welcome to the weblog body: Such a lovely day comments_count: 2 - taggings_count: 1 tags_count: 1 type: Post @@ -14,7 +13,6 @@ thinking: title: So I was thinking body: Like I hopefully always am comments_count: 1 - taggings_count: 1 tags_count: 1 type: SpecialPost diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml index bf049abbf1..4c98b10380 100644 --- a/activerecord/test/fixtures/topics.yml +++ b/activerecord/test/fixtures/topics.yml @@ -6,7 +6,7 @@ first: written_on: 2003-07-16t15:28:11.2233+01:00 last_read: 2004-04-15 bonus_time: 2005-01-30t15:28:00.00+01:00 - content: Have a nice day + content: "--- Have a nice day\n...\n" approved: false replies_count: 1 @@ -15,7 +15,7 @@ second: title: The Second Topic of the day author_name: Mary written_on: 2004-07-15t15:28:00.0099+01:00 - content: Have a nice day + content: "--- Have a nice day\n...\n" approved: true replies_count: 0 parent_id: 1 @@ -26,7 +26,7 @@ third: title: The Third Topic of the day author_name: Carl written_on: 2012-08-12t20:24:22.129346+00:00 - content: I'm a troll + content: "--- I'm a troll\n...\n" approved: true replies_count: 1 @@ -35,7 +35,7 @@ fourth: title: The Fourth Topic of the day author_name: Carl written_on: 2006-07-15t15:28:00.0099+01:00 - content: Why not? + content: "--- Why not?\n...\n" approved: true type: Reply parent_id: 3 @@ -45,5 +45,5 @@ fifth: title: The Fifth Topic of the day author_name: Jason written_on: 2013-07-13t12:11:00.0099+01:00 - content: Omakase + content: "--- Omakase\n...\n" approved: true diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index a762ad4bb5..6ceafe5858 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -14,3 +14,10 @@ class Club < ActiveRecord::Base "I'm sorry sir, this is a *private* club, not a *pirate* club" end end + +class SuperClub < ActiveRecord::Base + self.table_name = "clubs" + + has_many :memberships, class_name: 'SuperMembership', foreign_key: 'club_id' + has_many :members, through: :memberships +end diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index a1cb8d62b6..3ea17c3abf 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -8,6 +8,7 @@ module ContactFakeColumns table_name => 'id' } + column :id, :integer column :name, :string column :age, :integer column :avatar, :binary diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index edb75d333f..91e46f83e5 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -1,6 +1,8 @@ class Face < ActiveRecord::Base belongs_to :man, :inverse_of => :face belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face + # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly` + belongs_to :poly_man_without_inverse, :polymorphic => true # These is a "broken" inverse_of for the purposes of testing belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb index f4d127730c..4fbb6b226b 100644 --- a/activerecord/test/models/man.rb +++ b/activerecord/test/models/man.rb @@ -1,6 +1,7 @@ class Man < ActiveRecord::Base has_one :face, :inverse_of => :man has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man + has_one :polymorphic_face_without_inverse, :class_name => 'Face', :as => :poly_man_without_inverse has_many :interests, :inverse_of => :man has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man # These are "broken" inverse_of associations for the purposes of testing diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 5f01ab0a82..a29858213b 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -88,7 +88,7 @@ class Post < ActiveRecord::Base has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' - has_many :taggings, :as => :taggable + has_many :taggings, :as => :taggable, :counter_cache => :tags_count has_many :tags, :through => :taggings do def add_joins_and_select select('tags.*, authors.id as author_id') diff --git a/activerecord/test/models/publisher/article.rb b/activerecord/test/models/publisher/article.rb index 03a277bbdd..d73a8eb936 100644 --- a/activerecord/test/models/publisher/article.rb +++ b/activerecord/test/models/publisher/article.rb @@ -1,3 +1,4 @@ class Publisher::Article < ActiveRecord::Base has_and_belongs_to_many :magazines + has_and_belongs_to_many :tags end diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index f91f2ad2e9..a6c05da26a 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -8,6 +8,6 @@ class Tagging < ActiveRecord::Base belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id' belongs_to :blue_tag, -> { where :tags => { :name => 'Blue' } }, :class_name => 'Tag', :foreign_key => :tag_id belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key - belongs_to :taggable, :polymorphic => true, :counter_cache => true + belongs_to :taggable, :polymorphic => true, :counter_cache => :tags_count has_many :things, :through => :taggable end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 98fd0b6803..146190fc19 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -62,6 +62,11 @@ ActiveRecord::Schema.define do t.references :magazine end + create_table :articles_tags, force: true do |t| + t.references :article + t.references :tag + end + create_table :audit_logs, force: true do |t| t.column :message, :string, null: false t.column :developer_id, :integer, null: false @@ -79,6 +84,8 @@ ActiveRecord::Schema.define do create_table :author_addresses, force: true do |t| end + add_foreign_key :authors, :author_addresses + create_table :author_favorites, force: true do |t| t.column :author_id, :integer t.column :favorite_author_id, :integer @@ -185,7 +192,7 @@ ActiveRecord::Schema.define do t.text :body, null: false end t.string :type - t.integer :taggings_count, default: 0 + t.integer :tags_count, default: 0 t.integer :children_count, default: 0 t.integer :parent_id t.references :author, polymorphic: true @@ -564,7 +571,6 @@ ActiveRecord::Schema.define do end t.string :type t.integer :comments_count, default: 0 - t.integer :taggings_count, default: 0 t.integer :taggings_with_delete_all_count, default: 0 t.integer :taggings_with_destroy_count, default: 0 t.integer :tags_count, default: 0 @@ -775,6 +781,8 @@ ActiveRecord::Schema.define do t.integer :man_id t.integer :polymorphic_man_id t.string :polymorphic_man_type + t.integer :poly_man_without_inverse_id + t.string :poly_man_without_inverse_type t.integer :horrible_polymorphic_man_id t.string :horrible_polymorphic_man_type end @@ -849,10 +857,10 @@ ActiveRecord::Schema.define do t.integer :fk_id, null: false end - create_table :fk_test_has_pk, force: true do |t| + create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t| end - execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})" + execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'pk_id'})" execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})" end diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index b7aff4f47d..b5552c2755 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -7,7 +7,7 @@ ActiveRecord::Schema.define do execute "DROP TABLE fk_test_has_pk" rescue nil execute <<_SQL CREATE TABLE 'fk_test_has_pk' ( - 'id' INTEGER NOT NULL PRIMARY KEY + 'pk_id' INTEGER NOT NULL PRIMARY KEY ); _SQL @@ -16,7 +16,7 @@ _SQL 'id' INTEGER NOT NULL PRIMARY KEY, 'fk_id' INTEGER NOT NULL, - FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('id') + FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id') ); _SQL -end
\ No newline at end of file +end diff --git a/activerecord/test/support/ddl_helper.rb b/activerecord/test/support/ddl_helper.rb index 0107babaaf..43cb235e01 100644 --- a/activerecord/test/support/ddl_helper.rb +++ b/activerecord/test/support/ddl_helper.rb @@ -1,8 +1,8 @@ module DdlHelper def with_example_table(connection, table_name, definition = nil) - connection.exec_query("CREATE TABLE #{table_name}(#{definition})") + connection.execute("CREATE TABLE #{table_name}(#{definition})") yield ensure - connection.exec_query("DROP TABLE #{table_name}") + connection.execute("DROP TABLE #{table_name}") end end |