diff options
Diffstat (limited to 'activerecord')
107 files changed, 1595 insertions, 441 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 48005eee1a..2461dd517b 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,201 @@ +* Except keys of `build_record`'s argument from `create_scope` in `initialize_attributes`. + + Fixes #21893. + + *Yuichiro Kaneko* + +* Deprecate `connection.tables` on the SQLite3 and MySQL adapters. + Also deprecate passing arguments to `#tables`. + And deprecate `table_exists?`. + + The `#tables` method of some adapters (mysql, mysql2, sqlite3) would return + both tables and views while others (postgresql) just return tables. To make + their behavior consistent, `#tables` will return only tables in the future. + + The `#table_exists?` method would check both tables and views. To make + their behavior consistent with `#tables`, `#table_exists?` will check only + tables in the future. + + *Yuichiro Kaneko* + +* Improve support for non Active Record objects on `validates_associated` + + Skipping `marked_for_destruction?` when the associated object does not responds + to it make easier to validate virtual associations built on top of Active Model + objects and/or serialized objects that implement a `valid?` instance method. + + *Kassio Borges*, *Lucas Mazza* + +* Change connection management middleware to return a new response with + a body proxy, rather than mutating the original. + + *Kevin Buchanan* + +* Make `db:migrate:status` to render `1_some.rb` format migrate files. + + These files are in `db/migrate`: + + * 1_valid_people_have_last_names.rb + * 20150819202140_irreversible_migration.rb + * 20150823202140_add_admin_flag_to_users.rb + * 20150823202141_migration_tests.rb + * 2_we_need_reminders.rb + * 3_innocent_jointable.rb + + Before: + + $ bundle exec rake db:migrate:status + ... + + Status Migration ID Migration Name + -------------------------------------------------- + up 001 ********** NO FILE ********** + up 002 ********** NO FILE ********** + up 003 ********** NO FILE ********** + up 20150819202140 Irreversible migration + up 20150823202140 Add admin flag to users + up 20150823202141 Migration tests + + After: + + $ bundle exec rake db:migrate:status + ... + + Status Migration ID Migration Name + -------------------------------------------------- + up 001 Valid people have last names + up 002 We need reminders + up 003 Innocent jointable + up 20150819202140 Irreversible migration + up 20150823202140 Add admin flag to users + up 20150823202141 Migration tests + + *Yuichiro Kaneko* + +* Define `ActiveRecord::Sanitization.sanitize_sql_for_order` and use it inside + `preprocess_order_args`. + + *Yuichiro Kaneko* + +* Allow bigint with default nil for avoiding auto increment primary key. + + *Ryuta Kamizono* + +* Remove `DEFAULT_CHARSET` and `DEFAULT_COLLATION` in `MySQLDatabaseTasks`. + + We should omit the collation entirely rather than providing a default. + Then the choice is the responsibility of the server and MySQL distribution. + + *Ryuta Kamizono* + +* Alias `ActiveRecord::Relation#left_joins` to + `ActiveRecord::Relation#left_outer_joins`. + + *Takashi Kokubun* + +* Use advisory locking to raise a `ConcurrentMigrationError` instead of + attempting to migrate when another migration is currently running. + + *Sam Davies* + +* Added `ActiveRecord::Relation#left_outer_joins`. + + Example: + + User.left_outer_joins(:posts) + # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON + "posts"."user_id" = "users"."id" + + *Florian Thomas* + +* Support passing an array to `order` for SQL parameter sanitization. + + *Aaron Suggs* + +* Avoid disabling errors on the PostgreSQL connection when enabling the + `standard_conforming_strings` setting. Errors were previously disabled because + the setting wasn't writable in Postgres 8.1 and didn't exist in earlier + versions. Now Rails only supports Postgres 8.2+ we're fine to assume the + setting exists. Disabling errors caused problems when using a connection + pooling tool like PgBouncer because it's not guaranteed to have the same + connection between calls to `execute` and it could leave the connection + with errors disabled. + + Fixes #22101. + + *Harry Marr* + +* Set `scope.reordering_value` to `true` if `:reordering`-values are specified. + + Fixes #21886. + + *Hiroaki Izu* + +* Add support for bidirectional destroy dependencies. + + Fixes #13609. + + Example: + + class Content < ActiveRecord::Base + has_one :position, dependent: :destroy + end + + class Position < ActiveRecord::Base + belongs_to :content, dependent: :destroy + end + + *Seb Jacobs* + +* Includes HABTM returns correct size now. It's caused by the join dependency + only instantiates one HABTM object because the join table hasn't a primary key. + + Fixes #16032. + + Examples: + + before: + + Project.first.salaried_developers.size # => 3 + Project.includes(:salaried_developers).first.salaried_developers.size # => 1 + + after: + + Project.first.salaried_developers.size # => 3 + Project.includes(:salaried_developers).first.salaried_developers.size # => 3 + + *Bigxiang* + +* Add option to index errors in nested attributes + + For models which have nested attributes, errors within those models will + now be indexed if :index_errors is specified when defining a + has_many relationship, or if its set in the global config. + + Example: + + class Guitar < ActiveRecord::Base + has_many :tuning_pegs + accepts_nested_attributes_for :tuning_pegs + end + + class TuningPeg < ActiveRecord::Base + belongs_to :guitar + validates_numericality_of :pitch + end + + # Old style + guitar.errors["tuning_pegs.pitch"] = ["is not a number"] + + # New style (if defined globally, or set in has_many_relationship) + guitar.errors["tuning_pegs[1].pitch"] = ["is not a number"] + + *Michael Probber*, *Terence Sun* + +* Exit with non-zero status for failed database rake tasks. + + *Jay Hayes* + * Queries such as `Computer.joins(:monitor).group(:status).count` will now be interpreted as `Computer.joins(:monitor).group('computers.status').count` so that when `Computer` and `Monitor` have both `status` columns we don't @@ -5,21 +203,23 @@ *Rafael Sales* -* Add ability to default to `uuid` as primary key when generating database migrations +* Add ability to default to `uuid` as primary key when generating database migrations. + + Example: - config.generators do |g| - g.orm :active_record, primary_key_type: :uuid - end + config.generators do |g| + g.orm :active_record, primary_key_type: :uuid + end *Jon McCartie* -* Don't cache arguments in #find_by if they are an ActiveRecord::Relation +* Don't cache arguments in `#find_by` if they are an `ActiveRecord::Relation`. Fixes #20817 *Hiroaki Izu* -* Qualify column name inserted by `group` in calculation +* Qualify column name inserted by `group` in calculation. Giving `group` an unqualified column name now works, even if the relation has `JOIN` with another table which also has a column of the name. @@ -44,7 +244,7 @@ *Jake Worth* * Add an immutable string type to help reduce memory usage for apps which do - not need mutation detection on Strings. + not need mutation detection on strings. *Sean Griffin* @@ -62,7 +262,7 @@ *Yves Senn* -* No longer pass depreacted option `-i` to `pg_dump`. +* No longer pass deprecated option `-i` to `pg_dump`. *Paul Sadauskas* @@ -77,7 +277,7 @@ *Matthew Draper*, *Jean Boussier* -* Remove unused `pk_and_sequence_for` in AbstractMysqlAdapter. +* Remove unused `pk_and_sequence_for` in `AbstractMysqlAdapter`. *Ryuta Kamizono* @@ -103,16 +303,15 @@ *Jimmy Bourassa* -* Fixed taking precision into count when assigning a value to timestamp attribute +* Fixed taking precision into count when assigning a value to timestamp attribute. Timestamp column can have less precision than ruby timestamp In result in how big a fraction of a second can be stored in the database. - m = Model.create! - m.created_at.usec == m.reload.created_at.usec - # => false + m = Model.create! + m.created_at.usec == m.reload.created_at.usec # => false # due to different precision in Time.now and database column If the precision is low enough, (mysql default is 0, so it is always low @@ -134,7 +333,7 @@ *Yves Senn*, *Matthew Draper* * Add `ActiveRecord::Base.ignored_columns` to make some columns - invisible from ActiveRecord. + invisible from Active Record. *Jean Boussier* @@ -300,9 +499,9 @@ Example: - @users = User.where("name like ?", "%Alberto%") - @users.cache_key - => "/users/query-5942b155a43b139f2471b872ac54251f-3-20150714212107656125000" + @users = User.where("name like ?", "%Alberto%") + @users.cache_key + # => "/users/query-5942b155a43b139f2471b872ac54251f-3-20150714212107656125000" *Alberto Fernández-Capel* diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index d4f079e346..b806a2f832 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -149,7 +149,7 @@ module ActiveRecord class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc: end - # This error is raised when trying to eager load a poloymorphic association using a JOIN. + # This error is raised when trying to eager load a polymorphic association using a JOIN. # Eager loading polymorphic associations is only possible with # {ActiveRecord::Relation#preload}[rdoc-ref:QueryMethods#preload]. class EagerLoadPolymorphicError < ActiveRecordError diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index c7b396f3d4..d64ab64c99 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -163,9 +163,12 @@ module ActiveRecord @reflection = @owner.class._reflect_on_association(reflection_name) end - def initialize_attributes(record) #:nodoc: + def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc: + except_from_scope_attributes ||= {} skip_assign = [reflection.foreign_key, reflection.type].compact - attributes = create_scope.except(*(record.changed - skip_assign)) + assigned_keys = record.changed + assigned_keys += except_from_scope_attributes.keys.map(&:to_s) + attributes = create_scope.except(*(assigned_keys - skip_assign)) record.assign_attributes(attributes) set_inverse_instance(record) end @@ -248,7 +251,7 @@ module ActiveRecord def build_record(attributes) reflection.build_association(attributes) do |record| - initialize_attributes(record) + initialize_attributes(record, attributes) end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index dae468ba54..81c535d962 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -116,8 +116,7 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.add_destroy_callbacks(model, reflection) - name = reflection.name - model.after_destroy lambda { |o| o.association(name).handle_dependency } + model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency } end def self.define_validations(model, reflection) diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index a5c9f1666e..b888148841 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -62,13 +62,13 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.add_left_association(name, options) - belongs_to name, options + belongs_to name, required: false, **options self.left_reflection = _reflect_on_association(name) end def self.add_right_association(name, options) rhs_name = name.to_s.singularize.to_sym - belongs_to rhs_name, options + belongs_to rhs_name, required: false, **options self.right_reflection = _reflect_on_association(rhs_name) end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index f71b10120f..7864d4c536 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.valid_options(options) - super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type] + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type, :index_errors] end def self.valid_dependent_options diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index ca473beea3..0e4e951269 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -32,7 +32,7 @@ module ActiveRecord @alias_cache[node][column] end - class Table < Struct.new(:node, :columns) + class Table < Struct.new(:node, :columns) # :nodoc: def table Arel::Nodes::TableAlias.new node.table, node.aliased_table_name end @@ -103,9 +103,14 @@ module ActiveRecord join_root.drop(1).map!(&:reflection) end - def join_constraints(outer_joins) + def join_constraints(outer_joins, join_type) joins = join_root.children.flat_map { |child| - make_inner_joins join_root, child + + if join_type == Arel::Nodes::OuterJoin + make_left_outer_joins join_root, child + else + make_inner_joins join_root, child + end } joins.concat outer_joins.flat_map { |oj| @@ -150,7 +155,8 @@ module ActiveRecord message_bus.instrument('instantiation.active_record', payload) do result_set.each { |row_hash| - parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases) + parent_key = primary_key ? row_hash[primary_key] : row_hash + parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases) construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) } end @@ -175,6 +181,14 @@ module ActiveRecord [info] + child.children.flat_map { |c| make_outer_joins(child, c) } end + def make_left_outer_joins(parent, child) + tables = child.tables + join_type = Arel::Nodes::OuterJoin + info = make_constraints parent, child, tables, join_type + + [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) } + end + def make_inner_joins(parent, child) tables = child.tables join_type = Arel::Nodes::InnerJoin diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 3992a240b9..ecf6fb8643 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -54,6 +54,8 @@ module ActiveRecord autoload :BelongsTo, 'active_record/associations/preloader/belongs_to' end + NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) + # Eager loads the named associations for the given Active Record record(s). # # In this description, 'association name' shall refer to the name passed @@ -88,9 +90,6 @@ module ActiveRecord # [ :books, :author ] # { author: :avatar } # [ :books, { author: :avatar } ] - - NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) - def preload(records, associations, preload_scope = nil) records = Array.wrap(records).compact.uniq associations = Array.wrap(associations) @@ -107,6 +106,7 @@ module ActiveRecord private + # Loads all the given data into +records+ for the +association+. def preloaders_on(association, records, scope) case association when Hash @@ -132,6 +132,11 @@ module ActiveRecord } end + # Loads all the given data into +records+ for a singular +association+. + # + # Functions by instantiating a preloader class such as Preloader::HasManyThrough and + # call the +run+ method for each passed in class in the +records+ argument. + # # Not all records have the same class, so group then preload group on the reflection # itself so that if various subclass share the same association then we do not split # them unnecessarily @@ -181,6 +186,10 @@ module ActiveRecord def self.preloaded_records; []; end end + # Returns a class containing the logic needed to load preload the data + # and attach it to a relation. For example +Preloader::Association+ or + # +Preloader::HasManyThrough+. The class returned implements a `run` method + # that accepts a preloader. def preloader_for(reflection, owners, rhs_klass) return NullPreloader unless rhs_klass diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 29dd0643d6..e11a5cfb8a 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -134,7 +134,14 @@ module ActiveRecord else scope.joins!(reflection_scope.joins_values) end - scope.order! preload_values[:order] || values[:order] + + if order_values = preload_values[:order] || values[:order] + scope.order!(order_values) + end + + if preload_values[:reordering] || values[:reordering] + scope.reordering_value = true + end if preload_values[:readonly] || values[:readonly] scope.readonly! diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb index 5adffcd831..9939280fa4 100644 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb @@ -2,13 +2,8 @@ module ActiveRecord module Associations class Preloader class CollectionAssociation < Association #:nodoc: - private - def build_scope - super.order(preload_scope.values[:order] || reflection_scope.values[:order]) - end - def preload(preloader) associated_records_by_owner(preloader).each do |owner, records| association = owner.association(reflection.name) @@ -17,7 +12,6 @@ module ActiveRecord records.each { |record| association.set_inverse_instance(record) } end end - end end end diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb index 24728e9f01..c4add621ca 100644 --- a/activerecord/lib/active_record/associations/preloader/has_one.rb +++ b/activerecord/lib/active_record/associations/preloader/has_one.rb @@ -2,7 +2,6 @@ module ActiveRecord module Associations class Preloader class HasOne < SingularAssociation #:nodoc: - def association_key_name reflection.foreign_key end @@ -10,13 +9,6 @@ module ActiveRecord def owner_key_name reflection.active_record_primary_key end - - private - - def build_scope - super.order(preload_scope.values[:order] || reflection_scope.values[:order]) - end - end end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 56aa23b173..24aa47bb51 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -84,7 +84,9 @@ module ActiveRecord end scope.references! reflection_scope.values[:references] - scope = scope.order reflection_scope.values[:order] if scope.eager_loading? + if scope.eager_loading? && order_values = reflection_scope.values[:order] + scope = scope.order(order_values) + end end scope diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index cbdd4950a6..4ae585d3f5 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,7 +1,7 @@ require 'active_support/core_ext/enumerable' require 'active_support/core_ext/string/filters' require 'mutex_m' -require 'concurrent' +require 'concurrent/map' module ActiveRecord # = Active Record Attribute Methods diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index d35bc3e794..fc12c3f45a 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -140,6 +140,8 @@ module ActiveRecord included do Associations::Builder::Association.extensions << AssociationBuilderExtension + mattr_accessor :index_nested_attribute_errors, instance_writer: false + self.index_nested_attribute_errors = false end module ClassMethods # :nodoc: @@ -315,7 +317,7 @@ module ActiveRecord def validate_collection_association(reflection) if association = association_instance_get(reflection.name) if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) - records.each { |record| association_valid?(reflection, record) } + records.each_with_index { |record, index| association_valid?(reflection, record, index) } end end end @@ -323,14 +325,18 @@ module ActiveRecord # Returns whether or not the association is valid and applies any errors to # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> # enabled records if they're marked_for_destruction? or destroyed. - def association_valid?(reflection, record) + def association_valid?(reflection, record, index=nil) return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) unless valid = record.valid?(validation_context) if reflection.options[:autosave] record.errors.each do |attribute, message| - attribute = "#{reflection.name}.#{attribute}" + if index.nil? || (!reflection.options[:index_errors] && !ActiveRecord::Base.index_nested_attribute_errors) + attribute = "#{reflection.name}.#{attribute}" + else + attribute = "#{reflection.name}[#{index}].#{attribute}" + end errors[attribute] << message errors[attribute].uniq! end diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index bfedc4af3f..cfd8cbda67 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -292,10 +292,15 @@ module ActiveRecord end def destroy #:nodoc: + @_destroy_callback_already_called ||= false + return if @_destroy_callback_already_called + @_destroy_callback_already_called = true _run_destroy_callbacks { super } rescue RecordNotDestroyed => e @_association_destroy_exception = e false + ensure + @_destroy_callback_already_called = false end def touch(*) #:nodoc: 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 0d850c7625..486b7b6d25 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,5 +1,5 @@ require 'thread' -require 'concurrent' +require 'concurrent/map' require 'monitor' module ActiveRecord @@ -960,12 +960,11 @@ module ActiveRecord def call(env) testing = env['rack.test'] - response = @app.call(env) - response[2] = ::Rack::BodyProxy.new(response[2]) do + status, headers, body = @app.call(env) + proxy = ::Rack::BodyProxy.new(body) do ActiveRecord::Base.clear_active_connections! unless testing end - - response + [status, headers, proxy] rescue Exception ActiveRecord::Base.clear_active_connections! unless testing raise 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 e2ef56798b..abf0124562 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -202,13 +202,9 @@ module ActiveRecord # end # end # - # The table definitions - # The Columns are stored as a ColumnDefinition in the #columns attribute. class TableDefinition include ColumnMethods - # An array of ColumnDefinition objects, representing the column changes - # that have been defined. attr_accessor :indexes attr_reader :name, :temporary, :options, :as, :foreign_keys, :native 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 d5f8dbc8fc..5cacf6eddc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -263,7 +263,7 @@ module ActiveRecord yield td if block_given? - if options[:force] && table_exists?(table_name) + if options[:force] && data_source_exists?(table_name) drop_table(table_name, options) end @@ -1088,7 +1088,7 @@ module ActiveRecord if index_name.length > max_index_length raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" end - if table_exists?(table_name) && index_name_exists?(table_name, index_name, false) + if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false) raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" end index_columns = quoted_columns_for_index(column_names, options).join(", ") diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 402159ac13..4d4dc07b04 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -214,6 +214,11 @@ module ActiveRecord false end + # Does this adapter support application-enforced advisory locking? + def supports_advisory_locks? + false + end + # Should primary key values be selected from their corresponding # sequence before the insert statement? If true, next_sequence_value # is called before each insert to set the record's primary key. @@ -280,6 +285,20 @@ module ActiveRecord def enable_extension(name) end + # This is meant to be implemented by the adapters that support advisory + # locks + # + # Return true if we got the lock, otherwise false + def get_advisory_lock(lock_id) # :nodoc: + end + + # This is meant to be implemented by the adapters that support advisory + # locks. + # + # Return true if we released the lock, otherwise false + def release_advisory_lock(lock_id) # :nodoc: + end + # A list of extensions, to be filled in by adapters that support them. def extensions [] @@ -519,7 +538,7 @@ module ActiveRecord def translate_exception(exception, message) # override in derived class - ActiveRecord::StatementInvalid.new(message, exception) + ActiveRecord::StatementInvalid.new(message) end def without_prepared_statement?(binds) 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 251acf1c83..deef246c37 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -134,9 +134,7 @@ module ActiveRecord time: { name: "time" }, date: { name: "date" }, binary: { name: "blob" }, - blob: { name: "blob" }, boolean: { name: "tinyint", limit: 1 }, - bigint: { name: "bigint" }, json: { name: "json" }, } @@ -220,6 +218,20 @@ module ActiveRecord version >= '5.6.4' end + # 5.0.0 definitely supports it, possibly supported by earlier versions but + # not sure + def supports_advisory_locks? + version >= '5.0.0' + end + + def get_advisory_lock(lock_name, timeout = 0) # :nodoc: + select_value("SELECT GET_LOCK('#{lock_name}', #{timeout});").to_s == '1' + end + + def release_advisory_lock(lock_name) # :nodoc: + select_value("SELECT RELEASE_LOCK('#{lock_name}')").to_s == '1' + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -483,18 +495,43 @@ module ActiveRecord end def tables(name = nil) # :nodoc: + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #tables currently returns both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only return tables. + Use #data_sources instead. + MSG + + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG + end + + data_sources + end + + def data_sources sql = "SELECT table_name FROM information_schema.tables " sql << "WHERE table_schema = #{quote(@config[:database])}" select_values(sql, 'SCHEMA') end - alias data_sources tables def truncate(table_name, name = nil) execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name end def table_exists?(table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG + + data_source_exists?(table_name) + end + + def data_source_exists?(table_name) return false unless table_name.present? schema, name = table_name.to_s.split('.', 2) @@ -505,7 +542,6 @@ module ActiveRecord select_values(sql, 'SCHEMA').any? end - alias data_source_exists? table_exists? def views # :nodoc: select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA') @@ -853,9 +889,9 @@ module ActiveRecord def translate_exception(exception, message) case error_number(exception) when 1062 - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) when 1452 - InvalidForeignKey.new(message, exception) + InvalidForeignKey.new(message) else super end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb index 29e8c73d46..ca7dfda80d 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -3,7 +3,7 @@ module ActiveRecord module MySQL module ColumnMethods def primary_key(name, type = :primary_key, **options) - options[:auto_increment] = true if type == :bigint + options[:auto_increment] = true if type == :bigint && !options.key?(:default) super end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index 3c48d0554e..9dee3172f4 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -4,8 +4,11 @@ module ActiveRecord module ColumnDumper def column_spec_for_primary_key(column) spec = {} - if column.auto_increment? - spec[:id] = ':bigint' if column.bigint? + if column.bigint? + spec[:id] = ':bigint' + spec[:default] = schema_default(column) || 'nil' unless column.auto_increment? + spec[:unsigned] = 'true' if column.unsigned? + elsif column.auto_increment? spec[:unsigned] = 'true' if column.unsigned? return if spec.empty? else diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 42c4a14f00..773ecbe126 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -20,7 +20,7 @@ module ActiveRecord ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) rescue Mysql2::Error => error if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index fddb318553..89d18ee14e 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -38,7 +38,7 @@ module ActiveRecord ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config) rescue Mysql::Error => error if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index aaf5b2898b..a48d64f7bd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -70,6 +70,12 @@ module ActiveRecord # Returns the list of all tables in the schema search path. def tables(name = nil) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG + end + select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA') end @@ -87,6 +93,16 @@ module ActiveRecord # If the schema is not specified as part of +name+ then it will only find tables within # the current schema search path (regardless of permissions to access tables in other schemas) def table_exists?(name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG + + data_source_exists?(name) + end + + def data_source_exists?(name) name = Utils.extract_schema_qualified_name(name.to_s) return false unless name.identifier @@ -99,7 +115,6 @@ module ActiveRecord AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} SQL end - alias data_source_exists? table_exists? def views # :nodoc: select_values(<<-SQL, 'SCHEMA') diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 236c067fd5..719592349b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -19,12 +19,6 @@ require 'ipaddr' module ActiveRecord module ConnectionHandling # :nodoc: - VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout, - :client_encoding, :options, :application_name, :fallback_application_name, - :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count, - :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey, - :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service] - # Establishes a connection to the database that's used by all Active Record objects def postgresql_connection(config) conn_params = config.symbolize_keys @@ -36,7 +30,8 @@ module ActiveRecord conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database] # Forward only valid config params to PGconn.connect. - conn_params.keep_if { |k, _| VALID_CONN_PARAMS.include?(k) } + valid_conn_param_keys = PGconn.conndefaults_hash.keys + [:requiressl] + conn_params.slice!(*valid_conn_param_keys) # The postgres drivers don't allow the creation of an unconnected PGconn object, # so just pass a nil connection object for the time being. @@ -77,7 +72,6 @@ module ActiveRecord NATIVE_DATABASE_TYPES = { primary_key: "serial primary key", - bigserial: "bigserial", string: { name: "character varying" }, text: { name: "text" }, integer: { name: "integer" }, @@ -94,7 +88,6 @@ module ActiveRecord int8range: { name: "int8range" }, binary: { name: "bytea" }, boolean: { name: "boolean" }, - bigint: { name: "bigint" }, xml: { name: "xml" }, tsvector: { name: "tsvector" }, hstore: { name: "hstore" }, @@ -281,18 +274,18 @@ module ActiveRecord true end - # Enable standard-conforming strings if available. def set_standard_conforming_strings - old, self.client_min_messages = client_min_messages, 'panic' - execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil - ensure - self.client_min_messages = old + execute('SET standard_conforming_strings = on', 'SCHEMA') end def supports_ddl_transactions? true end + def supports_advisory_locks? + true + end + def supports_explain? true end @@ -311,6 +304,20 @@ module ActiveRecord postgresql_version >= 90300 end + def get_advisory_lock(lock_id) # :nodoc: + unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") + end + select_value("SELECT pg_try_advisory_lock(#{lock_id});") + end + + def release_advisory_lock(lock_id) # :nodoc: + unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") + end + select_value("SELECT pg_advisory_unlock(#{lock_id})") + end + def enable_extension(name) exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap { reload_type_map @@ -393,9 +400,9 @@ module ActiveRecord case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE) when UNIQUE_VIOLATION - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) when FOREIGN_KEY_VIOLATION - InvalidForeignKey.new(message, exception) + InvalidForeignKey.new(message) else super end @@ -580,7 +587,7 @@ module ActiveRecord @connection.exec_prepared(stmt_key, type_casted_binds) end rescue ActiveRecord::StatementInvalid => e - pgerror = e.original_exception + pgerror = e.cause # Get the PG code for the failure. Annoyingly, the code for # prepared statements whose return value may have changed is @@ -636,7 +643,7 @@ module ActiveRecord configure_connection rescue ::PG::Error => error if error.message.include?("does not exist") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -651,7 +658,7 @@ module ActiveRecord self.client_min_messages = @config[:min_messages] || 'warning' self.schema_search_path = @config[:schema_search_path] || @config[:schema_order] - # Use standard-conforming strings if available so we don't have to do the E'...' dance. + # Use standard-conforming strings so we don't have to do the E'...' dance. set_standard_conforming_strings # If using Active Record's time zone support configure the connection to return diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 9028c1fcb9..90df9b8825 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -33,7 +33,7 @@ module ActiveRecord ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config) rescue Errno::ENOENT => error if error.message.include?("No such file or directory") - raise ActiveRecord::NoDatabaseError.new(error.message, error) + raise ActiveRecord::NoDatabaseError else raise end @@ -312,11 +312,36 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== def tables(name = nil) # :nodoc: + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #tables currently returns both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only return tables. + Use #data_sources instead. + MSG + + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG + end + + data_sources + end + + def data_sources select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", 'SCHEMA') end - alias data_sources tables def table_exists?(table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG + + data_source_exists?(table_name) + end + + def data_source_exists?(table_name) return false unless table_name.present? sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'" @@ -324,7 +349,6 @@ module ActiveRecord select_values(sql, 'SCHEMA').any? end - alias data_source_exists? table_exists? def views # :nodoc: select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", 'SCHEMA') @@ -559,7 +583,7 @@ module ActiveRecord # Older versions of SQLite return: # column *column_name* is not unique when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/ - RecordNotUnique.new(message, exception) + RecordNotUnique.new(message) else super end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 142b6e8599..1250f8a3c3 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -193,8 +193,8 @@ module ActiveRecord } begin statement.execute(hash.values, self, connection).first - rescue TypeError => e - raise ActiveRecord::StatementInvalid.new(e.message, e) + rescue TypeError + raise ActiveRecord::StatementInvalid rescue RangeError nil end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 8fba6fcc35..7ded96f8fb 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -104,7 +104,7 @@ module ActiveRecord super end - class EnumType < Type::Value + class EnumType < Type::Value # :nodoc: def initialize(name, mapping) @name = name @mapping = mapping diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 533c86a6a9..1cd2c2ef8c 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -94,13 +94,21 @@ module ActiveRecord # Superclass for all database execution errors. # - # Wraps the underlying database error as +original_exception+. + # Wraps the underlying database error as +cause+. class StatementInvalid < ActiveRecordError - attr_reader :original_exception def initialize(message = nil, original_exception = nil) - @original_exception = original_exception - super(message) + if original_exception + ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ + "Exceptions will automatically capture the original exception.", caller) + end + + super(message || $!.try(:message)) + end + + def original_exception + ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) + cause end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 17e7c828b9..deb7d7d06d 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -875,9 +875,7 @@ module ActiveRecord self.pre_loaded_fixtures = false self.config = ActiveRecord::Base - self.fixture_class_names = Hash.new do |h, fixture_set_name| - h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config) - end + self.fixture_class_names = {} silence_warnings do define_singleton_method :use_transactional_tests do diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index c26842014d..589c70db0d 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -55,7 +55,7 @@ module ActiveRecord subclass = subclass_from_attributes(attrs) end - if subclass + if subclass && subclass != self subclass.new(*args, &block) else super @@ -167,17 +167,23 @@ module ActiveRecord end def find_sti_class(type_name) - if store_full_sti_class - ActiveSupport::Dependencies.constantize(type_name) - else - compute_type(type_name) + subclass = begin + if store_full_sti_class + ActiveSupport::Dependencies.constantize(type_name) + else + compute_type(type_name) + end + rescue NameError + raise SubclassNotFound, + "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " \ + "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " \ + "Please rename this column if you didn't intend it to be used for storing the inheritance class " \ + "or overwrite #{name}.inheritance_column to use another column for that information." + end + unless subclass == self || descendants.include?(subclass) + raise SubclassNotFound, "Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}" end - rescue NameError - raise SubclassNotFound, - "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " + - "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " + - "Please rename this column if you didn't intend it to be used for storing the inheritance class " + - "or overwrite #{name}.inheritance_column to use another column for that information." + subclass end def type_condition(table = arel_table) @@ -199,15 +205,7 @@ module ActiveRecord subclass_name = attrs.with_indifferent_access[inheritance_column] if subclass_name.present? - subclass = find_sti_class(subclass_name) - - if subclass.name != self.name - unless descendants.include?(subclass) - raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}") - end - - subclass - end + find_sti_class(subclass_name) end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index c8b96b8de0..ca2537cdc3 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -135,6 +135,14 @@ module ActiveRecord end end + class ConcurrentMigrationError < MigrationError #:nodoc: + DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze + + def initialize(message = DEFAULT_MESSAGE) + super + end + end + # = Active Record Migrations # # Migrations can manage the evolution of a schema used by several physical @@ -469,6 +477,7 @@ module ActiveRecord class Migration autoload :CommandRecorder, 'active_record/migration/command_recorder' + MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc: # This class is used to verify that all migrations have been run before # loading a web page if config.active_record.migration_error is set to :page_load @@ -958,10 +967,12 @@ module ActiveRecord end def get_all_versions(connection = Base.connection) - if connection.table_exists?(schema_migrations_table_name) - SchemaMigration.all.map { |x| x.version.to_i }.sort - else - [] + ActiveSupport::Deprecation.silence do + if connection.table_exists?(schema_migrations_table_name) + SchemaMigration.all.map { |x| x.version.to_i }.sort + else + [] + end end end @@ -987,14 +998,21 @@ module ActiveRecord Array(@migrations_paths) end + def match_to_migration_filename?(filename) # :nodoc: + File.basename(filename) =~ Migration::MigrationFilenameRegexp + end + + def parse_migration_filename(filename) # :nodoc: + File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + end + def migrations(paths) paths = Array(paths) files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }] migrations = files.map do |file| - version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/).first - + version, name, scope = parse_migration_filename(file) raise IllegalMigrationNameError.new(file) unless version version = version.to_i name = name.camelize @@ -1042,32 +1060,18 @@ module ActiveRecord alias :current :current_migration def run - migration = migrations.detect { |m| m.version == @target_version } - raise UnknownMigrationVersionError.new(@target_version) if migration.nil? - unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i)) - begin - execute_migration_in_transaction(migration, @direction) - rescue => e - canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : "" - raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace - end + if use_advisory_lock? + with_advisory_lock { run_without_lock } + else + run_without_lock end end def migrate - if !target && @target_version && @target_version > 0 - raise UnknownMigrationVersionError.new(@target_version) - end - - runnable.each do |migration| - Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger - - begin - execute_migration_in_transaction(migration, @direction) - rescue => e - canceled_msg = use_transaction?(migration) ? "this and " : "" - raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace - end + if use_advisory_lock? + with_advisory_lock { migrate_without_lock } + else + migrate_without_lock end end @@ -1092,10 +1096,45 @@ module ActiveRecord end def migrated - @migrated_versions ||= Set.new(self.class.get_all_versions) + @migrated_versions || load_migrated + end + + def load_migrated + @migrated_versions = Set.new(self.class.get_all_versions) end private + + def run_without_lock + migration = migrations.detect { |m| m.version == @target_version } + raise UnknownMigrationVersionError.new(@target_version) if migration.nil? + unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i)) + begin + execute_migration_in_transaction(migration, @direction) + rescue => e + canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : "" + raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace + end + end + end + + def migrate_without_lock + if !target && @target_version && @target_version > 0 + raise UnknownMigrationVersionError.new(@target_version) + end + + runnable.each do |migration| + Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger + + begin + execute_migration_in_transaction(migration, @direction) + rescue => e + canceled_msg = use_transaction?(migration) ? "this and " : "" + raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace + end + end + end + def ran?(migration) migrated.include?(migration.version.to_i) end @@ -1157,5 +1196,25 @@ module ActiveRecord def use_transaction?(migration) !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions? end + + def use_advisory_lock? + Base.connection.supports_advisory_locks? + end + + def with_advisory_lock + lock_id = generate_migrator_advisory_lock_id + got_lock = Base.connection.get_advisory_lock(lock_id) + raise ConcurrentMigrationError unless got_lock + load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock + yield + ensure + Base.connection.release_advisory_lock(lock_id) if got_lock + end + + MIGRATOR_SALT = 2053462845 + def generate_migrator_advisory_lock_id + db_name_hash = Zlib.crc32(Base.connection.current_database) + MIGRATOR_SALT * db_name_hash + end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index a9bd094a66..e3f304b0af 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -339,6 +339,9 @@ module ActiveRecord @columns = nil @columns_hash = nil @attribute_names = nil + direct_descendants.each do |descendant| + descendant.send(:reload_schema_from_cache) + end end # Guesses the table name, but does not decorate it with prefix and suffix information. diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 87a1988f2f..1f429cfd94 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -7,7 +7,7 @@ module ActiveRecord delegate :find_by, :find_by!, to: :all delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all delegate :find_each, :find_in_batches, :in_batches, to: :all - delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or, + delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index d940ac244a..5b1ea16f0b 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -34,7 +34,7 @@ db_namespace = namespace :db do end end - # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." + # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:purge:all to purge all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." task :purge => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.purge_current end @@ -100,12 +100,14 @@ db_namespace = namespace :db do file_list = ActiveRecord::Tasks::DatabaseTasks.migrations_paths.flat_map do |path| - # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern - Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do - version = ActiveRecord::SchemaMigration.normalize_migration_number($1) + Dir.foreach(path).map do |file| + next unless ActiveRecord::Migrator.match_to_migration_filename?(file) + + version, name, scope = ActiveRecord::Migrator.parse_migration_filename(file) + version = ActiveRecord::SchemaMigration.normalize_migration_number(version) status = db_list.delete(version) ? 'up' : 'down' - [status, version, $2.humanize] - end + [status, version, (name + scope).humanize] + end.compact end db_list.map! do |version| diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 5b9d45d871..a549b28f16 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -371,7 +371,7 @@ module ActiveRecord end def foreign_key - @foreign_key ||= options[:foreign_key] || derive_foreign_key + @foreign_key ||= options[:foreign_key] || derive_foreign_key.freeze end def association_foreign_key diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 392b462aa9..f100476374 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -4,7 +4,7 @@ module ActiveRecord # = Active Record \Relation class Relation MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, - :order, :joins, :references, + :order, :joins, :left_joins, :left_outer_joins, :references, :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index f5afc1000d..2dc52982c9 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -428,6 +428,27 @@ module ActiveRecord self end + # Performs a left outer joins on +args+: + # + # User.left_outer_joins(:posts) + # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id" + # + def left_outer_joins(*args) + check_if_method_has_arguments!(:left_outer_joins, args) + + args.compact! + args.flatten! + + spawn.left_outer_joins!(*args) + end + alias :left_joins :left_outer_joins + + def left_outer_joins!(*args) # :nodoc: + self.left_outer_joins_values += args + self + end + alias :left_joins! :left_outer_joins! + # Returns a new relation, which is the result of filtering the current relation # according to the conditions in the arguments. # @@ -878,6 +899,7 @@ module ActiveRecord arel = Arel::SelectManager.new(table) build_joins(arel, joins_values.flatten) unless joins_values.empty? + build_left_outer_joins(arel, left_outer_joins_values.flatten) unless left_outer_joins_values.empty? arel.where(where_clause.ast) unless where_clause.empty? arel.having(having_clause.ast) unless having_clause.empty? @@ -937,6 +959,19 @@ module ActiveRecord end end + def build_left_outer_joins(manager, outer_joins) + buckets = outer_joins.group_by do |join| + case join + when Hash, Symbol, Array + :association_join + else + raise ArgumentError, 'only Hash, Symbol and Array are allowed' + end + end + + build_join_query(manager, buckets, Arel::Nodes::OuterJoin) + end + def build_joins(manager, joins) buckets = joins.group_by do |join| case join @@ -952,6 +987,11 @@ module ActiveRecord raise 'unknown class: %s' % join.class.name end end + + build_join_query(manager, buckets, Arel::Nodes::InnerJoin) + end + + def build_join_query(manager, buckets, join_type) buckets.default = [] association_joins = buckets[:association_join] @@ -967,7 +1007,7 @@ module ActiveRecord join_list ) - join_infos = join_dependency.join_constraints stashed_association_joins + join_infos = join_dependency.join_constraints stashed_association_joins, join_type join_infos.each do |info| info.joins.each { |join| manager.from(join) } @@ -1045,6 +1085,9 @@ module ActiveRecord end def preprocess_order_args(order_args) + order_args.map! do |arg| + klass.send(:sanitize_sql_for_order, arg) + end order_args.flatten! validate_order_args(order_args) diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb index 14e1bf89fa..0a1814b3dd 100644 --- a/activerecord/lib/active_record/relation/record_fetch_warning.rb +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -23,11 +23,13 @@ module ActiveRecord end end + # :stopdoc: ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| payload = args.last QueryRegistry.queries << payload[:sql] end + # :startdoc: class QueryRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 5c3318651a..67d7f83cb4 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -12,6 +12,7 @@ module ActiveRecord # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation. # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. + # # Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) ) # # Performs a single join query with both where conditions. # @@ -37,11 +38,14 @@ module ActiveRecord end def merge!(other) # :nodoc: - if !other.is_a?(Relation) && other.respond_to?(:to_proc) + if other.is_a?(Hash) + Relation::HashMerger.new(self, other).merge + elsif other.is_a?(Relation) + Relation::Merger.new(self, other).merge + elsif other.respond_to?(:to_proc) instance_exec(&other) else - klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger - klass.new(self, other).merge + raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation" end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 1cf4b09bf3..0c15f45db9 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -53,6 +53,22 @@ module ActiveRecord end end + # Accepts an array, or string of SQL conditions and sanitizes + # them into a valid SQL fragment for a ORDER clause. + # + # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) + # # => "field(id, 1,3,2)" + # + # sanitize_sql_for_order("id ASC") + # # => "id ASC" + def sanitize_sql_for_order(condition) + if condition.is_a?(Array) && condition.first.to_s.include?('?') + sanitize_sql_array(condition) + else + condition + end + end + # Accepts a hash of SQL conditions and replaces those attributes # that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of] # relationship with their expanded aggregate attribute values. diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index b384529e75..51b9b17395 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -21,7 +21,7 @@ module ActiveRecord end def table_exists? - connection.table_exists?(table_name) + ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) } end def create_table(limit=nil) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index ea7927a435..c0c29a618c 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -94,8 +94,9 @@ module ActiveRecord rescue DatabaseAlreadyExists $stderr.puts "#{configuration['database']} already exists" rescue Exception => error - $stderr.puts error, *(error.backtrace) + $stderr.puts error $stderr.puts "Couldn't create database for #{configuration.inspect}" + raise end def create_all @@ -115,8 +116,9 @@ module ActiveRecord rescue ActiveRecord::NoDatabaseError $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error - $stderr.puts error, *(error.backtrace) + $stderr.puts error $stderr.puts "Couldn't drop #{configuration['database']}" + raise end def drop_all diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 8929aa85c8..100df9c6c6 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -1,8 +1,6 @@ module ActiveRecord module Tasks # :nodoc: class MySQLDatabaseTasks # :nodoc: - DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8' - DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci' ACCESS_DENIED_ERROR = 1045 delegate :connection, :establish_connection, to: ActiveRecord::Base @@ -87,12 +85,6 @@ module ActiveRecord Hash.new.tap do |options| options[:charset] = configuration['encoding'] if configuration.include? 'encoding' options[:collation] = configuration['collation'] if configuration.include? 'collation' - - # Set default charset only when collation isn't set. - options[:charset] ||= DEFAULT_CHARSET unless options[:collation] - - # Set default collation only when charset is also default. - options[:collation] ||= DEFAULT_COLLATION if options[:charset] == DEFAULT_CHARSET end end diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index 9ab64d0325..9ec3c8a94a 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -19,11 +19,15 @@ module ActiveRecord path = Pathname.new configuration['database'] file = path.absolute? ? path.to_s : File.join(root, path) - FileUtils.rm(file) if File.exist?(file) + FileUtils.rm(file) + rescue Errno::ENOENT => error + raise NoDatabaseError.new(error.message, error) end def purge drop + rescue NoDatabaseError + ensure create end diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb index 81d7ed39bb..850a7a4e09 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -1,4 +1,4 @@ -require 'concurrent' +require 'concurrent/map' module ActiveRecord module Type diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb index 63ba10c289..accc339d00 100644 --- a/activerecord/lib/active_record/type_caster.rb +++ b/activerecord/lib/active_record/type_caster.rb @@ -2,6 +2,6 @@ require 'active_record/type_caster/map' require 'active_record/type_caster/connection' module ActiveRecord - module TypeCaster + module TypeCaster # :nodoc: end end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index 868d08ed44..7ed8dcc313 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -1,6 +1,6 @@ module ActiveRecord module TypeCaster - class Connection + class Connection # :nodoc: def initialize(klass, table_name) @klass = klass @table_name = table_name diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index 4b1941351c..3a367b3999 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -1,6 +1,6 @@ module ActiveRecord module TypeCaster - class Map + class Map # :nodoc: def initialize(types) @types = types end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 32fbaf0a91..b14db85167 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -2,10 +2,16 @@ module ActiveRecord module Validations class AssociatedValidator < ActiveModel::EachValidator #:nodoc: def validate_each(record, attribute, value) - if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any? - record.errors.add(attribute, :invalid, options.merge(:value => value)) + if Array(value).reject { |r| valid_object?(r) }.any? + record.errors.add(attribute, :invalid, options.merge(value: value)) end end + + private + + def valid_object?(record) + (record.respond_to?(:marked_for_destruction?) && record.marked_for_destruction?) || record.valid? + end end module ClassMethods diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 62579a4a7a..abe1ea7c90 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -23,7 +23,8 @@ module ActiveRecord end def test_tables - tables = @connection.tables + tables = nil + ActiveSupport::Deprecation.silence { tables = @connection.tables } assert tables.include?("accounts") assert tables.include?("authors") assert tables.include?("tasks") @@ -31,9 +32,15 @@ module ActiveRecord end def test_table_exists? - assert @connection.table_exists?("accounts") - assert !@connection.table_exists?("nonexistingtable") - assert !@connection.table_exists?(nil) + ActiveSupport::Deprecation.silence do + assert @connection.table_exists?("accounts") + assert !@connection.table_exists?("nonexistingtable") + assert !@connection.table_exists?(nil) + end + end + + def test_table_exists_checking_both_tables_and_views_is_deprecated + assert_deprecated { @connection.table_exists?("accounts") } end def test_data_sources @@ -151,14 +158,16 @@ module ActiveRecord def test_uniqueness_violations_are_translated_to_specific_exception @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" - assert_raises(ActiveRecord::RecordNotUnique) do + error = assert_raises(ActiveRecord::RecordNotUnique) do @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" end + + assert_not_nil error.cause end unless current_adapter?(:SQLite3Adapter) def test_foreign_key_violations_are_translated_to_specific_exception - assert_raises(ActiveRecord::InvalidForeignKey) do + error = assert_raises(ActiveRecord::InvalidForeignKey) do # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method if @connection.prefetch_primary_key? id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) @@ -167,6 +176,8 @@ module ActiveRecord @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" end end + + assert_not_nil error.cause end def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false @@ -174,11 +185,13 @@ module ActiveRecord self.table_name = 'fk_test_has_fk' end - assert_raises(ActiveRecord::InvalidForeignKey) do + error = assert_raises(ActiveRecord::InvalidForeignKey) do has_fk = klass_has_fk.new has_fk.fk_id = 1231231231 has_fk.save(validate: false) end + + assert_not_nil error.cause end end @@ -231,13 +244,25 @@ module ActiveRecord unless current_adapter?(:PostgreSQLAdapter) def test_log_invalid_encoding - assert_raise ActiveRecord::StatementInvalid do + error = assert_raise ActiveRecord::StatementInvalid do @connection.send :log, "SELECT 'ы' FROM DUAL" do raise 'ы'.force_encoding(Encoding::ASCII_8BIT) end end + + assert_not_nil error.cause + end + end + + if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :SQLite3Adapter) + def test_tables_returning_both_tables_and_views_is_deprecated + assert_deprecated { @connection.tables } end end + + def test_passing_arguments_to_tables_is_deprecated + assert_deprecated { @connection.tables(:books) } + end end class AdapterTestWithoutTransaction < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index decac9e83b..390dd15b92 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -170,6 +170,34 @@ class MysqlConnectionTest < ActiveRecord::MysqlTestCase end end + def test_get_and_release_advisory_lock + lock_name = "test_lock_name" + + got_lock = @connection.get_advisory_lock(lock_name) + assert got_lock, "get_advisory_lock should have returned true but it didn't" + + assert_equal test_lock_free(lock_name), false, + "expected the test advisory lock to be held but it wasn't" + + released_lock = @connection.release_advisory_lock(lock_name) + assert released_lock, "expected release_advisory_lock to return true but it didn't" + + assert test_lock_free(lock_name), 'expected the test lock to be available after releasing' + end + + def test_release_non_existent_advisory_lock + lock_name = "fake_lock_name" + released_non_existent_lock = @connection.release_advisory_lock(lock_name) + assert_equal released_non_existent_lock, false, + 'expected release_advisory_lock to return false when there was no lock to release' + end + + protected + + def test_lock_free(lock_name) + @connection.select_value("SELECT IS_FREE_LOCK('#{lock_name}');") == '1' + end + private def with_example_table(&block) diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index 2e18f609fd..14dbdd375b 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -14,38 +14,30 @@ module ActiveRecord @db_name = db @omgpost = Class.new(ActiveRecord::Base) do + self.inheritance_column = :disabled self.table_name = "#{db}.#{table}" def self.name; 'Post'; end end - - @connection.create_table "mysql_doubles" - end - - teardown do - @connection.drop_table "mysql_doubles", if_exists: true - end - - class MysqlDouble < ActiveRecord::Base - self.table_name = "mysql_doubles" end def test_float_limits - @connection.add_column :mysql_doubles, :float_no_limit, :float - @connection.add_column :mysql_doubles, :float_short, :float, limit: 5 - @connection.add_column :mysql_doubles, :float_long, :float, limit: 53 - - @connection.add_column :mysql_doubles, :float_23, :float, limit: 23 - @connection.add_column :mysql_doubles, :float_24, :float, limit: 24 - @connection.add_column :mysql_doubles, :float_25, :float, limit: 25 - MysqlDouble.reset_column_information + @connection.create_table :mysql_doubles do |t| + t.float :float_no_limit + t.float :float_short, limit: 5 + t.float :float_long, limit: 53 + + t.float :float_23, limit: 23 + t.float :float_24, limit: 24 + t.float :float_25, limit: 25 + end - column_no_limit = MysqlDouble.columns.find { |c| c.name == 'float_no_limit' } - column_short = MysqlDouble.columns.find { |c| c.name == 'float_short' } - column_long = MysqlDouble.columns.find { |c| c.name == 'float_long' } + column_no_limit = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_no_limit' } + column_short = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_short' } + column_long = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_long' } - column_23 = MysqlDouble.columns.find { |c| c.name == 'float_23' } - column_24 = MysqlDouble.columns.find { |c| c.name == 'float_24' } - column_25 = MysqlDouble.columns.find { |c| c.name == 'float_25' } + column_23 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_23' } + column_24 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_24' } + column_25 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_25' } # Mysql floats are precision 0..24, Mysql doubles are precision 25..53 assert_equal 24, column_no_limit.limit @@ -55,6 +47,8 @@ module ActiveRecord assert_equal 24, column_23.limit assert_equal 24, column_24.limit assert_equal 53, column_25.limit + ensure + @connection.drop_table "mysql_doubles", if_exists: true end def test_schema @@ -65,13 +59,13 @@ module ActiveRecord assert_equal 'id', @omgpost.primary_key end - def test_table_exists? + def test_data_source_exists? name = @omgpost.table_name - assert @connection.table_exists?(name), "#{name} table should exist" + assert @connection.data_source_exists?(name), "#{name} data_source should exist" end - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") + def test_data_source_exists_wrong_schema + assert(!@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist") end def test_dump_indexes diff --git a/activerecord/test/cases/adapters/mysql/sp_test.rb b/activerecord/test/cases/adapters/mysql/sp_test.rb index 579c3273c6..7849248dcc 100644 --- a/activerecord/test/cases/adapters/mysql/sp_test.rb +++ b/activerecord/test/cases/adapters/mysql/sp_test.rb @@ -7,23 +7,24 @@ class MysqlStoredProcedureTest < ActiveRecord::MysqlTestCase def setup @connection = ActiveRecord::Base.connection + unless ActiveRecord::Base.connection.version >= '5.6.0' || Mysql.const_defined?(:CLIENT_MULTI_RESULTS) + skip("no stored procedure support") + end end # Test that MySQL allows multiple results for stored procedures # # In MySQL 5.6, CLIENT_MULTI_RESULTS is enabled by default. # http://dev.mysql.com/doc/refman/5.6/en/call.html - if ActiveRecord::Base.connection.version >= '5.6.0' || Mysql.const_defined?(:CLIENT_MULTI_RESULTS) - def test_multi_results - rows = @connection.select_rows('CALL ten();') - assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}" - assert @connection.active?, "Bad connection use by 'MysqlAdapter.select_rows'" - end + def test_multi_results + rows = @connection.select_rows('CALL ten();') + assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}" + assert @connection.active?, "Bad connection use by 'MysqlAdapter.select_rows'" + end - def test_multi_results_from_find_by_sql - topics = Topic.find_by_sql 'CALL topics(3);' - assert_equal 3, topics.size - assert @connection.active?, "Bad connection use by 'MysqlAdapter.select'" - end + def test_multi_results_from_find_by_sql + topics = Topic.find_by_sql 'CALL topics(3);' + assert_equal 3, topics.size + assert @connection.active?, "Bad connection use by 'MysqlAdapter.select'" end end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 31dc69a45b..b97eb3e228 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -17,7 +17,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def test_add_index # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed - def (ActiveRecord::Base.connection).table_exists?(*); true; end + def (ActiveRecord::Base.connection).data_source_exists?(*); true; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " @@ -60,7 +60,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_index_in_create - def (ActiveRecord::Base.connection).table_exists?(*); false; end + def (ActiveRecord::Base.connection).data_source_exists?(*); false; end %w(SPATIAL FULLTEXT UNIQUE).each do |type| expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB" @@ -78,7 +78,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_index_in_bulk_change - def (ActiveRecord::Base.connection).table_exists?(*); true; end + def (ActiveRecord::Base.connection).data_source_exists?(*); true; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end %w(SPATIAL FULLTEXT UNIQUE).each do |type| @@ -152,7 +152,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_indexes_in_create - ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false) + ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false) ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 000bcadebe..507d024bb6 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -131,4 +131,32 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase ensure @connection.execute "DROP TABLE `bar_baz`" end + + def test_get_and_release_advisory_lock + lock_name = "test_lock_name" + + got_lock = @connection.get_advisory_lock(lock_name) + assert got_lock, "get_advisory_lock should have returned true but it didn't" + + assert_equal test_lock_free(lock_name), false, + "expected the test advisory lock to be held but it wasn't" + + released_lock = @connection.release_advisory_lock(lock_name) + assert released_lock, "expected release_advisory_lock to return true but it didn't" + + assert test_lock_free(lock_name), 'expected the test lock to be available after releasing' + end + + def test_release_non_existent_advisory_lock + lock_name = "fake_lock_name" + released_non_existent_lock = @connection.release_advisory_lock(lock_name) + assert_equal released_non_existent_lock, false, + 'expected release_advisory_lock to return false when there was no lock to release' + end + + protected + + def test_lock_free(lock_name) + @connection.select_value("SELECT IS_FREE_LOCK('#{lock_name}');") == 1 + end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index faf2acb9cb..43957791b1 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -14,11 +14,43 @@ module ActiveRecord @db_name = db @omgpost = Class.new(ActiveRecord::Base) do + self.inheritance_column = :disabled self.table_name = "#{db}.#{table}" def self.name; 'Post'; end end end + def test_float_limits + @connection.create_table :mysql_doubles do |t| + t.float :float_no_limit + t.float :float_short, limit: 5 + t.float :float_long, limit: 53 + + t.float :float_23, limit: 23 + t.float :float_24, limit: 24 + t.float :float_25, limit: 25 + end + + column_no_limit = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_no_limit' } + column_short = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_short' } + column_long = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_long' } + + column_23 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_23' } + column_24 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_24' } + column_25 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_25' } + + # Mysql floats are precision 0..24, Mysql doubles are precision 25..53 + assert_equal 24, column_no_limit.limit + assert_equal 24, column_short.limit + assert_equal 53, column_long.limit + + assert_equal 24, column_23.limit + assert_equal 24, column_24.limit + assert_equal 53, column_25.limit + ensure + @connection.drop_table "mysql_doubles", if_exists: true + end + def test_schema assert @omgpost.first end @@ -27,13 +59,13 @@ module ActiveRecord assert_equal 'id', @omgpost.primary_key end - def test_table_exists? + def test_data_source_exists? name = @omgpost.table_name - assert @connection.table_exists?(name), "#{name} table should exist" + assert @connection.data_source_exists?(name), "#{name} data_source should exist" end - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") + def test_data_source_exists_wrong_schema + assert(!@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist") end def test_dump_indexes diff --git a/activerecord/test/cases/adapters/mysql2/sp_test.rb b/activerecord/test/cases/adapters/mysql2/sp_test.rb index 8b12945f24..cdaa2cca44 100644 --- a/activerecord/test/cases/adapters/mysql2/sp_test.rb +++ b/activerecord/test/cases/adapters/mysql2/sp_test.rb @@ -7,23 +7,24 @@ class Mysql2StoredProcedureTest < ActiveRecord::Mysql2TestCase def setup @connection = ActiveRecord::Base.connection + unless ActiveRecord::Base.connection.version >= '5.6.0' + skip("no stored procedure support") + end end # Test that MySQL allows multiple results for stored procedures # # In MySQL 5.6, CLIENT_MULTI_RESULTS is enabled by default. # http://dev.mysql.com/doc/refman/5.6/en/call.html - if ActiveRecord::Base.connection.version >= '5.6.0' - def test_multi_results - rows = @connection.select_rows('CALL ten();') - assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}" - assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select_rows'" - end + def test_multi_results + rows = @connection.select_rows('CALL ten();') + assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}" + assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select_rows'" + end - def test_multi_results_from_find_by_sql - topics = Topic.find_by_sql 'CALL topics(3);' - assert_equal 3, topics.size - assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select'" - end + def test_multi_results_from_find_by_sql + topics = Topic.find_by_sql 'CALL topics(3);' + assert_equal 3, topics.size + assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select'" end end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 722e2377c1..d559de3e28 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -90,7 +90,7 @@ module ActiveRecord end def test_tables_logs_name - @connection.tables('hello') + ActiveSupport::Deprecation.silence { @connection.tables('hello') } assert_equal 'SCHEMA', @subscriber.logged[0][1] end @@ -100,7 +100,7 @@ module ActiveRecord end def test_table_exists_logs_name - @connection.table_exists?('items') + ActiveSupport::Deprecation.silence { @connection.table_exists?('items') } assert_equal 'SCHEMA', @subscriber.logged[0][1] end @@ -209,5 +209,47 @@ module ActiveRecord ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => :default}})) end end + + def test_get_and_release_advisory_lock + lock_id = 5295901941911233559 + list_advisory_locks = <<-SQL + SELECT locktype, + (classid::bigint << 32) | objid::bigint AS lock_id + FROM pg_locks + WHERE locktype = 'advisory' + SQL + + got_lock = @connection.get_advisory_lock(lock_id) + assert got_lock, "get_advisory_lock should have returned true but it didn't" + + advisory_lock = @connection.query(list_advisory_locks).find {|l| l[1] == lock_id} + assert advisory_lock, + "expected to find an advisory lock with lock_id #{lock_id} but there wasn't one" + + released_lock = @connection.release_advisory_lock(lock_id) + assert released_lock, "expected release_advisory_lock to return true but it didn't" + + advisory_locks = @connection.query(list_advisory_locks).select {|l| l[1] == lock_id} + assert_empty advisory_locks, + "expected to have released advisory lock with lock_id #{lock_id} but it was still held" + end + + def test_release_non_existent_advisory_lock + fake_lock_id = 2940075057017742022 + with_warning_suppression do + released_non_existent_lock = @connection.release_advisory_lock(fake_lock_id) + assert_equal released_non_existent_lock, false, + 'expected release_advisory_lock to return false when there was no lock to release' + end + end + + protected + + def with_warning_suppression + log_level = @connection.client_min_messages + @connection.client_min_messages = 'error' + yield + @connection.client_min_messages = log_level + end end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 93e98ec872..7c9169f6e2 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -184,42 +184,42 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase @connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered end - def test_table_exists? + def test_data_source_exists? [Thing1, Thing2, Thing3, Thing4].each do |klass| name = klass.table_name - assert @connection.table_exists?(name), "'#{name}' table should exist" + assert @connection.data_source_exists?(name), "'#{name}' data_source should exist" end end - def test_table_exists_when_on_schema_search_path + def test_data_source_exists_when_on_schema_search_path with_schema_search_path(SCHEMA_NAME) do - assert(@connection.table_exists?(TABLE_NAME), "table should exist and be found") + assert(@connection.data_source_exists?(TABLE_NAME), "data_source should exist and be found") end end - def test_table_exists_when_not_on_schema_search_path + def test_data_source_exists_when_not_on_schema_search_path with_schema_search_path('PUBLIC') do - assert(!@connection.table_exists?(TABLE_NAME), "table exists but should not be found") + assert(!@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found") end end - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("foo.things"), "table should not exist") + def test_data_source_exists_wrong_schema + assert(!@connection.data_source_exists?("foo.things"), "data_source should not exist") end - def test_table_exists_quoted_names + def test_data_source_exists_quoted_names [ %("#{SCHEMA_NAME}"."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}")].each do |given| - assert(@connection.table_exists?(given), "table should exist when specified as #{given}") + assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") end with_schema_search_path(SCHEMA_NAME) do given = %("#{TABLE_NAME}") - assert(@connection.table_exists?(given), "table should exist when specified as #{given}") + assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") end end - def test_table_exists_quoted_table + def test_data_source_exists_quoted_table with_schema_search_path(SCHEMA_NAME) do - assert(@connection.table_exists?('"things.table"'), "table should exist") + assert(@connection.data_source_exists?('"things.table"'), "data_source should exist") end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 640df31e2e..a2fd1177a6 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -284,9 +284,9 @@ module ActiveRecord def test_tables with_example_table do - assert_equal %w{ ex }, @conn.tables + ActiveSupport::Deprecation.silence { assert_equal %w{ ex }, @conn.tables } with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer', 'people' do - assert_equal %w{ ex people }.sort, @conn.tables.sort + ActiveSupport::Deprecation.silence { assert_equal %w{ ex people }.sort, @conn.tables.sort } end end end @@ -297,7 +297,9 @@ module ActiveRecord WHERE type IN ('table','view') AND name <> 'sqlite_sequence' SQL assert_logged [[sql.squish, 'SCHEMA', []]] do - @conn.tables('hello') + ActiveSupport::Deprecation.silence do + @conn.tables('hello') + end end end @@ -316,7 +318,9 @@ module ActiveRecord WHERE type IN ('table','view') AND name <> 'sqlite_sequence' AND name = 'ex' SQL assert_logged [[sql.squish, 'SCHEMA', []]] do - assert @conn.table_exists?('ex') + ActiveSupport::Deprecation.silence do + assert @conn.table_exists?('ex') + end end end end diff --git a/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb new file mode 100644 index 0000000000..2b867965ba --- /dev/null +++ b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb @@ -0,0 +1,41 @@ +require 'cases/helper' +require 'models/content' + +class BidirectionalDestroyDependenciesTest < ActiveRecord::TestCase + fixtures :content, :content_positions + + def setup + Content.destroyed_ids.clear + ContentPosition.destroyed_ids.clear + end + + def test_bidirectional_dependence_when_destroying_item_with_belongs_to_association + content_position = ContentPosition.find(1) + content = content_position.content + assert_not_nil content + + content_position.destroy + + assert_equal [content_position.id], ContentPosition.destroyed_ids + assert_equal [content.id], Content.destroyed_ids + end + + def test_bidirectional_dependence_when_destroying_item_with_has_one_association + content = Content.find(1) + content_position = content.content_position + assert_not_nil content_position + + content.destroy + + assert_equal [content.id], Content.destroyed_ids + assert_equal [content_position.id], ContentPosition.destroyed_ids + end + + def test_bidirectional_dependence_when_destroying_item_with_has_one_association_fails_first_time + content = ContentWhichRequiresTwoDestroyCalls.find(1) + + 2.times { content.destroy } + + assert_equal content.destroyed?, true + end +end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index bc1bff79d3..0c09713971 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1179,6 +1179,24 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal 1, mary.unique_categorized_post_ids.length end + def test_preloading_has_one_using_reorder + klass = Class.new(ActiveRecord::Base) do + def self.name; "TempAuthor"; end + self.table_name = "authors" + has_one :post, class_name: "PostWithDefaultScope", foreign_key: :author_id + has_one :reorderd_post, -> { reorder(title: :desc) }, class_name: "PostWithDefaultScope", foreign_key: :author_id + end + + author = klass.first + # PRECONDITION: make sure ordering results in different results + assert_not_equal author.post, author.reorderd_post + + preloaded_reorderd_post = klass.preload(:reorderd_post).first.reorderd_post + + assert_equal author.reorderd_post, preloaded_reorderd_post + assert_equal Post.order(title: :desc).first.title, preloaded_reorderd_post.title + end + def test_preloading_polymorphic_with_custom_foreign_type sponsor = sponsors(:moustache_club_sponsor_for_groucho) groucho = members(:groucho) @@ -1203,12 +1221,6 @@ class EagerAssociationTest < ActiveRecord::TestCase end end - def test_join_eager_with_nil_order_should_generate_valid_sql - assert_nothing_raised(ActiveRecord::StatementInvalid) do - Post.includes(:comments).order(nil).where(:comments => {:body => "Thank you for the welcome"}).first - end - end - def test_deep_including_through_habtm # warm up habtm cache posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 20af436e02..ccb062f991 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -957,4 +957,29 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase projects = ProjectUnscopingDavidDefaultScope.includes(:developers).where(id: project.id) assert_equal 1, projects.first.developers.size end + + def test_preloaded_associations_size + assert_equal Project.first.salaried_developers.size, + Project.preload(:salaried_developers).first.salaried_developers.size + + assert_equal Project.includes(:salaried_developers).references(:salaried_developers).first.salaried_developers.size, + Project.preload(:salaried_developers).first.salaried_developers.size + + # Nested HATBM + first_project = Developer.first.projects.first + preloaded_first_project = + Developer.preload(projects: :salaried_developers). + first. + projects. + detect { |p| p.id == first_project.id } + + assert preloaded_first_project.salaried_developers.loaded?, true + assert_equal first_project.salaried_developers.size, preloaded_first_project.salaried_developers.size + end + + def test_has_and_belongs_to_many_is_useable_with_belongs_to_required_by_default + assert_difference "Project.first.developers_required_by_default.size", 1 do + Project.first.developers_required_by_default.create!(name: "Sean", salary: 50000) + end + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index eb94870a35..50ca6537cc 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -203,9 +203,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase bulb = car.bulbs.create assert_equal 'defaulty', bulb.name + end + + def test_build_and_create_from_association_should_respect_passed_attributes_over_default_scope + car = Car.create(name: 'honda') + + bulb = car.bulbs.build(name: 'exotic') + assert_equal 'exotic', bulb.name - bulb = car.bulbs.create(:name => 'exotic') + bulb = car.bulbs.create(name: 'exotic') assert_equal 'exotic', bulb.name + + bulb = car.awesome_bulbs.build(frickinawesome: false) + assert_equal false, bulb.frickinawesome + + bulb = car.awesome_bulbs.create(frickinawesome: false) + assert_equal false, bulb.frickinawesome end def test_build_from_association_should_respect_scope diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index cf730e4fe7..226ecf5447 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -1111,10 +1111,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_with_default_scope_on_the_target person = people(:michael) - assert_equal [posts(:thinking)], person.first_posts + assert_equal [posts(:thinking).id], person.first_posts.map(&:id) readers(:michael_authorless).update(first_post_id: 1) - assert_equal [posts(:thinking)], person.reload.first_posts + assert_equal [posts(:thinking).id], person.reload.first_posts.map(&:id) end def test_has_many_through_with_includes_in_through_association_scope diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb new file mode 100644 index 0000000000..4af791b758 --- /dev/null +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -0,0 +1,79 @@ +require "cases/helper" +require 'models/post' +require 'models/comment' +require 'models/author' +require 'models/essay' +require 'models/categorization' +require 'models/person' + +class LeftOuterJoinAssociationTest < ActiveRecord::TestCase + fixtures :authors, :essays, :posts, :comments, :categorizations, :people + + def test_construct_finder_sql_applies_aliases_tables_on_association_conditions + result = Author.left_outer_joins(:thinking_posts, :welcome_posts).to_a + assert_equal authors(:david), result.first + end + + def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations + assert_nothing_raised do + queries = capture_sql do + Person.left_outer_joins(:agents => {:agents => :agents}) + .left_outer_joins(:agents => {:agents => {:primary_contact => :agents}}).to_a + end + assert queries.any? { |sql| /agents_people_4/i =~ sql } + end + end + + def test_construct_finder_sql_executes_a_left_outer_join + assert_not_equal Author.count, Author.joins(:posts).count + assert_equal Author.count, Author.left_outer_joins(:posts).count + end + + def test_left_outer_join_by_left_joins + assert_not_equal Author.count, Author.joins(:posts).count + assert_equal Author.count, Author.left_joins(:posts).count + end + + def test_construct_finder_sql_ignores_empty_left_outer_joins_hash + queries = capture_sql { Author.left_outer_joins({}) } + assert queries.none? { |sql| /LEFT OUTER JOIN/i =~ sql } + end + + def test_construct_finder_sql_ignores_empty_left_outer_joins_array + queries = capture_sql { Author.left_outer_joins([]) } + assert queries.none? { |sql| /LEFT OUTER JOIN/i =~ sql } + end + + def test_left_outer_joins_forbids_to_use_string_as_argument + assert_raise(ArgumentError){ Author.left_outer_joins('LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"').to_a } + end + + def test_join_conditions_added_to_join_clause + queries = capture_sql { Author.left_outer_joins(:essays).to_a } + assert queries.any? { |sql| /writer_type.*?=.*?(Author|\?|\$1)/i =~ sql } + assert queries.none? { |sql| /WHERE/i =~ sql } + end + + def test_find_with_sti_join + scope = Post.left_outer_joins(:special_comments).where(:id => posts(:sti_comments).id) + + # The join should match SpecialComment and its subclasses only + assert scope.where("comments.type" => "Comment").empty? + assert !scope.where("comments.type" => "SpecialComment").empty? + assert !scope.where("comments.type" => "SubSpecialComment").empty? + end + + def test_does_not_override_select + authors = Author.select("authors.name, #{%{(authors.author_address_id || ' ' || authors.author_address_extra_id) as addr_id}}").left_outer_joins(:posts) + assert authors.any? + assert authors.first.respond_to?(:addr_id) + end + + test "the default scope of the target is applied when joining associations" do + author = Author.create! name: "Jon" + author.categorizations.create! + author.categorizations.create! special: true + + assert_equal [author], Author.where(id: author).left_outer_joins(:special_categorizations) + end +end diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 264b275181..2991ca8b76 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -172,5 +172,18 @@ module ActiveRecord assert_equal int_range, klass.type_for_attribute("my_int_range") end end + + test "attributes added after subclasses load are inherited" do + parent = Class.new(ActiveRecord::Base) do + self.table_name = "topics" + end + + child = Class.new(parent) + child.new # => force a schema load + + parent.attribute(:foo, Type::Value.new) + + assert_equal(:bar, child.new(foo: :bar).foo) + end end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 37ec3f1106..0df8f1f798 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -24,6 +24,8 @@ require 'models/molecule' require 'models/member' require 'models/member_detail' require 'models/organization' +require 'models/guitar' +require 'models/tuning_peg' class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase def test_autosave_validation @@ -397,6 +399,40 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib assert_not molecule.persisted?, 'Molecule should not be persisted when its electrons are invalid' end + def test_errors_should_be_indexed_when_passed_as_array + guitar = Guitar.new + tuning_peg_valid = TuningPeg.new + tuning_peg_valid.pitch = 440.0 + tuning_peg_invalid = TuningPeg.new + + guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid] + + assert_not tuning_peg_invalid.valid? + assert tuning_peg_valid.valid? + assert_not guitar.valid? + assert_equal ["is not a number"], guitar.errors["tuning_pegs[1].pitch"] + assert_not_equal ["is not a number"], guitar.errors["tuning_pegs.pitch"] + end + + def test_errors_should_be_indexed_when_global_flag_is_set + old_attribute_config = ActiveRecord::Base.index_nested_attribute_errors + ActiveRecord::Base.index_nested_attribute_errors = true + + molecule = Molecule.new + valid_electron = Electron.new(name: 'electron') + invalid_electron = Electron.new + + molecule.electrons = [valid_electron, invalid_electron] + + assert_not invalid_electron.valid? + assert valid_electron.valid? + assert_not molecule.valid? + assert_equal ["can't be blank"], molecule.errors["electrons[1].name"] + assert_not_equal ["can't be blank"], molecule.errors["electrons.name"] + ensure + ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config + end + def test_valid_adding_with_nested_attributes molecule = Molecule.new valid_electron = Electron.new(name: 'electron') diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index dbbcaa075d..d961f4710e 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -26,7 +26,7 @@ require 'models/bird' require 'models/car' require 'models/bulb' require 'rexml/document' -require 'concurrent/atomics' +require 'concurrent/atomic/count_down_latch' class FirstAbstractClass < ActiveRecord::Base self.abstract_class = true @@ -1204,42 +1204,10 @@ class BasicsTest < ActiveRecord::TestCase assert_equal last, Developer.all.merge!(:order => :salary).to_a.last end - def test_abstract_class - assert !ActiveRecord::Base.abstract_class? - assert LoosePerson.abstract_class? - assert !LooseDescendant.abstract_class? - end - def test_abstract_class_table_name assert_nil AbstractCompany.table_name end - def test_descends_from_active_record - assert !ActiveRecord::Base.descends_from_active_record? - - # Abstract subclass of AR::Base. - assert LoosePerson.descends_from_active_record? - - # Concrete subclass of an abstract class. - assert LooseDescendant.descends_from_active_record? - - # Concrete subclass of AR::Base. - assert TightPerson.descends_from_active_record? - - # Concrete subclass of a concrete class but has no type column. - assert TightDescendant.descends_from_active_record? - - # Concrete subclass of AR::Base. - assert Post.descends_from_active_record? - - # Abstract subclass of a concrete class which has a type column. - # This is pathological, as you'll never have Sub < Abstract < Concrete. - assert !StiPost.descends_from_active_record? - - # Concrete subclasses an abstract class which has a type column. - assert !SubStiPost.descends_from_active_record? - end - def test_find_on_abstract_base_class_doesnt_use_type_condition old_class = LooseDescendant Object.send :remove_const, :LooseDescendant @@ -1284,53 +1252,6 @@ class BasicsTest < ActiveRecord::TestCase ActiveRecord::Base.logger = original_logger end - def test_compute_type_success - assert_equal Author, ActiveRecord::Base.send(:compute_type, 'Author') - end - - def test_compute_type_nonexistent_constant - e = assert_raises NameError do - ActiveRecord::Base.send :compute_type, 'NonexistentModel' - end - assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message - assert_equal 'ActiveRecord::Base::NonexistentModel', e.name - end - - def test_compute_type_no_method_error - ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise NoMethodError }) do - assert_raises NoMethodError do - ActiveRecord::Base.send :compute_type, 'InvalidModel' - end - end - end - - def test_compute_type_on_undefined_method - error = nil - begin - Class.new(Author) do - alias_method :foo, :bar - end - rescue => e - error = e - end - - ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise e }) do - - exception = assert_raises NameError do - ActiveRecord::Base.send :compute_type, 'InvalidModel' - end - assert_equal error.message, exception.message - end - end - - def test_compute_type_argument_error - ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise ArgumentError }) do - assert_raises ArgumentError do - ActiveRecord::Base.send :compute_type, 'InvalidModel' - end - end - end - def test_clear_cache! # preheat cache c1 = Post.connection.schema_cache.columns('posts') diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 9cb70ee239..da65336305 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -161,7 +161,7 @@ class EachTest < ActiveRecord::TestCase end # posts.first will be ordered using id only. Title order scope should not apply here assert_not_equal first_post, posts.first - assert_equal posts(:welcome), posts.first + assert_equal posts(:welcome).id, posts.first.id end def test_find_in_batches_should_not_ignore_the_default_scope_if_it_is_other_then_order diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index dff6ea0fb0..d43668e57c 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -92,7 +92,14 @@ module ActiveRecord app = lambda { |_| [200, {}, body] } response_body = ConnectionManagement.new(app).call(@env)[2] assert response_body.respond_to?(:to_path) - assert_equal response_body.to_path, "/path" + assert_equal "/path", response_body.to_path + end + + test "doesn't mutate the original response" do + original_response = [200, {}, 'hi'] + app = lambda { |_| original_response } + ConnectionManagement.new(app).call(@env)[2] + assert_equal 'hi', original_response.last end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 7ef5c93a48..efa3e0455e 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -1,5 +1,5 @@ require "cases/helper" -require 'concurrent/atomics' +require 'concurrent/atomic/count_down_latch' module ActiveRecord module ConnectionAdapters diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index a0eaa66e94..f30ed4fcc8 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -955,3 +955,17 @@ class FixturesWithAbstractBelongsTo < ActiveRecord::TestCase assert_equal pirates(:blackbeard), doubloons(:blackbeards_doubloon).pirate end end + +class FixtureClassNamesTest < ActiveRecord::TestCase + def setup + @saved_cache = self.fixture_class_names.dup + end + + def teardown + self.fixture_class_names.replace(@saved_cache) + end + + test "fixture_class_names returns nil for unregistered identifier" do + assert_nil self.fixture_class_names['unregistered_identifier'] + end +end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index f67d85603a..52e3734dd0 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'models/author' require 'models/company' require 'models/person' require 'models/post' @@ -55,6 +56,53 @@ class InheritanceTest < ActiveRecord::TestCase end end + def test_compute_type_success + assert_equal Author, ActiveRecord::Base.send(:compute_type, 'Author') + end + + def test_compute_type_nonexistent_constant + e = assert_raises NameError do + ActiveRecord::Base.send :compute_type, 'NonexistentModel' + end + assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message + assert_equal 'ActiveRecord::Base::NonexistentModel', e.name + end + + def test_compute_type_no_method_error + ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise NoMethodError }) do + assert_raises NoMethodError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + end + end + + def test_compute_type_on_undefined_method + error = nil + begin + Class.new(Author) do + alias_method :foo, :bar + end + rescue => e + error = e + end + + ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise e }) do + + exception = assert_raises NameError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + assert_equal error.message, exception.message + end + end + + def test_compute_type_argument_error + ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise ArgumentError }) do + assert_raises ArgumentError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + end + end + def test_should_store_demodulized_class_name_with_store_full_sti_class_option_disabled without_store_full_sti_class do item = Namespaced::Company.new @@ -77,6 +125,32 @@ class InheritanceTest < ActiveRecord::TestCase end end + def test_descends_from_active_record + assert !ActiveRecord::Base.descends_from_active_record? + + # Abstract subclass of AR::Base. + assert LoosePerson.descends_from_active_record? + + # Concrete subclass of an abstract class. + assert LooseDescendant.descends_from_active_record? + + # Concrete subclass of AR::Base. + assert TightPerson.descends_from_active_record? + + # Concrete subclass of a concrete class but has no type column. + assert TightDescendant.descends_from_active_record? + + # Concrete subclass of AR::Base. + assert Post.descends_from_active_record? + + # Abstract subclass of a concrete class which has a type column. + # This is pathological, as you'll never have Sub < Abstract < Concrete. + assert !StiPost.descends_from_active_record? + + # Concrete subclasses an abstract class which has a type column. + assert !SubStiPost.descends_from_active_record? + end + def test_company_descends_from_active_record assert !ActiveRecord::Base.descends_from_active_record? assert AbstractCompany.descends_from_active_record?, 'AbstractCompany should descend from ActiveRecord::Base' @@ -84,6 +158,12 @@ class InheritanceTest < ActiveRecord::TestCase assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base' end + def test_abstract_class + assert !ActiveRecord::Base.abstract_class? + assert LoosePerson.abstract_class? + assert !LooseDescendant.abstract_class? + end + def test_inheritance_base_class assert_equal Post, Post.base_class assert_equal Post, SpecialPost.base_class @@ -223,7 +303,6 @@ class InheritanceTest < ActiveRecord::TestCase end end - def test_new_with_complex_inheritance assert_nothing_raised { Client.new(type: 'VerySpecialClient') } end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 84b0ff8fcb..0da58040c8 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -157,8 +157,10 @@ module ActiveRecord teardown do %w[horses new_horses].each do |table| - if ActiveRecord::Base.connection.table_exists?(table) - ActiveRecord::Base.connection.drop_table(table) + ActiveSupport::Deprecation.silence do + if ActiveRecord::Base.connection.table_exists?(table) + ActiveRecord::Base.connection.drop_table(table) + end end end ActiveRecord::Migration.verbose = @verbose_was @@ -189,14 +191,14 @@ module ActiveRecord def test_migrate_up migration = InvertibleMigration.new migration.migrate(:up) - assert migration.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses"), "horses should exist" } end def test_migrate_down migration = InvertibleMigration.new migration.migrate :up migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end def test_migrate_revert @@ -204,11 +206,11 @@ module ActiveRecord revert = InvertibleRevertMigration.new migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } revert.migrate :down - assert migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end def test_migrate_revert_by_part @@ -216,18 +218,24 @@ module ActiveRecord received = [] migration = InvertibleByPartsMigration.new migration.test = ->(dir){ - assert migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") + end received << dir } migration.migrate :up assert_equal [:both, :up], received - assert !migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert !migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") + end migration.migrate :down assert_equal [:both, :up, :both, :down], received - assert migration.connection.table_exists?("horses") - assert !migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert migration.connection.table_exists?("horses") + assert !migration.connection.table_exists?("new_horses") + end end def test_migrate_revert_whole_migration @@ -236,20 +244,20 @@ module ActiveRecord revert = RevertWholeMigration.new(klass) migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } revert.migrate :down - assert migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end end def test_migrate_nested_revert_whole_migration revert = NestedRevertWholeMigration.new(InvertibleRevertMigration) revert.migrate :down - assert revert.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert revert.connection.table_exists?("horses") } revert.migrate :up - assert !revert.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !revert.connection.table_exists?("horses") } end def test_migrate_revert_change_column_default @@ -314,24 +322,24 @@ module ActiveRecord def test_legacy_up LegacyMigration.migrate :up - assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } end def test_legacy_down LegacyMigration.migrate :up LegacyMigration.migrate :down - assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } end def test_up LegacyMigration.up - assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } end def test_down LegacyMigration.up LegacyMigration.down - assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } end def test_migrate_down_with_table_name_prefix @@ -340,7 +348,7 @@ module ActiveRecord migration = InvertibleMigration.new migration.migrate(:up) assert_nothing_raised { migration.migrate(:down) } - assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" } ensure ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = '' end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 3846ba8e7f..707a2d1da1 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -209,7 +209,7 @@ class LogSubscriberTest < ActiveRecord::TestCase Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join end - unless current_adapter?(:Mysql2Adapter) + if ActiveRecord::Base.connection.prepared_statements def test_binary_data_is_not_logged Binary.create(data: 'some binary data') wait diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 83e50048ec..2ff9cf8cf5 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -405,9 +405,9 @@ module ActiveRecord def test_drop_table_if_exists connection.create_table(:testings) - assert connection.table_exists?(:testings) + ActiveSupport::Deprecation.silence { assert connection.table_exists?(:testings) } connection.drop_table(:testings, if_exists: true) - assert_not connection.table_exists?(:testings) + ActiveSupport::Deprecation.silence { assert_not connection.table_exists?(:testings) } end def test_drop_table_if_exists_nothing_raised diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 8fd08fe4ce..0a7b57455c 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -12,7 +12,9 @@ module ActiveRecord teardown do %w(artists_musics musics_videos catalog).each do |table_name| - connection.drop_table table_name if connection.tables.include?(table_name) + ActiveSupport::Deprecation.silence do + connection.drop_table table_name if connection.table_exists?(table_name) + end end end @@ -82,62 +84,62 @@ module ActiveRecord connection.create_join_table :artists, :musics connection.drop_join_table :artists, :musics - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_drop_join_table_with_strings connection.create_join_table :artists, :musics connection.drop_join_table 'artists', 'musics' - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_drop_join_table_with_the_proper_order connection.create_join_table :videos, :musics connection.drop_join_table :videos, :musics - assert !connection.tables.include?('musics_videos') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('musics_videos') } end def test_drop_join_table_with_the_table_name connection.create_join_table :artists, :musics, table_name: :catalog connection.drop_join_table :artists, :musics, table_name: :catalog - assert !connection.tables.include?('catalog') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') } end def test_drop_join_table_with_the_table_name_as_string connection.create_join_table :artists, :musics, table_name: 'catalog' connection.drop_join_table :artists, :musics, table_name: 'catalog' - assert !connection.tables.include?('catalog') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') } end def test_drop_join_table_with_column_options connection.create_join_table :artists, :musics, column_options: {null: true} connection.drop_join_table :artists, :musics, column_options: {null: true} - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_create_and_drop_join_table_with_common_prefix with_table_cleanup do connection.create_join_table 'audio_artists', 'audio_musics' - assert_includes connection.tables, 'audio_artists_musics' + ActiveSupport::Deprecation.silence { assert connection.table_exists?('audio_artists_musics') } connection.drop_join_table 'audio_artists', 'audio_musics' - assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't" + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('audio_artists_musics'), "Should have dropped join table, but didn't" } end end private def with_table_cleanup - tables_before = connection.tables + tables_before = connection.data_sources yield ensure - tables_after = connection.tables - tables_before + tables_after = connection.data_sources - tables_before tables_after.each do |table| connection.drop_table table diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 84ec657398..edbc8abe4d 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -164,7 +164,7 @@ class ReferencesWithoutForeignKeySupportTest < ActiveRecord::TestCase t.references :testing_parent, foreign_key: true end - assert_includes @connection.tables, "testings" + assert_includes @connection.data_sources, "testings" end end end diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 6d742d3f2f..8eb027d53f 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -15,7 +15,7 @@ module ActiveRecord end def teardown - rename_table :octopi, :test_models if connection.table_exists? :octopi + ActiveSupport::Deprecation.silence { rename_table :octopi, :test_models if connection.table_exists? :octopi } super end @@ -83,7 +83,7 @@ module ActiveRecord enable_extension!('uuid-ossp', connection) connection.create_table :cats, id: :uuid assert_nothing_raised { rename_table :cats, :felines } - assert connection.table_exists? :felines + ActiveSupport::Deprecation.silence { assert connection.table_exists? :felines } ensure disable_extension!('uuid-ossp', connection) connection.drop_table :cats, if_exists: true diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 10f1c7216f..c3c204cf9f 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1,6 +1,7 @@ -require "cases/helper" -require "cases/migration/helper" +require 'cases/helper' +require 'cases/migration/helper' require 'bigdecimal/util' +require 'concurrent/atomic/count_down_latch' require 'models/person' require 'models/topic' @@ -522,6 +523,79 @@ class MigrationTest < ActiveRecord::TestCase end end + if ActiveRecord::Base.connection.supports_advisory_locks? + def test_migrator_generates_valid_lock_id + migration = Class.new(ActiveRecord::Migration).new + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + assert ActiveRecord::Base.connection.get_advisory_lock(lock_id), + "the Migrator should have generated a valid lock id, but it didn't" + assert ActiveRecord::Base.connection.release_advisory_lock(lock_id), + "the Migrator should have generated a valid lock id, but it didn't" + end + + def test_generate_migrator_advisory_lock_id + # It is important we are consistent with how we generate this so that + # exclusive locking works across migrator versions + migration = Class.new(ActiveRecord::Migration).new + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + current_database = ActiveRecord::Base.connection.current_database + salt = ActiveRecord::Migrator::MIGRATOR_SALT + expected_id = Zlib.crc32(current_database) * salt + + assert lock_id == expected_id, "expected lock id generated by the migrator to be #{expected_id}, but it was #{lock_id} instead" + assert lock_id.is_a?(Fixnum), "expected lock id to be a Fixnum, but it wasn't" + assert lock_id.bit_length <= 63, "lock id must be a signed integer of max 63 bits magnitude" + end + + def test_migrator_one_up_with_unavailable_lock + assert_no_column Person, :last_name + + migration = Class.new(ActiveRecord::Migration) { + def version; 100 end + def migrate(x) + add_column "people", "last_name", :string + end + }.new + + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + with_another_process_holding_lock(lock_id) do + assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.migrate } + end + + assert_no_column Person, :last_name, + "without an advisory lock, the Migrator should not make any changes, but it did." + end + + def test_migrator_one_up_with_unavailable_lock_using_run + assert_no_column Person, :last_name + + migration = Class.new(ActiveRecord::Migration) { + def version; 100 end + def migrate(x) + add_column "people", "last_name", :string + end + }.new + + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + with_another_process_holding_lock(lock_id) do + assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.run } + end + + assert_no_column Person, :last_name, + "without an advisory lock, the Migrator should not make any changes, but it did." + end + end + protected # This is needed to isolate class_attribute assignments like `table_name_prefix` # for each test case. @@ -531,6 +605,30 @@ class MigrationTest < ActiveRecord::TestCase def self.base_class; self; end } end + + def with_another_process_holding_lock(lock_id) + thread_lock = Concurrent::CountDownLatch.new + test_terminated = Concurrent::CountDownLatch.new + + other_process = Thread.new do + begin + conn = ActiveRecord::Base.connection_pool.checkout + conn.get_advisory_lock(lock_id) + thread_lock.count_down + test_terminated.wait # hold the lock open until we tested everything + ensure + conn.release_advisory_lock(lock_id) + ActiveRecord::Base.connection_pool.checkin(conn) + end + end + + thread_lock.wait # wait until the 'other process' has the lock + + yield + + test_terminated.count_down + other_process.join + end end class ReservedWordsMigrationTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 2ff6938e7b..dbf088f455 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -313,9 +313,9 @@ class MigratorTest < ActiveRecord::TestCase _, migrator = migrator_class(3) ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true - assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') + ActiveSupport::Deprecation.silence { assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') } migrator.migrate("valid", 1) - assert ActiveRecord::Base.connection.table_exists?('schema_migrations') + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?('schema_migrations') } end def test_migrator_forward diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 31686bde3f..b2e59e3970 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -744,9 +744,10 @@ class PersistenceTest < ActiveRecord::TestCase assert !topic.approved? assert_equal "The First Topic", topic.title - assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do + error = assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do topic.update_attributes(id: 3, title: "Hm is it possible?") end + assert_not_nil error.cause assert_not_equal "Hm is it possible?", Topic.find(3).title topic.update_attributes(id: 1234) @@ -965,4 +966,17 @@ class PersistenceTest < ActiveRecord::TestCase widget.reset_column_information end end + + def test_reset_column_information_resets_children + child = Class.new(Topic) + child.new # force schema to load + + ActiveRecord::Base.connection.add_column(:topics, :foo, :string) + Topic.reset_column_information + + assert_equal "bar", child.new(foo: :bar).foo + ensure + ActiveRecord::Base.connection.remove_column(:topics, :foo) + Topic.reset_column_information + end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index daa3271777..bca50dd008 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -44,7 +44,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase conn = ActiveRecord::Base.connection_pool.checkout ActiveRecord::Base.connection_pool.checkin conn @connection_count += 1 - ActiveRecord::Base.connection.tables + ActiveRecord::Base.connection.data_sources rescue ActiveRecord::ConnectionTimeoutError @timed_out += 1 end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 5e4ba47988..d883784553 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -224,7 +224,7 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase end teardown do - @connection.drop_table(:barcodes) if @connection.table_exists? :barcodes + @connection.drop_table(:barcodes, if_exists: true) end def test_any_type_primary_key @@ -280,6 +280,32 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) con.reconnect! end end + + class PrimaryKeyBigintNilDefaultTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + self.use_transactional_tests = false + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table(:bigint_defaults, id: :bigint, default: nil, force: true) + end + + def teardown + @connection.drop_table :bigint_defaults, if_exists: true + end + + test "primary key with bigint allows default override via nil" do + column = @connection.columns(:bigint_defaults).find { |c| c.name == 'id' } + assert column.bigint? + assert_not column.auto_increment? + end + + test "schema dump primary key with bigint default nil" do + schema = dump_table_schema "bigint_defaults" + assert_match %r{create_table "bigint_defaults", id: :bigint, default: nil}, schema + end + end end if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 88d2dd55ab..d0f60a84b5 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -22,13 +22,17 @@ module ActiveRecord def sanitize_sql(sql) sql end + + def sanitize_sql_for_order(sql) + sql + end end def relation @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| + (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select, :left_joins]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal [:foo], relation.public_send("#{method}_values") diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 675149556f..f46d414b95 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -20,6 +20,10 @@ module ActiveRecord def self.table_name 'fake_table' end + + def self.sanitize_sql_for_order(sql) + sql + end end def test_construction @@ -231,6 +235,13 @@ module ActiveRecord assert_equal 3, relation.where(id: post.id).pluck(:id).size end + def test_merge_raises_with_invalid_argument + assert_raises ArgumentError do + relation = Relation.new(FakeKlass, :b, nil) + relation.merge(true) + end + end + def test_respond_to_for_non_selected_element post = Post.select(:title).first assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception" diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 7521f0573a..cd23c1b3e1 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -297,6 +297,11 @@ class RelationTest < ActiveRecord::TestCase assert_equal 3, tags.length end + def test_finding_with_sanitized_order + query = Tag.order(["field(id, ?)", [1,3,2]]).to_sql + assert_match(/field\(id, 1,3,2\)/, query) + end + def test_finding_with_order_limit_and_offset entrants = Entrant.order("id ASC").limit(2).offset(1) diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 2a2c2bc8d0..43f133b12d 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -353,6 +353,38 @@ class SchemaDumperTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' $stdout = original end + + def test_schema_dump_with_table_name_prefix_and_ignoring_tables + original, $stdout = $stdout, StringIO.new + + create_cat_migration = Class.new(ActiveRecord::Migration) do + def change + create_table("cats") do |t| + end + create_table("omg_cats") do |t| + end + end + end + + original_table_name_prefix = ActiveRecord::Base.table_name_prefix + original_schema_dumper_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + ActiveRecord::Base.table_name_prefix = 'omg_' + ActiveRecord::SchemaDumper.ignore_tables = ["cats"] + migration = create_cat_migration.new + migration.migrate(:up) + + stream = StringIO.new + output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string + + assert_match %r{create_table "omg_cats"}, output + refute_match %r{create_table "cats"}, output + ensure + migration.migrate(:down) + ActiveRecord::Base.table_name_prefix = original_table_name_prefix + ActiveRecord::SchemaDumper.ignore_tables = original_schema_dumper_ignore_tables + + $stdout = original + end end class SchemaDumperDefaultsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index a93fa57257..723a7618ba 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -21,28 +21,21 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.create @configuration end - def test_creates_database_with_default_encoding_and_collation + def test_creates_database_with_no_default_options @connection.expects(:create_database). - with('my-app-db', charset: 'utf8', collation: 'utf8_unicode_ci') + with('my-app-db', {}) ActiveRecord::Tasks::DatabaseTasks.create @configuration end - def test_creates_database_with_given_encoding_and_default_collation - @connection.expects(:create_database). - with('my-app-db', charset: 'utf8', collation: 'utf8_unicode_ci') - - ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge('encoding' => 'utf8') - end - - def test_creates_database_with_given_encoding_and_no_collation + def test_creates_database_with_given_encoding @connection.expects(:create_database). with('my-app-db', charset: 'latin1') ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge('encoding' => 'latin1') end - def test_creates_database_with_given_collation_and_no_encoding + def test_creates_database_with_given_collation @connection.expects(:create_database). with('my-app-db', collation: 'latin1_swedish_ci') @@ -111,7 +104,7 @@ module ActiveRecord def test_database_created_by_root assert_permissions_granted_for "pat" @connection.expects(:create_database). - with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') + with('my-app-db', {}) ActiveRecord::Tasks::DatabaseTasks.create @configuration end @@ -203,9 +196,9 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.purge @configuration end - def test_recreates_database_with_the_default_options + def test_recreates_database_with_no_default_options @connection.expects(:recreate_database). - with('test-db', charset: 'utf8', collation: 'utf8_unicode_ci') + with('test-db', {}) ActiveRecord::Tasks::DatabaseTasks.purge @configuration end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index c3fd0b2383..c31f94b2f2 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -60,7 +60,7 @@ module ActiveRecord $stderr.expects(:puts). with("Couldn't create database for #{@configuration.inspect}") - ActiveRecord::Tasks::DatabaseTasks.create @configuration + assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration } end def test_create_when_database_exists_outputs_info_to_stderr diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 750d5e42dc..0aea0c3b38 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -53,7 +53,7 @@ module ActiveRecord $stderr.expects(:puts). with("Couldn't create database for #{@configuration.inspect}") - ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' } end end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index bff5ffa65e..584a3dc0d8 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -45,6 +45,18 @@ class AssociationValidationTest < ActiveRecord::TestCase assert t.valid? end + def test_validates_associated_without_marked_for_destruction + reply = Class.new do + def valid? + true + end + end + Topic.validates_associated(:replies) + t = Topic.new + t.define_singleton_method(:replies) { [reply.new] } + assert t.valid? + end + def test_validates_associated_with_custom_message_using_quotes Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes" Topic.validates_presence_of :content diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index e80d8bd584..d50ae74e35 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -45,7 +45,7 @@ module ViewBehavior def test_table_exists view_name = Ebook.table_name # TODO: switch this assertion around once we changed #tables to not return views. - assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } end def test_views_ara_valid_data_sources @@ -87,7 +87,7 @@ class ViewWithPrimaryKeyTest < ActiveRecord::TestCase end def drop_view(name) - @connection.execute "DROP VIEW #{name}" if @connection.table_exists? name + @connection.execute "DROP VIEW #{name}" if @connection.view_exists? name end end @@ -106,7 +106,7 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase end teardown do - @connection.execute "DROP VIEW paperbacks" if @connection.table_exists? "paperbacks" + @connection.execute "DROP VIEW paperbacks" if @connection.view_exists? "paperbacks" end def test_reading @@ -125,7 +125,8 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase def test_table_exists view_name = Paperback.table_name - assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + # TODO: switch this assertion around once we changed #tables to not return views. + ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } end def test_column_definitions @@ -167,7 +168,7 @@ class UpdateableViewTest < ActiveRecord::TestCase end teardown do - @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books" + @connection.execute "DROP VIEW printed_books" if @connection.view_exists? "printed_books" end def test_update_record @@ -209,8 +210,7 @@ class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase end def drop_view(name) - @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.table_exists? name - + @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.view_exists? name end end end diff --git a/activerecord/test/fixtures/content.yml b/activerecord/test/fixtures/content.yml new file mode 100644 index 0000000000..0d12ee03dc --- /dev/null +++ b/activerecord/test/fixtures/content.yml @@ -0,0 +1,3 @@ +content: + id: 1 + title: How to use Rails diff --git a/activerecord/test/fixtures/content_positions.yml b/activerecord/test/fixtures/content_positions.yml new file mode 100644 index 0000000000..9e85773f8e --- /dev/null +++ b/activerecord/test/fixtures/content_positions.yml @@ -0,0 +1,3 @@ +content_positions: + id: 1 + content_id: 1 diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index a6e83fe353..c1e491e5c5 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -1,6 +1,7 @@ class Bulb < ActiveRecord::Base default_scope { where(:name => 'defaulty') } belongs_to :car, :touch => true + scope :awesome, -> { where(frickinawesome: true) } attr_reader :scope_after_initialize, :attributes_after_initialize diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index 81263b79d1..778c22b1f6 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -4,6 +4,7 @@ class Car < ActiveRecord::Base has_many :funky_bulbs, class_name: 'FunkyBulb', dependent: :destroy has_many :failed_bulbs, class_name: 'FailedBulb', dependent: :destroy has_many :foo_bulbs, -> { where(:name => 'foo') }, :class_name => "Bulb" + has_many :awesome_bulbs, -> { awesome }, class_name: "Bulb" has_one :bulb diff --git a/activerecord/test/models/content.rb b/activerecord/test/models/content.rb new file mode 100644 index 0000000000..140e1dfc78 --- /dev/null +++ b/activerecord/test/models/content.rb @@ -0,0 +1,40 @@ +class Content < ActiveRecord::Base + self.table_name = 'content' + has_one :content_position, dependent: :destroy + + def self.destroyed_ids + @destroyed_ids ||= [] + end + + before_destroy do |object| + Content.destroyed_ids << object.id + end +end + +class ContentWhichRequiresTwoDestroyCalls < ActiveRecord::Base + self.table_name = 'content' + has_one :content_position, foreign_key: 'content_id', dependent: :destroy + + after_initialize do + @destroy_count = 0 + end + + before_destroy do + @destroy_count += 1 + if @destroy_count == 1 + throw :abort + end + end +end + +class ContentPosition < ActiveRecord::Base + belongs_to :content, dependent: :destroy + + def self.destroyed_ids + @destroyed_ids ||= [] + end + + before_destroy do |object| + ContentPosition.destroyed_ids << object.id + end +end diff --git a/activerecord/test/models/guitar.rb b/activerecord/test/models/guitar.rb new file mode 100644 index 0000000000..cd068ff53d --- /dev/null +++ b/activerecord/test/models/guitar.rb @@ -0,0 +1,4 @@ +class Guitar < ActiveRecord::Base + has_many :tuning_pegs, index_errors: true + accepts_nested_attributes_for :tuning_pegs +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 81a18188d4..23cebe2602 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -185,6 +185,7 @@ class SubStiPost < StiPost end class FirstPost < ActiveRecord::Base + self.inheritance_column = :disabled self.table_name = 'posts' default_scope { where(:id => 1) } @@ -193,6 +194,7 @@ class FirstPost < ActiveRecord::Base end class PostWithDefaultInclude < ActiveRecord::Base + self.inheritance_column = :disabled self.table_name = 'posts' default_scope { includes(:comments) } has_many :comments, :foreign_key => :post_id @@ -204,6 +206,7 @@ class PostWithSpecialCategorization < Post end class PostWithDefaultScope < ActiveRecord::Base + self.inheritance_column = :disabled self.table_name = 'posts' default_scope { order(:title) } end @@ -225,11 +228,13 @@ class PostWithIncludesDefaultScope < ActiveRecord::Base end class SpecialPostWithDefaultScope < ActiveRecord::Base + self.inheritance_column = :disabled self.table_name = 'posts' default_scope { where(:id => [1, 5,6]) } end class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base + self.inheritance_column = :disabled self.table_name = 'posts' has_many :comments, class_name: "CommentThatAutomaticallyAltersPostBody", foreign_key: :post_id @@ -239,6 +244,7 @@ class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base end class PostWithAfterCreateCallback < ActiveRecord::Base + self.inheritance_column = :disabled self.table_name = 'posts' has_many :comments, foreign_key: :post_id @@ -248,6 +254,7 @@ class PostWithAfterCreateCallback < ActiveRecord::Base end class PostWithCommentWithDefaultScopeReferencesAssociation < ActiveRecord::Base + self.inheritance_column = :disabled self.table_name = 'posts' has_many :comment_with_default_scope_references_associations, foreign_key: :post_id has_one :first_comment, class_name: "CommentWithDefaultScopeReferencesAssociation", foreign_key: :post_id diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index b034e0e267..efa8246f1e 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -15,6 +15,14 @@ class Project < ActiveRecord::Base belongs_to :firm has_one :lead_developer, through: :firm, inverse_of: :contracted_projects + begin + previous_value, ActiveRecord::Base.belongs_to_required_by_default = + ActiveRecord::Base.belongs_to_required_by_default, true + has_and_belongs_to_many :developers_required_by_default, class_name: "Developer" + ensure + ActiveRecord::Base.belongs_to_required_by_default = previous_value + end + attr_accessor :developers_log after_initialize :set_developers_log diff --git a/activerecord/test/models/tuning_peg.rb b/activerecord/test/models/tuning_peg.rb new file mode 100644 index 0000000000..1252d6dc1d --- /dev/null +++ b/activerecord/test/models/tuning_peg.rb @@ -0,0 +1,4 @@ +class TuningPeg < ActiveRecord::Base + belongs_to :guitar + validates_numericality_of :pitch +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 690ab1af4b..99098017d7 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -114,7 +114,7 @@ ActiveRecord::Schema.define do create_table :bulbs, force: true do |t| t.integer :car_id t.string :name - t.boolean :frickinawesome + t.boolean :frickinawesome, default: false t.string :color end @@ -207,6 +207,14 @@ ActiveRecord::Schema.define do add_index :companies, [:firm_id, :type], name: "company_partial_index", where: "rating > 10" add_index :companies, :name, name: 'company_name_index', using: :btree + create_table :content, force: true do |t| + t.string :title + end + + create_table :content_positions, force: true do |t| + t.integer :content_id + end + create_table :vegetables, force: true do |t| t.string :name t.integer :seller_id @@ -348,6 +356,10 @@ ActiveRecord::Schema.define do t.column :key, :string end + create_table :guitar, force: true do |t| + t.string :color + end + create_table :inept_wizards, force: true do |t| t.column :name, :string, null: false t.column :city, :string, null: false @@ -851,6 +863,11 @@ ActiveRecord::Schema.define do t.belongs_to :ship end + create_table :tuning_pegs, force: true do |t| + t.integer :guitar_id + t.float :pitch + end + create_table :tyres, force: true do |t| t.integer :car_id end diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb index 2d1651454d..666c1b6a14 100644 --- a/activerecord/test/support/schema_dumping_helper.rb +++ b/activerecord/test/support/schema_dumping_helper.rb @@ -1,7 +1,7 @@ module SchemaDumpingHelper def dump_table_schema(table, connection = ActiveRecord::Base.connection) old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables - ActiveRecord::SchemaDumper.ignore_tables = connection.tables - [table] + ActiveRecord::SchemaDumper.ignore_tables = connection.data_sources - [table] stream = StringIO.new ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) stream.string |