diff options
Diffstat (limited to 'activerecord')
124 files changed, 1477 insertions, 502 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 72b770e2d5..ea8c8088a9 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,4 +1,122 @@ -* Fix undesirable RangeError by Type::Integer. Add Type::UnsignedInteger. +* Fix `reaping_frequency` option when the value is a string. + + This usually happens when it is configured using `DATABASE_URL`. + + *korbin* + +* Fix error message when trying to create an associated record and the foreign + key is missing. + + Before this fix the following exception was being raised: + + NoMethodError: undefined method `val' for #<Arel::Nodes::BindParam:0x007fc64d19c218> + + Now the message is: + + ActiveRecord::UnknownAttributeError: unknown attribute 'foreign_key' for Model. + + *Rafael Mendonça França* + +* When a table has a composite primary key, the `primary_key` method for + SQLite3 and PostgreSQL adapters was only returning the first field of the key. + Ensures that it will return nil instead, as Active Record doesn't support + composite primary keys. + + Fixes #18070. + + *arthurnn* + +* `validates_size_of` / `validates_length_of` do not count records, + which are `marked_for_destruction?`. + + Fixes #7247. + + *Yves Senn* + +* Ensure `first!` and friends work on loaded associations. + + Fixes #18237. + + *Sean Griffin* + +* `eager_load` preserves readonly flag for associations. + + Closes #15853. + + *Takashi Kokubun* + +* Provide `:touch` option to `save()` to accommodate saving without updating + timestamps. + + Fixes #18202. + + *Dan Olson* + +* Provide a more helpful error message when an unsupported class is passed to + `serialize`. + + Fixes #18224. + + *Sean Griffin* + +* Add bigint primary key support for MySQL. + + Example: + + create_table :foos, id: :bigint do |t| + end + + *Ryuta Kamizono* + +* Support for any type primary key. + + Fixes #14194. + + *Ryuta Kamizono* + +* Dump the default `nil` for PostgreSQL UUID primary key. + + *Ryuta Kamizono* + +* Add a `:foreign_key` option to `references` and associated migration + methods. The model and migration generators now use this option, rather than + the `add_foreign_key` form. + + *Sean Griffin* + +* Don't raise when writing an attribute with an out-of-range datetime passed + by the user. + + *Grey Baker* + +* Replace deprecated `ActiveRecord::Tasks::DatabaseTasks#load_schema` with + `ActiveRecord::Tasks::DatabaseTasks#load_schema_for`. + + *Yves Senn* + +* Fixes bug with 'ActiveRecord::Type::Numeric' that causes negative values to + be marked as having changed when set to the same negative value. + + Closes #18161. + + *Daniel Fox* + +* Introduce `force: :cascade` option for `create_table`. Using this option + will recreate tables even if they have dependent objects (like foreign keys). + `db/schema.rb` now uses `force: :cascade`. This makes it possible to + reload the schema when foreign keys are in place. + + *Matthew Draper*, *Yves Senn* + +* `db:schema:load` and `db:structure:load` no longer purge the database + before loading the schema. This is left for the user to do. + `db:test:prepare` will still purge the database. + + Closes #17945. + + *Yves Senn* + +* Fix undesirable RangeError by `Type::Integer`. Add `Type::UnsignedInteger`. *Ryuta Kamizono* diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index 2950f05b11..7c2197229d 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2014 David Heinemeier Hansson +Copyright (c) 2004-2015 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc index 569685bd45..7e3460365b 100644 --- a/activerecord/RUNNING_UNIT_TESTS.rdoc +++ b/activerecord/RUNNING_UNIT_TESTS.rdoc @@ -20,11 +20,11 @@ example: Simply executing <tt>bundle exec rake test</tt> is equivalent to the following: - $ bundle exec rake test_mysql - $ bundle exec rake test_mysql2 - $ bundle exec rake test_postgresql - $ bundle exec rake test_sqlite3 - $ bundle exec rake test_sqlite3_mem + $ bundle exec rake test:mysql + $ bundle exec rake test:mysql2 + $ bundle exec rake test:postgresql + $ bundle exec rake test:sqlite3 + $ bundle exec rake test:sqlite3_mem There should be tests available for each database backend listed in the {Config File}[rdoc-label:label-Config+File]. (the exact set of available tests is diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 471769a962..c5cd0c89f7 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Object-relational mapper framework (part of Rails).' s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.' - s.required_ruby_version = '>= 2.1.0' + s.required_ruby_version = '>= 2.2.0' s.license = 'MIT' @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '~> 6.0' + s.add_dependency 'arel', '7.0.0.alpha' end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 9028970a3d..2eec62846b 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2014 David Heinemeier Hansson +# Copyright (c) 2004-2015 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -62,6 +62,7 @@ module ActiveRecord autoload :Serialization autoload :StatementCache autoload :Store + autoload :TableMetadata autoload :Timestamp autoload :Transactions autoload :Translation diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 5a84792f45..f2b44913db 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -1,7 +1,7 @@ module ActiveRecord class AssociationRelation < Relation - def initialize(klass, table, association) - super(klass, table) + def initialize(klass, table, predicate_builder, association) + super(klass, table, predicate_builder) @association = association end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index cd5fdd5964..14af55f327 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -116,6 +116,7 @@ module ActiveRecord autoload :Association, 'active_record/associations/association' autoload :SingularAssociation, 'active_record/associations/singular_association' autoload :CollectionAssociation, 'active_record/associations/collection_association' + autoload :ForeignAssociation, 'active_record/associations/foreign_association' autoload :CollectionProxy, 'active_record/associations/collection_proxy' autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 0c3234ed24..f9c9f8afda 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -56,11 +56,11 @@ module ActiveRecord @connection = connection end - def aliased_table_for(table_name, aliased_name) + def aliased_table_for(table_name, aliased_name, **table_options) if aliases[table_name].zero? # If it's zero, we can have our table_name aliases[table_name] = 1 - Arel::Table.new(table_name) + Arel::Table.new(table_name, table_options) else # Otherwise, we need to use an alias aliased_name = connection.table_alias_for(aliased_name) @@ -73,7 +73,7 @@ module ActiveRecord else aliased_name end - Arel::Table.new(table_name).alias(table_alias) + Arel::Table.new(table_name, table_options).alias(table_alias) end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index f1c36cd047..0d8e4ba870 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -121,7 +121,7 @@ module ActiveRecord # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the # through association's scope) def target_scope - AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all) + AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all) end # Loads the \target if needed and returns it. diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 0ac10531e5..53f65920e1 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -66,7 +66,8 @@ module ActiveRecord chain.map do |reflection| alias_tracker.aliased_table_for( table_name_for(reflection, klass, refl), - table_alias_for(reflection, refl, reflection != refl) + table_alias_for(reflection, refl, reflection != refl), + type_caster: klass.type_caster, ) end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 81fdd681de..c63b42e2a0 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -73,11 +73,11 @@ module ActiveRecord # Checks whether record is different to the current target, without loading it def different_target?(record) - record.id != owner[reflection.foreign_key] + record.id != owner._read_attribute(reflection.foreign_key) end def replace_keys(record) - owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)] + owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class)) end def remove_keys @@ -85,7 +85,7 @@ module ActiveRecord end def foreign_key_present? - owner[reflection.foreign_key] + owner._read_attribute(reflection.foreign_key) end # NOTE - for now, we're only supporting inverse setting from belongs_to back onto @@ -99,12 +99,13 @@ module ActiveRecord if options[:primary_key] owner.send(reflection.name).try(:id) else - owner[reflection.foreign_key] + owner._read_attribute(reflection.foreign_key) end end def stale_state - owner[reflection.foreign_key] && owner[reflection.foreign_key].to_s + result = owner._read_attribute(reflection.foreign_key) + result && result.to_s end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 7b6aefe345..16b1228b8a 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -597,8 +597,8 @@ module ActiveRecord if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) assoc = owner.association(reflection.through_reflection.name) assoc.reader.any? { |source| - target = source.send(reflection.source_reflection.name) - target.respond_to?(:include?) ? target.include?(record) : target == record + target_reflection = source.send(reflection.source_reflection.name) + target_reflection.respond_to?(:include?) ? target_reflection.include?(record) : target_reflection == record } || target.include?(record) else target.include?(record) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 060b2278d9..dc42b19a83 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -29,10 +29,11 @@ module ActiveRecord # instantiation of the actual post records. class CollectionProxy < Relation delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope) + delegate :find_nth, to: :scope def initialize(klass, association) #:nodoc: @association = association - super klass, klass.arel_table + super klass, klass.arel_table, klass.predicate_builder merge! association.scope(nullify: false) end diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb new file mode 100644 index 0000000000..fe48ecec29 --- /dev/null +++ b/activerecord/lib/active_record/associations/foreign_association.rb @@ -0,0 +1,11 @@ +module ActiveRecord::Associations + module ForeignAssociation + def foreign_key_present? + if reflection.klass.primary_key + owner.attribute_present?(reflection.active_record_primary_key) + else + false + end + end + end +end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 93084e0dcf..d7f655d00c 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -6,6 +6,7 @@ module ActiveRecord # If the association has a <tt>:through</tt> option further specialization # is provided by its child HasManyThroughAssociation. class HasManyAssociation < CollectionAssociation #:nodoc: + include ForeignAssociation def handle_dependency case options[:dependent] @@ -153,14 +154,6 @@ module ActiveRecord end end - def foreign_key_present? - if reflection.klass.primary_key - owner.attribute_present?(reflection.association_primary_key) - else - false - end - end - def concat_records(records, *) update_counter_if_success(super, records.length) 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 3f4d3bfc08..7a050ca224 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -13,21 +13,6 @@ module ActiveRecord @through_association = nil end - # Returns the size of the collection by executing a SELECT COUNT(*) query - # if the collection hasn't been loaded, and by calling collection.size if - # it has. If the collection will likely have a size greater than zero, - # and if fetching the collection will be needed afterwards, one less - # SELECT query will be generated by using #length instead. - def size - if has_cached_counter? - owner._read_attribute cached_counter_attribute_name(reflection) - elsif loaded? - target.size - else - super - end - end - def concat(*records) unless owner.new_record? records.flatten.each do |record| diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index e6095d84dc..74b8c53758 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -2,6 +2,7 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < SingularAssociation #:nodoc: + include ForeignAssociation def handle_dependency case options[:dependent] diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index cf63430a97..66e997c3c8 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -94,7 +94,7 @@ module ActiveRecord # def initialize(base, associations, joins) @alias_tracker = AliasTracker.create(base.connection, joins) - @alias_tracker.aliased_table_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1 + @alias_tracker.aliased_table_for(base.table_name, base.table_name, type_caster: base.type_caster) # Updates the count for base.table_name to 1 tree = self.class.make_tree associations @join_root = JoinBase.new base, build(tree, base) @join_root.children.each { |child| construct_tables! @join_root, child } @@ -186,9 +186,13 @@ module ActiveRecord def table_aliases_for(parent, node) node.reflection.chain.map { |reflection| + if reflection.klass + type_caster = reflection.klass.type_caster + end alias_tracker.aliased_table_for( reflection.table_name, - table_alias_for(reflection, parent, reflection != node.reflection) + table_alias_for(reflection, parent, reflection != node.reflection), + type_caster: type_caster, ) } end @@ -257,6 +261,7 @@ module ActiveRecord construct(model, node, row, rs, seen, model_cache, aliases) else model = construct_model(ar_parent, node, row, model_cache, id, aliases) + model.readonly! seen[parent.base_klass][primary_id][node.base_klass][id] = model construct(model, node, row, rs, seen, model_cache, aliases) end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 5dede5527d..c1ef86a95b 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -43,16 +43,23 @@ module ActiveRecord constraint = build_constraint(klass, table, key, foreign_table, foreign_key) + predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) scope_chain_items = scope_chain[scope_chain_index].map do |item| if item.is_a?(Relation) item else - ActiveRecord::Relation.create(klass, table).instance_exec(node, &item) + ActiveRecord::Relation.create(klass, table, predicate_builder) + .instance_exec(node, &item) end end scope_chain_index += 1 - scope_chain_items.concat [klass.send(:build_default_scope, ActiveRecord::Relation.create(klass, table))].compact + relation = ActiveRecord::Relation.create( + klass, + table, + predicate_builder, + ) + scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| left.merge right diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 7d6523dbc4..a6e1a24360 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -33,7 +33,7 @@ module ActiveRecord end def query_scope(ids) - scope.where(association_key.in(ids)) + scope.where(association_key_name => ids) end def table diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 83fcefa64d..b7edac791e 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -150,7 +150,7 @@ module ActiveRecord BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) end - def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc + def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc: if klass.respond_to?(name, true) if superklass.respond_to?(name, true) klass.method(name).owner != superklass.method(name).owner diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 033e71f7b9..d5702accaf 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -76,6 +76,10 @@ module ActiveRecord private + def changes_include?(attr_name) + super || attribute_changed_in_place?(attr_name) + end + def calculate_changes_from_defaults @changed_attributes = nil self.class.column_defaults.each do |attr, orig_value| 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 87274dd4e1..777f7ab4d7 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -12,7 +12,11 @@ module ActiveRecord if value.is_a?(Array) value.map { |v| type_cast_from_user(v) } elsif value.respond_to?(:in_time_zone) - value.in_time_zone || super + begin + value.in_time_zone || super + rescue ArgumentError + nil + end end end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 3288108a6a..aafb990bc1 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -53,9 +53,9 @@ module ActiveRecord # store_listing.price_in_cents # => 10 # # Users may also define their own custom types, as long as they respond to the methods - # defined on the value type. The `type_cast` method on your type object will be called + # defined on the value type. The +type_cast+ method on your type object will be called # with values both from the database, and from your controllers. See - # `ActiveRecord::Attributes::Type::Value` for the expected API. It is recommended that your + # +ActiveRecord::Attributes::Type::Value+ for the expected API. It is recommended that your # type objects inherit from an existing type, or the base value type. # # class MoneyType < ActiveRecord::Type::Integer @@ -122,6 +122,7 @@ module ActiveRecord end def clear_caches_calculated_from_columns + @arel_table = nil @attributes_builder = nil @column_names = nil @column_types = nil diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index f978fbd0a4..bb01231bca 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -22,6 +22,7 @@ require 'active_record/log_subscriber' require 'active_record/explain_subscriber' require 'active_record/relation/delegation' require 'active_record/attributes' +require 'active_record/type_caster' module ActiveRecord #:nodoc: # = Active Record @@ -141,7 +142,7 @@ module ActiveRecord #:nodoc: # # In addition to the basic accessors, query methods are also automatically available on the Active Record object. # Query methods allow you to test whether an attribute value is present. - # For numeric values, present is defined as non-zero. + # Additionally, when dealing with numeric values, a query method will return false if the value is zero. # # For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call # to determine whether the user has a name: diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 523d492a48..497ce8c15c 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -298,7 +298,7 @@ module ActiveRecord private - def create_or_update #:nodoc: + def create_or_update(*) #:nodoc: _run_save_callbacks { super } end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index d3d7396c91..9ea22ed798 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -8,6 +8,7 @@ module ActiveRecord def initialize(object_class = Object) @object_class = object_class + check_arity_of_constructor end def dump(obj) @@ -33,6 +34,16 @@ module ActiveRecord obj end + + private + + def check_arity_of_constructor + begin + load(nil) + rescue ArgumentError + raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." + end + end end end end 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 33165cd8fa..6235745fb2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -363,7 +363,7 @@ module ActiveRecord conn.expire end - release owner + release conn, owner @available.add conn end @@ -376,7 +376,7 @@ module ActiveRecord @connections.delete conn @available.delete conn - release conn.owner + release conn, conn.owner @available.add checkout_new_connection if @available.any_waiting? end @@ -424,10 +424,12 @@ module ActiveRecord end end - def release(owner) + def release(conn, owner) thread_id = owner.object_id - @reserved_connections.delete thread_id + if @reserved_connections[thread_id] == conn + @reserved_connections.delete thread_id + end end def new_connection diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 679878d860..143d7d9574 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -19,7 +19,7 @@ module ActiveRecord # Cast a +value+ to a type that the database understands. For example, # SQLite does not understand dates, so this method will convert a Date # to a String. - def type_cast(value, column) + def type_cast(value, column = nil) if value.respond_to?(:quoted_id) && value.respond_to?(:id) return value.id end 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 5c95b95184..18ff869ea6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -30,7 +30,7 @@ module ActiveRecord def visit_ColumnDefinition(o) sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) column_sql = "#{quote_column_name(o.name)} #{sql_type}" - add_column_options!(column_sql, column_options(o)) unless o.primary_key? + add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql end @@ -65,6 +65,8 @@ module ActiveRecord column_options[:column] = o column_options[:first] = o.first column_options[:after] = o.after + column_options[:auto_increment] = o.auto_increment + column_options[:primary_key] = o.primary_key column_options end @@ -89,14 +91,17 @@ module ActiveRecord if options[:auto_increment] == true sql << " AUTO_INCREMENT" end + if options[:primary_key] == true + sql << " PRIMARY KEY" + end sql end def quote_default_expression(value, column) column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale) - column.cast_type ||= type_for_column(column) + value = type_for_column(column).type_cast_for_database(value) - @conn.quote(value, column) + @conn.quote(value) end def options_include_default?(options) 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 537e21029e..1cf1600d81 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, :cast_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type, :cast_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key @@ -68,6 +68,84 @@ module ActiveRecord end end + class ReferenceDefinition # :nodoc: + def initialize( + name, + polymorphic: false, + index: false, + foreign_key: false, + type: :integer, + **options + ) + @name = name + @polymorphic = polymorphic + @index = index + @foreign_key = foreign_key + @type = type + @options = options + + if polymorphic && foreign_key + raise ArgumentError, "Cannot add a foreign key to a polymorphic relation" + end + end + + def add_to(table) + columns.each do |column_options| + table.column(*column_options) + end + + if index + table.index(column_names, index_options) + end + + if foreign_key + table.foreign_key(foreign_table_name, foreign_key_options) + end + end + + protected + + attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options + + private + + def as_options(value, default = {}) + if value.is_a?(Hash) + value + else + default + end + end + + def polymorphic_options + as_options(polymorphic, options) + end + + def index_options + as_options(index) + end + + def foreign_key_options + as_options(foreign_key) + end + + def columns + result = [["#{name}_id", type, options]] + if polymorphic + result.unshift(["#{name}_type", :string, polymorphic_options]) + end + result + end + + def column_names + columns.map(&:first) + end + + def foreign_table_name + name.to_s.pluralize + end + end + # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # @@ -94,11 +172,12 @@ module ActiveRecord # An array of ColumnDefinition objects, representing the column changes # that have been defined. attr_accessor :indexes - attr_reader :name, :temporary, :options, :as + attr_reader :name, :temporary, :options, :as, :foreign_keys def initialize(types, name, temporary, options, as = nil) @columns_hash = {} @indexes = {} + @foreign_keys = {} @native = types @temporary = temporary @options = options @@ -286,6 +365,10 @@ module ActiveRecord indexes[column_name] = options end + def foreign_key(table_name, options = {}) # :nodoc: + foreign_keys[table_name] = options + end + # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and # <tt>:updated_at</tt> to the table. See SchemaStatements#add_timestamps # @@ -297,24 +380,20 @@ module ActiveRecord column(:updated_at, :datetime, options) end - # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. - # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+ - # by default, the <tt>:type</tt> option can be used to specify a different type. + # Adds a reference. Optionally adds a +type+ column, if the + # +:polymorphic+ option is provided. +references+ and +belongs_to+ + # are acceptable. The reference column will be an +integer+ by default, + # the +:type+ option can be used to specify a different type. A foreign + # key will be created if the +:foreign_key+ option is passed. # # t.references(:user) # t.references(:user, type: "string") # t.belongs_to(:supplier, polymorphic: true) # # See SchemaStatements#add_reference - def references(*args) - options = args.extract_options! - polymorphic = options.delete(:polymorphic) - index_options = options.delete(:index) - type = options.delete(:type) || :integer + def references(*args, **options) args.each do |col| - column("#{col}_id", type, options) - column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic - index(polymorphic ? %w(type id).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options + ReferenceDefinition.new(col, **options).add_to(self) end end alias :belongs_to :references @@ -333,6 +412,7 @@ module ActiveRecord column.null = options[:null] column.first = options[:first] column.after = options[:after] + column.auto_increment = options[:auto_increment] column.primary_key = type == :primary_key || options[:primary_key] column end @@ -422,33 +502,36 @@ module ActiveRecord end # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. # - # ====== Creating a simple column # t.column(:name, :string) + # + # See TableDefinition#column for details of the options you can use. def column(column_name, type, options = {}) @base.add_column(name, column_name, type, options) end - # Checks to see if a column exists. See SchemaStatements#column_exists? + # Checks to see if a column exists. + # + # See SchemaStatements#column_exists? def column_exists?(column_name, type = nil, options = {}) @base.column_exists?(name, column_name, type, options) end # Adds a new index to the table. +column_name+ can be a single Symbol, or - # an Array of Symbols. See SchemaStatements#add_index + # an Array of Symbols. # - # ====== Creating a simple index # t.index(:name) - # ====== Creating a unique index # t.index([:branch_id, :party_id], unique: true) - # ====== Creating a named index # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party') + # + # See SchemaStatements#add_index for details of the options you can use. def index(column_name, options = {}) @base.add_index(name, column_name, options) end - # Checks to see if an index exists. See SchemaStatements#index_exists? + # Checks to see if an index exists. + # + # See SchemaStatements#index_exists? def index_exists?(column_name, options = {}) @base.index_exists?(name, column_name, options) end @@ -456,30 +539,37 @@ module ActiveRecord # Renames the given index on the table. # # t.rename_index(:user_id, :account_id) + # + # See SchemaStatements#rename_index def rename_index(index_name, new_index_name) @base.rename_index(name, index_name, new_index_name) end - # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps + # Adds timestamps (+created_at+ and +updated_at+) columns to the table. # - # t.timestamps null: false + # t.timestamps(null: false) + # + # See SchemaStatements#add_timestamps def timestamps(options = {}) @base.add_timestamps(name, options) end # Changes the column's definition according to the new options. - # See TableDefinition#column for details of the options you can use. # # t.change(:name, :string, limit: 80) # t.change(:description, :text) + # + # See TableDefinition#column for details of the options you can use. def change(column_name, type, options = {}) @base.change_column(name, column_name, type, options) end - # Sets a new default value for a column. See SchemaStatements#change_column_default + # Sets a new default value for a column. # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) + # + # See SchemaStatements#change_column_default def change_default(column_name, default) @base.change_column_default(name, column_name, default) end @@ -488,20 +578,19 @@ module ActiveRecord # # t.remove(:qualification) # t.remove(:qualification, :experience) + # + # See SchemaStatements#remove_columns def remove(*column_names) @base.remove_columns(name, *column_names) end # Removes the given index from the table. # - # ====== Remove the index_table_name_on_column in the table_name table - # t.remove_index :column - # ====== Remove the index named index_table_name_on_branch_id in the table_name table - # t.remove_index column: :branch_id - # ====== Remove the index named index_table_name_on_branch_id_and_party_id in the table_name table - # t.remove_index column: [:branch_id, :party_id] - # ====== Remove the index named by_branch_party in the table_name table - # t.remove_index name: :by_branch_party + # t.remove_index(:branch_id) + # t.remove_index(column: [:branch_id, :party_id]) + # t.remove_index(name: :by_branch_party) + # + # See SchemaStatements#remove_index def remove_index(options = {}) @base.remove_index(name, options) end @@ -509,6 +598,8 @@ module ActiveRecord # Removes the timestamp columns (+created_at+ and +updated_at+) from the table. # # t.remove_timestamps + # + # See SchemaStatements#remove_timestamps def remove_timestamps(options = {}) @base.remove_timestamps(name, options) end @@ -516,17 +607,19 @@ module ActiveRecord # Renames a column. # # t.rename(:description, :name) + # + # See SchemaStatements#rename_column def rename(column_name, new_column_name) @base.rename_column(name, column_name, new_column_name) end - # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. - # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+ - # by default, the <tt>:type</tt> option can be used to specify a different type. + # Adds a reference. Optionally adds a +type+ column, if + # <tt>:polymorphic</tt> option is provided. # # t.references(:user) # t.references(:user, type: "string") # t.belongs_to(:supplier, polymorphic: true) + # t.belongs_to(:supplier, foreign_key: true) # # See SchemaStatements#add_reference def references(*args) @@ -538,7 +631,6 @@ module ActiveRecord alias :belongs_to :references # Removes a reference. Optionally removes a +type+ column. - # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. # # t.remove_references(:user) # t.remove_belongs_to(:supplier, polymorphic: true) @@ -552,10 +644,12 @@ module ActiveRecord end alias :remove_belongs_to :remove_references - # Adds a column or columns of a specified type + # Adds a column or columns of a specified type. # # t.string(:goat) # t.string(:goat, :sheep) + # + # See SchemaStatements#add_column [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| define_method column_type do |*args| options = args.extract_options! @@ -565,6 +659,10 @@ module ActiveRecord end end + def foreign_key(*args) # :nodoc: + @base.add_foreign_key(name, *args) + end + private def native @base.native_database_types 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 fd52cdf716..24afd9c5da 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -132,6 +132,7 @@ module ActiveRecord # Make a temporary table. # [<tt>:force</tt>] # Set to true to drop the table before creating it. + # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. # [<tt>:as</tt>] # SQL to use to generate the table. When this option is used, the block is @@ -203,7 +204,17 @@ module ActiveRecord end result = execute schema_creation.accept td - td.indexes.each_pair { |c, o| add_index(table_name, c, o) } unless supports_indexes_in_create? + + unless supports_indexes_in_create? + td.indexes.each_pair do |column_name, index_options| + add_index(table_name, column_name, index_options) + end + end + + td.foreign_keys.each_pair do |other_table_name, foreign_key_options| + add_foreign_key(table_name, other_table_name, foreign_key_options) + end + result end @@ -361,8 +372,12 @@ module ActiveRecord # Drops a table from the database. # - # Although this command ignores +options+ and the block if one is given, it can be helpful - # to provide these in a migration's +change+ method so it can be reverted. + # [<tt>:force</tt>] + # Set to +:cascade+ to drop dependent objects as well. + # Defaults to false. + # + # Although this command ignores most +options+ and the block if one is given, + # it can be helpful to provide these in a migration's +change+ method so it can be reverted. # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) execute "DROP TABLE #{quote_table_name(table_name)}" @@ -571,9 +586,8 @@ module ActiveRecord # rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name' # def rename_index(table_name, old_name, new_name) - if new_name.length > allowed_index_name_length - raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" - end + validate_index_length!(table_name, new_name) + # this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance) old_index_def = indexes(table_name).detect { |i| i.name == old_name } return unless old_index_def @@ -622,17 +636,16 @@ module ActiveRecord # # add_belongs_to(:products, :supplier, polymorphic: true) # - # ====== Create a supplier_id, supplier_type columns and appropriate index + # ====== Create supplier_id, supplier_type columns and appropriate index # # add_reference(:products, :supplier, polymorphic: true, index: true) # - def add_reference(table_name, ref_name, options = {}) - polymorphic = options.delete(:polymorphic) - index_options = options.delete(:index) - type = options.delete(:type) || :integer - add_column(table_name, "#{ref_name}_id", type, options) - add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic - add_index(table_name, polymorphic ? %w[type id].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options + # ====== Create a supplier_id column and appropriate foreign key + # + # add_reference(:products, :supplier, foreign_key: true) + # + def add_reference(table_name, *args) + ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self)) end alias :add_belongs_to :add_reference @@ -981,6 +994,12 @@ module ActiveRecord "fk_rails_#{SecureRandom.hex(5)}" end end + + def validate_index_length!(table_name, new_name) + if new_name.length > allowed_index_name_length + raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" + 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 c4506885ed..308e4c77bd 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -21,6 +21,7 @@ module ActiveRecord autoload :IndexDefinition autoload :ColumnDefinition autoload :ChangeColumnDefinition + autoload :ForeignKeyDefinition autoload :TableDefinition autoload :Table autoload :AlterTable 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 69582ebb6f..d083e413a5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -6,6 +6,13 @@ module ActiveRecord class AbstractMysqlAdapter < AbstractAdapter include Savepoints + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + def primary_key(name, type = :primary_key, options = {}) + options[:auto_increment] ||= type == :bigint + super + end + end + class SchemaCreation < AbstractAdapter::SchemaCreation def visit_AddColumn(o) add_column_position!(super, column_options(o)) @@ -487,11 +494,13 @@ module ActiveRecord end def drop_table(table_name, options = {}) - execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}" + execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end def rename_index(table_name, old_name, new_name) if supports_rename_index? + validate_index_length!(table_name, new_name) + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}" else super @@ -859,6 +868,10 @@ module ActiveRecord end end + def create_table_definition(name, temporary, options, as = nil) # :nodoc: + TableDefinition.new(native_database_types, name, temporary, options, as) + end + class MysqlString < Type::String # :nodoc: def type_cast_for_database(value) case value diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index dd303c73d5..af307b57a4 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -16,7 +16,7 @@ module ActiveRecord attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function delegate :type, :precision, :scale, :limit, :klass, :accessor, - :number?, :binary?, :changed?, + :text?, :number?, :binary?, :changed?, :type_cast_from_user, :type_cast_from_database, :type_cast_for_database, :type_cast_for_schema, to: :cast_type 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 997613d7be..6bd1b8ecae 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -5,6 +5,7 @@ module ActiveRecord class Bytea < Type::Binary # :nodoc: def type_cast_from_database(value) return if value.nil? + return value.to_s if value.is_a?(Type::Binary::Data) PGconn.unescape_bytea(super) 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 2d2fede4e8..b2a42e9ebb 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 @@ -8,6 +8,10 @@ module ActiveRecord def initialize(type) @type = type end + + def text? + false + end end 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 033e0324bb..97b4fd3d08 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -3,14 +3,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Uuid < Type::Value # :nodoc: - RFC_4122 = %r{\A\{?[a-fA-F0-9]{4}-? - [a-fA-F0-9]{4}-? - [a-fA-F0-9]{4}-? - [1-5][a-fA-F0-9]{3}-? - [8-Bab][a-fA-F0-9]{3}-? - [a-fA-F0-9]{4}-? - [a-fA-F0-9]{4}-? - [a-fA-F0-9]{4}-?\}?\z}x + ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x alias_method :type_cast_for_database, :type_cast_from_database @@ -19,7 +12,7 @@ module ActiveRecord end def type_cast(value) - value.to_s[RFC_4122, 0] + value.to_s[ACCEPTABLE_UUID, 0] end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index b37630a04c..a9522e152f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -125,10 +125,8 @@ module ActiveRecord # a record (as primary keys cannot be +nil+). This might be done via the # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. def primary_key(name, type = :primary_key, options = {}) - return super unless type == :uuid - options[:default] = options.fetch(:default, 'uuid_generate_v4()') - options[:primary_key] = true - column name, type, options + options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid + super end def new_column_definition(name, type, options) # :nodoc: 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 f29b793a3f..a90adcf4aa 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -4,15 +4,6 @@ module ActiveRecord class SchemaCreation < AbstractAdapter::SchemaCreation private - def visit_ColumnDefinition(o) - sql = super - if o.primary_key? && o.type != :primary_key - sql << " PRIMARY KEY " - add_column_options!(sql, column_options(o)) - end - sql - end - def column_options(o) column_options = super column_options[:array] = o.array @@ -120,6 +111,10 @@ module ActiveRecord SQL end + def drop_table(table_name, options = {}) + execute "DROP TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" + end + # Returns true if schema exists. def schema_exists?(name) exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 @@ -390,15 +385,15 @@ module ActiveRecord # Returns just a table's primary key def primary_key(table) - row = exec_query(<<-end_sql, 'SCHEMA').rows.first + pks = exec_query(<<-end_sql, 'SCHEMA').rows SELECT attr.attname FROM pg_attribute attr - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] + INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '#{quote_table_name(table)}'::regclass end_sql - - row && row.first + return nil unless pks.count == 1 + pks[0][0] end # Renames a table. @@ -488,9 +483,8 @@ module ActiveRecord end def rename_index(table_name, old_name, new_name) - if new_name.length > allowed_index_name_length - raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" - end + validate_index_length!(table_name, new_name) + execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 6ef47d8a11..02cafc8079 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -445,8 +445,8 @@ module ActiveRecord def initialize_type_map(m) # :nodoc: register_class_with_limit m, 'int2', OID::Integer - m.alias_type 'int4', 'int2' - m.alias_type 'int8', 'int2' + register_class_with_limit m, 'int4', OID::Integer + register_class_with_limit m, 'int8', OID::Integer m.alias_type 'oid', 'int2' m.register_type 'float4', OID::Float.new m.alias_type 'float8', 'float4' diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 0f7e0fac01..f3d2b25bfe 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -418,10 +418,9 @@ module ActiveRecord end def primary_key(table_name) #:nodoc: - column = table_structure(table_name).find { |field| - field['pk'] == 1 - } - column && column['name'] + pks = table_structure(table_name).select { |f| f['pk'] > 0 } + return nil unless pks.count == 1 + pks[0]['name'] end def remove_index!(table_name, index_name) #:nodoc: @@ -578,23 +577,12 @@ module ActiveRecord rename.each { |a| column_mappings[a.last] = a.first } from_columns = columns(from).collect(&:name) columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} + from_columns_to_copy = columns.map { |col| column_mappings[col] } quoted_columns = columns.map { |col| quote_column_name(col) } * ',' + quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * ',' - quoted_to = quote_table_name(to) - - raw_column_mappings = Hash[columns(from).map { |c| [c.name, c] }] - - exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| - sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" - - column_values = columns.map do |col| - quote(row[column_mappings[col]], raw_column_mappings[col]) - end - - sql << column_values * ', ' - sql << ')' - exec_query sql - end + exec_query("INSERT INTO #{quote_table_name(to)} (#{quoted_columns}) + SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}") end def sqlite_version diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 8f51590c99..984af79642 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionHandling - RAILS_ENV = -> { (Rails.env if defined?(Rails)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] } + RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] } DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" } # Establishes the connection to the database. Accepts a hash as input where diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index c2d5582f02..38b2d632d2 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -131,6 +131,7 @@ module ActiveRecord return super if block_given? || primary_key.nil? || default_scopes.any? || + current_scope || columns_hash.include?(inheritance_column) || ids.first.kind_of?(Array) @@ -234,7 +235,7 @@ module ActiveRecord # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) } # end def arel_table # :nodoc: - @arel_table ||= Arel::Table.new(table_name) + @arel_table ||= Arel::Table.new(table_name, type_caster: type_caster) end # Returns the Arel engine. @@ -247,10 +248,18 @@ module ActiveRecord end end + def predicate_builder # :nodoc: + @predicate_builder ||= PredicateBuilder.new(table_metadata) + end + + def type_caster # :nodoc: + TypeCaster::Map.new(self) + end + private - def relation #:nodoc: - relation = Relation.create(self, arel_table) + def relation # :nodoc: + relation = Relation.create(self, arel_table, predicate_builder) if finder_needs_type_condition? relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) @@ -258,6 +267,10 @@ module ActiveRecord relation end end + + def table_metadata # :nodoc: + TableMetadata.new(self, arel_table) + end end # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 101889638d..7d8e0a2063 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -37,10 +37,9 @@ module ActiveRecord reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } 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(:all) - }, primary_key) - connection.update stmt + unscoped.where(primary_key => object.id).update_all( + counter_name => object.send(counter_association).count(:all) + ) end return true end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index ced694ba9a..9f053453bd 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -11,7 +11,7 @@ module ActiveRecord # # == Usage # - # Active Records support optimistic locking if the field +lock_version+ is present. Each update to the + # Active Record supports optimistic locking if the +lock_version+ field is present. Each update to the # record increments the +lock_version+ column and the locking facilities ensure that records instantiated twice # will let the last one saved raise a +StaleObjectError+ if the first was also updated. Example: # @@ -80,17 +80,15 @@ module ActiveRecord begin relation = self.class.unscoped - stmt = relation.where( - relation.table[self.class.primary_key].eq(id).and( - relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col))) - ) - ).arel.compile_update( - arel_attributes_with_values_for_update(attribute_names), - self.class.primary_key + affected_rows = relation.where( + self.class.primary_key => id, + lock_col => previous_lock_value, + ).update_all( + attribute_names.map do |name| + [name, _read_attribute(name)] + end.to_h ) - affected_rows = self.class.connection.update stmt - unless affected_rows == 1 raise ActiveRecord::StaleObjectError.new(self, "update") end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 3cac465440..46f4794010 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -39,7 +39,7 @@ module ActiveRecord class PendingMigrationError < MigrationError#:nodoc: def initialize - if defined?(Rails) + if defined?(Rails.env) super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}") else super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate") diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 92ad9c9101..641512d323 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -147,7 +147,7 @@ module ActiveRecord @quoted_table_name = nil @arel_table = nil @sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name - @relation = Relation.create(self, arel_table) + @predicate_builder = nil end # Returns a quoted version of the table name, used to construct SQL statements. @@ -298,12 +298,12 @@ module ActiveRecord connection.schema_cache.clear_table_cache!(table_name) if table_exists? @arel_engine = nil + @arel_table = nil @column_names = nil @column_types = nil @content_columns = nil @default_attributes = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column - @relation = nil end private diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 8a2a06f2ca..0f9b52f69f 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -81,6 +81,9 @@ module ActiveRecord # # Note that the model will _not_ be destroyed until the parent is saved. # + # Also note that the model will not be destroyed unless you also specify + # its id in the updated hash. + # # === One-to-many # # Consider a member that has a number of posts: diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb index dbf4564ae5..edb5066fa0 100644 --- a/activerecord/lib/active_record/no_touching.rb +++ b/activerecord/lib/active_record/no_touching.rb @@ -45,7 +45,7 @@ module ActiveRecord NoTouching.applied_to?(self.class) end - def touch(*) + def touch(*) # :nodoc: super unless no_touching? end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 1fc82f05d4..f53c5f17ef 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -109,6 +109,10 @@ module ActiveRecord # validate: false, validations are bypassed altogether. See # ActiveRecord::Validations for more information. # + # By default, #save also sets the +updated_at+/+updated_on+ attributes to + # the current time. However, if you supply <tt>touch: false</tt>, these + # timestamps will not be updated. + # # There's a series of callbacks associated with +save+. If any of the # <tt>before_*</tt> callbacks return +false+ the action is cancelled and # +save+ returns +false+. See ActiveRecord::Callbacks for further @@ -116,21 +120,25 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save(*) - create_or_update + def save(*args) + create_or_update(*args) rescue ActiveRecord::RecordInvalid false end # Saves the model. # - # If the model is new a record gets created in the database, otherwise + # If the model is new, a record gets created in the database, otherwise # the existing record gets updated. # # With <tt>save!</tt> validations always run. If any of them fail # ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations # for more information. # + # By default, #save! also sets the +updated_at+/+updated_on+ attributes to + # the current time. However, if you supply <tt>touch: false</tt>, these + # timestamps will not be updated. + # # There's a series of callbacks associated with <tt>save!</tt>. If any of # the <tt>before_*</tt> callbacks return +false+ the action is cancelled # and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See @@ -138,8 +146,8 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save!(*) - create_or_update || raise(RecordNotSaved.new(nil, self)) + def save!(*args) + create_or_update(*args) || raise(RecordNotSaved.new(nil, self)) end # Deletes the record in the database and freezes this instance to @@ -498,9 +506,9 @@ module ActiveRecord relation end - def create_or_update + def create_or_update(*args) raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? - result = new_record? ? _create_record : _update_record + result = new_record? ? _create_record : _update_record(*args) result != false end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 4daf2a0e2b..04c2be045d 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -319,7 +319,7 @@ db_namespace = namespace :db do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? ActiveRecord::Schema.verbose = false - ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA'] + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA'] ensure if should_reconnect ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) @@ -329,7 +329,7 @@ db_namespace = namespace :db do # desc "Recreate the test database from an existent structure.sql file" task :load_structure => %w(db:test:purge) do - ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA'] + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA'] end # desc "Recreate the test database from a fresh schema" diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index daafb0b645..cdafa6a50a 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -16,17 +16,17 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation - attr_reader :table, :klass, :loaded + attr_reader :table, :klass, :loaded, :predicate_builder alias :model :klass alias :loaded? :loaded - def initialize(klass, table, values = {}) + def initialize(klass, table, predicate_builder, values = {}) @klass = klass @table = table @values = values @offsets = {} @loaded = false - @predicate_builder = PredicateBuilder.new(klass, table) + @predicate_builder = predicate_builder end def initialize_copy(other) @@ -569,7 +569,7 @@ module ActiveRecord [name, binds.fetch(name.to_s) { case where.right when Array then where.right.map(&:val) - else + when Arel::Nodes::Casted, Arel::Nodes::Quoted where.right.val end }] @@ -633,10 +633,6 @@ module ActiveRecord "#<#{self.class.name} [#{entries.join(', ')}]>" end - protected - - attr_reader :predicate_builder - private def exec_queries diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 20d24b409b..4f0502ae75 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -27,7 +27,7 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the starting point for the batch processing. + # * <tt>:start</tt> - Specifies the primary key value to start from. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond @@ -77,7 +77,7 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the starting point for the batch processing. + # * <tt>:start</tt> - Specifies the primary key value to start from. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 357861caaa..088bc203b7 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -397,7 +397,7 @@ module ActiveRecord else if relation.limit_value limited_ids = limited_ids_for(relation) - limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids)) + limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) end relation.except(:limit, :offset) end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index a27f990f74..afb0b208c3 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -22,7 +22,7 @@ module ActiveRecord # build a relation to merge in rather than directly merging # the values. def other - other = Relation.create(relation.klass, relation.table) + other = Relation.create(relation.klass, relation.table, relation.predicate_builder) hash.each { |k, v| if k == :joins if Hash === v diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 67e646bf18..567efce8ae 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,23 +1,26 @@ module ActiveRecord class PredicateBuilder # :nodoc: - @handlers = [] + require 'active_record/relation/predicate_builder/array_handler' + require 'active_record/relation/predicate_builder/association_query_handler' + require 'active_record/relation/predicate_builder/base_handler' + require 'active_record/relation/predicate_builder/basic_object_handler' + require 'active_record/relation/predicate_builder/class_handler' + require 'active_record/relation/predicate_builder/range_handler' + require 'active_record/relation/predicate_builder/relation_handler' - autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler' - autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler' + delegate :resolve_column_aliases, to: :table - def initialize(klass, table) - @klass = klass + def initialize(table) @table = table - end - - def resolve_column_aliases(hash) - hash = hash.dup - hash.keys.grep(Symbol) do |key| - if klass.attribute_alias? key - hash[klass.attribute_alias(key)] = hash.delete key - end - end - hash + @handlers = [] + + register_handler(BasicObject, BasicObjectHandler.new(self)) + register_handler(Class, ClassHandler.new(self)) + register_handler(Base, BaseHandler.new(self)) + register_handler(Range, RangeHandler.new(self)) + register_handler(Relation, RelationHandler.new) + register_handler(Array, ArrayHandler.new(self)) + register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) end def build_from_hash(attributes) @@ -26,35 +29,16 @@ module ActiveRecord end def expand(column, value) - queries = [] - # Find the foreign key when using queries such as: # Post.where(author: author) # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && reflection = klass._reflect_on_association(column) - if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value) - queries << self.class.build(table[reflection.foreign_type], base_class.name) - end - - column = reflection.foreign_key + if table.associated_with?(column) + value = AssociationQueryValue.new(table.associated_table(column), value) end - queries << self.class.build(table[column], value) - queries - end - - def polymorphic_base_class_from_value(value) - case value - when Relation - value.klass.base_class - when Array - val = value.compact.first - val.class.base_class if val.is_a?(Base) - when Base - value.class.base_class - end + build(table.arel_attribute(column), value) end def self.references(attributes) @@ -79,46 +63,24 @@ module ActiveRecord # ) # end # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler) - def self.register_handler(klass, handler) + def register_handler(klass, handler) @handlers.unshift([klass, handler]) end - register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) }) - register_handler(Class, ->(attribute, value) { deprecate_class_handler; attribute.eq(value.name) }) - register_handler(Base, ->(attribute, value) { attribute.eq(value.id) }) - register_handler(Range, ->(attribute, value) { attribute.between(value) }) - register_handler(Relation, RelationHandler.new) - register_handler(Array, ArrayHandler.new) - - def self.build(attribute, value) + def build(attribute, value) handler_for(value).call(attribute, value) end - def self.handler_for(object) - @handlers.detect { |klass, _| klass === object }.last - end - private_class_method :handler_for - - def self.deprecate_class_handler - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing a class as a value in an Active Record query is deprecated and - will be removed. Pass a string instead. - MSG - end - protected - attr_reader :klass, :table + attr_reader :table def expand_from_hash(attributes) return ["1=0"] if attributes.empty? attributes.flat_map do |key, value| if value.is_a?(Hash) - arel_table = Arel::Table.new(key) - association = klass._reflect_on_association(key) - builder = self.class.new(association && association.klass, arel_table) - + builder = self.class.new(table.associated_table(key)) builder.expand_from_hash(value) else expand(key, value) @@ -141,5 +103,9 @@ module ActiveRecord attributes end + + def handler_for(object) + @handlers.detect { |klass, _| klass === object }.last + end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 4cba297be5..4b5f5773a0 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -3,6 +3,10 @@ require 'active_support/core_ext/string/filters' module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + def call(attribute, value) values = value.map { |x| x.is_a?(Base) ? x.id : x } nils, values = values.partition(&:nil?) @@ -14,20 +18,24 @@ module ActiveRecord values_predicate = case values.length when 0 then NullPredicate - when 1 then attribute.eq(values.first) + when 1 then predicate_builder.build(attribute, values.first) else attribute.in(values) end unless nils.empty? - values_predicate = values_predicate.or(attribute.eq(nil)) + values_predicate = values_predicate.or(predicate_builder.build(attribute, nil)) end - array_predicates = ranges.map { |range| attribute.between(range) } + array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) } array_predicates.unshift(values_predicate) array_predicates.inject { |composite, predicate| composite.or(predicate) } end - module NullPredicate + protected + + attr_reader :predicate_builder + + module NullPredicate # :nodoc: def self.or(other) other end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb new file mode 100644 index 0000000000..aabcf20c1d --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -0,0 +1,58 @@ +module ActiveRecord + class PredicateBuilder + class AssociationQueryHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + queries = {} + + table = value.associated_table + if value.base_class + queries[table.association_foreign_type] = value.base_class.name + end + + queries[table.association_foreign_key] = value.ids + predicate_builder.build_from_hash(queries) + end + + protected + + attr_reader :predicate_builder + end + + class AssociationQueryValue # :nodoc: + attr_reader :associated_table, :value + + def initialize(associated_table, value) + @associated_table = associated_table + @value = value + end + + def ids + value + end + + def base_class + if associated_table.polymorphic_association? + @base_class ||= polymorphic_base_class_from_value + end + end + + private + + def polymorphic_base_class_from_value + case value + when Relation + value.klass.base_class + when Array + val = value.compact.first + val.class.base_class if val.is_a?(Base) + when Base + value.class.base_class + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb new file mode 100644 index 0000000000..6fa5b16f73 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class BaseHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + predicate_builder.build(attribute, value.id) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb new file mode 100644 index 0000000000..6cec75dc0a --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class BasicObjectHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + attribute.eq(value) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb new file mode 100644 index 0000000000..ed313fc9d4 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb @@ -0,0 +1,27 @@ +module ActiveRecord + class PredicateBuilder + class ClassHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + print_deprecation_warning + predicate_builder.build(attribute, value.name) + end + + protected + + attr_reader :predicate_builder + + private + + def print_deprecation_warning + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing a class as a value in an Active Record query is deprecated and + will be removed. Pass a string instead. + MSG + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb new file mode 100644 index 0000000000..1b3849e3ad --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class RangeHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + attribute.between(value) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 57d66bce4b..01bddea6c9 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -67,7 +67,7 @@ module ActiveRecord private def relation_with(values) # :nodoc: - result = Relation.create(klass, table, values) + result = Relation.create(klass, table, predicate_builder, values) result.extend(*extending_values) if extending_values.any? result end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 6c103e331f..f5aa60a69a 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -88,7 +88,7 @@ module ActiveRecord # # => "address_street='123 abc st.' and address_city='chicago'" def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) table = Arel::Table.new(table_name).alias(default_table_name) - predicate_builder = PredicateBuilder.new(self, table) + predicate_builder = PredicateBuilder.new(TableMetadata.new(self, table)) ActiveSupport::Deprecation.warn(<<-EOWARN) sanitize_sql_hash_for_conditions is deprecated, and will be removed in Rails 5.0 EOWARN @@ -107,7 +107,8 @@ sanitize_sql_hash_for_conditions is deprecated, and will be removed in Rails 5.0 def sanitize_sql_hash_for_assignment(attrs, table) c = connection attrs.map do |attr, value| - "#{c.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value, c, columns_hash[attr.to_s])}" + value = type_for_attribute(attr.to_s).type_cast_for_database(value) + "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" end.join(', ') end @@ -163,10 +164,8 @@ sanitize_sql_hash_for_conditions is deprecated, and will be removed in Rails 5.0 end end - def quote_bound_value(value, c = connection, column = nil) #:nodoc: - if column - c.quote(value, column) - elsif value.respond_to?(:map) && !value.acts_like?(:string) + def quote_bound_value(value, c = connection) #:nodoc: + if value.respond_to?(:map) && !value.acts_like?(:string) if value.respond_to?(:empty?) && value.empty? c.quote(nil) else diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 77aa2efc47..2a570e1323 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -121,12 +121,12 @@ HEADER tbl.print ", id: :bigserial" elsif pkcol.sql_type == 'uuid' tbl.print ", id: :uuid" - tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function + tbl.print %Q(, default: #{pkcol.default_function.inspect}) end else tbl.print ", id: false" end - tbl.print ", force: true" + tbl.print ", force: :cascade" tbl.puts " do |t|" # then dump all non-primary key columns diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb new file mode 100644 index 0000000000..11e33e8dfe --- /dev/null +++ b/activerecord/lib/active_record/table_metadata.rb @@ -0,0 +1,53 @@ +module ActiveRecord + class TableMetadata # :nodoc: + delegate :foreign_type, :foreign_key, to: :association, prefix: true + + def initialize(klass, arel_table, association = nil) + @klass = klass + @arel_table = arel_table + @association = association + end + + def resolve_column_aliases(hash) + hash = hash.dup + hash.keys.grep(Symbol) do |key| + if klass.attribute_alias? key + hash[klass.attribute_alias(key)] = hash.delete key + end + end + hash + end + + def arel_attribute(column_name) + arel_table[column_name] + end + + def associated_with?(association_name) + klass && klass._reflect_on_association(association_name) + end + + def associated_table(table_name) + return self if table_name == arel_table.name + + association = klass._reflect_on_association(table_name) + if association && !association.polymorphic? + association_klass = association.klass + arel_table = association_klass.arel_table + else + type_caster = TypeCaster::Connection.new(klass.connection, table_name) + association_klass = nil + arel_table = Arel::Table.new(table_name, type_caster: type_caster) + end + + TableMetadata.new(association_klass, arel_table, association) + end + + def polymorphic_association? + association && association.polymorphic? + end + + protected + + attr_reader :klass, :arel_table, :association + end +end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 1228de2bfd..69aceb66b1 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -188,44 +188,39 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) end - def load_schema(format = ActiveRecord::Base.schema_format, file = nil) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - This method will act on a specific connection in the future. - To act on the current connection, use `load_schema_current` instead. - MSG - - load_schema_current(format, file) - end - - def schema_file(format = ActiveSupport::Base.schema_format) - case format - when :ruby - File.join(db_dir, "schema.rb") - when :sql - File.join(db_dir, "structure.sql") - end - end - - # This method is the successor of +load_schema+. We should rename it - # after +load_schema+ went through a deprecation cycle. (Rails > 4.2) - def load_schema_for(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc: + def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc: file ||= schema_file(format) case format when :ruby check_schema_file(file) - purge(configuration) ActiveRecord::Base.establish_connection(configuration) load(file) when :sql check_schema_file(file) - purge(configuration) structure_load(configuration, file) else raise ArgumentError, "unknown format #{format.inspect}" end end + def load_schema_for(*args) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + This method was renamed to `#load_schema` and will be removed in the future. + Use `#load_schema` instead. + MSG + load_schema(*args) + end + + def schema_file(format = ActiveSupport::Base.schema_format) + case format + when :ruby + File.join(db_dir, "schema.rb") + when :sql + File.join(db_dir, "structure.sql") + end + end + def load_schema_current_if_exists(format = ActiveRecord::Base.schema_format, file = nil, environment = env) if File.exist?(file || schema_file(format)) load_schema_current(format, file, environment) @@ -234,7 +229,7 @@ module ActiveRecord def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) each_current_configuration(environment) { |configuration| - load_schema_for configuration, format, file + load_schema configuration, format, file } ActiveRecord::Base.establish_connection(environment.to_sym) end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 936a18d99a..20e4235788 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -57,8 +57,8 @@ module ActiveRecord super end - def _update_record(*args) - if should_record_timestamps? + def _update_record(*args, touch: true, **options) + if touch && should_record_timestamps? current_time = current_time_from_proper_timezone timestamp_attributes_for_update_in_model.each do |column| @@ -67,7 +67,7 @@ module ActiveRecord write_attribute(column, current_time) end end - super + super(*args) end def should_record_timestamps? diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 01e8f69b02..de701edca0 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -2,7 +2,9 @@ module ActiveRecord # See ActiveRecord::Transactions::ClassMethods for documentation. module Transactions extend ActiveSupport::Concern + #:nodoc: ACTIONS = [:create, :destroy, :update] + #:nodoc: CALLBACK_WARN_MESSAGE = "Currently, Active Record suppresses errors raised " \ "within `after_rollback`/`after_commit` callbacks and only print them to " \ "the logs. In the next version, these errors will no longer be suppressed. " \ @@ -358,14 +360,12 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc: @_start_transaction_state[:id] = id - unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:new_record] = @new_record - end - unless @_start_transaction_state.include?(:destroyed) - @_start_transaction_state[:destroyed] = @destroyed - end + @_start_transaction_state.reverse_merge!( + new_record: @new_record, + destroyed: @destroyed, + frozen?: frozen?, + ) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 - @_start_transaction_state[:frozen?] = frozen? end # Clear the new record state and id of a record. diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb index d10778eeb6..7b2bee2c42 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activerecord/lib/active_record/type/decimal.rb @@ -16,7 +16,7 @@ module ActiveRecord def cast_value(value) case value when ::Float - BigDecimal(value, float_precision) + convert_float_to_big_decimal(value) when ::Numeric, ::String BigDecimal(value, precision.to_i) else @@ -28,6 +28,14 @@ module ActiveRecord end end + def convert_float_to_big_decimal(value) + if precision + BigDecimal(value, float_precision) + else + value.to_d + end + end + def float_precision if precision.to_i > ::Float::DIG + 1 ::Float::DIG + 1 diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb index fa43266504..674f996f38 100644 --- a/activerecord/lib/active_record/type/numeric.rb +++ b/activerecord/lib/active_record/type/numeric.rb @@ -29,7 +29,7 @@ module ActiveRecord # 'wibble'.to_i will give zero, we want to make sure # that we aren't marking int zero to string zero as # changed. - value.to_s !~ /\A\d+\.?\d*\z/ + 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 3191a868ef..3cac03464e 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -29,7 +29,7 @@ module ActiveRecord def changed_in_place?(raw_old_value, value) return false if value.nil? - subtype.changed_in_place?(raw_old_value, coder.dump(value)) + subtype.changed_in_place?(raw_old_value, type_cast_for_database(value)) end def accessor diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb index fbc0af2c5a..cf95e25be0 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activerecord/lib/active_record/type/string.rb @@ -21,6 +21,10 @@ module ActiveRecord end end + def text? + true + end + private def cast_value(value) diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 9456a4a56c..60ae47db3d 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -50,6 +50,10 @@ module ActiveRecord # These predicates are not documented, as I need to look further into # their use, and see if they can be removed entirely. + def text? # :nodoc: + false + end + def number? # :nodoc: false end @@ -91,8 +95,8 @@ module ActiveRecord # 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`. + # +type_cast_from_database+ and +type_cast_from_user+ for all values + # except +nil+. def cast_value(value) # :doc: value end diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb new file mode 100644 index 0000000000..63ba10c289 --- /dev/null +++ b/activerecord/lib/active_record/type_caster.rb @@ -0,0 +1,7 @@ +require 'active_record/type_caster/map' +require 'active_record/type_caster/connection' + +module ActiveRecord + module TypeCaster + end +end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb new file mode 100644 index 0000000000..9e4a130b40 --- /dev/null +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -0,0 +1,34 @@ +module ActiveRecord + module TypeCaster + class Connection + def initialize(connection, table_name) + @connection = connection + @table_name = table_name + end + + def type_cast_for_database(attribute_name, value) + return value if value.is_a?(Arel::Nodes::BindParam) + type = type_for(attribute_name) + type.type_cast_for_database(value) + end + + protected + + attr_reader :connection, :table_name + + private + + def type_for(attribute_name) + if connection.schema_cache.table_exists?(table_name) + column_for(attribute_name).cast_type + else + Type::Value.new + end + end + + def column_for(attribute_name) + connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] + end + end + end +end diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb new file mode 100644 index 0000000000..03c9e8ff83 --- /dev/null +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module TypeCaster + class Map + def initialize(types) + @types = types + end + + def type_cast_for_database(attr_name, value) + return value if value.is_a?(Arel::Nodes::BindParam) + type = types.type_for_attribute(attr_name.to_s) + type.type_cast_for_database(value) + end + + protected + + attr_reader :types + end + end +end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index a6c8ff7f3a..f27adc9c40 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -88,3 +88,4 @@ end require "active_record/validations/associated" require "active_record/validations/uniqueness" require "active_record/validations/presence" +require "active_record/validations/length" diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb new file mode 100644 index 0000000000..ef5a6cbbe7 --- /dev/null +++ b/activerecord/lib/active_record/validations/length.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module Validations + class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc: + def validate_each(record, attribute, association_or_value) + if association_or_value.respond_to?(:loaded?) && association_or_value.loaded? + association_or_value = association_or_value.target.reject(&:marked_for_destruction?) + end + super + end + end + + module ClassMethods + # See <tt>ActiveModel::Validation::LengthValidator</tt> for more information. + def validates_length_of(*attr_names) + validates_with LengthValidator, _merge_attributes(attr_names) + end + + alias_method :validates_size_of, :validates_length_of + end + end +end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 3e8afe37a8..f52f91e89c 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -16,9 +16,8 @@ module ActiveRecord value = map_enum_attribute(finder_class, attribute, value) relation = build_relation(finder_class, table, attribute, value) - relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? + relation = relation.where.not(finder_class.primary_key => record.id) if record.persisted? relation = scope_relation(record, table, relation) - relation = finder_class.unscoped.where(relation) relation = relation.merge(options[:conditions]) if options[:conditions] if relation.exists? @@ -60,17 +59,21 @@ module ActiveRecord end column = klass.columns_hash[attribute_name] - value = klass.connection.type_cast(value, column) + value = klass.type_for_attribute(attribute_name).type_cast_for_database(value) + value = klass.connection.type_cast(value) if value.is_a?(String) && column.limit value = value.to_s[0, column.limit] end - if !options[:case_sensitive] && value.is_a?(String) + value = Arel::Nodes::Quoted.new(value) + + comparison = if !options[:case_sensitive] && value && column.text? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else klass.connection.case_sensitive_comparison(table, attribute, column, value) end + klass.unscoped.where(comparison) end def scope_relation(record, table, relation) @@ -81,7 +84,7 @@ module ActiveRecord else scope_value = record._read_attribute(scope_item) end - relation = relation.and(table[scope_item].eq(scope_value)) + relation = relation.where(scope_item => scope_value) end relation diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb index fb0fbb4759..f7bf6987c4 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb @@ -15,8 +15,5 @@ class <%= migration_class_name %> < ActiveRecord::Migration <% attributes_with_index.each do |attribute| -%> add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> <% end -%> -<% attributes.select(&:reference?).reject(&:polymorphic?).each do |attribute| -%> - add_foreign_key :<%= table_name %>, :<%= attribute.name.pluralize %> -<% end -%> end end diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index 7df9bcad25..ae9c74fd05 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -4,9 +4,6 @@ class <%= migration_class_name %> < ActiveRecord::Migration <% attributes.each do |attribute| -%> <%- if attribute.reference? -%> add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> - <%- unless attribute.polymorphic? -%> - add_foreign_key :<%= table_name %>, :<%= attribute.name.pluralize %> - <%- end -%> <%- else -%> add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> <%- if attribute.has_index? -%> @@ -29,9 +26,6 @@ class <%= migration_class_name %> < ActiveRecord::Migration <%- if migration_action -%> <%- if attribute.reference? -%> remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> - <%- unless attribute.polymorphic? -%> - remove_foreign_key :<%= table_name %>, :<%= attribute.name.pluralize %> - <%- end -%> <%- else -%> <%- if attribute.has_index? -%> remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 28106d3772..85db8f4614 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -99,6 +99,12 @@ module ActiveRecord end end + def test_composite_primary_key + with_example_table '`id` INT(11), `number` INT(11), foo INT(11), PRIMARY KEY (`id`, `number`)' do + assert_nil @conn.primary_key('ex') + end + end + def test_tinyint_integer_typecasting with_example_table '`status` TINYINT(4)' do insert(@conn, { 'status' => 2 }, 'ex') diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb index d8a954efa8..a2206153e9 100644 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb @@ -9,15 +9,11 @@ module ActiveRecord end def test_type_cast_true - c = Column.new(nil, 1, Type::Boolean.new) - assert_equal 1, @conn.type_cast(true, nil) - assert_equal 1, @conn.type_cast(true, c) + assert_equal 1, @conn.type_cast(true) end def test_type_cast_false - c = Column.new(nil, 1, Type::Boolean.new) - assert_equal 0, @conn.type_cast(false, nil) - assert_equal 0, @conn.type_cast(false, c) + assert_equal 0, @conn.type_cast(false) end end end diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 03627135b2..0e641ba3bf 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -47,8 +47,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal 1, attributes["archived"] 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) end test "test type casting without emulated booleans" do @@ -60,8 +59,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal 1, attributes["archived"] 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) end test "with booleans stored as 1 and 0" do diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb index f67f21fab1..2b01d941b8 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -18,7 +18,7 @@ module ActiveRecord explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain assert_match %r(developers |.* const), explain - assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` IN (1)), explain + assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain assert_match %r(audit_logs |.* ALL), explain end end diff --git a/activerecord/test/cases/adapters/postgresql/cidr_test.rb b/activerecord/test/cases/adapters/postgresql/cidr_test.rb new file mode 100644 index 0000000000..54b679d3ab --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/cidr_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" +require "ipaddr" + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter + class CidrTest < ActiveRecord::TestCase + test "type casting IPAddr for database" do + type = OID::Cidr.new + ip = IPAddr.new("255.0.0.0/8") + ip2 = IPAddr.new("127.0.0.1") + + assert_equal "255.0.0.0/8", type.type_cast_for_database(ip) + assert_equal "127.0.0.1/32", type.type_cast_for_database(ip2) + end + + test "casting does nothing with non-IPAddr objects" do + type = OID::Cidr.new + + assert_equal "foo", type.type_cast_for_database("foo") + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index d2dd04b84b..6ffb4c9f33 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -18,7 +18,7 @@ module ActiveRecord explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(QUERY PLAN), explain assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain end end end diff --git a/activerecord/test/cases/adapters/postgresql/integer_test.rb b/activerecord/test/cases/adapters/postgresql/integer_test.rb new file mode 100644 index 0000000000..7f8751281e --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/integer_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" +require "active_support/core_ext/numeric/bytes" + +class PostgresqlIntegerTest < ActiveRecord::TestCase + class PgInteger < ActiveRecord::Base + end + + def setup + @connection = ActiveRecord::Base.connection + + @connection.transaction do + @connection.create_table "pg_integers", force: true do |t| + t.integer :quota, limit: 8, default: 2.gigabytes + end + end + end + + teardown do + @connection.execute "drop table if exists pg_integers" + end + + test "schema properly respects bigint ranges" do + assert_equal 2.gigabytes, PgInteger.new.quota + end +end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index 87183174f2..54cff192c1 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -10,7 +10,7 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection @connection.execute("set lc_monetary = 'C'") - @connection.create_table('postgresql_moneys') do |t| + @connection.create_table('postgresql_moneys', force: true) do |t| t.column "wealth", "money" t.column "depth", "money", default: "150.55" end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index c3c696b871..6bb2b26cd5 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -54,6 +54,12 @@ module ActiveRecord end end + def test_composite_primary_key + with_example_table 'id serial, number serial, PRIMARY KEY (id, number)' do + assert_nil @connection.primary_key('ex') + end + end + def test_primary_key_raises_error_if_table_not_found assert_raises(ActiveRecord::StatementInvalid) do @connection.primary_key('unobtainium') diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 11d5173d37..9ac0036d66 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -10,47 +10,21 @@ module ActiveRecord end def test_type_cast_true - c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') - assert_equal 't', @conn.type_cast(true, nil) - assert_equal 't', @conn.type_cast(true, c) + assert_equal 't', @conn.type_cast(true) end def test_type_cast_false - c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') - assert_equal 'f', @conn.type_cast(false, nil) - assert_equal 'f', @conn.type_cast(false, c) - end - - def test_type_cast_cidr - ip = IPAddr.new('255.0.0.0/8') - c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'cidr') - assert_equal ip, @conn.type_cast(ip, c) - end - - def test_type_cast_inet - ip = IPAddr.new('255.1.0.0/8') - c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'inet') - assert_equal ip, @conn.type_cast(ip, c) + assert_equal 'f', @conn.type_cast(false) end def test_quote_float_nan nan = 0.0/0 - c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float') - assert_equal "'NaN'", @conn.quote(nan, c) + assert_equal "'NaN'", @conn.quote(nan) end def test_quote_float_infinity infinity = 1.0/0 - c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float') - assert_equal "'Infinity'", @conn.quote(infinity, c) - end - - def test_quote_cast_numeric - fixnum = 666 - c = PostgreSQLColumn.new(nil, nil, Type::String.new, 'varchar') - assert_equal "'666'", @conn.quote(fixnum, c) - c = PostgreSQLColumn.new(nil, nil, Type::Text.new, 'text') - assert_equal "'666'", @conn.quote(fixnum, c) + assert_equal "'Infinity'", @conn.quote(infinity) end def test_quote_time_usec diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index fac21996ed..d5d2dd16e2 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -69,13 +69,18 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase assert_equal 'foobar', uuid.guid_before_type_cast end - def test_rfc_4122_regex + def test_acceptable_uuid_regex # Valid uuids ['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11', '{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}', 'a0eebc999c0b4ef8bb6d6bb9bd380a11', 'a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11', - '{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}'].each do |valid_uuid| + '{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}', + # The following is not a valid RFC 4122 UUID, but PG doesn't seem to care, + # so we shouldn't block it either. (Pay attention to "fb6d" – the "f" here + # is invalid – it must be one of 8, 9, A, B, a, b according to the spec.) + '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}', + ].each do |valid_uuid| uuid = UUIDType.new guid: valid_uuid assert_not_nil uuid.guid end @@ -87,7 +92,6 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase 0.0, true, 'Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11', - '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}', 'a0eebc999r0b4ef8ab6d6bb9bd380a11', 'a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11', '{a0eebc99-bb6d6bb9-bd380a11}'].each do |invalid_uuid| @@ -112,6 +116,23 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase output = dump_table_schema "uuid_data_type" assert_match %r{t.uuid "guid"}, output end + + def test_uniqueness_validation_ignores_uuid + klass = Class.new(ActiveRecord::Base) do + self.table_name = "uuid_data_type" + validates :guid, uniqueness: { case_sensitive: false } + + def self.name + "UUIDType" + end + end + + record = klass.create!(guid: "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11") + duplicate = klass.new(guid: record.guid) + + assert record.guid.present? # Ensure we actually are testing a UUID + assert_not duplicate.valid? + end end class PostgresqlLargeKeysTest < ActiveRecord::TestCase @@ -126,7 +147,7 @@ class PostgresqlLargeKeysTest < ActiveRecord::TestCase def test_omg schema = dump_table_schema "big_serials" - assert_match "create_table \"big_serials\", id: :bigserial, force: true", schema + assert_match "create_table \"big_serials\", id: :bigserial", schema end def teardown @@ -211,6 +232,7 @@ end class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase include PostgresqlUUIDHelper + include SchemaDumpingHelper setup do enable_extension!('uuid-ossp', connection) @@ -234,6 +256,11 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first assert_nil col_desc["default"] end + + def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil + schema = dump_table_schema "pg_uuids" + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema) + end end end diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index de6e35ef57..7d66c44798 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -18,7 +18,7 @@ module ActiveRecord explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain assert_match(/(SCAN )?TABLE audit_logs/, explain) end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index ac8332e2fa..df497e761c 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -15,73 +15,52 @@ module ActiveRecord def test_type_cast_binary_encoding_without_logger @conn.extend(Module.new { def logger; end }) - 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, column) + assert_equal expected, @conn.type_cast(binary) end def test_type_cast_symbol - assert_equal 'foo', @conn.type_cast(:foo, nil) + assert_equal 'foo', @conn.type_cast(:foo) end def test_type_cast_date date = Date.today expected = @conn.quoted_date(date) - assert_equal expected, @conn.type_cast(date, nil) + assert_equal expected, @conn.type_cast(date) end def test_type_cast_time time = Time.now expected = @conn.quoted_date(time) - assert_equal expected, @conn.type_cast(time, nil) + assert_equal expected, @conn.type_cast(time) end def test_type_cast_numeric - assert_equal 10, @conn.type_cast(10, nil) - assert_equal 2.2, @conn.type_cast(2.2, nil) + assert_equal 10, @conn.type_cast(10) + assert_equal 2.2, @conn.type_cast(2.2) end def test_type_cast_nil - assert_equal nil, @conn.type_cast(nil, nil) + assert_equal nil, @conn.type_cast(nil) end def test_type_cast_true - c = Column.new(nil, 1, Type::Integer.new) - assert_equal 't', @conn.type_cast(true, nil) - assert_equal 1, @conn.type_cast(true, c) + assert_equal 't', @conn.type_cast(true) end def test_type_cast_false - c = Column.new(nil, 1, Type::Integer.new) - assert_equal 'f', @conn.type_cast(false, nil) - assert_equal 0, @conn.type_cast(false, c) - end - - def test_type_cast_string - assert_equal '10', @conn.type_cast('10', nil) - - c = Column.new(nil, 1, Type::Integer.new) - assert_equal 10, @conn.type_cast('10', c) - - c = Column.new(nil, 1, Type::Float.new) - assert_equal 10.1, @conn.type_cast('10.1', c) - - c = Column.new(nil, 1, Type::Binary.new) - assert_equal '10.1', @conn.type_cast('10.1', c) - - c = Column.new(nil, 1, Type::Date.new) - assert_equal '10.1', @conn.type_cast('10.1', c) + assert_equal 'f', @conn.type_cast(false) end def test_type_cast_bigdecimal bd = BigDecimal.new '10.0' - assert_equal bd.to_f, @conn.type_cast(bd, nil) + assert_equal bd.to_f, @conn.type_cast(bd) end def test_type_cast_unknown_should_raise_error obj = Class.new.new - assert_raise(TypeError) { @conn.type_cast(obj, nil) } + assert_raise(TypeError) { @conn.type_cast(obj) } end def test_type_cast_object_which_responds_to_quoted_id @@ -94,14 +73,14 @@ module ActiveRecord 10 end }.new - assert_equal 10, @conn.type_cast(quoted_id_obj, nil) + assert_equal 10, @conn.type_cast(quoted_id_obj) quoted_id_obj = Class.new { def quoted_id "'zomg'" end }.new - assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) } + assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } end def test_quoting_binary_strings diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 9d09ff49c7..029663e7f4 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -405,6 +405,12 @@ module ActiveRecord end end + def test_composite_primary_key + with_example_table 'id integer, number integer, foo integer, PRIMARY KEY (id, number)' do + assert_nil @conn.primary_key('ex') + end + end + def test_supports_extensions assert_not @conn.supports_extensions?, 'does not support extensions' end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index db8fd92c1f..fdb437d11d 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1328,7 +1328,6 @@ class EagerAssociationTest < ActiveRecord::TestCase 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? @@ -1340,6 +1339,10 @@ class EagerAssociationTest < ActiveRecord::TestCase # has-many :through david = Author.where(id: "1").eager_load(:readonly_comments).first! assert david.readonly_comments.first.readonly? + + # belongs_to + post = Post.where(id: "1").eager_load(:author).first! + assert post.author.readonly? end test "preloading a polymorphic association with references to the associated table" do diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index d3b74aa616..21a45042fa 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -31,6 +31,8 @@ require 'models/student' require 'models/pirate' require 'models/ship' require 'models/tyre' +require 'models/subscriber' +require 'models/subscription' class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase fixtures :authors, :posts, :comments @@ -43,12 +45,59 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa end end +class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase + fixtures :authors, :essays, :subscribers, :subscriptions, :people + + def test_custom_primary_key_on_new_record_should_fetch_with_query + subscriber = Subscriber.new(nick: 'webster132') + assert !subscriber.subscriptions.loaded? + + assert_queries 1 do + assert_equal 2, subscriber.subscriptions.size + end + + assert_equal subscriber.subscriptions, Subscription.where(subscriber_id: 'webster132') + end + + def test_association_primary_key_on_new_record_should_fetch_with_query + author = Author.new(:name => "David") + assert !author.essays.loaded? + + assert_queries 1 do + assert_equal 1, author.essays.size + end + + assert_equal author.essays, Essay.where(writer_id: "David") + end + + def test_has_many_custom_primary_key + david = authors(:david) + assert_equal david.essays, Essay.where(writer_id: "David") + end + + def test_has_many_assignment_with_custom_primary_key + david = people(:david) + + assert_equal ["A Modest Proposal"], david.essays.map(&:name) + david.essays = [Essay.create!(name: "Remote Work" )] + assert_equal ["Remote Work"], david.essays.map(&:name) + end + + def test_blank_custom_primary_key_on_new_record_should_not_run_queries + author = Author.new + assert !author.essays.loaded? + + assert_queries 0 do + assert_equal 0, author.essays.size + end + end +end class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :comments, - :people, :posts, :readers, :taggings, :cars, :essays, - :categorizations, :jobs, :tags + :posts, :readers, :taggings, :cars, :jobs, :tags, + :categorizations def setup Client.destroyed_client_ids.clear @@ -1578,39 +1627,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - def test_custom_primary_key_on_new_record_should_fetch_with_query - author = Author.new(:name => "David") - assert !author.essays.loaded? - - assert_queries 1 do - assert_equal 1, author.essays.size - end - - assert_equal author.essays, Essay.where(writer_id: "David") - end - - def test_has_many_custom_primary_key - david = authors(:david) - assert_equal david.essays, Essay.where(writer_id: "David") - end - - def test_has_many_assignment_with_custom_primary_key - david = people(:david) - - assert_equal ["A Modest Proposal"], david.essays.map(&:name) - david.essays = [Essay.create!(name: "Remote Work" )] - assert_equal ["Remote Work"], david.essays.map(&:name) - end - - def test_blank_custom_primary_key_on_new_record_should_not_run_queries - author = Author.new - assert !author.essays.loaded? - - assert_queries 0 do - assert_equal 0, author.essays.size - end - end - def test_calling_first_or_last_with_integer_on_association_should_not_load_association firm = companies(:first_firm) firm.clients.create(:name => 'Foo') diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index a69f7a5262..9b6757e256 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -273,6 +273,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal account, firm.reload.account end + def test_create_with_inexistent_foreign_key_failing + firm = Firm.create(name: 'GlobalMegaCorp') + + assert_raises(ActiveRecord::UnknownAttributeError) do + firm.create_account_with_inexistent_foreign_key + end + end + def test_build firm = Firm.new("name" => "GlobalMegaCorp") firm.save @@ -566,6 +574,12 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal author.post, post end + def test_has_one_loading_for_new_record + post = Post.create!(author_id: 42, title: 'foo', body: 'bar') + author = Author.new(id: 42) + assert_equal post, author.post + end + def test_has_one_relationship_cannot_have_a_counter_cache assert_raise(ArgumentError) do Class.new(ActiveRecord::Base) do diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index c6769edcbf..72963fd56c 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -264,6 +264,11 @@ class AssociationProxyTest < ActiveRecord::TestCase end end + test "first! works on loaded associations" do + david = authors(:david) + assert_equal david.posts.first, david.posts.reload.first! + end + def test_reset_unloads_target david = authors(:david) david.posts.reload diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb index c0491bbee5..c2ec92c40d 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -3,6 +3,8 @@ require 'models/topic' require 'models/task' class DateTimeTest < ActiveRecord::TestCase + include InTimeZone + def test_saves_both_date_and_time with_env_tz 'America/New_York' do with_timezone_config default: :utc do @@ -29,6 +31,14 @@ class DateTimeTest < ActiveRecord::TestCase assert_nil task.ending end + def test_assign_bad_date_time_with_timezone + in_time_zone "Pacific Time (US & Canada)" do + task = Task.new + task.starting = '2014-07-01T24:59:59GMT' + assert_nil task.starting + end + end + def test_assign_empty_date topic = Topic.new topic.last_read = '' diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index eb9b1a2d74..98cf60a8c4 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -688,7 +688,14 @@ class DirtyTest < ActiveRecord::TestCase serialize :data end - klass.create!(data: "foo") + binary = klass.create!(data: "\\\\foo") + + assert_not binary.changed? + + binary.data = binary.data.dup + + assert_not binary.changed? + binary = klass.last assert_not binary.changed? @@ -698,6 +705,21 @@ class DirtyTest < ActiveRecord::TestCase assert binary.changed? end + test "attribute_changed? doesn't compute in-place changes for unrelated attributes" do + test_type_class = Class.new(ActiveRecord::Type::Value) do + define_method(:changed_in_place?) do |*| + raise + end + end + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'people' + attribute :foo, test_type_class.new + end + + model = klass.new(first_name: "Jim") + assert model.first_name_changed? + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 5c98be342f..02dc5d3ad3 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -15,9 +15,11 @@ require 'models/customer' require 'models/toy' require 'models/matey' require 'models/dog' +require 'models/car' +require 'models/tyre' class FinderTest < ActiveRecord::TestCase - fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations + fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations, :cars def test_find_by_id_with_hash assert_raises(ActiveRecord::StatementInvalid) do @@ -1101,6 +1103,26 @@ class FinderTest < ActiveRecord::TestCase end end + test "find on a scope does not perform statement caching" do + honda = cars(:honda) + zyke = cars(:zyke) + tyre = honda.tyres.create! + tyre2 = zyke.tyres.create! + + assert_equal tyre, honda.tyres.custom_find(tyre.id) + assert_equal tyre2, zyke.tyres.custom_find(tyre2.id) + end + + test "find_by on a scope does not perform statement caching" do + honda = cars(:honda) + zyke = cars(:zyke) + tyre = honda.tyres.create! + tyre2 = zyke.tyres.create! + + assert_equal tyre, honda.tyres.custom_find_by(id: tyre.id) + assert_equal tyre2, zyke.tyres.custom_find_by(id: tyre2.id) + end + protected def bind(statement, *vars) if vars.first.is_a?(Hash) diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 0338669016..fe6323ab02 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -304,7 +304,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_eager_load_belongs_to_primary_key_quoting con = Account.connection - assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)/) do + assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1/) do Account.all.merge!(:includes => :firm).find(1) end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 5a4b1fb919..ee43f07dd7 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -33,8 +33,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase p1 = Person.find(1) assert_equal 0, p1.lock_version - Person.expects(:quote_value).with(0, Person.columns_hash[Person.locking_column]).returns('0').once - p1.first_name = 'anika2' p1.save! diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index d91e7142b3..d774cfebc4 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -415,5 +415,36 @@ module ActiveRecord yield end end + + if ActiveRecord::Base.connection.supports_foreign_keys? + class ChangeSchemaWithDependentObjectsTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :trains + @connection.create_table(:wagons) { |t| t.references :train } + @connection.add_foreign_key :wagons, :trains + end + + teardown do + [:wagons, :trains].each do |table| + @connection.drop_table(table) if @connection.table_exists?(table) + end + end + + def test_create_table_with_force_cascade_drops_dependent_objects + skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + # can't re-create table referenced by foreign key + assert_raises(ActiveRecord::StatementInvalid) do + @connection.create_table :trains, force: true + end + + # can recreate referenced table with force: :cascade + @connection.create_table :trains, force: :cascade + assert_equal [], @connection.foreign_keys(:wagons) + end + end + end end end diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb new file mode 100644 index 0000000000..bba8897a0d --- /dev/null +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -0,0 +1,101 @@ +require 'cases/helper' + +if ActiveRecord::Base.connection.supports_foreign_keys? +module ActiveRecord + class Migration + class ReferencesForeignKeyTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.execute("drop table if exists testings") + @connection.execute("drop table if exists testing_parents") + end + + test "foreign keys can be created with the table" do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + fk = @connection.foreign_keys("testings").first + assert_equal "testings", fk.from_table + assert_equal "testing_parents", fk.to_table + end + + test "no foreign key is created by default" do + @connection.create_table :testings do |t| + t.references :testing_parent + end + + assert_equal [], @connection.foreign_keys("testings") + end + + test "options hash can be passed" do + @connection.change_table :testing_parents do |t| + t.integer :other_id + t.index :other_id, unique: true + end + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: { primary_key: :other_id } + end + + fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" } + assert_equal "other_id", fk.primary_key + end + + test "foreign keys cannot be added to polymorphic relations when creating the table" do + @connection.create_table :testings do |t| + assert_raises(ArgumentError) do + t.references :testing_parent, polymorphic: true, foreign_key: true + end + end + end + + test "foreign keys can be created while changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + fk = @connection.foreign_keys("testings").first + assert_equal "testings", fk.from_table + assert_equal "testing_parents", fk.to_table + end + + test "foreign keys are not added by default when changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent + end + + assert_equal [], @connection.foreign_keys("testings") + end + + test "foreign keys accept options when changing the table" do + @connection.change_table :testing_parents do |t| + t.integer :other_id + t.index :other_id, unique: true + end + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent, foreign_key: { primary_key: :other_id } + end + + fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" } + assert_equal "other_id", fk.primary_key + end + + test "foreign keys cannot be added to polymorphic relations when changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + assert_raises(ArgumentError) do + t.references :testing_parent, polymorphic: true, foreign_key: true + end + end + end + end + end +end +end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 6fc4731f01..cf5a5de3a0 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -878,4 +878,35 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal "Welcome to the weblog", post.title assert_not post.new_record? end + + class SaveTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + def test_save_touch_false + widget = Class.new(ActiveRecord::Base) do + connection.create_table :widgets, force: true do |t| + t.string :name + t.timestamps null: false + end + + self.table_name = :widgets + end + + instance = widget.create!({ + name: 'Bob', + created_at: 1.day.ago, + updated_at: 1.day.ago + }) + + created_at = instance.created_at + updated_at = instance.updated_at + + instance.name = 'Barb' + instance.save!(touch: false) + assert_equal instance.created_at, created_at + assert_equal instance.updated_at, updated_at + ensure + ActiveRecord::Base.connection.drop_table :widgets + end + end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index 98888150a8..287a3f33ea 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -35,6 +35,22 @@ class PooledConnectionsTest < ActiveRecord::TestCase end end + def checkout_checkin_connections_loop(pool_size, loops) + ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5})) + @connection_count = 0 + @timed_out = 0 + loops.times do + begin + conn = ActiveRecord::Base.connection_pool.checkout + ActiveRecord::Base.connection_pool.checkin conn + @connection_count += 1 + ActiveRecord::Base.connection.tables + rescue ActiveRecord::ConnectionTimeoutError + @timed_out += 1 + end + end + end + def test_pooled_connection_checkin_one checkout_checkin_connections 1, 2 assert_equal 2, @connection_count @@ -42,6 +58,20 @@ class PooledConnectionsTest < ActiveRecord::TestCase assert_equal 1, ActiveRecord::Base.connection_pool.connections.size end + def test_pooled_connection_checkin_two + checkout_checkin_connections_loop 2, 3 + assert_equal 3, @connection_count + assert_equal 0, @timed_out + assert_equal 2, ActiveRecord::Base.connection_pool.connections.size + end + + def test_pooled_connection_remove + ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.5})) + old_connection = ActiveRecord::Base.connection + extra_connection = ActiveRecord::Base.connection_pool.checkout + ActiveRecord::Base.connection_pool.remove(extra_connection) + assert_equal ActiveRecord::Base.connection, old_connection + end private diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index f19a6ea5e3..2dfc53f132 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -195,6 +195,30 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase end end +class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Barcode < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true) + end + + teardown do + @connection.execute("DROP TABLE IF EXISTS barcodes") + end + + def test_any_type_primary_key + assert_equal "code", Barcode.primary_key + + column_type = Barcode.type_for_attribute(Barcode.primary_key) + assert_equal :string, column_type.type + assert_equal 42, column_type.limit + end +end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -209,7 +233,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) end end -if current_adapter?(:PostgreSQLAdapter) +if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) class PrimaryKeyBigSerialTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -218,17 +242,24 @@ if current_adapter?(:PostgreSQLAdapter) setup do @connection = ActiveRecord::Base.connection - @connection.create_table(:widgets, id: :bigserial) { |t| } + if current_adapter?(:PostgreSQLAdapter) + @connection.create_table(:widgets, id: :bigserial, force: true) + else + @connection.create_table(:widgets, id: :bigint, force: true) + end end teardown do - @connection.drop_table :widgets + @connection.execute("DROP TABLE IF EXISTS widgets") end - def test_bigserial_primary_key - assert_equal "id", Widget.primary_key - assert_equal :integer, Widget.columns_hash[Widget.primary_key].type + test "primary key column type with bigserial" do + column_type = Widget.type_for_attribute(Widget.primary_key) + assert_equal :integer, column_type.type + assert_equal 8, column_type.limit + end + test "primary key with bigserial are automatically numbered" do widget = Widget.create! assert_not_nil widget.id end diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 4c94c2fd0d..2443f10269 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -25,7 +25,7 @@ module ActiveRecord end def relation - @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table + @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table, Post.predicate_builder end (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| @@ -99,7 +99,7 @@ module ActiveRecord end test '#reorder!' do - relation = self.relation.order('foo') + @relation = self.relation.order('foo') assert relation.reorder!('bar').equal?(relation) assert_equal ['bar'], relation.order_values @@ -116,7 +116,7 @@ module ActiveRecord end test 'reverse_order!' do - relation = Post.order('title ASC, comments_count DESC') + @relation = Post.order('title ASC, comments_count DESC') relation.reverse_order! diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb index 4057835688..0cc081fced 100644 --- a/activerecord/test/cases/relation/predicate_builder_test.rb +++ b/activerecord/test/cases/relation/predicate_builder_test.rb @@ -4,11 +4,13 @@ require 'models/topic' module ActiveRecord class PredicateBuilderTest < ActiveRecord::TestCase def test_registering_new_handlers - PredicateBuilder.register_handler(Regexp, proc do |column, value| + Topic.predicate_builder.register_handler(Regexp, proc do |column, value| Arel::Nodes::InfixOperation.new('~', column, Arel.sql(value.source)) end) assert_match %r{["`]topics["`].["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql + ensure + Topic.reset_column_information end end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 3280945d09..f7cb471984 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -23,19 +23,19 @@ module ActiveRecord end def test_construction - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal FakeKlass, relation.klass assert_equal :b, relation.table assert !relation.loaded, 'relation is not loaded' end def test_responds_to_model_and_returns_klass - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal FakeKlass, relation.model end def test_initialize_single_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end @@ -43,19 +43,19 @@ module ActiveRecord end def test_multi_value_initialize - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) Relation::MULTI_VALUE_METHODS.each do |method| assert_equal [], relation.send("#{method}_values"), method.to_s end end def test_extensions - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal [], relation.extensions end def test_empty_where_values_hash - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal({}, relation.where_values_hash) relation.where! :hello @@ -63,19 +63,20 @@ module ActiveRecord end def test_has_values - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) relation.where! relation.table[:id].eq(10) assert_equal({:id => 10}, relation.where_values_hash) end def test_values_wrong_table - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) relation.where! Comment.arel_table[:id].eq(10) assert_equal({}, relation.where_values_hash) end def test_tree_is_not_traversed - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 left = relation.table[:id].eq(10) right = relation.table[:id].eq(10) combine = left.and right @@ -84,24 +85,25 @@ module ActiveRecord end def test_table_name_delegates_to_klass - relation = Relation.new FakeKlass.new('posts'), :b + relation = Relation.new(FakeKlass.new('posts'), :b, Post.predicate_builder) assert_equal 'posts', relation.table_name end def test_scope_for_create - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal({}, relation.scope_for_create) end def test_create_with_value - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) hash = { :hello => 'world' } relation.create_with_value = hash assert_equal hash, relation.scope_for_create end def test_create_with_value_with_wheres - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 relation.where! relation.table[:id].eq(10) relation.create_with_value = {:hello => 'world'} assert_equal({:hello => 'world', :id => 10}, relation.scope_for_create) @@ -109,9 +111,10 @@ module ActiveRecord # FIXME: is this really wanted or expected behavior? def test_scope_for_create_is_cached - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) assert_equal({}, relation.scope_for_create) + # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 relation.where! relation.table[:id].eq(10) assert_equal({}, relation.scope_for_create) @@ -126,31 +129,31 @@ module ActiveRecord end def test_empty_eager_loading? - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert !relation.eager_loading? end def test_eager_load_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation.eager_load! :b assert relation.eager_loading? end def test_references_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal [], relation.references_values relation = relation.references(:foo).references(:omg, :lol) assert_equal ['foo', 'omg', 'lol'], relation.references_values end def test_references_values_dont_duplicate - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation = relation.references(:foo).references(:foo) assert_equal ['foo'], relation.references_values end test 'merging a hash into a relation' do - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation = relation.merge where: :lol, readonly: true assert_equal [:lol], relation.where_values @@ -158,7 +161,7 @@ module ActiveRecord end test 'merging an empty hash into a relation' do - assert_equal [], Relation.new(FakeKlass, :b).merge({}).where_values + assert_equal [], Relation.new(FakeKlass, :b, nil).merge({}).where_values end test 'merging a hash with unknown keys raises' do @@ -166,7 +169,7 @@ module ActiveRecord end test '#values returns a dup of the values' do - relation = Relation.new(FakeKlass, :b).where! :foo + relation = Relation.new(FakeKlass, :b, nil).where! :foo values = relation.values values[:where] = nil @@ -174,12 +177,12 @@ module ActiveRecord end test 'relations can be created with a values hash' do - relation = Relation.new(FakeKlass, :b, where: [:foo]) + relation = Relation.new(FakeKlass, :b, nil, where: [:foo]) assert_equal [:foo], relation.where_values end test 'merging a single where value' do - relation = Relation.new(FakeKlass, :b) + relation = Relation.new(FakeKlass, :b, nil) relation.merge!(where: :foo) assert_equal [:foo], relation.where_values end @@ -192,13 +195,13 @@ module ActiveRecord end end - relation = Relation.new(klass, :b) + relation = Relation.new(klass, :b, nil) relation.merge!(where: ['foo = ?', 'bar']) assert_equal ['foo = bar'], relation.where_values end def test_merging_readonly_false - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) readonly_false_relation = relation.readonly(false) # test merging in both directions assert_equal false, relation.merge(readonly_false_relation).readonly_value diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 3a0398d08d..edb2d7fa7d 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1663,7 +1663,9 @@ class RelationTest < ActiveRecord::TestCase test 'using a custom table affects the wheres' do table_alias = Post.arel_table.alias('omg_posts') - relation = ActiveRecord::Relation.new Post, table_alias + table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias) + predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) + relation = ActiveRecord::Relation.new(Post, table_alias, predicate_builder) relation.where!(:foo => "bar") node = relation.arel.constraints.first.grep(Arel::Attributes::Attribute).first diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 01c686f934..a094136766 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -40,6 +40,11 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "schema_migrations"}, output end + def test_schema_dump_uses_force_cascade_on_create_table + output = dump_table_schema "authors" + assert_match %r{create_table "authors", force: :cascade}, output + end + def test_schema_dump_excludes_sqlite_sequence output = standard_dump assert_no_match %r{create_table "sqlite_sequence"}, output diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 56a0e92e1d..c261a5b1c0 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -256,4 +256,18 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal("second", t.content) assert_equal("second", t.reload.content) end + + def test_nil_is_not_changed_when_serialized_with_a_class + Topic.serialize(:content, Array) + + topic = Topic.new(content: nil) + + assert_not topic.content_changed? + end + + def test_classes_without_no_arg_constructors_are_not_supported + assert_raises(ArgumentError) do + Topic.serialize(:content, Regexp) + end + end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index cf50bd4ddb..0bbb4bb79e 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -562,6 +562,21 @@ class TransactionTest < ActiveRecord::TestCase assert !@second.destroyed?, 'not destroyed' end + def test_restore_frozen_state_after_double_destroy + topic = Topic.create + reply = topic.replies.create + + Topic.transaction do + topic.destroy # calls #destroy on reply (since dependent: destroy) + reply.destroy + + raise ActiveRecord::Rollback + end + + assert_not reply.frozen? + assert_not topic.frozen? + end + def test_sqlite_add_column_in_transaction return true unless current_adapter?(:SQLite3Adapter) diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb index da30de373e..34ed1d7b19 100644 --- a/activerecord/test/cases/type/decimal_test.rb +++ b/activerecord/test/cases/type/decimal_test.rb @@ -15,6 +15,11 @@ module ActiveRecord assert_equal BigDecimal.new("123.0"), type.type_cast_from_user(123.0) end + def test_type_cast_from_float_with_unspecified_precision + type = Decimal.new + assert_equal 22.68.to_d, type.type_cast_from_user(22.68) + end + def test_type_cast_decimal_from_rational_with_precision type = Decimal.new(precision: 2) assert_equal BigDecimal("0.33"), type.type_cast_from_user(Rational(1, 3)) @@ -33,6 +38,14 @@ module ActiveRecord type = Decimal.new assert_equal BigDecimal("1"), type.type_cast_from_user(value) end + + def test_changed? + type = Decimal.new + + assert type.changed?(5.0, 5.0, '5.0wibble') + assert_not type.changed?(5.0, 5.0, '5.0') + assert_not type.changed?(-5.0, -5.0, '-5.0') + end end end end diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb index 5942f77e18..ff956b7680 100644 --- a/activerecord/test/cases/type/integer_test.rb +++ b/activerecord/test/cases/type/integer_test.rb @@ -41,12 +41,20 @@ module ActiveRecord assert_nil type.type_cast_from_user(1.0/0.0) end + test "casting booleans for database" do + type = Type::Integer.new + assert_equal 1, type.type_cast_for_database(true) + assert_equal 0, type.type_cast_for_database(false) + end + test "changed?" do 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?(-5, -5, '-5') + assert_not type.changed?(-5, -5, '-5.0') assert_not type.changed?(nil, nil, nil) end diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index 4a92da38ce..2c0e282761 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require 'models/owner' require 'models/pet' +require 'models/person' class LengthValidationTest < ActiveRecord::TestCase fixtures :owners @@ -44,4 +45,21 @@ class LengthValidationTest < ActiveRecord::TestCase assert o.valid? end end + + def test_validates_size_of_reprects_records_marked_for_destruction + assert_nothing_raised { Owner.validates_size_of :pets, minimum: 1 } + owner = Owner.new + assert_not owner.save + assert owner.errors[:pets].any? + pet = owner.pets.build + assert owner.valid? + assert owner.save + + pet_count = Pet.count + assert_not owner.update_attributes pets_attributes: [ {_destroy: 1, id: pet.id} ] + assert_not owner.valid? + assert owner.errors[:pets].any? + assert_equal pet_count, Pet.count + end + end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 42f7fb4680..5a56616eb9 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -72,6 +72,7 @@ class Firm < Company # Oracle tests were failing because of that as the second fixture was selected has_one :account_using_primary_key, -> { order('id') }, :primary_key => "firm_id", :class_name => "Account" has_one :account_using_foreign_and_primary_keys, :foreign_key => "firm_name", :primary_key => "name", :class_name => "Account" + has_one :account_with_inexistent_foreign_key, class_name: 'Account', foreign_key: "inexistent" has_one :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete has_one :account_limit_500_with_hash_conditions, -> { where :credit_limit => 500 }, :foreign_key => "firm_id", :class_name => "Account" diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index 2e3a9a3681..cedb774b10 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -17,6 +17,8 @@ class Owner < ActiveRecord::Base after_commit :execute_blocks + accepts_nested_attributes_for :pets, allow_destroy: true + def blocks @blocks ||= [] end diff --git a/activerecord/test/models/tyre.rb b/activerecord/test/models/tyre.rb index bc3444aa7d..e50a21ca68 100644 --- a/activerecord/test/models/tyre.rb +++ b/activerecord/test/models/tyre.rb @@ -1,3 +1,11 @@ class Tyre < ActiveRecord::Base belongs_to :car + + def self.custom_find(id) + find(id) + end + + def self.custom_find_by(*args) + find_by(*args) + end end |